Summary
Under edition-2 Mechanism A (ONE model + condition: perturbations, PEtab-aligned), PyBNF
simulates the full {action} × {condition-variant} cross-product but scores only the
diagonal — each experiment's own action under its own condition. For N experiments and
M condition: mutants on one model, that is N×(M+1) simulations to obtain N scored
series. The remaining N×(M+1) − N off-diagonal simulations (e.g. the WT action run under
the n78g mutant → suffix WTn78g) are computed on every objective evaluation and then discarded.
This is the deeper root behind #483. There, the wasted off-diagonal suffixes surfaced as a hard
crash (KeyError: 'WTn78gMEK_pRDS') because the am sampler's output_trajectory bookkeeping
allocates buffers only for the scored diagonal keys but the write loop iterated every raw suffix.
That crash is now fixed (guard the write to allocated keys, ae21d212, closes #483) — but the
redundant work remains, on all job types, not just am.
Why it matters beyond the crash
- Every job type pays the cost. The
de path on the identical model/condition/experiment
setup runs to completion today, but it still runs the full N×(M+1) simulations per generation
and simply never references the off-diagonal results. The KeyError was am-specific; the wasted
compute is not.
- It scales multiplicatively. For the Miller et al. 2026 MEK-isoform job (N=5 experiments,
M=4 cell-line condition: mutants) that is 5×5 = 25 simulations per evaluation for 5 scored
series — a 5× overhead — and it grows with both the experiment count and the condition count.
Proposed direction
Simulate (and emit) only the scored (action, condition) pairs — the diagonal that the
objective and output_trajectory/output_noise_trajectory actually consume — instead of the
full cross-product. This removes both the wasted work and the buffer key-space mismatch at their
source (making the #483 write guard defensive rather than load-bearing).
Caveat — verify the off-diagonal pairs are truly dead work first
Step one should be to confirm nothing quietly depends on the off-diagonal suffixes before pruning
them, so we don't turn a perf win into a silent regression:
- Cross-condition / cross-model constraints. BPSL
.prop/.con constraints (e.g. the
Miller job's WT.obs < KO.obs) reference scored suffixes, which are all diagonal — but this
should be checked against Constraint.get_key suffix resolution, not assumed.
- Any consumer that iterates
res.out[model] broadly (trajectory emission, PEtab export,
diagnostics) rather than by scored data-key.
If any real consumer needs an off-diagonal pair, that pair is by definition part of the scored/used
set and should be added to the diagonal explicitly, not left as an implicit cross-product artifact.
Context
Summary
Under edition-2 Mechanism A (ONE model +
condition:perturbations, PEtab-aligned), PyBNFsimulates the full
{action} × {condition-variant}cross-product but scores only thediagonal — each experiment's own action under its own condition. For N experiments and
M
condition:mutants on one model, that is N×(M+1) simulations to obtain N scoredseries. The remaining N×(M+1) − N off-diagonal simulations (e.g. the
WTaction run underthe
n78gmutant → suffixWTn78g) are computed on every objective evaluation and then discarded.This is the deeper root behind #483. There, the wasted off-diagonal suffixes surfaced as a hard
crash (
KeyError: 'WTn78gMEK_pRDS') because theamsampler'soutput_trajectorybookkeepingallocates buffers only for the scored diagonal keys but the write loop iterated every raw suffix.
That crash is now fixed (guard the write to allocated keys,
ae21d212, closes #483) — but theredundant work remains, on all job types, not just
am.Why it matters beyond the crash
depath on the identical model/condition/experimentsetup runs to completion today, but it still runs the full
N×(M+1)simulations per generationand simply never references the off-diagonal results. The KeyError was
am-specific; the wastedcompute is not.
M=4 cell-line
condition:mutants) that is 5×5 = 25 simulations per evaluation for 5 scoredseries — a 5× overhead — and it grows with both the experiment count and the condition count.
Proposed direction
Simulate (and emit) only the scored
(action, condition)pairs — the diagonal that theobjective and
output_trajectory/output_noise_trajectoryactually consume — instead of thefull cross-product. This removes both the wasted work and the buffer key-space mismatch at their
source (making the #483 write guard defensive rather than load-bearing).
Caveat — verify the off-diagonal pairs are truly dead work first
Step one should be to confirm nothing quietly depends on the off-diagonal suffixes before pruning
them, so we don't turn a perf win into a silent regression:
.prop/.conconstraints (e.g. theMiller job's
WT.obs < KO.obs) reference scored suffixes, which are all diagonal — but thisshould be checked against
Constraint.get_keysuffix resolution, not assumed.res.out[model]broadly (trajectory emission, PEtab export,diagnostics) rather than by scored data-key.
If any real consumer needs an off-diagonal pair, that pair is by definition part of the scored/used
set and should be added to the diagonal explicitly, not left as an implicit cross-product artifact.
Context
am+output_trajectoryKeyError), commitae21d212..propconstraint simdata resolution).one model + four
condition:cell-line perturbations.