Skip to content

fix(am): record every experiment's trajectory, normalized once; refuse what would be wrong (#990) - #994

Merged
wshlavacek merged 4 commits into
mainfrom
fix/990-am-trajectory-outputs
Oct 15, 2026
Merged

wshlavacek merged 4 commits into
mainfrom
fix/990-am-trajectory-outputs

Conversation

@wshlavacek

@wshlavacek wshlavacek commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #990. Closes #998. These are the am trajectory-output defects found while reviewing #988.

What changes

  1. Every experiment gets its trajectory files, including conditioned experiments (edition 2) and mutants (edition 1).
    • Buffers are keyed by each experiment's real output suffix (epi2hi), from model.get_suffixes(), instead of by the action suffix.
    • Under edition 2, action/condition pairs that no experiment: line names are left out.
    • Buffers are created at the length of the first result that supplies them, so time_length no longer decides it.
    • Matching stays exact (_keys_of, from fix(am): draw output_noise_trajectory from each column's own noise model (#986) #988), so epi cannot catch epi2.
    • A run resumed from an older backup works out its experiments from the saved config.
  2. Normalization no longer crashes, and is applied once. The data reaching am is already normalized by the scoring path, so output_trajectory now records exactly what the objective scores, without the second normalization that crashed. Two combinations are refused at load:
    • output_noise_trajectory on a normalized column, because a count drawn around a normalized prediction has no meaning;
    • either output on a column normalized with scale, because that factor is applied inside the objective and is not in the simulated series.
  3. normalization = scale on a noise family with no additive scale is refused at load (Configuration._require_additive_scale). The check reads the family's additive_on, not its class name. Of gaussian/normal, lognormal, lnnormal, laplace, student_t and neg_bin, only neg_bin lacks one.
    • The 'FailedSimulation' object has no attribute 'out' crash is fixed. A genuine failure can reach that line, because a chain's first result is always accepted.
    • That change also fixes local_objective_eval = 1 and smoothing > 1, which crashed every am run that wrote trajectories ('Result' has no attribute 'out').
  4. A misspelled output_trajectory / output_noise_trajectory name is an error at load, naming the unmatched name and the observables, functions and species it could have matched.
    • The old first-result warning is now an error, for names the load check can't settle (for example under postprocess).
    • Two experiments whose trajectories would land in one file are refused, as is an experiment whose output length changes between results.
  5. An empty ChainRecord survives a pickle round trip. mh and pt can back up a slow chain before its first result. -r then raised IndexError at that chain's first result. am cannot reach this, because of its generation barrier.

Behaviour changes worth knowing

  • Every job type other than am now refuses output_trajectory and output_noise_trajectory at load. They were silently ignored before.
    • The six dream/p_dream configs in examples/sampler_benchmarking set them; those lines are removed.
  • The new name check caught a real typo in examples/HIVdynamics_aMCMC/pt409/pt409.conf: lnVV is now lnV.

Sibling paths

Path Status
am, worker scoring changed; end-to-end tested
am, master scoring (local_objective_eval = 1) changed; crashed before; end-to-end tested
am with smoothing > 1 / parallelize_models > 1 same change; unit-tested only
edition-2 conditions, edition-1 mutants changed; end-to-end tested
pre-equilibration experiments same rule; not tested
bngsim BNGL ODE end-to-end tested
BNG2.pl, RoadRunner, bngsim SBML/Antimony, network-free same path through get_suffixes; not run
resume (-r) old-backup tests
mh, pt, dream, p_dream, de, check now refuse the keys; tested
shared ChainRecord fixed for all samplers

Tests and oracles

Of the 36 new or rewritten tests, 31 fail on main. The other 5 cover noise families that were already accepted.

  • Closed-form oracles:
    • (exp(-k t) + 0.1)/1.1 under floor 0.1, init, edition 2, with a conditioned experiment;
    • CumInf/max and 0.6*CumInf for an edition-1 mutant.
  • Refusal tests each assert that the error names its cause.

Files run, one at a time, on the bngsim build from bngsim main:

  • test_adaptive_mcmc 115
  • test_basic_mcmc_class + test_bayesian_diagnostics 86
  • test_config_class 112
  • test_config_unused_keys 187
  • test_edition 75
  • test_lwf_config 82
  • test_noise_model_config 67
  • test_hmc 52
  • test_linear_profiling 79
  • test_dream_class 23
  • test_seed_determinism 23
  • test_sampler_integration 13
  • and about a dozen smaller files

All passed. ruff and tools/changelog.py check are clean.

Not covered here (to be filed)

Review

Two independent review rounds, then a short check of the final commit:

…e what would be wrong (#990)

- Trajectory buffers are keyed by each experiment's output suffix (conditions and
  edition-1 mutants included) and allocated from the result, instead of by the
  action suffixes in time_length, which a conditioned experiment does not have.
  Edition-2 action x condition pairs that are no experiment are left out.
- The recorded data is read wherever the objective scored it, and is not
  normalized a second time: output_trajectory holds the normalized series the
  objective scores. This also stops am failing on a chain whose first
  simulation failed, and under local_objective_eval = 1 or smoothing > 1.
- Refused at load: a trajectory name no model or measurement formula defines
  (with the names it could match); either output on a column normalized with
  scale; output_noise_trajectory on a normalized column; normalization = scale
  on a noise family with no additive scale (neg_bin); and either output under
  any job type but am. A name no experiment produces, two experiments that
  would share a file, and an output whose length changes stop the run at the
  first simulation that shows it.
- An empty ChainRecord comes back from a pickle able to take an append, so -r
  no longer stops with IndexError when an mh or pt chain had no result at the
  backup.
- The dream / p_dream benchmark examples drop the trajectory keys they could
  not honour, and the pt409 example's lnVV typo is corrected to lnV.
…#990, #998)

- The job-type refusal of output_trajectory / output_noise_trajectory moves
  from configuration loading to the start of a fresh run, so a finished run
  whose conf set them under dream still loads and reads back as before.
- from_pybnf raises, naming the cause, when it cannot recover the parameter
  scales (no .conf, one that does not load, an undeclared sampled parameter),
  instead of returning log parameters in natural space.
- The noise-under-normalization refusal reads only the compiled per-suffix
  normalization, not the legacy per-file keys kept beside it, and names the
  experiment.
- A result without a finite score records nothing and runs no name check, so
  the master- and worker-scoring paths end alike.
- Two experiments that would share a trajectory file are refused when the
  configuration loads, from the columns each is sure to have.
- -r from a backup made before #990 says which experiments' files start late.
- The docs state the _Cum rule of output_trajectory as it is.
- find_t_length, _load_t_length and config['time_length'] are removed: am no
  longer reads the length, and every job type was refused valid simulate
  actions computing it.

Closes #998
The bridge read n_steps and t_end with defaults of its own (100 and 100), so
n_output_steps=>20 gave 101 rows where BNG2.pl writes 21, a step-less action
101 where BNG2.pl writes 2, and an action without t_end ran to t = 100 where
BNG2.pl refuses it. find_t_length used to refuse the first two at load by
accident; removing it exposed the mismatch.

- New pybnf/bngsim_model/grid.py applies BNGAction.pm's rules on every path
  (the .net simulate and protocol paths, NFsim simulate, network and NFsim
  scans): n_output_steps is n_steps, one step by default, n_steps ahead of
  sample_times, 3 or more sample_times, t_end required without them. On NFsim,
  n_steps and t_end > t_start are required, except for the sample_times
  bngsim's NFsim takes.
- An action BNG2.pl refuses is refused with BNG2.pl's message when the fit sets
  up a bngsim model, outside the fall-back to BNG2.pl, and again when it runs.
- Edition-2 synthesized actions name sample_times or n_steps and t_end, so
  they are unchanged: an edition-2 am fit over every experiment shape (ODE, SSA
  and NFsim) writes byte-identical results before and after.
…the #872 exception (#998)

- An action naming argfile, max_sim_steps, output_step_interval, or
  print_on_stop other than 1 is classified unsupported by the bngsim bridge:
  BNG2.pl reads these to decide its output rows, and the bridge does not. auto
  now runs such a model on BNG2.pl, and bngl_backend = bngsim refuses it,
  naming the option. An argfile action was refused with the claim that BNG2.pl
  refuses it, which it does not.
- grid.py and the bngl_backend docs no longer claim the same grid on both
  backends without qualification: sample_times that start after t_start are
  still integrated from the first sample time on bngsim (#872, open).
- Adds the re-review's grid tests, with the #872 case marked xfail (strict).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment