Wavefront openPMD I/O, grid origins, and beamline position - #155
Draft
ChristopherMayes wants to merge 1 commit into
Draft
Wavefront openPMD I/O, grid origins, and beamline position#155ChristopherMayes wants to merge 1 commit into
ChristopherMayes wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds openPMD
EXT_Wavefrontread/write support forWavefront, gives the class a grid origin and a beamline position, and fixes several correctness bugs found along the way.beamphysics/wavefront/openpmd.pyis new;wavefront.py,propagators.py, andinterfaces/genesis.pyare modified.openPMD support
Wavefront.from_openpmdandWavefront.write_openpmdread and write openPMD 2.0 files using theEXT_Wavefrontextension. Both transverse polarizations are written into oneelectricFieldrecord as componentsxandy; thezcomponent is never written. Datasets are stored in(z, y, x)order so that each transverse slice is one contiguous block, and are written a slice at a time so no transposed copy of the field is materialized. AnyaxisLabelspermutation of(x, y, z)is honored on read.Extension attributes are carried by
Wavefront.attrs, a typedWavefrontAttrsdataclass rather than a free-form dict. Fields use snake_case Python names (beamline,radius_of_curvature_x, ...) that map to the extension's camelCase names on disk, so a misspelled attribute raisesTypeErrorinstead of being silently dropped. Attributes the extension does not define yet round-trip verbatim throughattrs.other, following whatreaders.load_field_attrsandwriters.write_pmd_fieldalready do forFieldMesh. This matters more here than there:EXT_Wavefrontis still a draft, so a file written against a newer revision would otherwise lose data, silently, just by passing through this class.Frequency-domain and k-space files are rejected on read rather than silently misinterpreted, as
Wavefrontcannot represent them.Grid origin
WavefrontandWavefrontKgainxmid/ymid/zmid, the position of the grid midpoint, defaulting to 0.0 for a grid centered on the origin.xmin,xmax, andxvecderive from them.This fixes a real bug:
cropandpadpreviously discarded the coordinates of the surviving samples, so an asymmetric operation silently translated the physics. Both now shift the midpoint to compensate, through a_resized_midshook that is a no-op onWavefrontKbecause discarding k samples does not move the real-space grid.The name completes the
xmin/xmid/xmaxfamily and matches the underscore-free convention already used bydx,nx, andxvec. It is deliberately notx0, which already means beam center onfrom_gaussian, and notx_offset, which reads as openPMD'sgridGlobalOffset— a different quantity, being the first sample rather than the midpoint, half a grid cell away.Beamline position
s_positionis a first-class field onWavefrontandWavefrontK: the position of this plane along the beamline, in whatever frame the caller picked. On disk it is the extension's requiredzCoordinateand Genesis4'srefposition, both of which now round-trip. Genesis4 previously wrote a hardcoded0.driftadvances it by the propagation distance. The mesh's ownzaxis is the intra-pulse coordinate and is co-moving — the paraxial kernel is purely transverse, applied slice by slice with no piston term — sos_positionis the only place a record of the propagation can go. This followsParticleGroup, wheredriftadvances the realt/zcoordinates rather than a separate register. Operations that change the representation rather than the position (crop,pad,to_kspace, an applied lens phase) leave it alone, exactly as they leaveParticleGroup.zalone.The name avoids
zrefand similar, which would read as a member of the mesh family (zmin/zmid/zmax/zvec) while belonging to a different axis and a different frame. Baresis also avoided because the two communities invert the convention: in Genesis4 beam filessis the in-bunch coordinate andz/refpositionis the undulator position, and this repo already encodes the FEL side by mapping Genesis4'szposto a column nameds.Bug fixes
cropandpadtranslated the field on asymmetric operations (above).drift_wavefront_advancedscaleddx/dyby the magnification but notxmid/ymid, so an off-axis wavefront was magnified about the wrong point. Both of its quadratic phases are referenced tox = y = 0.crop,pad, and bothdrift_wavefront_basicreturns shared the mutableattrsobject with the original, becausedataclasses.replacerebinds rather than copies. Editing metadata on a propagated wavefront reached back into the source.to_kspaceandto_rspacealready copied, so the behavior was inconsistent.auto_crop(apply=False)returnednp.int64despite anintannotation, so the returned dict did not serialize cleanly.auto_cropdoctests referenced an undefined name and could never have run. Replaced with a working example.WavefrontAttrs.from_pmdwrote both spellings when handed an alias collision such as{"zCoordinate": 1, "s_position": 2}, and nested anotherkey intoother={'other': {...}}instead of merging it. Both now behave correctly, the former by raising.w.attrs = {...}after construction left a raw dict in place, sow.attrs.beamlineraisedAttributeError. Coercion moved from__post_init__toWavefrontBase.__setattr__._repr_html_reported none of the new state. It now shows the grid midpoint,s_position, and each set attribute, all omitted when they carry no information, and shares one row builder with_repr_pretty_so the two cannot drift. Values are HTML-escaped, sinceattrs.othercan carry arbitrary strings from a file straight into the notebook DOM. The k-space table now reportsdkx/dky/dkzin rad/m rather than the derived real-space spacing._repr_pretty_never ran. IPython walks the MRO and takes the first class whose__dict__holds_repr_pretty_or__repr__, and the dataclass-generated__repr__onWavefrontshadowed the base class method, sorepr()of a wavefront printed the entire field array. The two dataclasses now setrepr=Falseand the base supplies a short__repr__giving shape, wavelength ands_position.Changes visible to other codes
These change bytes on disk and are stated explicitly so downstream readers can move with them. Record and attribute names are unchanged.
gridUnitSIis now a 3-element array rather than a scalar, andgridUnitDimensionis written.gridGlobalOffsetis written fromxmin/ymin/zmininstead of zeros, and is applied on read. A file missing it reads as a centered grid, not asxmin = 0.gridUnitSIandunitSIrather than assuming SI.refpositioncarriess_positioninstead of always0.zCoordinateaccumulates across drifts rather than being whatever the caller passed at write time.Breaking API changes
Wavefront.write_openpmd(..., s_position=...),write_wavefront_openpmd(..., s_position=...), andwavefront_write_genesis4(..., refposition=...)no longer accept those keywords. Both writers take the value from the wavefront. A write-time override let the file disagree with the object it came from, which defeats the point of making the position a coordinate. Setw.s_positionorreplace(w, s_position=...)before writing. Of these, onlyrefpositionpredates this branch; nothing in the repo passed it.attrsrenames, as that layer is new in this branch.Testing
1898 passing, up from a much smaller wavefront suite.
tests/test_wavefront.pygains coverage for the openPMD layout as a contract (asserting bytes on disk, not just round trips), foreign files written in either axis order, corrupted and incomplete files, unknown attributes surviving a round trip, iteration disambiguation, the grid origin under asymmetric crop and pad,s_positionunder propagation versus representation changes, Genesis4refpositionin both directions, and metadata independence across every derivation path.Known limitations, not addressed here
WavefrontAttrs.copy()is one level deep, so a nested mutable insideotheris still shared between a wavefront and its derivatives. Deepcopying would cost on everydrift.n,xvec's origin at index(n-1)/2disagrees with theifftshiftorigin at indexn//2bydx/2. Oddnis exact. Changing it would move existing plots.drift_wavefront_basicloops overfield[:, :, iz]out of a C-order(nx, ny, nz)array, which is the maximally strided slice. A(nz, ny, nx)internal order would make slices contiguous but breaks the publicshapecontract and every plotting and statistics path.write_genesis4raises on a nonzeroxmid. An asymmetriccropcan produce one, making crop-then-write-Genesis4 an error by design.Acknowledgment
Developed with Claude (Anthropic) via GitHub Copilot in VS Code. All design decisions, physics judgments, and the final review were made by the author; the model drafted code, tests, and documentation under direction.