Repository navigation
A5: enforce absolute tolerance floors on near-zero reference fields - #211
Open
pdlourenco wants to merge 2 commits into
Open
pdlourenco wants to merge 2 commits into
pdlourenco wants to merge 2 commits into
Conversation
…elds Implements review-v2 plan item A5 (standing rule 1 of ADR-0002, carried forward by ADR-0007, was a review convention: 78 of 113 fields had no atol and 8 held exact or ~1e-16 zeros): - testdata/refAtolViolations.m: a field holding an exact zero, or whose smallest finite magnitude is below 1e-12 of its largest, must carry <key>_atol (Response_real/_imag resolve to Response_*, as in the validator). generate_reference.m refuses to write such a vector; the Octave validator fails it; the Python consumer mirrors the rule (_atol_violations) with a unit test and a per-vector check. - Floors added for the 8 offending fields, each rtol x max|field| rounded up to one significant figure: Response_atol for siso_bt, siso_bt_large_M, siso_btfdr and freqmap_bt; W_atol, Phi_xz_imag_atol, DFT_imag_atol in internals (W_atol also resolves the min-deps 1-ULP failure, plan A9); Frequency_atol in spectrogram (0 Hz bin). - Default-tolerance audit: explicit Frequency_rel 1e-12 for the five vectors that relied on the default (as every other frequency grid), and n_rel 0 for model_order (an integer: exact, matching Python). Tolerance blocks are edited directly, with the generator's matching entries in the same commit (rule 4); a local regeneration reproduces all 32 tolerance blocks byte-for-byte. No payload bytes change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pre-push review: under atol + rtol*|expected| a floor of rtol x max also loosens mid-range entries (~16x on a 0.52 Response_real entry). The new floors match the sibling vectors and the measured cross-engine residual scale -- Response_atol and DFT_imag_atol 1e-12, Phi_xz_imag_atol and the spectrogram Frequency_atol 1e-14 (W_atol 1e-15 unchanged) -- and still pass both consumers. README states the rule accordingly; refAtolViolations errors on a non-numeric output field instead of skipping it (Python already raised). Tolerance blocks re-verified byte-for-byte against a regeneration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 8, 2026
pdlourenco
commented
Oct 8, 2026
pdlourenco
left a comment
Owner
Author
There was a problem hiding this comment.
Review of PR #211 at 658bda3: A5, absolute tolerance floors (stacked on #210)
Posted as a comment review because this session's GitHub identity is the PR author. Verdict: request changes. The floors themselves are sound and no comparison becomes vacuous. Before merge:
- the gate needs a test of its own failing branch (1);
- the clamp-capable
NoiseSpectrumfields need floors (2);- the MATLAB, Octave and validator jobs must actually run on this head (4). None has run yet.
The merge path after #210 is your call (Merge notes).
Method. Two reviewers with fresh context each took one lens: (1) the gate and the floor values, and (2) the 10 changed vectors, provenance and CI. An independent verifier then tried to refute each finding. A separate agent trial-merged all five open PRs and simulated the post-merge bot regeneration.
The work included:
- the Octave 8.4 validator and generator on this head;
- the full Python suite;
- per-field residual measurements against the R2025a bytes in both engines;
- mutations of the gate, the floors and the values;
- a Python 3.10 / numpy 1.22 / scipy 1.8 venv for the A9 W case.
Disclosure: this session wrote the remediation plan (item A5); it did not write this PR.
Should-fix (before merge)
- Nothing tests the MATLAB/Octave gate itself (
testdata/refAtolViolations.m).- It is the only implementation behind both the generator's refusal (
generate_reference.m:960-966) and the Octave structural check (validate_reference.m:69-78), and every committed vector passes it. - Mutation: change line 39 from
[key '_atol']to[key '_rel'], then strip the floors from 6 vectors. The validator still reports32 passed, 0 failed. - Python's
TestToleranceFloorstests only the Python copy and has no NaN/Inf case. Without the non-finite filter,[nan, 0, 1]is no longer flagged, and nothing notices. - Fix: add a
selfTestAtolFloors()besideselfTestNonFinite()(validate_reference.m:39), with these cases:- the Python fixture;
[NaN 0 1]and[Inf 1e-20 1], both flagged;- all-NaN, skipped;
- a ragged cell holding a 0, flagged;
- a char field, raising
refAtolViolations:unsupported.
- Add the NaN/Inf and ragged cases to the Python unit test too.
- It is the only implementation behind both the generator's refusal (
NoiseSpectrumcan be exactly 0 by spec, and the gate then stops the generator. SPEC §2.7 clamps Φ̂_v to 0.- Under Octave's
rng(42)stream,siso_bt'sNoiseSpectrumhas two exact zeros, andwriteJSONerrors on vector 1. The base branch's generator ran all 32 vectors under the same Octave. - That breaks the cloud workflow plan §10.1 relies on ("generator logic can be developed in CLOUD"). The same trip could hit R2025a after any change that lands a frequency on the clamp.
testdata/README.md:116already says to floor every field that can hold a zero. Yet 9NoiseSpectrum-carrying vectors have noNoiseSpectrum_atol; onlybtfdr_vecresandtimeseries_etfecarry one.- Recommended: add
NoiseSpectrum_atol(1e-12, as the siblings use, citing §2.7) to the clamp-capable vectors, in the generator and the committed blocks per rule 4. The time-series vectors return Φ̂_y and can be left out. - Either way, name the engine in the PR body's "generate_reference.m runs with the gate passing".
- Under Octave's
- The README's "measured cross-engine residual scale" isn't backed by a recorded measurement, and "a floor binds only near zero" isn't true for W (
testdata/README.md:55-58, commit658bda3).- Measured here, residuals are ≤ ~1e-14 in Octave and ≤ 5.4e-15 in Python. So the 1e-12 floors sit about two orders above the residual, not at it. That is fine as headroom, but it should be described accurately.
W_atol = W_rel = 1e-15deliberately also loosens W's mid-range entries (20 of 21), absorbing the A9 1-ULP misses. This is correct; the README just doesn't say so.- Fix: reword along these lines: "floors sit about two orders above the observed cross-engine residual (≤ ~1e-14 for the spectral fields) and well below rtol·|typical entry|, so they bind only at near-zero entries. The exception is
W_atol, which also covers W's mid-range 1-ULP differences."
- No MATLAB, Octave or validator job has run on this head. Only the push-triggered Python 3.10–3.14 jobs and Python lint ran, because the base isn't
main.- Retargeting alone won't start them: the
editedevent isn't in any workflow's trigger types. - None of those jobs is a required check, so even once they run they won't block the merge.
- The MATLAB job's generator step is the only place the new
writeJSONgate runs under R2025a; plan §10.3 row A5 asks for that run. - Before merge: after retargeting and resolving the conflicts below, check by hand that these are green:
MATLAB tests & examples, including "Generate cross-language reference data";Octave tests & examples;- both validators.
- Say in the PR body that these have to be checked by hand.
- Retargeting alone won't start them: the
Nits
- A NaN, Inf or negative floor passes the gate. Both gates check only that
<key>_atolexists, and in Octave a NaN atol makes every comparison pass. Python still fails the value test. The hole is in tolerance consumption and predates this PR. Validating each_rel/_atolas a finite, non-negative scalar incompareOutputsand_tolwould close it; it could be a follow-up with A4. - Python's non-numeric error is a side effect of the ragged-list fallback. The message names a character, and a numeric string is accepted. Octave raises
refAtolViolations:unsupported. Raise explicitly in_atol_violations. - The complex-key resolution is written in three places:
refAtolViolations.m:20-24,compareOutputsand_atol_violations. A drift would fail closed. Still, add a pointer comment incompareOutputs, or factor out a shared helper when A4 generalises the complex keys. testdata/README.md:120-121is narrower than CONTRIBUTING rule 4. It still limits hand-edited tolerance blocks to "the same PR that adds the vector". Rule 4 allows any edit made with matching generator entries, which is what this PR relies on. Reconcile the two.- PR body: add the note
testdata/README.md:39-41asks for: the canonical R2025a round trip after merge re-stamps provenance on these 10 vectors, which still sayaee812a, and their tolerance blocks already match the generator.
Merge notes (cross-PR): the 10 vector edits will conflict
- Why it conflicts: every push to
mainre-stamps provenance in all 32 single-line vectors. So once #210 merges (or anything else does), each of the 10 vectors edited here conflicts as a whole file. GitHub will refuse the merge until that is resolved. - The options, your choice:
- (a) Merge #211 into #210's branch first, then merge #210. That gives one regeneration and no JSON conflict, but it widens #210's scope.
- (b) Recommended: merge #210 and wait for the bot commit. Then retarget #211 and merge
origin/maininto it (orgit rebase --onto origin/main bf71afeif #210 was squash-merged).- Resolve each of the 10 files by taking
main's bytes and re-inserting only this PR's flat"tolerance":{…}object. The other agent checked this mechanically: 10/10 files stay valid JSON, withmain's input, output and provenance and this PR's tolerance. Key order and1E-xxformatting already match the generator. - Don't hand-merge payloads or regenerate under Octave, and don't merge #211 while a bot regeneration job is still running.
- Resolve each of the 10 files by taking
- #209: also conflicts with this PR in one hunk of
test_cross_validation.py. The resolution is in the #209 review.
Verified
- Gate rule: it matches the plan's A5 text (an exact zero, or min < 1e-12·max over finite magnitudes). It is keyed on what the comparator actually reads (
Response_real/_imag→Response_atol). - Coverage:
- The base had 8 near-zero fields with no floor and 6 fields on the default rtol. This PR covers all 14. After it: 0 near-zero fields without a floor and 0 on the default rtol, out of 113 fields.
- All 8 F8b / §4.1 rule-1 fields are floored.
- The 10 JSON files: only the tolerance object changed, as pure insertions. Payload and provenance are byte-identical, and the payload bytes are the canonical R2025a bytes from bot commit
7848f20. No locally generated bytes were committed. Rule 4 permits the hand edit. - Generator matches the committed blocks: the generator's tolerance structs reproduce all 32 committed tolerance blocks (keys, order and values). A canonical regeneration therefore yields the same keys.
- No vacuous floor: each new atol is ≤ 4e-13 of max|field|, and only the zero or noise entries fall inside it.
- Threshold margin: every unfloored field has min/max ≥ 1.3e-8, so R2025a ULP drift can't flip the 1e-12 gate on
main. - W_atol: backed by a measurement. Base fails
test_hann_windowon numpy 1.22 / scipy 1.8 at W entries 0.0245 and 0.0545; this head passes 137/137. - Mutations:
- Dropping a floor fails both consumers.
- Perturbing each floored entry by 10× its floor fails the Octave validator for all 7 fields.
- Python checks W and Frequency per part. Response is checked as a complex modulus (|G| ≈ 0.5 at Nyquist), so it isn't vacuous. Phi_xz and DFT are Octave-only until A4, as the hand-off note says.
- Exactness:
n_rel 0is exact in both consumers.Frequency_reltightens from 1e-6 to 1e-12 and still passes, at 2.2e-16. - Tests: Octave validator 32/32. Python 484 passed, 4 skipped, matching the PR.
ruffclean.
Generated by Claude Code
This branch has not been deployed
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.
Summary
Implements plan item A5 of the review-v2 remediation plan (
docs/plans/2026-10-04-review-v2-remediation-plan.md). It also covers the W-tolerance part of A9.The defect: standing rule 1 (every field has an absolute floor) was a review convention only. Per the analysis (§4.1 rule 1, F8b), 78 of 113 output fields had no
atol, 8 of them held exact or ~1e-16 zeros, and 6 relied on the defaultrtol.The gate:
testdata/refAtolViolations.m): a field holding an exact zero, or whose smallest finite magnitude is below1e-12of its largest, must carry<key>_atol. Keys resolve as in the validator:Response_real/_imag→Response_*.generate_reference.mrefuses to write such a vector._atol_violations), with a unit test and a per-vector check.The floors added, at the cross-engine residual scale the sibling vectors already use:
Response_atol1e-12:siso_bt,siso_bt_large_M,siso_btfdr,freqmap_bt.internals:W_atol1e-15 (also the plan A9 min-deps 1-ULP fix),Phi_xz_imag_atol1e-14,DFT_imag_atol1e-12.spectrogram:Frequency_atol1e-14, for the 0 Hz bin.Default-tolerance audit:
Frequency_rel1e-12 for the five vectors still on the default, as every other frequency grid already has.model_order.n_rel0: the order is an integer, so the comparison is exact, matching Python's==.Rule 4 (payloads change only by regeneration):
Spec sections touched
None. This is no algorithmic change; it touches the reference-vector tolerances and the consumers.
For the maintainer
CONTRIBUTING.mdrule 1 still says every field carries anatol; the gate enforces it for near-zero fields only. I left the governance wording alone. Options: reword rule 1 to the enforced form, or keep the stronger aspiration and add floors to the rest later.Phi_xz_imag_atol,DFT_imag_atoland the fiveFrequency_relentries are read by the Octave validator only today. Python comparesPhi_*as complex using_real_rel, and doesn't compareDFT_*or thoseFrequencyfields at all (analysis F2/F4). A4's structural loop picks them up.1e-12threshold is the plan's own A5 wording, so I recorded no ADR.Type of change
Checks
658bda3:rtol × max|field|, which loosened mid-range entries (about 16× on a 0.52Response_realentry) — replaced with residual-scale floors;validate_reference.m: 32/32 under both Octave 11.3 and MATLAB R2024a.W_atolfromreference_internals.jsonfails the vector in both consumers.generate_reference.mruns with the gate passing. Nomatlab/changes.🤖 Generated with Claude Code