Skip to content

feat: osiris wrapper - #279

Merged
physicistphil merged 59 commits into
ergodicio:mainfrom
physicistphil:osiris-wrapper
Aug 11, 2026
Merged

feat: osiris wrapper#279
physicistphil merged 59 commits into
ergodicio:mainfrom
physicistphil:osiris-wrapper

Conversation

@physicistphil

Copy link
Copy Markdown
Contributor

Todo:

  1. Merge outputs into a single file (xarray) instead of uploading h5
  2. Build reasonable diagnostic / plots (mimicking what we have for vlasov). Right now it's just a few useful plots

physicistphil and others added 15 commits May 26, 2026 13:01
functions (should probably be moved to another branch). Added density
and temperature profiles. Fixed LaTeX issues
regenerates from the saved NetCDF artifacts alone — no rerun, no raw
MS/ tree. Made list_diagnostics/load_series/load_hist_energy dispatch
between the MS/ HDF5 tree and a binary/ NetCDF dir, made field-energy
source-agnostic, and now persist HIST/energy.nc in save_run_datasets.
Added 3 tests
- regen harness: rebuild the full canned plot set from saved NetCDFs (no rerun)
- f(p) and delta-f lineouts; temperature profile from phase-space Maxwellian fits
- number-density profiles (initial/final/late-mean); 2-panel equal-aspect omega-k
- phase-space & spacetime: space on x-axis, cropped to box, log-contrast floor
- proper-LaTeX titles (prose vs math)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion+reflection plot (should probably be moved to osiris-lpi repo)
sync-up.sh script doesn't delete ongoing osiris runs
@joglekara

Copy link
Copy Markdown
Member

is this ready to go?

@physicistphil

Copy link
Copy Markdown
Contributor Author

Gave it a read through; should be good to go. Idk if the tests are actually all that useful but I left them in ¯_(ツ)_/¯

@physicistphil
physicistphil marked this pull request as ready for review June 17, 2026 00:00
BaseOsiris.write_units() previously returned {} so OSIRIS runs logged an empty units.yaml. Derive the physical reference scales (wp0, tp0, n0, v0, x0, c_light, beta, box_length, sim_duration) from the deck's simulation.n0 (density) or simulation.omega_p0 (frequency); when both are present, n0 wins, as in OSIRIS. This mirrors the canonical key set the other adept solvers emit so OSIRIS runs are comparable in MLflow.

Adds skin_depth_normalization and skin_depth_normalization_from_frequency to normalization.py. OSIRIS has no single global reference temperature (species carry per-species thermal momenta), so the temperature-dependent keys (T0/nuee/logLambda_ee) are omitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@physicistphil

Copy link
Copy Markdown
Contributor Author

test-pip-install failure — not caused by this PR

