Add new options to genesis4_par_to_data: wrap, z0, equal_weights, cut…#106
Add new options to genesis4_par_to_data: wrap, z0, equal_weights, cut…#106ChristopherMayes wants to merge 25 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 9 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Ken Lauer <152229072+ken-lauer@users.noreply.github.com>
There was a problem hiding this comment.
So long as your .h5 files remain in git history, they still take up space in the repository.
In any case:
I'd rather we version control the data elsewhere (or make it available as a tarball to download) than generate a conda env + test data on the fly during the test suite. That feels rather fragile.
Or just go back to version-controlling it here - it's not that big of a deal overall.
Regardless of what you do here, rebase to clean up the history and remove any unnecessary h5 objects there.
There was a problem hiding this comment.
I made the files smaller with sample = 10 -> sample = 100, committed.
Co-authored-by: Ken Lauer <152229072+ken-lauer@users.noreply.github.com>
|
You missed this important bit from my comment:
That will fix the |
tldr;
Summary
This branch overhauls how Genesis4
.par(raw particle) HDF5 files are read into openPMD-beamphysics. It adds a first-classParticleGroup.from_genesis4constructor, significantly expands the conversion options, makes the conversion reproducible via an explicit RNG, and supports downsampling huge (especially one4one) files at read time without ever loading the full beam. The example data is no longer committed to the repository; it is generated on the fly by a script that installs Genesis4 into an isolated conda environment, used by both the docs build and the test suite in CI.What changed
New public API
ParticleGroup.from_genesis4(...)classmethod (beamphysics/particles.py) — load a Genesis4.parfile directly into aParticleGroup. This is the recommended entry point; it forwards togenesis4_par_to_data.genesis4_par_to_datareworkbeamphysics/interfaces/genesis.pyNew keyword arguments:
wrapFalsezposition modulo the slice length (thetataken mod 2π).z00.0zoffset applied to slice index 1.slicesNoneequal_weightsFalsen_particle.cutoff0.0cutoff=1.6e-19drops sub-electron slices). Defaults to0.0(keep everything). Zero-current / non-finite slices are always dropped. Ignored for one4one beams.n_particleNoneequal_weights.rngNonenumpy.random.Generatoror integer seed for smearing and resampling (reproducible).Behavioral and implementation changes:
smeardefault changedTrue→False(BREAKING forgenesis4_par_to_data). Particles are now loaded at their literal slice positions by default, which produces a comb-like longitudinal distribution whensample > 1. Smearing is opt-in (smear=True).smear=Trueexplicitly to restore the previous behavior.smearandwrapare now independent (intentional design change). The old code always appliedtheta/(2π) % 1before smearing; wrapping is now a separate, explicit user directive. Callers who want the old always-wrapped smear should passsmear=True, wrap=True. Withoutwrap,thetavalues outside[0, 2π)(e.g. from slippage in post-undulator dumps) place particles outside their nominal slice, faithfully reflecting the file contents.zreconstruction.zis computed directly from the slice index as(ix - 1) * slicespacing + z0rather than from a runningi0counter. This is robust to skipped/zero-current/missing slices (verified equivalent to the old counter for well-formed files).currentdatasets), before any bulk reads. Consequences:n_particleapportionment runs over the surviving slices, so filtered slices cannot silently consume part of the requested count — the output hits the target exactly, even in combination withcutoff.n_particle, only the selected rows are read from the file (load_parfile_slice_data(group, sel=...)with sorted indices), so bulk I/O scales withn_particle, not with the file size, and the full beam is never materialized.n_particleandequal_weightsare mutually exclusive and raise aValueErrorwhen combined. They serve different needs:n_particlethins the beam while retaining the relative slice weights;equal_weightsresamples the existing particles to a single common weight (never upsampling).np.random.default_rng(rng); subsample selections useshuffle=Falseand sorted indices (statistically equivalent, faster, and required for direct HDF5 row reads).qe(matching Genesis4's own&sort, which resets each slice current tonpart·qe), so sorted and unsorted files reconstruct identically. Thecutoffis disabled for one4one beams.equal_weightsshort-circuit. When every slice already has the same per-particle weight (e.g. an unsubsampled one4one beam), the per-slice resampling is skipped.sample = round(slicespacing / slicelength)(wasint(...)), avoiding float truncation.FileNotFoundErrordirectly (no racyos.path.existspre-check).rng: Optional[int | np.random.Generator]).New helpers and a typed slice container
genesis4_parfile_scalars(h5)— read scalar metadata (slicelength,slicespacing,slicecount, …).genesis4_parfile_slice_groups(h5)— slice group names, matched positively by name pattern (slice+ digits) and structure and sorted by slice index, so new metadata entries in future Genesis4 versions are ignored automatically.genesis4_parfile_slice_counts(h5)— per-slice particle counts from dataset shapes only (no bulk data); the building block for both the count helper and then_particlepre-pass.genesis4_parfile_n_particle(h5)— total particle count (sumof the slice counts); cheap for huge files and handy for choosing ann_particletarget.load_parfile_slice_data(group, sel=None)→ParfileSliceDatanamedtuple (x, px, y, py, gamma, theta, current);selreads only the given (sorted) rows.str,pathlib.Path, or an openh5py.Filehandle.Example data is generated, not committed
.par.h5/.out.h5files are no longer stored in the repository (they are gitignored). They are generated from the committed inputsdocs/examples/data/genesis4/{genesis4.in, one4one.in, genesis4.lat}byrun.sh(genesis4 genesis4.inandgenesis4 one4one.in, withset -e).scripts/generate_genesis4_example_data.bash(new) creates an isolated conda environment (genesis4-example-data) containing only the conda-forgegenesis4package — Genesis4 is not a dependency ofenvironment.yml— and runsrun.shwith it. The environment is reused if it already exists.gh-pages.ymlruns the script before executing the notebooks;tests.yaml(conda) runs it before pytest. The pip test workflow has no Genesis4, so the Genesis4 particle tests skip cleanly there.seed = 123456), so regeneration is reproducible for a given Genesis4 version. The quiet-loading example is a 100 pC Gaussian withsample = 10; the one4one example is a low-charge Gaussian (934 electrons) so that tail slices are empty and per-slice counts vary.Tests and docs
tests/test_genesis4_particles.py(new, 23 tests): scalars/slice-group helpers, default charge (100 pC within 1e-3), smear reproducibility,z0/wrap/slicesbehavior, cutoff filtering, one4one loading (uniformqeweights, no division by zero, cutoff not applied),n_particlesubsampling (exact counts, exact charge conservation, uniform one4one weights, no-op above the total, exact count in combination withcutoff),equal_weights(uniform weights, charge preserved, never upsamples), and then_particle/equal_weightsmutual-exclusion error. A session fixture generates the data viarun.shwhen missing and skips if thegenesis4executable is unavailable. Test parameters carry type annotations.docs/examples/read_examples.ipynb: the Genesis4 section documents (in full sentences) the default comb-like load,smearwithrngreproducibility,equal_weights(never upsampling),n_particlethinning in the quiet-loading mode (9216 → 1000, plotted), the one4one mode (uniformqeweights,cutoffinapplicability), and subsampling on read for huge one4one files (count helper, 934 → 200 with conserved charge, and the ≥-total no-op). The notebook does not generate the data itself; the markdown points readers at the generation script.Verification
beamphysics-dev(1842 passed, 1 xpassed), including the 23 Genesis4 particle tests.read_examples.ipynbexecutes end-to-end with no error cells; outputs show 99.74 pC recovered against the 100 pC input target, exactn_particlecounts, and exact charge conservation on subsampling.Genesis4 source verification
Cross-checked against the Genesis-1.3-Version4 source to confirm the particle
interpretation is correct:
src/IO/writeBeamHDF5.cpp,writeGlobal): the HDF5 dataset names are swapped relative to the internal C++ variable names, and the interface reads them by the HDF5 name (correctly):slicelength=beam->reflength= λ₀ (reference wavelength / bucket length) → used asds_slice.slicespacing=beam->slicelength=sample·λ₀ (distance between written slices) → used ass_spacing.sample = round(slicespacing / slicelength)is exact. ✓px/py(include/Particle.h,src/Loading/QuietLoading.cppL195–197,src/Loading/ImportTransformation.cppL128) storeγ·x′ ≈ γ·βₓ(the file's"rad"unit label refers topx/γ = x′). Sop_x[eV/c] = px · mₑc²as the interface does. ✓thetais the ponderomotive phase in radians; one bucket = 2π, so the in-slice longitudinal positionz = (theta / 2π)·λ₀is correct. ✓slice%6.6d, indexi+1), so absolutez = (ix − 1)·slicespacing + z0places slice 1 atz0. ✓samplebuckets, i.e. a lengthslicespacing, soQ_slice = current · slicespacing / cand the per-particle weightQ_slice / n1is correct. Confirmed empirically: the example recovers 99.74 pC against a 100 pC input target.Conclusion: the particle model and unit conversions match the Genesis4 implementation.
Downstream usage (lume-genesis)
The only known external consumer of
genesis4_par_to_datais lume-genesis, viaLoadableParticleGroupH5File.load()→load_particle_group(h5, smear=True)→genesis4_par_to_data(h5, smear=smear).smeardefault change is safe for lume-genesis. Itsload_particle_groupdefinessmear: bool = Trueand always passessmear=smearexplicitly, so flipping the beamphysics default does not change which value it passes.thetaextends outside[0, 2π): the old code always wrapped before smearing, while the new code wraps only withwrap=True. If the previous behavior is desired downstream, lume-genesis should passwrap=Truealongsidesmear=True.wrap,z0,slices,equal_weights,cutoff,n_particle,rng) are not reachable through lume-genesis yet;load_particle_grouponly forwardssmear. They would need to be plumbed through (or the call switched toParticleGroup.from_genesis4) to be usable downstream.Caveats and follow-ups
smear=True → Falsechanges output for existing callers ofgenesis4_par_to_data. Worth calling out in release notes.smear=Truecallers: wrapping is no longer implied by smearing (intentional; see above). Passwrap=Trueto reproduce the old always-wrapped positions.shuffle=False, sorted selections): seeded results differ from earlier commits, but same-seed calls remain reproducible.equal_weightsin the reader vsParticleGroup.resample(equal_weights=True): the reader resamples per slice, without replacement, with a seeded RNG (preserving the slice charge profile exactly);statistics.resample_particlessamples globally, with replacement, using global NumPy random state. A possible future cleanup is to lift the per-slice algorithm intostatistics.resample_particlesand delegate.mamba/conda create+conda run) was validated for syntax but first exercised end-to-end in CI.git rm --cached docs/examples/data/genesis4/{end.par.h5,genesis4.out.h5,one4one.par.h5,one4one.out.h5}.