Repository navigation
Conversation
…mparator
compare_trajectories intersected the two column sets, so a leg that had
silently dropped an observable -- or invented one -- compared clean at
max_rel_err=0.0. That is the failure a fixed-seed NFsim gate is most often
asked about: a population ceasing to be reported is exactly how a
seed-handling regression presents, and the comparator scored it a perfect
match.
Adds an explicit column-set policy rather than changing the default:
COLUMNS_INTERSECT (default, unchanged) compares only shared observables.
Kept for callers whose column set is an input, not a
claim -- test_parity_ode.py compares a BNG2 .gdat
against a BNG3 one, two independent writers.
COLUMNS_EXACT requires the same set of observable names first, and
names the ones that differ. Order is deliberately not
part of it: values are located by name.
Every caller that makes a trajectory-identity claim moves to exact:
the tier-P sweep child record, the fixed-seed / direct-vs-XML / site-state
gates in test_parity_nfsim.py, the three native-NFsim comparisons in
test_parity_nfsim_seed.py, and the synthetic legs in
test_stochastic_comparator.py. The full caller/mode table is in the
compare_trajectories docstring; adding a caller without a mode gets
intersect, so the blind spot is now a stated choice rather than a default.
test_parity_nfsim_seed._identity_report is folded onto the shared
comparator: its value, shape and time-grid checks now delegate to
compare_trajectories(columns=COLUMNS_EXACT, rtol=0.0, atol=0.0), and it
keeps only the ordered-column check the shared comparator deliberately does
not make. Two comparators remain, not three.
Separately, docs/CI_PARITY.md:38 and docs/BNG3_INTEGRATION_PLAN.md:39 have
described a four-model fixed-seed endpoint set since 2026-09-15 while
parity.yml's node hard-coded model_name = "simple_system". The gate is now
parametrized over corpus.tier_nf() rather than the documentation narrowed:
all four were measured against native NFsim at rtol=atol=0 first.
Measured, all four tier-NF models, seed 7, t_end 50, n_steps 50,
construction_path=direct, native NFsim RuleWorld/nfsim @ c51c7a3,
sha256 093707031f70e0c376179e1d8bf89ab1373b3132b6211d7d9759f1d646c4ce9e,
engine built in this worktree at 9f840a5,
sha256 887c81252c567073db2c1029d5f37805dd82c2a475ffb8db4583797eec5de8a6:
localfunc intact+exact ok=True max_rel_err=0.0
motor intact+exact ok=True max_rel_err=0.0
simple_system intact+exact ok=True max_rel_err=0.0
tlbr intact+exact ok=True max_rel_err=0.0
Fault injection on the same real native .gdat output, read through the
shared parse_gdat (the reader the native-NFsim and BNG2 oracles also feed):
localfunc dropped intersect ok=True max_rel_err=0.0
dropped exact ok=False max_rel_err=inf
"observable column sets differ: only in ref
['AB_motif'], only in test []"
phantom intersect ok=True max_rel_err=0.0
phantom exact ok=False max_rel_err=inf
"observable column sets differ: only in ref [],
only in test ['Ophantom']"
motor / simple_system / tlbr: identical shape, naming MotCCW / Xtotal /
Lfree as the dropped observable. The intersect leg is the defect; the
exact leg is the fix.
What the two halves of this rest on, because they are not the same:
CONSUMER-BACKED. parse_gdat is the shared reader for every oracle .gdat
in this tree, so the values above are read by the same code the
native-NFsim and BNG2 oracles feed, and the native leg is an
independently built binary, not BNG3's embedded NFsim.
NOT CONSUMER-BACKED. The column-set policy was, before this PR, backed by
nothing: no caller asserted it, which is why the blind spot survived. The
evidence for exact mode is this PR's own fault injection plus
tests/validation/test_compare_trajectories.py -- a test and an assertion
written by the same change whose behaviour it pins. That is a real check
and it does not independently corroborate the oracle the way parse_gdat
does.
not_claimed: no test was run against the BNG2 oracle leg. BNG2.pl is not
present on this host, so test_parity_ode.py skips and the one caller left
on intersect is verified by its contract and its new unit test, not by an
end-to-end comparison. Its behaviour is unchanged either way.
|
Superseded after six-file residual audit and merge of PR #91 at exact head 9aff259, remote merge b08e1be. The replacement carries exact/intersection column policies, duplicate-column rejection, strict oracle callers, the two previously uncovered default-intersection/time-only contracts, and corrected four-model seed documentation. Redundant endpoint expansion, explicit exact mode on identical stochastic fixtures, and a behavior-neutral helper refactor were not duplicated; the working child native-extension path was preserved. Final-head hosted checks were 41 successful and 5 skipped; fresh own-source comparator tests passed 12/12 and four seed cases collected. Original author branch preserved. The separate michment fixed-cohort strict failure remains unresolved. |
The defect
tests/validation/compare.py::compare_trajectoriescomputed its shared set as an intersection:So a leg that had silently dropped an observable, or invented one, was compared only on the columns both legs still had — and scored a perfect match. That is the defect class this comparator is most likely to be asked about: a population ceasing to be reported is precisely how a seed-handling or observable-emission regression presents, and
max_rel_err = 0.0reads as "identical trajectory".Failing-first, verbatim
Fault injection on synthetic legs (ref columns
['time','Otot','RD_Ba']),rtol=0.0, atol=0.0:Same injection repeated against real native-NFsim
.gdatoutput for each tier-NF model, read through the sharedparse_gdat, both policies side by side (seed 7, t_end 50, n_steps 50,construction_path=direct):Passing-after
Same instrument, same real legs, intact — all four tier-NF models at
rtol=atol=0:Pinned permanently in
tests/validation/test_compare_trajectories.py(7 tests), which states both policies — including the intersect blind spot, so the default cannot drift back silently.What changed
An explicit
columnsparameter. The default is unchanged.COLUMNS_INTERSECT(default)test_parity_ode.py::test_ode_parityCOLUMNS_EXACTAn unknown mode raises
ValueErrorrather than defaulting.Every caller, and the mode it now uses
exact — trajectory-identity claims:
test_parity_nfsim.py::_SWEEP_CHILDdirect-vs-XML sweep record (a lost observable previously recorded ascomparedat 0.0 in the tier-P report)test_parity_nfsim.py::test_nf_fixed_seed_direct_matches_native_at_final_endpointtest_parity_nfsim.py::test_nf_ast_direct_matches_xmltest_parity_nfsim.py::test_nf_seed_site_state_is_resolved_by_nametest_parity_nfsim_seed.py::test_fixed_seed_reproduces_the_same_trajectory_on_both_legstest_parity_nfsim_seed.py::test_seed_state_order_matches_native_nfsimtest_parity_nfsim_seed.py::test_an_family_seed_state_order_matches_native_nfsimtest_parity_nfsim_seed.py::_identity_report(delegates; keeps only the ordered-column check)test_stochastic_comparator.py, 2 sites (fixed known column list)intersect — one deliberate caller:
test_parity_ode.py::test_ode_paritycompares a BNG2-produced.gdatagainst a BNG3-produced one. Two independent writers: the column set is an input, not a claim, and the committed reference's observable block need not enumerate everything the engine emits. It is a numeric comparator over a column set allowed to vary and makes no claim about which observables exist. Unchanged.The full table also lives in the
compare_trajectoriesdocstring, so it travels with the code._identity_reportfolded, not duplicatedtest_parity_nfsim_seed._identity_reportno longer carries its own value/shape/time comparison. It delegates those tocompare_trajectories(..., columns=COLUMNS_EXACT, rtol=0.0, atol=0.0)and keeps exactly the one check the shared comparator deliberately does not make: ordered column identity. Two comparators remain, not three.Which half of the module is consumer-backed, and which is not
Adjacent to the numbers, because they are not the same kind of claim.
Consumer-backed.
parse_gdatis the shared reader for every oracle.gdatin this tree. The trajectory values compared above are read by the same code that feeds the native-NFsim and BNG2 oracles, and the native leg is an independently built binary (RuleWorld/nfsim @ c51c7a3, sha256093707031f70e0c376179e1d8bf89ab1373b3132b6211d7d9759f1d646c4ce9e) — never BNG3's embedded NFsim. That half has a consumer outside the file.Not consumer-backed. The column-set policy. Before this PR nothing asserted it, which is exactly why the blind spot survived. Its evidence is this PR's own fault injection plus a test written by the same change whose behaviour it pins. That is a real check and it fails on the defect — but it does not independently corroborate the oracle the way
parse_gdatdoes, and it should not be read as if it did.The adjacent gap:
simple_systemvs the documented four-model setdocs/CI_PARITY.md:38anddocs/BNG3_INTEGRATION_PLAN.md:39have described a four-model fixed-seed direct/native endpoint set since the 2026-09-15 checkpoint.parity.yml's node —test_nf_fixed_seed_direct_matches_native_at_final_endpoint, the onlytests/validation/module any CI job runs — hard-codedmodel_name = "simple_system". The doc described a gate that measured one model.The code was changed, not the doc.
nfsimParitymeasured all four against native NFsim atrtol=atol=0before the parametrization went in (their measurement, reproduced here on the worktree engine in the tables above), so the documented claim was the truthful side and the gate now measures what it says. Softening a correct claim to match an under-tested gate would have been the wrong direction.The fixed-seed gate is now parametrized over
corpus.tier_nf()(simple_system,tlbr,motor,localfunc) andparity.yml's-kselector is unchanged.Verification
tests/validation/test_compare_trajectories.py+test_stochastic_comparator.py+test_compare_net.py: 30 passed.tests/validation/test_parity_nfsim.py+test_parity_nfsim_seed.py -m "nf and not slow"(the selectionparity.ymlruns, extended to this branch): 21 passed, 0 skipped, 0 deselected-by-oracle.test_nf_fixed_seed_direct_matches_native_at_final_endpoint[{localfunc,motor,simple_system,tlbr}].Artifact provenance. The engine is not the shared
build/cpp:sha256 887c81252c567073db2c1029d5f37805dd82c2a475ffb8db4583797eec5de8a6, built in this worktree at9f840a5. The shared build atBNG3/build/cppis 15cpp/commits behindmain; the numbers above do not describe it. The oracle issha256 093707031f70e0c376179e1d8bf89ab1373b3132b6211d7d9759f1d646c4ce9e.not_claimed
BNG2.plis not present on this host, sotest_parity_ode.pyskips. The one caller left onintersectis verified by its stated contract and by the unit test that pins the policy — not by an end-to-end comparison. Its behaviour is byte-for-byte unchanged either way; I did not switch it and then discover it needed switching._SWEEP_CHILDtoexactmeans a model whose XML and direct legs report different observable names now recordsmax_rel_err=infwith the noteobservable column sets differ, where it previously recorded0.0andcompared. That is the intended change and it is the correct reading — but the sweep isslow, runs in no CI job, and I did not run it. Whether any committed tier-P model trips it is unmeasured by me.@TierParityExpandis re-measuring the sweep at the full horizon and has been told this change is mine.exact. Ordered identity lives only in_identity_report, with three callers inside one file. Promoting it into the shared mode would have madeexactreject a leg that reports the same observables in a different order — which this comparator, which aligns by name, handles correctly. That is a deliberate boundary, not an oversight.Files
tests/validation/compare.py—COLUMNS_INTERSECT/COLUMNS_EXACT, the exact-mode gate, the caller/mode table,ValueErroron an unknown modetests/validation/test_compare_trajectories.py— NEW, 7 tests pinning both policiestests/validation/test_parity_nfsim.py— 4 call sites to exact; fixed-seed gate parametrized overcorpus.tier_nf()tests/validation/test_parity_nfsim_seed.py— 3 call sites to exact;_identity_reportfolded onto the shared comparatortests/validation/test_stochastic_comparator.py— 2 call sites to exactdocs/CI_PARITY.md— one paragraph naming the four-model set and the exact modetests/validation/test_parity_ode.py— deliberately unchanged