Skip to content

fix(config): refuse only output keys something reads; count every writer of a key (#1004 review follow-ups) - #1011

Merged
wshlavacek merged 3 commits into
mainfrom
fix/1004-review-followups
Oct 16, 2026
Merged

wshlavacek merged 3 commits into
mainfrom
fix/1004-review-followups

Conversation

@wshlavacek

@wshlavacek wshlavacek commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1009. Closes #910.

Follow-up to #1004. This fixes the four findings from that PR's independent review. #1004 was merged at its reviewed head before these were done.

1. Only refuse an output key that something reads (#1004 introduced the false refusal)

#1004 refused any configuration where two (action, condition) pairs spell the same output key. That included a key nothing reads: experiments Egf (condition Inh) and EgfTnf (condition TnfInh) give two unmeasured pairs that both spell EgfTnfInh, and emit pruning runs neither of them.

In edition 2, a colliding key is now refused only when something reads it:

  • data keys;
  • each experiment's own output;
  • constraint keys and cross-references;
  • postprocessing targets;
  • the keys of actions no experiment created, which always run.

The check runs again after postprocessing targets load. Edition 1 keeps the blanket refusal, since there everything runs.

Tests confirm both directions:

  • A read key that collides really is overwritten. With the check disabled, data comes back at k1 = 0.9 instead of 0.3.
  • An unread key is never scored.

An unused condition that completes a scored key now gets a hint: every experiment runs under every condition of its model unless nothing reads the result.

2. A pre-equilibration's hidden phase output overwrote a real experiment (older; silent)

A pre-equilibrated experiment x also writes x_preequil. An experiment really named x_preequil had its output replaced. With steady-state data this was silent: its term was 987.654, the phase's steady state, where it should have been about 0. The collision check now counts every output an action writes, registered or not, so that configuration is refused in either order, naming the phase.

3. Unanchored legacy mutant-file match (older; silent)

config.py's re.match(f'.*(?={name})', ename) let xbc.exp under mutant b, with actions x and xbc, load and be scored against the unmutated action xbc. The file stem is now resolved from the model's (action, mutant) pairs and must belong to this mutant; otherwise the configuration is refused. xb.exp and xbcb.exp still load.

4. Duplicate action suffixes

Duplicate suffixes are now refused at load. This covers a legacy time_course key next to experiment: e1, simulate plus parameter_scan on one suffix, and a time_course reusing a model-file suffix. Two kinds stay valid:

  • a continue=>1 that resumes the run before it;
  • two actions of the same kind in the model file, where BNG2.pl keeps the later output (Miller2025 MEK1_KO.bngl does this).

output_pairs now keeps the last action too, so multiple shooting agrees with execute.

Tests and sweeps

  • New tests: 18 in tests/test_output_key_resolution.py, plus the fix(gradient): resolve each output from the (action, condition) pair that produced it; a no-column route requests none (#850, #1002, #833) #1004 reviewer's tests/test_output_keys_review_1004.py. Its formerly-xfail test now passes.

  • Before this change: 8 of the new tests and the reviewer's xfail fail on dd4c6a3a.

  • Both bngsim builds: the config, edition, emit-pruning, pre-equilibration, steady-state, shooting, gradient-oracle, PEtab and am test files all pass.

  • Sweeps: four load sweeps match main exactly:

    Sweep Configs Load
    Examples 553 549
    BNGL-Models jobs 102 78
    PEtab imports 24 19
    PEtab benchmarks 5 5

    The failures are the same configs failing with the same messages as on main.

ruff and tools/changelog.py check are clean. ADR-0153 has a new item 20.

After independent review (a5c1ea7, 70efedc)

The first review found a silent regression and a false refusal. Both are fixed, along with three older items the PR text had claimed handled:

The second review found two more problems, fixed in 70efedc:

  • A continue=>1 with an explicit t_start later than the model time was appended time-shifted (B(7) came back as the closed-form B(5)). It is now refused, as BNG2.pl refuses it.
  • Multiple shooting falsely refused an unlabelled resetParameters().

The reviewer's tests are committed. This PR conflicts with #1005 in net_model.py, and #1005's new data-on-the-grid check must learn about appended continue chains. #1005 lands first; this branch will then merge main and resolve both.

…ter of a key (#850 follow-up)

Follow-ups from the independent review of #1004.

1. False refusal. An edition-2 model runs a pair only when its key is read (the emit set,
   #484), so two pairings nothing measures that spell one key (Egf under TnfInh and EgfTnf
   under Inh, both EgfTnfInh) never run and overwrite nothing; #1004 refused them. Edition 2
   now refuses a collision only on a key something reads: a data key, an experiment's own
   output, a constraint's home or cross-reference, a postprocessing target (the check runs
   again once those load), and every key of an action no experiment synthesized, which runs
   unpruned. With the check switched off, a refused key really is overwritten (data comes back
   at k1 = 0.9 instead of 0.3). Edition 1 keeps the blanket refusal. A pairing the user never
   asked for (an unused condition completing a scored key) now gets a hint saying why it runs.

2. Silent overwrite by a pre-equilibration phase. A pre-equilibrated experiment x stores its
   unmeasured phase under x_preequil; an experiment named x_preequil was scored against it
   (987.654 where about 0 was right for steady-state data). The check now counts every output
   an action line writes, registered or not, so this is refused, naming the phase.

3. Edition-1 mutant data files. re.match('.*(?=b)') accepted xbc.exp under mutant b and
   scored it against the unmutated action xbc. The file stem is now resolved from the model's
   (action, mutant) pairs and must be this mutant's output; otherwise the file and mutant are
   named in a refusal.

4. Duplicate action suffixes. Two actions registering one suffix (a legacy time_course key
   reusing an experiment's name) kept whichever ran last, while multiple shooting segmented the
   first. Refused at load, except what BNG2.pl itself does with a model file: a simulate with
   continue=>1 resuming the run before it, and two model-file actions of one kind, the later
   one's output kept (the Miller 2025 MEK isoform models). Every backend keeps that last
   output, and output_pairs now keeps the last action too.

The reviewer's test file is added with its xfail marker removed. Docs, ADR-0153 item 20 and
the changelog are updated. Every example config, BNGL-Models job and PEtab import in the
review's sweeps loads exactly as on main.
… refuses fed runs (#850, #1009, #910)

Follow-ups from the independent review of #1011.

F1. A constraint reads an output through its enforcement variables (at X.obs=v, split at,
between X=v1, Y=v2) and through a dotted reference from another model's constraint set; the
reads set counted neither, so two pairs spelling a key only such a read used were admitted, and
on BNG2.pl (or a non-separable bngsim model) the constraint scored whichever ran last (1.30079
where 0.2611 was right). _constraint_ref_suffixes reads all nine attributes, and the consumed
set and the emit set both collect dotted references from every constraint set, gating only the
home suffix by model. The emit set also no longer prunes the pair an at-variable reads on a
separable model (#484), which failed at the first evaluation.

F2. A suffix-less model-file simulate counted as a second writer of time_course whatever its
order. It now counts only when the bngsim backends may run the model and it comes after the
last registered writer of that name; BNG2.pl writes it to <prefix>.gdat. The <name>_preequil
phase still counts unconditionally.

F3 (#1009). BNG2.pl appends a continue=>1 run to the output of the same suffix; the bngsim .net
path kept only the continued segment (an objective of 6159 where about 0 was right with
ind_var_rounding = 1). It now appends the rows and the forward sensitivities, dropping the
repeated start row, and refuses a continued run whose columns or sensitivity request differ or
which starts before the run it continues ended. The change is local to how the continued run is
stored. A network-free shared-suffix continue is refused at load: NFsim cannot continue.

F4 (#910). Multiple shooting segments a .net run from the model's own state, replaying no
earlier action, so it now refuses a scored run that an earlier simulate, scan, setParameter,
setConcentration or restored snapshot feeds with no reset in between, naming the action. That
covers two model-file simulate lines of one suffix (execute keeps the second, from the first's
end state) and the .net pre-equilibration protocol of #910.

F5. The docs and changelog said unread pairs are never run; they may run (BNG2.pl, RoadRunner,
job_type = check, a non-separable bngsim model), but nothing reads them.

The review's test file is added; its strict-xfail continue test now passes, so that marker is
removed. ADR-0153 item 20 and the changelog are corrected, with a fragment for #1009.
…ime; ms seeds the default parameter slot (#1009, #910)

Second review of #1011.

R2-1. A simulate with continue=>1 and an explicit t_start other than the current model time
carried the state where the last run ended and labelled it t_start, so the appended rows were the
trajectory shifted in time (B at t = 7 came back as B(5) = 4.8209) and were scored. BNG2.pl
aborts this ("t_start must equal current model time for continuation"). _prepare_simulate_run now
refuses it, comparing at 15 significant digits and naming the action and both times; the
network-free bridge keeps the model time and refuses the same. The bridge test that pinned the
old behaviour (t_start = 25 after a run ending at 50) now expects the refusal, and a continuation
from the model time still runs.

R2-3. The multiple-shooting fresh-start gate refused an unlabelled resetParameters() with no save
before it. The backend's default parameter slot holds the parameters the run was given, so the
gate now seeds it the same way.

Adds a changelog fragment for #910 and the review's updated test file. Its two sample_times-chain
cases pass on this branch, which does not yet carry #1005's grid check, so they are not marked
xfail.
wshlavacek added a commit that referenced this pull request Oct 15, 2026
…pshots

R1: the load-time check of data against rows judged every literal
sample_times writer of a suffix. execute keeps the last writer's output
(a run that does not continue writes the suffix afresh, in BNG2.pl too),
so valid jobs were refused (a later step grid or list, a continue=>1
chain) and data only an overwritten action wrote got through. The check
now judges the kept output only (grid.kept_outputs, kept_output_rows):
skipped for a continue=>1 chain, a scan, or rows not known at load, and
judging a kept literal step grid too (matched to the objective's
tolerance; not under ind_var_rounding = 1 or a time_error fit).

R2: stop_if on a parameter_scan or bifurcate was ignored on the bridge,
so each point was read at t_end. Each independent or continuation point
now runs on the scan's own output grid with the stop condition and takes
the stop row, as BNG2.pl's simulate at each point does. A steady-state
scan's stop_if is not the bridge's (auto hands it to BNG2.pl); a
carried-state scan with stop_if is refused when it runs.

R3: the parity claims say where BNG2.pl's stop row can differ (it checks
the condition against CVODE's internal state on an irregular grid).

R4: a simulate_protocol model refused under bionetgen (or without
bngsim) is given the simulate_protocol remedy, not "set bngsim".

R5: a snapshot keeps the stopped-state flag of the state it saved, so a
gradient run or scored scan continuing from a stopped state is refused
naming the stop, not a missing pre-equilibration seed.

R6: the changelog says that data times with more than 15 significant
digits are now compared as BNG2.pl writes them.

The note on a continued run's start row no longer depends on the bridge
writing it (#1011).
@wshlavacek
wshlavacek merged commit 31ead5f into main Oct 16, 2026
8 checks passed
@wshlavacek
wshlavacek deleted the fix/1004-review-followups branch October 16, 2026 00:58
wshlavacek added a commit that referenced this pull request Oct 16, 2026
Conflict in BngsimModel._execute_actions after _run_prepared_simulate:
the run settles first (_settle_after_run: the clock at the last row,
the stop_if row put back, #1010), then #1011's block appends a
continue=>1 run to the output of its suffix (#1009).

The load-time check of data against the kept output's rows now judges
a continue=>1 chain on the union of its runs' grids: kept_outputs
follows the model's clock from 0 through every run whose rows are
known, and a continued run's rows start on it. A chain is judged only
where BNG2.pl and the bridge write the same rows, each continued run
starting where the run before it on the suffix ended; a continued run
whose suffix has no output yet, or that starts after that output ends,
writes its start row on the bridge alone, and a scan moves BNG2.pl's
clock and not the bridge's. With bngl_backend = bionetgen a line with
no suffix writes no suffixed output, as in #1011's ambiguity check.

Two #1011 tests met #872's refusals and are updated:
- a network-free continue=>1 is refused at load whatever its t_start,
  as BNG2.pl's simulate_nf refuses it (BNGAction.pm 906-909);
- with the ambiguity check off, the suffix-less warm-up's rows
  (0..3) now refuse the data at 4..10 at load; the run that shows the
  overwrite switches that check off too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment