Skip to content

convert: carry an assignment-rule species through cBNGL instead of refusing it (#1400) - #1401

Merged
wshlavacek merged 5 commits into
lanl:mainfrom
Montana:fix/1400-rule-species-refusal-reason
Nov 1, 2026
Merged

wshlavacek merged 5 commits into
lanl:mainfrom
Montana:fix/1400-rule-species-refusal-reason

Conversation

@Montana

@Montana Montana commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Closes #1400: both items, the wording and the carried column.

What was wrong

bngl_capability_report refused every model with a species an SBML assignment rule sets, saying "the flat .net round-trip cannot carry the rule ... (GH #205/#223)".

The fix

Item 1, the wording. The note and its comment state the real mechanism, the stale citations are gone, and the test module's docstring and refusal row are corrected.

Item 2, the column is carried, as the issue sketches:

  • Writer. A rule species' molecule is named __ar_<rule> (AR_MOLECULE_PREFIX), the name of the function or observable that carries its rule: @comp1:$__ar_B() beside B(). _molecule_names takes the carriers, so the writer and the round-trip's aligner (_name_aligned_perm) build the same names. A species whose mark would collide with an existing name is left unmarked and keeps the refusal.
  • Reader. _ar_report_map_from_net pairs a fixed @comp::__ar_X() with the function or non-identity observable X. An unmarked fixed species keeps the exact-name rule it had: a BNGL species name is a pattern, and only the mark says it is a rule's. An unfixed marked species is not paired.
  • Gate. sbml_to_bngl(validate="bng2") now also runs _ar_report_delta, which gains back_names and back_perm because BNG2.pl's names and order are not the source's. The report carries max_ar_report_delta, and rhs_faithful requires both deltas, as on the .net path. roundtrip_deltas returns all three; roundtrip_rhs_delta keeps its signature.
  • Policy. A marked species is a bngsim_only note, warned and never refused, as net writer: warn when a rule species is written so only bngsim reads it as meant (#1274) #1345 made it on the .net path, because BNG2.pl's own run still holds the column. The docs say so (interchange.md).
  • New refusal. A model with no reactions is now refused by name. BNG2.pl's generate_network aborts on one ("Nothing to do: no reaction rules defined"). BIOMD0000000566 and 567 are rule species alone: refused for those before, they would now be written as files that do not build.

Measured

The issue's models, write_bngl → BNG2.pl generate_network → Model.from_net, against Model.from_sbml (t = 0-5, rtol 1e-10):

model rule column, main this branch
B := 2*A, C reads B held at 20 (source 1.64) 1.64169998, every column to 1e-9
B := A*A held at 100 exact
$B consumed by J3: B -> D; k3*B held exact
B := 2*A in a compartment of volume 2 held exact

The corpus: every model under rr_parity/models with a rule species (289), through sbml_to_bngl(validate="bng2", strict=False):

  • 175 are no longer refused for their rule species.
    • 174 pass the gate: the rule column within 6.8e-14 and the right-hand side within 6.8e-11.
    • 1 is refused by the gate: BIOMD0000000664 reports g2_0 as 1 at t = 0 in the source and 0 in the reload, and both agree afterwards. Under strict it is refused, as it should be.
  • 80 are still refused for other reasons, none of them for a species that could not be marked.
  • 34 do not load or build, as on main: 26 under-specified, 6 model errors, and the two reaction-less models above, which this PR now refuses up front.

Tests

test_bngl_rule_species.py (11), every one failing on main:

  • the species is written as __ar_B beside B() and warned, not refused;
  • a species that cannot be marked is still refused;
  • a model with no reactions is refused;
  • the reader pairs only a marked, fixed species: not an unmarked $Y() beside Y(), nor an unfixed __ar_Z();
  • the column check reads a reordered reload in its own order;
  • through BNG2.pl: every column, the rule's included, comes back on all four of the issue's models; the gate checks the column and passes; and BNG2.pl's own simulate still holds it (.cdat: 20.0, the others to 1e-6). That last one flags the bngsim_only note as stale if BNG2.pl ever carries the column.

test_sbml_to_bngl.py:

  • test_validate_bng2_gate patches the new seam and adds a forced rule-column divergence, which raises as an RHS one does;
  • BIOMD0000000009 leaves test_refuses_fail_loud, since it is no longer refused for this.

Mutations

mutant fails
N1: the reader ignores the mark 6
N2: the writer does not mark 7
N3: the aligner without the marks the gate test
N4: the gate ignores the column test_validate_bng2_gate
N5: the column check ignores back_perm the reorder test
N6: the mark ignores collisions the cannot-mark test
N7: the reader pairs an unfixed marked species the reader test

Checked

  • Full suite: 16,849 passed, 138 skipped. pre-commit run --all-files and sphinx -W are clean.

Not in this PR

  • BNG2.pl's own run still holds the column (its .gdat carries the rule's value with print_functions=>1). Only bngsim reads the pair back; that is the bngsim_only note.
  • BIOMD0000000664's t = 0 report of an alias rule (g2_0 := g2): the gate refuses it, and why the source reports 1 there is a question of its own.

Montana and others added 5 commits October 29, 2026 19:10
…fusing it (lanl#1400)

write_bngl refused every model with a species an SBML assignment rule sets,
saying the round trip cannot carry the rule. It was carried, as the function
its rate laws read; only the species' own column was lost, held by BNG2.pl
and by bngsim reloading BNG2.pl's .net, whose pattern name never matched the
rule's. The writer now names the molecule __ar_<rule>, the reader pairs it
with that function, and validate="bng2" checks the column too
(max_ar_report_delta). It is a bngsim_only warning, as on the .net path. A
model with no reactions, which generate_network aborts on, is refused.
…e a changed species count by name

What the review found on top of the PR's change, which round-trips every
edge case tried (amount-valued at V=1, alias and sum rules, a rule nothing
reads, a chain, a parameter-only rule, two compartments, time):

- The cBNGL corpus harness (parity_checks/rr_parity/cbngl_bng2_validate.py)
  says it measures "exactly as the production sbml_to_bngl(validate="bng2")
  gate does", but still called roundtrip_rhs_delta, which drops the column
  check. The 175 models this PR newly accepts were certified on a probe that
  cannot see their column (dy/dt = 0 on both sides), and BIOMD0000000664,
  which the gate refuses, came out "faithful". It calls roundtrip_deltas now,
  with an ar_report_mismatch status.
- roundtrip_deltas promises a ConversionError whenever faithfulness cannot be
  established. A reload with a species count the source does not have raised
  an IndexError from the new column check (yb[back_perm] = y), ahead of the
  right-hand side's ValueError, so sbml_to_bngl(validate="bng2") leaked it
  and the harness, which catches ConversionError only, crashed instead of
  writing the species_mismatch row it has a branch for. _name_aligned_perm
  refuses the count by name.
- ConversionReport.summary() printed the RHS delta only: a --gate run refused
  for the column alone printed "max|Δ dy/dt| = 0.00e+00" and exited 1. It
  prints max_ar_report_delta too.
- Stale text: the writer's comment still said these species are refused and
  the gate cannot see them; _ar_report_map_from_net's docstring said a
  BNG2.pl .net never pairs; test_sbml_to_bngl's docstring listed the species
  among the refusals; the sbml_to_bngl docstring, the --gate help and the
  interchange guide called the gate RHS-only; the bngsim_only note said
  "function" where an alias or sum rule is a Molecules observable.
- BIOMD0000000664's t = 0 difference, left open in the PR, is BNG2.pl
  rewriting the pulse time()>=0.0 to time()>0.0, the artifact _bng2.py
  documents; the column really reads 0 for 1 in row 0, so the refusal is
  right. The changelog says why.
- test_bngl_rule_species's BNG2.pl helper left a mkdtemp directory behind per
  test; it writes under tmp_path.

Three tests, each failing on the PR's head: the harness's status for a
column-only, an RHS-only and a clean delta; the summary line; the refused
species count.
…tation the column check uses

_name_aligned_perm returns perm[i] = the reload's index of source species i.
The new column check reads the reload at the same state the right way
(yb[back_perm] = y), but _rhs_delta, the gate's other half, fed the reload
y[perm] and read its derivative back as bb[perm] = b: the inverse, twice.
For a reorder that is its own inverse the two agree, and the PR's reorder
test used one ([2, 1, 0]), so it could not tell them apart.

On a reload in the 3-cycle order [C, B, A] for the source's [A, C, B]
(perm [2, 0, 1]), a faithful network read |Δ dy/dt| = 1.0 through
_rhs_delta and 0.0 through the column check. _rhs_delta now builds
yr[perm] = y and reads b[perm]. BNG2.pl keeps the species block's order for
this writer's ground patterns, so the round trips measured so far had the
identity permutation and do not move (BIOMD0000000009, 321, 664,
MODEL1112110002 as before).

The reorder test is that 3-cycle now, and pins both probes and
_name_aligned_perm's direction: through the permutation each agrees, through
its inverse each does not. It fails on the previous _rhs_delta.

Left as is: the column check probes at t = 0, where BNG2.pl's >= -> >
rewrite moves BIOMD0000000664's pulse, while the right-hand side probes only
at t > 0. A derivative at one instant leaves the trajectory alone; the
reported column at t = 0 is row 0 of every run, and it reads 0 where the
source reads 1, so that refusal stands.
The suite legs have no BNG2.pl, so test_bngl_rule_species.py's six round
trips through it skip there, and BIOMD0000000009 left test_refuses_fail_loud,
a corpus case those legs skip: 5 more on every leg (ubuntu 228 -> 233,
lapack 202 -> 207, macos-15 190 -> 195; main at 0c1732d, run 41309929676).
ubuntu-latest crossed the 230 ceiling. Raised by that 5, as for lanl#1215's.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cBNGL writer: the assignment-rule species refusal is right, but its reason is wrong (the rule IS carried; only the species' column freezes)

2 participants