Repository navigation
perf(sbml): keep the GH #88 step bound only where the roots provably cannot reach (#274) - #277
Merged
Merged
Conversation
…cannot reach (#274) #262 measured that no corpus model's answer the periodic step bound changes, and pre-committed to the consequence: narrow it to the shapes roots cannot reach, or retire it. This narrows it. Retiring would have been wrong -- see the fixture below, which is the first thing in the suite to witness the bound's necessity since #259 removed the old one. The bound and the #72/#231/#259 roots address the same hazard -- an integrator step spanning a schedule edge -- by different means. Where a root already forces the stop, max_step is pure cost: it shortens every step over the whole horizon, not just the ones near an edge. ## The predicate Value-position reachability from the ODE RHS through the assignment rules, pruning every piecewise condition that IS a registered root. A time-dependent floor/ceiling/modulo influences the RHS two ways and only one of them is rooted: * through a piecewise CONDITION -- `exposure = piecewise(D, frac < w, 0)`. A root on that condition forces a stop exactly at the edge, so the branch cannot be stepped over and the bound adds nothing. Pruned. * as a VALUE -- `dose = D * frac`, or `k = k0 * i` with `i = floor(time/24)`. The sawtooth jump is an RHS discontinuity in its own right, at an instant no relational brackets. The bound is what keeps a step from spanning it. Corpus: kept on 10 of the 25, dropped on 15, cutting 1,015,702 internal steps to 519,472 (-49%) with no answer moving -- MODEL0406553884 -60%, MODEL0406793751 -44%, MODEL0847869198 -40%, MODEL1708310001 -29%. That last one is the check that matters. It is the model #259 gave the roots to, and it has an exact segmented oracle (953.07) confirming they resolve its schedule. The predicate drops its bound without being told, and the existing oracle test -- unchanged in its numbers -- now passes with no bound at all. ## Why not a root count The tempting predicate, "apply the bound only when the model registered no roots", is unsound, and measurably rather than theoretically so. Three corpus models are rooted yet carry a periodic disc node reaching the RHS ungated: BIOMD0000000577, BIOMD0000000589, MODEL1006230027. The clearest is BIOMD0000000589, 15 of whose 16 schedule conditions read `i == 0`, `i == 1`, ... on `i := floor(time/24)` -- equalities, which the inequality-only emitters cannot root, on a step function that genuinely moves with time. `_EQ_GATED_PULSE` makes that concrete and is the acceptance case #274 asked for, since the corpus has the shape but no instance where it costs an answer. Its pulse is gated by an equality on a floor while an unrelated `time < 5` threshold IS rooted, so the model has roots and still needs the bound: bound on, rtol 1e-9 y(10) = 36.788 vs exact 36.788 1,891 steps bound on, rtol 1e-11 y(10) = 36.788 vs exact 36.788 2,451 steps bound off, rtol 1e-9 y(10) = 182.212 +395% 294 steps bound off, rtol 1e-11 y(10) = 182.212 +395% 463 steps Tol-stably wrong is the signature of a pulse never sampled at all. A root-count predicate returns 182.212 here. ## Measurement notes, for whoever extends this The reachability walk is deliberately ONE pass carrying context down, not two passes compared afterwards. libsbml returns a fresh Python wrapper from every getChild() call, so id(node) is not stable across traversals and any "was this node also seen over there" test silently answers no -- which reads as full root coverage and drops the bound everywhere. A first cut of the measurement did exactly that and reported 2/25 kept, including six zero-root models, which is impossible on its face and is what exposed it. Pruning uses only the time scan's emitters, so a piecewise gated on integrated state is treated as ungated and keeps the bound. Conservative in the safe direction: the cost of a needless bound is steps, the cost of a wrongly dropped one is a silent wrong answer. `_periodic_time_disc_max_step` keeps its pre-#274 behaviour when called without the registered-condition set, so a caller that has not run the root scan cannot drop a bound it has no basis to drop. ## Test churn Model._periodic_disc_max_step is now None for the 15 dropped models, so four tests asserting "a bound is derived" needed rewriting. Each inverted rather than weakened, and the correctness assertions they carried are untouched and still pass -- which is the evidence the drops were safe: - the derivation arithmetic (window/3) moved to _EQ_GATED_PULSE, a fixture that still earns a bound; - _floor_dose_sbml now asserts the opposite -- its edge is a rootable inequality, so #274 declines -- while its closed-form tests run unbounded and still hit the oracle; - MODEL1708310001's oracle test asserts `is None` and keeps its 953.07; - MODEL0847869198 compares default-unbounded against a forced 0.05 ceiling, well under the 0.333 the pre-#274 loader derived, so the agreement cannot be an artifact of too coarse a ceiling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #274.
#262 measured that no corpus model's answer the GH #88 periodic step bound
changes, and pre-committed to the consequence: narrow it to the shapes roots
cannot reach, or retire it. This narrows it. Retiring would have been wrong,
and the fixture below is why — it is the first thing in the suite to witness the
bound's necessity since #259 removed the old one.
The bound and the #72/#231/#259 roots address the same hazard — an integrator
step spanning a schedule edge — by different means. Where a root already forces
the stop,
max_stepis pure cost: it shortens every step over the whole horizon,not just the ones near an edge.
The predicate
Value-position reachability from the ODE RHS through the assignment rules,
pruning every piecewise condition that is a registered root. A time-dependent
floor/ceiling/moduloinfluences the RHS two ways, and only one of them isrooted:
exposure = piecewise(D, frac < w, 0). Aroot on that condition forces a stop exactly at the edge, so the branch cannot
be stepped over and the bound adds nothing. Pruned.
dose = D * frac, ork = k0 * iwithi = floor(time/24).The sawtooth jump is an RHS discontinuity in its own right, at an instant no
relational brackets. The bound is what keeps a step from spanning it.
Corpus: kept on 10 of 25, dropped on 15, cutting 1,015,702 internal steps
to 519,472 (-49%) with no answer moving.
MODEL0406793751MODEL0406553884MODEL1708310001MODEL0847869198MODEL1708310001is the check that matters. It is the model #259 gave the rootsto, and it has an exact segmented oracle (953.07) confirming they resolve its
schedule. The predicate drops its bound without being told, and that model's
oracle test — numbers unchanged — now passes with no bound at all.
Why not a root count
The tempting predicate, "apply the bound only when the model registered no
roots", is unsound — measurably, not theoretically. Three corpus models are
rooted yet carry a periodic disc node reaching the RHS ungated:
BIOMD0000000577,BIOMD0000000589,MODEL1006230027. The clearest isBIOMD0000000589, 15 of whose 16 schedule conditions readi == 0,i == 1, …on
i := floor(time/24)— equalities, which the inequality-only emitterscannot root, on a step function that genuinely moves with time.
_EQ_GATED_PULSEmakes that concrete, and is the acceptance case #274 asked for:the corpus has the shape but no instance where it costs an answer. Its pulse is
gated by an equality on a floor while an unrelated
time < 5threshold isrooted, so the model has roots and still needs the bound.
Tol-stably wrong is the signature of a pulse never sampled at all. A root-count
predicate returns 182.212 here.
Notes for whoever extends this
two passes compared afterwards. libsbml returns a fresh Python wrapper from
every
getChild()call, soid(node)is not stable across traversals and any"was this node also seen over there" test silently answers no — which reads as
full root coverage and drops the bound everywhere. A first cut of the
measurement did exactly that and reported 2/25 kept, including six zero-root
models; impossible on its face, which is what exposed it.
state is treated as ungated and keeps the bound. Conservative in the safe
direction: a needless bound costs steps, a wrongly dropped one costs a silent
wrong answer.
_periodic_time_disc_max_stepkeeps its pre-Narrow or retire the GH #88 periodic step bound: no corpus model needs it, but a root-count predicate is not a sound narrowing #274 behaviour when called withoutthe registered-condition set, so a caller that has not run the root scan cannot
drop a bound it has no basis to drop.
Test churn
Model._periodic_disc_max_stepis nowNonefor the 15 dropped models, so fourtests asserting "a bound is derived" needed rewriting. Each inverted rather
than weakened, and the correctness assertions they carried are untouched and
still pass — which is the evidence the drops were safe:
window/3) moved to_EQ_GATED_PULSE, a fixturethat still earns a bound;
_floor_dose_sbmlnow asserts the opposite — its edge is a rootable inequality,so Narrow or retire the GH #88 periodic step bound: no corpus model needs it, but a root-count predicate is not a sound narrowing #274 declines — while its closed-form tests run unbounded and still hit the
oracle;
MODEL1708310001's oracle test assertsis Noneand keeps its 953.07;MODEL0847869198compares default-unbounded against a forced 0.05 ceiling,well under the 0.333 the pre-Narrow or retire the GH #88 periodic step bound: no corpus model needs it, but a root-count predicate is not a sound narrowing #274 loader derived, so the agreement cannot be an
artifact of too coarse a ceiling.
Verification
Full suite 4,180 passed, 30 skipped, run against this branch rebased onto
#276 and with the C extension rebuilt for its
src/model.cpp/src/model_builder.cppchanges.ruff check/format --checkclean.