Trajectory artifact storage#
pyrite.montecarlo.trajectories writes one HDF5 file per captured case. The user workflow is in Working with results; the authoritative layout is the module docstring. This page records why schema 3 stores row fields the way it does and what that was measured to cost.
Schema 3 layout decisions#
Schema 3 changes only how row fields (one entry per segment) sit on disk. read_trajectory_artifact(...).transport still returns the transported mapping with the same dtype, shape, row order and values. Spectra replayed from a schema-3 artifact equal those from schema 2.
Chunked, compressed rows. Each row dataset uses 4,096-row chunks with the
shufflefilter and LZF (_ROW_CHUNK,_ROW_FILTERS). Rows are written in bounded blocks aligned to whole chunks, so device-resident captures still download one block at a time.Per-electron index.
/electron_indexholdselectron_id,row_startandrow_stopfor each stored history run. Streaming (iter_electron_row_blocks), history selection (select_trajectorieswith histories,--firstor--sample) and export read run boundaries from it instead of scanning or bisectingelectron_id.Per-history constants.
t0_angis stored once per index entry (pyrite_kind = "electron_rows"). Before choosing that form, the writer checks that the value is bitwise identical across every row of each history. If any history varies, the field stays per-row.Narrow IDs.
electron_id,track_id,parent_idandflight_idare stored asint32, andsubstep_idasint16(orint32when needed). Each narrowing applies only when the field’s full range fits; the transported dtype is kept inpyrite_dtype./transport_orderand the index arrays useint32when they fit.One reader seam. Raw
/transportdatasets no longer equal the transported arrays, so every reader goes throughtransport_view(handle). It restores the transported dtype, expands per-history fields to rows, and slices rows like an h5py dataset. Schema 1/2 nodes pass through it unchanged. The view caches node lookups, because h5py attribute and group access cost more than a small selected read.
Benchmarks#
Case (a): CPU sweep case#
Case (a) is a 2,000-electron, 30 keV silicon sweep case: 2 µm slab, 30° tilt, midpoint energy model, default coupled radiative fields, lockstep CPU transport. It produced 294,506 segments, and its transport took 1.72 s. Times are the best of three on the development laptop with a warm page cache. “Select 10” selects and reads 10 histories spread across the file.
Filter and chunk choice at 4,096-row chunks, with the schema-2 writer for reference:
Layout |
Bytes/segment |
Write (share of transport) |
Full read |
Stream (65,536-row blocks) |
Select 10 |
|---|---|---|---|---|---|
schema 2 (uncompressed) |
207.7 |
0.070 s (4%) |
0.063 s |
0.044 s |
24.0 ms |
schema 3, LZF + shuffle |
89.2 |
0.31 s (18%) |
0.19 s |
0.16 s |
27.0 ms |
schema 3, gzip-1 + shuffle |
84.6 |
0.50 s (29%) |
0.16 s |
0.13 s |
24.6 ms |
schema 3, gzip-4 + shuffle |
83.4 |
0.58 s (34%) |
0.17 s |
0.14 s |
26.3 ms |
Effect of chunk size with LZF + shuffle:
Chunk rows |
2,048 |
4,096 |
8,192 |
16,384 |
65,536 |
|---|---|---|---|---|---|
Bytes/segment |
89.9 |
89.2 |
88.6 |
88.4 |
88.6 |
Select 10 |
19 ms |
28 ms |
45 ms |
78 ms |
138 ms |
Decisions:
LZF over gzip. Schema 3 cuts case (a) by 57% (207.7 to 89.2 B/segment). Only LZF keeps the write cost under a quarter of transport time. gzip saves another 5 to 6% of the file but costs 29 to 34% of transport.
Portability cost. LZF ships with h5py but not with the HDF5 C library. Tools outside h5py, such as
h5dump, HDFView or MATLAB, need the LZF filter plugin (for example the one inhdf5plugin) to read row datasets.4,096-row chunks. A selected history decompresses whole chunks, so selection time grows with chunk size, while compression barely improves past 4,096 rows. At 4,096 rows, a 10-history selection costs about the same as schema 2.
Where the bytes go: in case (a), the eight float64 row fields r_mid, v_hat, L_ang, t_start_ang, t_end_ang, E_start_keV, E_end_keV and E_repr_keV make up 82 of the 89 bytes per segment. Floating-point mantissas compress poorly without loss. Further savings would need a lossy representation, which schema 3 does not adopt. Sparse fields compress almost to nothing (hard-radiative vectors take 0.3 B/segment), as do narrowed IDs and the per-history t0_ang.
Full reads and streaming are about three times slower than schema 2 because of decompression. At under 1 µs per segment they remain small next to transport, which took about 6 µs per segment here.
Case (b): large GPU capture#
Case (b) is the trajectory_demo hopg geometry at 30 keV: 45° tilt, 180° azimuth, a 1 µm spot, 30,000 electrons, and a 20 µm (2×10⁵ Å) slab. It ran on the remote GPU box (RTX 5080, 8 CPU cores, shared and time-sliced) with the CUDA transport core and produced 6,869,145 segments. Transport took 2.0 s. Each layout was timed once, so the timings are noisy. “Uncompressed” is the schema-3 writer with filters, ID narrowing and per-history storage turned off, which is the schema-2 row layout.
Layout |
Size |
Bytes/segment |
Write (share of transport) |
Full read |
Stream (1 Mi / 64 Ki rows) |
Select 10 |
|---|---|---|---|---|---|---|
uncompressed (schema-2-like) |
1.28 GiB |
199.7 |
1.55 s (76%) |
0.69 s |
0.67 / 1.18 s |
10 ms |
schema 3, host segments |
0.57 GiB |
89.4 |
7.0 s (345%) |
5.6 s |
5.1 / 3.7 s |
38 ms |
schema 3, device-resident segments |
0.57 GiB |
89.4 |
5.7 s (248%) |
3.1 s |
3.5 / 3.2 s |
162 ms |
The size reduction carries over at scale: 55%, from 1.28 to 0.57 GiB. The write budget does not. LZF compresses about 1 million segments per second on this host’s CPU, but the CUDA core transports about 3.4 million per second. A schema-3 capture therefore costs several times the GPU transport it records, against about 0.8× uncompressed. Case (a)’s 25% bound holds only for CPU transport. Ways to bring GPU captures back under budget, none adopted here:
an opt-in uncompressed or lighter filter for GPU-transported cases;
compressing on a writer thread that overlaps the spectrum phase;
storing floats at lower precision.
Host and device-resident captures both read back equal to the transported arrays.
Compatibility#
New runner captures record a data-only resolved physics view in
settings.resolved_physics: descriptor IDs, model-generation markers, original
case parameters, and per-case consumed-table digests. Full and bounded spectrum
replay compare this snapshot with current resolution before scoring. Changed
models or table digests require new trajectories. Backend diagnostics and
descriptor documentation are outside this comparison.
Captures without this optional snapshot retain their historical replay checks; readers do not populate omitted selectors with current defaults or infer past table inventories. Geometry-only reading and export remain available regardless of current model or table availability. The snapshot does not change the case digest or the HDF5 schema version.
Readers accept schemas 1 to 3. Schema-2 files keep their plain row datasets and have no /electron_index. Selection and streaming then fall back to bisection and boundary scans, and give the same selections and blocks. tests/data/trajectory_artifacts/schema2_hopg_lockstep.h5 is a genuine schema-2 capture. The tests check that it replays bit for bit, both directly and after being rewritten as schema 3. tests/helpers/trajectory_schema.py rewrites any new artifact into the schema-1 or schema-2 layout for compatibility tests.