The test-pip-install failure is a pre-existing break on main (started 2026-06-15, runs #287/#288; green on 2026-06-10). This branch just inherited it.

Root cause: unpinned dependency drift between matplotlib and scienceplots.

  • import adeptadept/_lpse2d/helpers.py does import scienceplots
  • scienceplots 2.2.1 (latest) calls plt.style.core.read_style_directory(...) at import time
  • matplotlib 3.11.0 removed the matplotlib.style.core attribute, so the import raises:
AttributeError: module 'matplotlib.style' has no attribute 'core'

pyproject.toml pins neither package, and test-pip-install does a clean, unconstrained install — so it picked up the freshly released matplotlib 3.11.0. The other test jobs use a constrained environment, which is why only this check is red. No fixed scienceplots release exists yet (2.2.1 is latest and still references the removed attribute).

Suggested fix: pin matplotlib<3.11 in pyproject.toml until scienceplots ships a compatible release. Probably best as a small standalone PR against main rather than folded in here.

@joglekara

Copy link
Copy Markdown
Member

Read through this — looks good to merge from my side. The wrapper architecture is clean, the security surface on the subprocess runner is fine (list-form Popen, no shell), and the synthesized-fixture tests (deck roundtrip / post / plots / units) give real CI coverage. One thing I'd like fixed before it goes in:

Hardcoded local paths in the example configs. configs/osiris/twostream-1d.yaml and twostream-1d-short.yaml both point deck: and binary: at /home/phil/Desktop/pic/..., so the shipped examples won't run for anyone else. Since this PR already adds in-repo example decks under tests/test_osiris/decks/, could you repoint the configs at those (e.g. deck: tests/test_osiris/decks/two-stream-1d) and drop the hardcoded binary: in favor of the OSIRIS_BIN / OSIRIS_BIN_<dim>D env fallback the runner already supports? That makes the examples runnable out of the box.

Otherwise LGTM — nice work on the docstrings and the NetCDF regen path. (Separately: the test-pip-install red is the pre-existing matplotlib/scienceplots break on main, not this PR.)

@physicistphil

Copy link
Copy Markdown
Contributor Author

Fixed the hardcoded paths. Also updated the docs/ markdown file (osiris-adept-usage.md). Another note: the canned plots were written for 1D sims, so 2D may cause issues (I haven't tried any 2D runs), but I figure we can cross that bridge when we get to it. Otherwise it should be good to go

@joglekara
joglekara self-requested a review June 22, 2026 22:05
joglekara
joglekara previously approved these changes Jun 22, 2026

@joglekara joglekara left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

physicistphil and others added 15 commits July 14, 2026 17:06
write_units now derives w_laser (rad/s), laser_wavelength (nm), laser_a0,
and laser_intensity — the peak intensity of a linearly polarized drive in
W/cm^2 (eps0 c E0^2/2 with E0 = a0 m_e c w_laser/e, i.e. the ICF convention
I * lam_um^2 = 1.37e18 * a0^2) — from the deck's antenna / zpulse_speckle /
zpulse section and the reference wp0. Laser-less decks are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine convention for _vlasov1d (and _vlasov2d, _pic1d) is the
RMS/standard-deviation thermal speed: Maxwellian exp(-v^2/2) at T=1,
L0 = lambda_De, code wavenumber k*lambda_De. normalization.py:91 was the
sole outlier with v0 = sqrt(2 T0/m_e), which made every logged physical
unit (v0, x0, c_light, box_length) wrong by sqrt(2), every dimensional
string input (um box sizes, gradient scale lengths, laser k0) off by
sqrt(2), and a 'normalizing_temperature: 2000eV' run physically a
4000 eV plasma. Dimensionless numeric-input dynamics were unaffected.

Also fixes a sign error in the NRL Coulomb logarithm
(log(n^0.5 / T^-1.25) == log(n^0.5 * T^+1.25)), which produced
logLambda_ee = -11.8 and a negative logged nuee at 2000 eV / 1.5e21cc;
the correct values are +7.22 and a positive rate.

vth_norm() is deliberately untouched: its only callers are in vfp1d,
which is self-consistently built on the sqrt(2T/m) convention.

Adds tests/test_vlasov1d/test_units_boundary.py: dimensional input in,
dimensional quantity out, checked against CODATA constants and the NRL
formulary - the units-boundary test class whose absence let this bug
survive every existing suite.

See VLASOV1D_CONVENTIONS_AUDIT.md (F1, F2) and
ADEPT_CONVENTIONS_AUDIT.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in write_units

_tf1d re-implements the debye normalization inline; it carried the same
v0 = sqrt(2T/m) outlier (its engine, like _vlasov1d, runs in sqrt(T/m)
units) and the same log(n^0.5 / T^-1.25) sign error. Diagnostics-only:
the logged units were wrong; dynamics are unaffected (grid beta is
written but never consumed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T0/mass

- Dougherty.compute_vbar returned the raw first moment n*u while its
  callers (find_self_consistent_beta, the drag term) treat it as a mean
  velocity. The drag therefore centered on n*u and momentum was not
  conserved wherever n(x) != 1. Now returns sum(v f)/sum(f).
- The Krook target Maxwellian was hard-coded to exp(-v^2/2) (T=1,
  m=1): any species with T0 != 1 was dragged toward T=1. It is now
  built with variance T0/mass from the species' bulk parameters, which
  are threaded through cfg.grid.species_params (new T0 entry).
- New test drives the collision operator in isolation with a 50%
  density modulation and a finite drift: per-cell density, momentum,
  and energy conservation, and the relaxed mean velocity must equal u0
  (the old operator drifts it toward n*u0).

Audit findings F4, F5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rgy monitor

- Field moments p and q were centered on the raw first moment n*u;
  they are now centered on the true mean velocity. The first moment is
  saved under the honest name 'j' and 'v' is now j/n (previously 'v'
  held the flux while the same integrand was called 'mean_j' in the
  scalars).
- The field-moment '-flogf' computed +int f log|f| dv, the exact
  negative of the scalar of the same name; both now agree.
- Adds mean_kinetic_energy / mean_field_energy / mean_total_energy
  scalars (electrostatic energy monitor, with the 1/2 factors). A
  conservation monitor of this kind would have caught the sqrt(2)
  convention bug far earlier.

Audit findings F6, F7, F12.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dx was xmax/nx and the interpolation period was xmax; both are now the
box length (xmax - xmin). Latent for all shipped configs (xmin: 0.0)
but wrong spacing, kx grid, and advection wrap for any xmin != 0.

Audit finding F11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GridConfig.c_light (set only by wavepacket.yaml) was never read -
beta/c_light are always derived from the normalization. The
AmpereSolver class docstring claimed j = sum_s (q_s/m_s) int v f dv;
the code correctly has no 1/m_s.

Audit findings F12.1, F12.4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ey driver pair was a backscatter-matched triad under the old
c_hat = 11.30 (w_pump - w_seed = 1.16 = old EPW frequency) with
placeholder k0 = 1.0 on both drivers. Retuned to a matched triad under
the corrected c_hat = 15.984: pump (k0 = +0.162949, w0 = 2.79), seed
(k0 = -0.086080, w0 = 1.700942), EPW at k*lambda_De = 0.249. Matching
condition and conventions documented in the config.

Audit finding F3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- config.md gains a 'Normalization convention' section: v0 =
  sqrt(T0/m_e) (sigma convention), L0 = lambda_De, k in 1/lambda_De,
  Maxwellian exp(-v^2/2) at T=1, Bohm-Gross w^2 = 1 + 3k^2.
- Species v0/T0 documented as code-units-only (they bypass
  normalize()); drift and thermal width now share the same unit.
- FP/Krook 'baseline' documented as a rate in units of wp0 (no 2pi),
  with the newly logged nuee_norm as the reference scale, and the O(1)
  caveat between the Dougherty nu and the NRL nu_ee.
- Super-Gaussian alpha documented (code comment + config.md): it fixes
  <v^4>/<v^2> = 3 T0/mass for all m; the variance equals T0/mass only
  at m=2 (x1.24 at m=3, x1.37 at m=4). Documentation only, per review.

Audit findings F8, F9, F10 (nuee_norm logging itself landed with the
species_params change in an earlier commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixtures locked in the sqrt(2)-wrong units (c_light 11.302, v0
2.652e7 m/s, x0 12.14 nm, negative nuee). Regenerated under the
corrected normalization and the new config surface: c_light 15.984,
v0 1.876e7 m/s, x0 8.584 nm, logLambda_ee +7.22, positive nuee, new
nuee_norm, species_params T0, c_light knob removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-mortem of job 56005549 (srs-Ln100-Te4-5x-ions): at the every-3-step
field cadence the per-run drainer thread sustained only ~2/3 of the
~16 MB/s dump rate, so the /dev/shm staging backlog grew ~2.2 GiB/min,
filled the node's 256 GiB of RAM in ~1.8 h, and every later OSIRIS HDF5
write failed on ENOSPC (truncated 2 KB dumps, 2 OOM kills, 26 h TIMEOUT
at 16% of tmax). Details in osiris-lpi NOTES.md + dev_docs/
stream-drainer-recommendations.md.

Throughput (the drainer needed ~1.5x; this buys ~20x):
- StreamWriter batches appends and lands them chunk-aligned: one
  resize_dimension + one slice write per batch instead of a gzip
  read-modify-write of the whole ~1 MiB chunk per row (~40x write
  amplification). Measured 23x on the production field shape
  (1765 vs 76 appends/s; the job needed ~55/s per sim).
- Default compression gzip 4 -> 1 (the drainer is compression-bound;
  offline postproc can recompress).
- Discovery is cached: the full MS/ rglob walk, whose cost grows with
  the backlog, runs every rediscover_every polls instead of every poll.
- No thread pool on purpose: h5py serializes all HDF5 calls behind a
  process-global lock, so batching is where the throughput is.

Safety:
- Backlog spill valve (staging mode): past spill_backlog_files/_bytes,
  or under floor_free_bytes on the staging fs, a diagnostic flips to
  mirror-only draining (plain copy to persist MS/, ~10x cheaper), so
  the ramdisk keeps draining no matter what; the NetCDF is caught up
  from the mirror at finalize (mirror consumed + pruned in
  discard_grid_h5 mode). OSIRIS must never see ENOSPC on a dump.
- Corrupt dumps are quarantined (*.h5.bad) and the stream continues;
  previously one bad dump dropped the writer and the watcher re-hit the
  same file every poll, forever. Bookkeeping is now iteration-based
  (recovered from the iter coordinate on resume), not positional, so
  skipped dumps cannot shift slots.

Observability:
- One stats line per stats_every_s (streamed/spilled/backlog/
  quarantined + staging free space) so a smoke can assert the
  steady-state backlog is flat while OSIRIS runs; checking the ramdisk
  after the job proves nothing.
- Repeated identical errors log once per error_log_every occurrences.

API and on-disk schema unchanged (runner.py untouched); all 17 existing
stream tests pass unmodified, plus new coverage for quarantine and the
spill valve in both staging modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
physicistphil and others added 4 commits August 9, 2026 14:07
…nches

The dispersion overlays on the (k, omega) plots assumed the waves live at
the simulation reference density (omega_p = 1). SRS decks are normalized
to critical density with the plasma near quarter-critical, so the
Bohm-Gross and EM branches were drawn a factor of 2 too high in omega.

- plot_omega_k / plot_omega_k_figure / save_canned_plots take omega_p
  (= sqrt(n/n_ref)); the Langmuir label shows the value when != 1.
- New BAM overlay (show_bam): shaded beam-acoustic band
  omega = k*v_phi, v_phi in bam_vph (default 2.8-5 v_th, the BAM root
  phase-velocity range of Yin et al., PRE 73, 025401 (2006)). Driven SRS
  moves the ES daughter wave below the Langmuir branch into this band.
- Manifest wiring via canned_plot_kwargs: output.overlay_density
  (0.25 -> omega_p = 0.5) and output.bam (bool).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
output.ion_acoustic: true draws omega = +/- k c_s for every ion species
in the run's rendered deck (os-stdin in the run dir or its parent, so the
regen/binary layout works too). Per-species c_s in code units comes from

    c_s^2 = (Z T_e + 3 T_i)/m_i c^2 = v_th_e^2 / rqm + 3 u_th_i^2

(Z cancels through rqm = m_i/Z), walking the deck's ordered species/udist
pairs; the electron temperature is the first rqm<0 species' uth. Ion-free
or deck-less runs draw nothing, so the option is safe to leave on.

Deck keys keep their slice suffixes (uth(1:3)), hence the base-name
lookup helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	adept/_base_.py
#	adept/_vlasov1d/modules.py
#	adept/_vlasov1d/solvers/pushers/fokker_planck.py
#	docs/source/solvers/vlasov1d/config.md
#	tests/test_vlasov1d/test_units_boundary.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@physicistphil

Copy link
Copy Markdown
Contributor Author

Should be good to go. Recent changes are mostly plotting / quality of life items -- core wrapper remains largely unchanged

@joglekara joglekara left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through this — the code itself looks careful (the crash-salvage logic and the reap/mirror discipline in stream.py in particular). The gaps I found are all wiring rather than logic.

The one thing I'd like before merge: wire the tests into CI

tests/test_osiris/ is 115 tests across 9 files and none of them have ever run on CI — .github/workflows/cpu-tests.yaml has no osiris paths-filter and no test-osiris job.

The nice thing is that they don't need OSIRIS. I ran the suite at 5bb8dd8 with OSIRIS_BIN/OSIRIS_BIN_1D explicitly unset:

107 passed, 8 skipped in 32.95s

The 8 skips are the only ones that genuinely can't run headless — 7 in test_io_and_plots.py want a pre-existing run dir (OSIRIS_TWOSTREAM_RUN) and exactly 1 wants a built osiris-1D.e. Everything else is pure Python: fake bash binaries for the subprocess driver, synthetic HDF5 for the converter/post/plots, and the four checked-in decks for the parser.

file tests covers
test_stream.py 20 concurrent H5→NetCDF converter, reap/mirror/quarantine
test_deck_roundtrip.py 18 parser vs. the 4 real decks
test_plots_new_views.py 18 plotting
test_post_netcdf.py 17 post-processing / consolidation
test_adaptive_box.py 13 density-gradient box scaling
test_runner.py 7/8 subprocess driver, error classification, binary discovery
test_units.py 7 skin-depth normalization
test_diagnostics_plots.py 7 diagnostics

33 s for that is a good trade. The deck parser and the stream converter are the two I most want guarded — a silent regression in either corrupts a real run's data rather than failing loudly.

Second reason: adding the filter folds osiris into the &shared anchor, so edits to adept/_base_.py and adept/normalization.py start triggering it. This PR touches both, which is exactly the coupling worth catching.

Should be ~15 lines copying the pic1d block.

Two papercuts (happy for these to be follow-ups)

.gitignore ignores directories nothing writes to. It adds osiris_runs/ and scratch/, but the default run_root is ./checkpoints everywhere — runner.py:120, base.py:175, both configs, and the docs. checkpoints/ isn't ignored, so a local run leaves untracked GB-scale h5/nc sitting in the repo.

docs/osiris-adept-usage.md is outside the docs build. It's 348 good lines but it sits outside docs/source/, so Sphinx never picks it up, and there's no docs/source/solvers/osiris/config.md. Every other solver has one and CLAUDE.md asks for it on new config options.

Notes, not asks

  • h5py is imported at module scope in adept/osiris/io.py but isn't in pyproject.toml — it arrives transitively via h5netcdf. Works today, silent break if that backend ever changes.
  • stage_discard_h5 (default off) unlinks the scratch HDF5 after w.append() but before the NetCDF batch flushes, so a crash inside that window loses those slices from both copies. It's documented as lossy-by-design — just flagging that it's not one to enable for a run that's expensive to repeat.
  • write_units() returns {} for the shipped two-stream deck, since that deck sets neither n0 nor omega_p0, so units.yaml comes out empty for the example config. Populates fine on the LPI decks (13 keys). Working as documented, just mildly surprising.

Things I checked that are clean

  • Deck parser round-trips all four real decks exactly at dict level, and no unquoted token gets silently promoted to a quoted string (that was my main worry — it would change deck semantics while still passing the round-trip assertion).
  • subprocess.Popen takes a list, no shell=True, env merge is explicit.
  • Every deletion path in stream.py early-returns when persist_dir is None, so the default non-staged path deletes nothing; the mirror does copyfileos.replace.
  • The _base_.py change is a genuine no-op for the other solvers — ADEPTModule.__init__ only does self.cfg = cfg, so original_cfg equals what was being dumped before.

physicistphil and others added 3 commits August 10, 2026 15:46
Add an osiris paths filter (adept/osiris/** + tests/test_osiris/**) and a
test-osiris job. The suite needs no OSIRIS binary: the runner smoke test
and real-data IO tests skip unless OSIRIS_BIN_1D / a run dir are provided.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rence

Move docs/osiris-adept-usage.md to docs/source/solvers/osiris/overview.md
inline with the other solvers, and pull the manifest schema, override
semantics, binary resolution, and adaptive box sizing into a new
config.md reference. Register both pages in the index.rst toctrees,
solvers.md, and the top-level doc link lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@physicistphil

Copy link
Copy Markdown
Contributor Author

OSIRIS tests wired in, docs updated/rearranged, and the .gitignore has been updated. Should be good to go

@physicistphil
physicistphil merged commit 4256345 into ergodicio:main Aug 11, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants