ER Full-Period Batch¶
sopran.experimental.kaguya.er_batch is a CLI that runs finite-bin fits and \(R_m\) profiles over the full mission on saved paired distributions.
The same commands run on a workstation and as Slurm array jobs on the Kyoto University supercomputer.
Inputs and outputs are Store datasets split into daily Parquet, so rsync can move them as they are.
| Dataset | Layer | variant | Contents |
|---|---|---|---|
experimental.kaguya.er.paired_distributions |
features |
Input build name | Folded counts, exposures, and window records per 16 s window; one shard per day |
experimental.kaguya.er.finite_bin_fits |
models |
Run name | Fits, profiles, and candidates per window; one shard per day |
Commands take the form python -m sopran.experimental.kaguya.er_batch [--data-root ROOT] <command>.
Without --data-root, the Store from SOPRAN_DATA_ROOT (or the SOPRAN configuration) is used.
It needs the experimental and kaguya extras.
Flow¶
export-catalog → verify-inputs → prepare → run (parallel, resumable) → status → finalize → compare
| Command | Action |
|---|---|
export-catalog --catalog PATH --input-variant V [--workers N] |
Convert a SQLite catalog to daily shards; existing days are skipped |
verify-inputs --input-variant V |
Check input shard checksums after a transfer |
prepare --input-variant V --run-variant R [--groups G] [--fit-setting name=JSON] |
Freeze settings, inputs, and code and environment hashes in run.json |
run --run-variant R [--group i \| --days D...] [--workers N] |
Fit the given days; only unfinished days are processed |
status --run-variant R |
Show completed days and heartbeats |
finalize --run-variant R |
Recompute checksums of completed shards and register them in the Store catalog |
compare --run-variant R --reference FILE... |
Compare objectives, \(B_\mathrm{eff}\), and candidates of the same windows with daily Parquet from another run |
Preparing inputs¶
Inputs are either converted from a SQLite catalog of per-window paired distributions, or built from source data with the SOPRAN parts below.
Converting a SQLite catalog¶
The catalog has one table, windows.
| Column | Type | Contents |
|---|---|---|
sample_id |
TEXT | ID of the 16 s window |
day |
TEXT | UTC day (YYYY-MM-DD) |
record |
TEXT | Window record (JSON); time, b_sc_nT, affected_side, field_connection_status, field_radial_abs_cosine_min, magnetic_direction_max_deviation_deg, and reason are read |
maps |
BLOB | .npz; NULL for windows without a distribution |
maps holds the arrays in FLOAT_ARRAYS and BOOL_ARRAYS. Folded arrays are energy × pitch_folded,
and global_* arrays are energy × pitch.
python -m sopran.experimental.kaguya.er_batch --data-root /path/to/sopran_data export-catalog \
--catalog /path/to/catalog.sqlite --input-variant <build> --workers 3
The conversion moves folded counts, exposures, fit_valid, and pair_observed, plus all-pitch counts, exposures,
and energy and pitch edges, from each window's npz into row-major flattened list columns. It also checks that every array is representable losslessly as float64 or bool.
Derived quantities of earlier fits (observed_log_ratio, fitted_log_ratio, beam_ratio) are not carried over.
The window record (JSON) and the sha256 of record plus npz (source_sha256) are kept, and days.json lists window counts,
distribution counts, and checksums for every day, including days without input.
Building from source data with SOPRAN parts¶
Parts are also available to build input shards from source data without a SQLite catalog.
Their defaults match the rules of the September 2026 full-period run (halekas_mission_v1).
Assembling a full-period run (parallel days, resuming, logs) is left to your own script.
| Stage | Part |
|---|---|
| Native records and LMAG geometry for one day | KaguyaErInstrument(mission).native_records(day) |
| Aggregation into 16 s windows and diagnostics | er_windows.calendar_windows, window_record, window_input_diagnostic |
| Connection screening with SVM3D | moon.connection.trace_connection, classify_connection (ConnectionPolicy) |
| Paired distribution | er_windows.window_pitch_records → electron_reflection.maps.build_paired_map |
| Writing and registration | er_batch.window_input_row → er_batch.write_input_days |
import numpy as np
import sopran as spn
from sopran.experimental.electron_reflection.maps import build_paired_map
from sopran.experimental.kaguya import er_batch
from sopran.experimental.kaguya.er import KaguyaErInstrument
from sopran.experimental.kaguya.er_windows import (
calendar_windows, usable_record_indices, window_input_diagnostic,
window_pitch_records, window_record,
)
from sopran.experimental.moon.connection import (
classify_connection, trace_connection, unconnected_window,
)
from sopran.experimental.moon.svm3d import SVM3DShellGrid
day = spn.day("2008-02-10")
records = KaguyaErInstrument(spn.Kaguya(download="never")).native_records(day)
usable = np.unique(np.concatenate(
[usable_record_indices(a, np.arange(records.times.size)) for a in records.arrays.values()]
))
traces = trace_connection(
SVM3DShellGrid.load("svm3d_shell.npz"),
records.position_km[usable], records.magnetic_field_nT[usable],
)
connection = classify_connection(
traces, records.times[usable].astype("datetime64[s]").astype(int) // 16
)
rows = []
for window_id, indices in calendar_windows(records.times, day):
record = window_record(window_id, indices, records)
record.update(connection.get(window_id, unconnected_window()))
record["joint_input_diagnostic"] = window_input_diagnostic(indices, records.arrays)
usable_here = any(usable_record_indices(a, indices).size for a in records.arrays.values())
reason = "no_records" if not indices.size else None if usable_here else "no_usable_geometry"
if reason is None and not record["connection_eligible"]:
reason = "connection_" + record["field_connection_status"]
if reason:
record["reason"] = reason
maps = None if reason else build_paired_map(window_pitch_records(records, indices))
rows.append(er_batch.window_input_row(record, maps))
er_batch.write_input_days(spn.Store(), "<build>", {"2008-02-10": rows})
The parts follow these rules.
window_recordderives the maximum field-direction deviation from the LMAG samples in the window, andfield_radial_abs_cosine_minfrom the cosine between \(B\) and \(r\) at the record times.build_paired_maporients each record so the affected side is at low pitch angles, distributes it onto a 20–1500 eV, 24 energy × 16 pitch grid by bin overlap, sums counts and exposures, and folds into affected and reference.fit_validmarks the range where both sides of the pair have positive exposure and counts, with at least three cells per energy row.- Connection screening traces the SVM3D field plus a uniform residual in 0.5 km steps, stopping at 6.1 km altitude and at most 1000 km.
Windows where the inward trace reaches the shell, the opposite one does not, and the side does not change within the window are
model_connected.
Freezing and running¶
prepare records in run.json the settings (defaults of FiniteBinFitSettings() plus --fit-setting overrides) and the input catalog checksum.
--min-fit-energy E removes rows whose centre energy is below E eV from the fitted cells and records it as config.min_fit_energy_eV.
It also records the sha256 of the fitting code and the Rust extension, and the versions of Python, numpy, scipy, and polars.
run compares these with the current environment at startup and stops on any difference. To change settings, prepare a new run name.
run processes days one at a time and spreads each day's windows over worker processes (spawn).
Workers pin OMP_NUM_THREADS and similar variables to 1. --workers defaults to the allocated CPUs (affinity on Linux), and 0 runs in process.
Every 256 windows are saved to work/<day>/part-*.parquet; when a day finishes, shards/day=<day>.parquet and days/<day>.json are written.
Rerunning skips completed days and never recomputes saved parts.
Creating a file named STOP in the run directory makes every invocation save and exit after its current window.
Each invocation writes only its own days' files, so array jobs may run at the same time.
--groups G splits days into G groups balanced by distribution count, and run --group i processes group i.
Each window is fitted with the default count-likelihood settings. Profiles are computed only for windows that meet the geometry conditions
(\(B_\mathrm{sc} > 0\), field-direction deviation ≤ 20°, angle to radial < 80°, model_connected) and have no field_flags.
Profiles use 13 points over \(R_m\) ±1 dex and give the 95% interval and minimum_fit. Column meanings are in the schema.json written by finalize.
Running on the Kyoto University supercomputer¶
Scripts are in scripts/hpc/kyoto/. Kyoto's Slurm is extended: resources are requested with
--rsc p=PROCS:t=THREADS:c=CORES:m=MEMORY, the -p queue is mandatory, and programs start with srun
(KUDPC batch processing manual).
The templates follow this format, but confirm queue names, limits, and array-job environment variables in a first pilot.
-
Move the data. Input datasets are managed and thus in the default list, but here only the inputs are selected by name.
sopran-store --data-root /path/to/sopran_data sync-list \ --dataset experimental.kaguya.er.paired_distributions > files.txt rsync -av --delay-updates --files-from=files.txt /path/to/sopran_data/ <host>:$SOPRAN_DATA_ROOT/After the transfer, run
sopran-store rebuildandverify-inputson the destination. -
Build the environment on a login node. Clone SOPRAN with git and run the following; it installs uv and Rust under
$HOMEand builds SOPRAN (non-editable) into a version-pinned venv.SOPRAN_SRC=$HOME/src/sopran ENV_ROOT=/LARGE0/grXXXXX/$USER/sopran-env \ bash scripts/hpc/kyoto/setup_env.shAlternatively, install the manylinux x86_64 wheel built by CI (
publish.yml) with--no-deps. -
Check and freeze on the login node.
source $ENV_ROOT/venv/bin/activate export SOPRAN_DATA_ROOT=/LARGE0/grXXXXX/$USER/sopran_data python -m sopran.experimental.kaguya.er_batch verify-inputs --input-variant <build> python -m sopran.experimental.kaguya.er_batch prepare --input-variant <build> \ --run-variant <run> --groups 16 -
Check agreement with a pilot. Create a separate run with
--daysset to one day already completed locally, and run one task (-a 0). Transfer that day's Parquet from the local run and compare withcompare. Linux and Windows implement exponential and logarithm functions differently, so exact agreement is not expected; check that relative objective differences are small and candidates agree. -
Submit production. Edit
-p,-t,--rsc, and-a 0-(G-1)inscripts/hpc/kyoto/er_fit.sbatch, exportENV_ROOT,SOPRAN_DATA_ROOT, andRUN_VARIANT, andsbatch. Tasks that time out continue where they stopped when resubmitted with the same command. -
Collect results. Confirm completion with
statusand register withfinalize. Bring results back with the list fromsopran-store sync-list --dataset experimental.kaguya.er.finite_bin_fits --variant <run>, then runsopran-store rebuildlocally.
Validation¶
tests/test_er_batch.py checks that conversion is lossless, the prepare → run → finalize → scan flow, resuming from parts, STOP,
rejection after code changes, balanced day groups, and agreement between spawn workers and in-process runs.
Comparisons on real data use compare, and their results are recorded per run in working/.