fix(#108): scale-relative zero-mode separation + feat(#102): hypothesis evidence matrix - #107
Conversation
…ity gate Additive, report-only slice of issue #102 — no verdict/class/tier/confidence behaviour changes (726-test suite green, anchors untouched). - ClassificationResult.hypothesis_matrix: one entry per hypothesis over the FULL A1-A12 taxonomy (decision rungs + A12 fallback + schema-reserved A6/A7/A9) with supporting measurements, counterevidence, missing required evidence, an explicit fail-closed claim_floor and the per-class ordinal support_score. RESERVED/UNEVALUABLE floor to UNDEFINED, NOT_SUPPORTED to NOT_EXCLUDED (absence of support is not proof of absence, #70 A5), SUPPORTED to the verdict the hypothesis would receive as winner — so the winner's floor equals the reported verdict exactly (pinned). - Declarative ladder: _ladder_spec() defines the priority chain as rungs of atomic _Condition predicates; _hypothesis_ladder (decision + shadow report) and hypothesis_evidence_matrix are both derived from that one spec, so the audit surface cannot drift from the decision. - support_score (issue #102 rename option 1): honestly-named ordinal twin of the legacy confidence field; identical value, documented NON-probabilistic semantics; calibrated replacement stays gated on the preregistered validation design. - Reachability gate: REACHABLE_A_CLASSES (9) is the coverage denominator; RESERVED_A_CLASSES exported; reserved classes excluded from claims. - Docs (taxonomy explanation, README, tutorial), CHANGELOG, 57 new tests in tests/test_hypothesis_matrix.py; ruff + mypy clean. No MANIFEST_SCHEMA bump: run-manifest contract untouched (additive report fields with defaults). CITATION.cff untouched: no result or methodology change — report surface only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…rage guard CodeQL on PR #107 flagged the constant as an unused global: it was only re-exported and read by tests. Instead of suppressing the finding, the constant now does real work in the package: _reachable_coverage_check() verifies at import that the ladder rungs plus the A12 fallback emit exactly the reachable taxonomy (A_CLASSES minus RESERVED_A_CLASSES), failing closed on drift. This is defense in depth for installed environments where the AST-level reachability test never runs. Two new tests cover the guard's accept and fail paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4492b8306a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
## #108 — zero-mode separation (CHANGES NUMERICAL RESULTS on rescaled input) The filter deciding which modes are the steady state used an absolute floor |lambda| > 1e-10, duplicated across five modules. A Liouvillian carries rate dimension, so this broke in both directions under L -> cL: * small c: every genuine mode fell below the floor -> D1 collapsed to 0.0 (firing the gapless F5 reach leg) and D19 overlap collapsed to 0.0, raising a FALSE A11/F4 Mpemba candidate on the highest-priority rung. Measured end-to-end: verdict moved A12/none NOT_EXCLUDED -> A11/F4 CANDIDATE between c=1 and c=1e-10 on a textbook amplitude-damped qubit. * large c: the round-off zero mode (~eps*||L||) rose above the floor and was counted as genuine, yielding a NEGATIVE gap (-1.4e-6 at c=1e10), impossible for a GKSL generator by definition. Canonical correction, no free parameter: spectral_zero_tolerance() derives the threshold from the spectrum as ZERO_MODE_RTOL * max|lambda| (spectral radius: homogeneous degree one, unitary-similarity invariant). At unit scale it reproduces the historical floor, so all 21 anchors and the full suite stay green byte-identically; only rescaled generators change. Legacy floor kept as an explicit atol= opt-in on every affected function (mirrors #99). D1 deliberately does not clamp at zero: a positive Re(lambda) after the fix is a genuinely unstable non-GKSL mode, and masking it would trade one silent failure for another. ## Codex review follow-ups on #102 matrix * support_score is None unless the hypothesis is SUPPORTED. _confidence answers 'what grade as the winner' and keys on a subset of each firing rule, so a failed rung printed a confirmation-grade number beside its own counterevidence (kreiss=11, petermann_max=1 -> A3 at 0.85). * Conditions whose own inputs are present are now evaluated even when a sibling is unevaluable, so a partial run keeps its usable evidence; the rung status still degrades to UNEVALUABLE. * Trust boundary documented: the matrix reports the ev dict, it does not re-validate it; the typed EnsembleEvidence check lives at the diagnose() boundary, same contract as classify_mechanism. * CITATION.cff: extended the 'Pending for the next cut' block per the 2026-08-09 convention (unreleased capabilities must not enter the abstract of the cited release), flagging #108 as a results-changing correction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
Response to the Codex review (commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 472c4b2478
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…red/optional keys, overflow-safe fits ## Zero-mode threshold now scales with eigensolver backward error (P1) The first #108 fix used tol = 1e-10 * max|lambda|, which imposes a fixed dynamic-range ceiling of 1e10 and discards genuine slow modes -- exactly the METASTABLE (A5) regime the library exists to study. Reproduced: two damping channels at rates 1.0 and 1e-12 (true gap 5e-13) reported a gap of 5e-1, wrong by ten orders of magnitude. Corrected to tol = ZERO_MODE_EPS_FACTOR * eps * max|lambda|. A computed eigenvalue is uncertain to order eps*||L||, so that -- not a fixed ratio -- is the scale on which 'indistinguishable from zero' is decided. Calibrated by measurement across amplitude damping, Rabi-driven damping, dephasing, a strongly non-normal near-defective generator and a 64-dim 3-qubit chain, each at c in {1, 1e6, 1e12}: the numerical zero mode never exceeded 1.94 * eps * max|lambda|, so the default factor keeps ~500x headroom while resolving genuine modes further down than the pre-#108 absolute floor did at unit scale. ## Fitter is overflow-safe on long time grids (P1, was the CI failure) M0-M3b now clip the exponent. A slow generator in small rate units yields a legitimately long grid (t up to 5e10); the optimiser probes a negative decay rate and np.exp overflows -- inf/nan residuals give least-squares no gradient to step back from, so the failure mode is silent non-convergence on valid input, not merely a log line. The bound is 345 rather than the ~709 where exp itself overflows, because M3a multiplies by the polynomial prefactor (A + B t) and a clip at 709 turns an exp overflow into a multiply overflow one line later. Bit-identical in the well-conditioned regime, pinned by test. ## Matrix: required vs optional evidence keys (P2) _Condition now separates keys the predicate INDEXES (required -> UNEVALUABLE when absent) from keys it reads via .get with a documented default (optional -> reported in the new missing_optional column, but still decided). Declaring a defaulted key required made the matrix report UNEVALUABLE for evidence the ladder happily fires on -- the two disagreeing precisely on the partially collected evidence the matrix exists to describe. The same key can be required for one rung and optional for another, which a single mixed list could not express. A parametrised test now pins matrix/ladder agreement under every single-key omission. ## Fail-closed: legacy atol no longer bypasses spectrum validation (P2) The atol opt-in returned early, so liouvillian_gap([0, nan], atol=1e-10) silently reported 0.0. A compatibility switch may restore the old THRESHOLD; it must not restore the old silent acceptance of corrupted solver output. ## Claim qualified, not broadened (P2) CHANGELOG and CITATION.cff no longer say the mechanism verdict is unit- invariant: the A10/F5 branch still gates on rate-dimensioned henrici_eta (open in #101). #108 removes a different, independent source of unit dependence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
Response to the second Codex review (commit
|
| slow rate | true gap | reported (before) |
|---|---|---|
| 1e-2 | 5.0e-03 | 5.0e-03 ✓ |
| 1e-6 | 5.0e-07 | 5.0e-07 ✓ |
| 1e-10 | 5.0e-11 | 5.0e-01 ✗ |
| 1e-12 | 5.0e-13 | 5.0e-01 ✗ |
Adopting the suggested basis: the threshold is now a multiple of the eigensolver backward error, ZERO_MODE_EPS_FACTOR * eps * max|lambda|. A computed eigenvalue is uncertain to order eps·‖L‖, so that — not a fixed ratio — is the scale on which "indistinguishable from zero" is decided.
The factor is calibrated by measurement rather than chosen: across amplitude damping, Rabi-driven damping, dephasing, a strongly non-normal near-defective generator and a 64-dim 3-qubit chain, each at c ∈ {1, 1e6, 1e12}, the numerical zero mode never exceeded 1.94 · eps · max|lambda|. The default keeps ~500× headroom above that while resolving genuine modes further down than even the pre-#108 absolute floor did at unit scale. Pinned by test_genuine_slow_modes_survive_the_zero_mode_filter, test_slow_mode_survival_is_also_unit_invariant (separation and rescaling must compose, not conflict) and test_threshold_sits_far_above_measured_round_off, which asserts the calibration claim rather than trusting it.
✅ P1 "Prevent the rate-rescaling test from overflowing the fitter" — already fixed, and by the route you preferred
This was the CI failure, diagnosed independently before the review arrived; the review's analysis matches exactly. Of the two options offered I took stabilise the fit, since the test scaling is legitimate physics (a slow generator in small rate units genuinely produces a long grid) and inf/nan residuals give least-squares no gradient to step back from — the real failure mode is silent non-convergence on valid input, not merely a promoted warning.
M0–M3b now clip the exponent at 345, not at the ~709 where np.exp itself overflows: M3a multiplies by the polynomial prefactor (A + B t), so a clip at 709 converts an exp overflow into a multiply overflow one line later. A test pins that the clip is bit-identical in the well-conditioned regime, so no ordinary fit moves.
✅ P2 "Align F5's required keys with its defaulted predicate" — adopted
Correct: _f5_reach reads ev.get("gap", 0.0), so declaring gap required made the matrix report UNEVALUABLE for evidence the ladder fires on. _Condition now separates keys the predicate indexes (required → UNEVALUABLE) from keys it reads with a documented default (optional → surfaced in a new missing_optional column, still decided).
One refinement the review's framing surfaced: the same key is required for one rung and optional for another (gap_to_gns_ratio is indexed by F3 but defaulted by F5), which a single mixed missing list could not express — hence the split column rather than a reclassification. A parametrised test now pins matrix/ladder agreement under every single-key omission, and encodes the deliberate asymmetry: the matrix is the robust reporter, the ladder raises rather than deciding from a value nobody measured, and both refusing to rule is agreement.
✅ P2 "Validate the spectrum before honoring the absolute override" — adopted
Correct and a genuine fail-closed hole: liouvillian_gap([0, nan], atol=1e-10) silently returned 0.0. Validation now runs before the override. A compatibility switch may restore the old threshold; it must not restore the old silent acceptance of corrupted solver output.
✅ P2 "Qualify the mechanism-verdict invariance claim" — adopted
Right, and exactly the kind of overclaim this repo's conventions exist to prevent. My wording asserted general verdict invariance while A10/F5 still gates on rate-dimensioned henrici_eta > 1.0. Both CHANGELOG.md and CITATION.cff now scope the claim to zero-mode-induced verdict changes and name the remaining #101 limitation explicitly.
❌ P1 "Use absolute imports for the new scale helper" — declined
AGENTS.md does say "absolute from liouscope, no .. traversal", but the entire package is written the other way: from .._consts import, from .._types import, from ..numerics.linalg import appear in essentially every module, including the files being edited here. Following the suggestion would make the five new import lines the only absolute ones in the package and leave each touched file internally inconsistent.
This is a real divergence between the stated convention and the codebase, but it is repo-wide and predates this PR, so the fix is a single mechanical sweep with its own review — not five inconsistent lines smuggled in on a numerics PR. Worth its own issue if the convention is the side that should win.
Full suite 824 passed, ruff and mypy clean.
Generated by Claude Code
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f4247692e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…valuable, alias contract Five findings, all valid; two were genuine defects in the previous commit. ## Zero-mode eps now follows the spectrum's dtype (P1) The threshold is a multiple of the eigensolver backward error, but eps was hard-coded to float64. A single-precision spectrum has round-off ~1e-7 relative, so the threshold sat ~9 decades below that solver's noise and the numerical steady-state eigenvalue survived as physical -- reintroducing the negative gap this function exists to prevent. eps is now taken from the real dtype underlying the spectrum, with a float64 fallback for integer/object input. ## The exponent cap is one-sided (P2) Clipping the NEGATIVE side truncated genuine, perfectly representable decay: exp(-400) = 1.9e-174 was reported as exp(-345) = 1.5e-150, a factor of 1e24, growing past 1e150 by exponent -700. Every model acquired an artificial constant tail that distorts residuals, fitted offsets and AICc on high-dynamic-range trajectories -- trading an overflow for a silent bias. Only the positive side needs the overflow cap; underflow is exact in the limit and NumPy's default error state ignores it. ## A12 fallback is unevaluable while any rung is unknown (P2) A false any_fired does not establish "no mechanism applies" when a rung was UNEVALUABLE -- the missing evidence could have made it fire. Claiming A12 SUPPORTED there asserts more than the run measured. A fired rung still refutes A12 outright; only the unknown case degrades. ## support_score inherits confidence on the legacy path (P2) An older caller supplying only confidence left support_score at its NaN sentinel, breaking the documented alias contract precisely on the backward-compatible path: migrated code lost the score for legacy results and exports emitted a non-finite tag. __post_init__ now fills an omitted value; an explicitly supplied one is left untouched. ## Compatibility claim corrected (P2) CHANGELOG and the constant's rationale no longer claim the new default reproduces the historical floor at unit scale -- it is ~2.2e-13, not 1e-10, so modes in that band are now classified as genuine WITHOUT any rescaling. That is the intended improvement (it is what rescues metastable slow modes) and no anchor system carries a mode in the band, which is why reference behaviour is unchanged; but "only rescaled generators change" was false. Full suite 830 passed; ruff and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
Response to the third Codex review (commit
|
| exponent | true | with symmetric clip |
|---|---|---|
| −300 | 5.148e-131 | 5.148e-131 ✓ |
| −400 | 1.915e-174 | 1.474e-150 ✗ |
| −700 | 9.860e-305 | 1.474e-150 ✗ |
A factor of 1e24 at −400, past 1e150 by −700 — an artificial constant tail on every model, distorting residuals, fitted offsets and AICc on high-dynamic-range trajectories. I had traded an overflow for a silent bias, which is the worse of the two.
The cap is now one-sided, as suggested. Underflow needs no guard: it is exact in the limit and NumPy's default error state ignores it (verified — np.exp(-800) only raises under an explicit errstate(all="raise"), which the suite does not set). Pinned by test_clip_does_not_truncate_representable_decay (exact equality at −300/−400/−700/−745) and test_extreme_decay_underflows_to_zero_without_warning.
✅ P2 "Make the A12 fallback unevaluable when a rung is unknown"
Correct, and it is the same epistemics the rest of the matrix already applies: not any_fired does not establish "no mechanism applies" when a rung could not be evaluated — the missing value could have made it fire. Calling A12 SUPPORTED there claims more than the run measured.
A12 is now UNEVALUABLE (floor UNDEFINED, no score) whenever any rung is unevaluable and none fired. A fired rung still refutes A12 outright, which the second new test pins so the fix does not overshoot into "unknown whenever anything is missing".
✅ P2 "Derive the new alias from legacy confidence"
Correct — the alias contract broke precisely on the path it exists to serve. __post_init__ now fills an omitted support_score from confidence; an explicitly supplied value is left untouched (both directions pinned).
✅ P2 "Correct the stated unit-scale compatibility"
Correct, and this one I should have caught when switching to the backward-error basis: 1e3 · eps64 ≈ 2.22e-13 is not the historical 1e-10, so modes in that band are now classified as genuine without any rescaling — "only rescaled generators change" was false.
Both CHANGELOG.md and the constant's rationale now state the actual change. The direction is the intended improvement (it is what rescues metastable slow modes), and no anchor system carries a mode in that band — which is why the reference behaviour is unchanged, rather than the claim that nothing outside rescaling moved.
Full suite 830 passed, ruff and mypy clean.
Generated by Claude Code
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9bc909f518
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…st invariance claims Two findings, both valid. The first exposed the #108 defect class in the FITTING path and, following it up, showed my own end-to-end test was asserting something the library explicitly does not guarantee. ## Fit seed rate is grid-relative (P1) — CHANGES NUMERICAL RESULTS initial_guess_m0 floored the seeded decay rate at an absolute 1e-3. A rate is 1/time, so on a grid spanning t = 5e10 the seed sat 1e7 above the true rate, in a region where exp(-alpha t) has underflowed flat and the optimiser has no gradient. The fit returned THE FLOOR ITSELF as the measured rate: beta_D == beta_D_linear == 1e-3 on a system whose true rate was 5e-11 -- corrupting D5/D17 while every spectral quantity looked healthy, and my test certified that report because it only checked the gap and the verdict. The floor is now a dimensionless decay depth over the fitted window (ALPHA_SEED_FLOOR_FRAC / t_span), which reproduces 1e-3 exactly at the t_span = 5 used throughout the suite, so seeds there and the anchors are unchanged. The too-few-positive fallback rate is likewise grid-relative. Model outputs are additionally magnitude-bounded: capping the exponent bounds exp, but M3a multiplies by (A + B t), unbounded in the parameters, and least-squares squares the product inside its own normal equations -- the overflow reappeared in SciPy's trf rather than in this module. ## The end-to-end test asserted invariance the library does not claim Following the finding up revealed the class is NOT unit-invariant: measured A10/F5 at c = 10 and c = 1e3 where c = 1 gives A12. That is exactly the documented #101 henrici_eta scale dependence, so my test_diagnose_verdict_is_invariant_under_rate_rescale was green by luck on its particular c values while claiming something the README lists as a known limitation. Rebuilt into three honest tests: the spectral/Mpemba evidence #108 actually fixes IS invariant; fitted rates scale by c over c in [1e-6, 1e10]; and the class non-invariance is now PINNED, so nobody later mistakes it for an invariance guarantee. When #101 slice C lands that test should fail and be replaced by a genuine assertion. ## NaN required evidence is unevaluable (P2) The matrix used a presence-only check, so a required key holding NaN -- the library's own "not computed" sentinel -- read as collected evidence: every comparison against NaN is False, which looks like "threshold not met" and let the A12 fallback conclude no mechanism applies. Infinities are deliberately NOT swept in: a floored gap drives gap_to_gns_ratio to inf by design. ## Residual limitation recorded, not asserted away At c = 1e-10 the least-squares convergence criteria stop tracking the rescaling, so fitted-rate invariance is asserted only over c in [1e-6, 1e10]. Opened as its own issue; non-dimensionalising the fit is the proper fix and touches every fitted quantity, so it needs anchor review and its own PR. Full suite 844 passed; ruff and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
The evidence dict is typed dict[str, float], so the isinstance guard was unreachable-by-type and failed the enforcing mypy gate. math.isnan alone carries the same runtime semantics: absent -> unavailable, NaN -> unavailable, finite or infinite -> available (infinities are legitimate measured values). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8008445c34
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…cision Reverts the dtype branch added in the third review round. That change was made on an unreproduced claim and introduced a real defect, which the fifth review round then correctly caught. ## What the measurement shows NumPy/SciPy solve eigenproblems in DOUBLE regardless of the input dtype. A complex64 generator returns a complex64 array whose numerical zero mode sits at ~1.5e-19 -- double-level, not single-level. The storage dtype therefore says nothing about the precision the backward error was actually incurred in. Deriving eps from it inflated the threshold to ~1.2e-4 relative and discarded clearly resolved slow modes: a complex64 two-channel generator with rates 1.0 and 1e-4 (true gap 5e-5) reported 5e-1 -- corrupting by four orders of magnitude exactly the metastable case the widened tolerance existed to protect. The finding that motivated the dtype branch (a complex64 Rabi-damped generator scaled by 1e-3 allegedly reporting a gap near -1.7e-11) does not reproduce: measured, that case returns 2.000000e-04 with the round-off mode at 1.46e-19, comfortably below any threshold. I noted at the time that it did not reproduce and changed the code anyway; that was the error. A genuinely single-precision spectrum (external or GPU solver) is the one case where a coarser threshold is right, and it is indistinguishable from a downcast double result by inspection -- so it belongs to the caller, via the existing rtol argument, and is documented as such. Tests now pin the measured behaviour in both directions: the tolerance is independent of storage dtype, a downcast metastable spectrum keeps its slow branch, and the round-off mode is still excluded so no negative gap returns. Full suite 845 passed; ruff and mypy clean (exit codes checked directly this time -- the previous round's mypy failure reached CI because a piped `tail` masked its exit status). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
Response to the fifth Codex review (commit
|
| storage dtype | reported gap (true: 5e-5) |
|---|---|
| complex128 | 5.000000e-05 ✓ |
| complex64 | 5.000000e-01 ✗ |
The cause is the dtype-derived epsilon added in round 3. Measured, NumPy/SciPy solve eigenproblems in double regardless of input dtype: a complex64 generator returns a complex64 array whose numerical zero mode sits at ~1.5e-19 — double-level, not single-level. The storage dtype says nothing about the precision the backward error was actually incurred in, so keying the threshold to it inflated the cutoff to ~1.2e-4 relative and discarded modes four decades above the solver's real noise floor — corrupting precisely the metastable case the widened tolerance existed to protect.
On the round-3 finding that motivated it
The claim there was that a complex64 Rabi-damped generator scaled by 1e-3 reports a gap near -1.7e-11 instead of 2e-4. Measured on this branch:
complex128: eig dtype=complex128 min|lambda|=1.2065e-19 gap=2.000000e-04
complex64 : eig dtype=complex64 min|lambda|=1.4626e-19 gap=2.000000e-04
It does not reproduce, in either dtype. I said as much at the time — "the defect did not reproduce through that particular entry point" — and changed the code anyway, on the reasoning that the correction was cheap and defensive. That was the mistake: a cheap change to a numerical threshold is not free, and this one traded a hypothetical failure for a measured one. The rule I should have followed is the repo's own: no change to a numerical contract without a reproduction.
The genuine single-precision case (an external or GPU solver) is real but indistinguishable from a downcast double result by inspection, so it belongs to the caller through the existing rtol argument, and is now documented there rather than guessed at from array metadata.
Tests pin the measured behaviour in both directions: the tolerance is independent of storage dtype, a downcast metastable spectrum keeps its slow branch, and the round-off mode is still excluded so no negative gap returns.
Also in this push
The previous commit's mypy failure (redundant-expr on the NaN guard) reached CI because I piped mypy through tail, which masks the exit status — the command chain reported success and pushed. Fixed, and gate exit codes are now checked directly rather than through a pipe.
Full suite 845 passed, ruff and mypy clean.
Generated by Claude Code
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1038f7e32f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
## Root fix: dense eig/Schur boundaries promote to complex128
Self-review of the round-5 revert exposed that my public claim "NumPy/SciPy
solve eigenproblems in double regardless of input dtype" was HALF wrong, and
the wrong half is the half the library uses. Measured on a generic matrix
whose entries are not exactly representable:
numpy.linalg.eigvals(complex64): max|dlambda| = 3.3e-07 (= input casting
error; numpy._commonType computes in cdouble always, casts result back)
scipy.linalg.eig(complex64): max|dlambda| = 1.0e-05 (~30x more: scipy
dispatches by dtype and genuinely runs single-precision cgeev)
LiouScope's dense paths all go through scipy, and eig_nonhermitian even
documents "Always uses LAPACK zgeev" -- a contract the code did not enforce.
It now does: eig_nonhermitian, the Mpemba layer, Petermann/Henrici and D24
promote to complex128 before solving. This supersedes both dtype review
rounds at the root: every backward-error tolerance is calibrated against the
double solve, and now the double solve is guaranteed. Representation error
already present in caller-supplied single-precision data is the caller's
data quality and is not masked. My round-3 measurement (zero mode at 1.5e-19
for complex64) was an artifact of exactly representable matrix entries, not
evidence of double computation -- recorded so the wrong inference is not
repeated.
## Sixth Codex review, all four adopted
* Stale CHANGELOG claim: the round-3 "eps from the spectrum's own dtype"
sentence survived the round-5 revert. Now describes the fixed
double-precision epsilon and the external-solver limitation.
* rtol passthrough: liouvillian_gap / oscillating_mode_gap / spectral_spread /
lep_proximity now expose the rtol multiplier, so a caller with eigenvalues
from a genuinely single-precision external/GPU solver can widen the filter
scale-relatively instead of reverting to an absolute floor. Pinned with the
reviewer's own example (displaced zero mode at 1e-7 * radius).
* Factor multiplication could overflow before _bounded saw the product
(amplitude probe 1e200 -> inf on arrival; 0 * inf -> NaN). Models now
evaluate under a suppressed overflow errstate and _bounded maps non-finite
intermediates to the saturation bound -- both encode "absurdly far from the
data", which is what the optimiser needs to hear.
* RESERVED_A_CLASSES (and the other exported taxonomy mappings) are now
MappingProxyType: a consumer pop("A6") could previously desynchronise the
taxonomy from the import-time coverage guard that certifies it, since the
guard compares against the precomputed REACHABLE_A_CLASSES tuple.
Full suite 849 passed; ruff and mypy clean (exit codes checked directly).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
Correction to my fifth-round comment + response to the sixth review (commit
|
| solver, complex64 input | max|Δλ| vs. double solve |
|---|---|
numpy.linalg.eigvals |
3.3e-07 — consistent with input-casting error only; numpy.linalg._commonType computes in cdouble always and casts the result back |
scipy.linalg.eig |
1.0e-05 — ~30× more: scipy dispatches by dtype and genuinely runs single-precision cgeev (SciPy LAPACK docs) |
LiouScope's dense eigen/Schur paths all go through scipy, and eig_nonhermitian even documents "Always uses LAPACK zgeev" — a contract the code did not enforce. My round-3 measurement (zero mode at 1.5e-19 for a complex64 Liouvillian) was an artifact of exactly-representable matrix entries, not evidence of double computation; I drew a general conclusion from a special case, twice.
The root fix, superseding both dtype rounds
All dense eigen/Schur boundaries (eig_nonhermitian, the Mpemba layer, Petermann/Henrici, D24) now promote to complex128 before solving, making the documented zgeev contract true. Every backward-error tolerance in this PR is calibrated against the double solve — now the double solve is guaranteed, so the threshold model is valid by construction rather than by luck of the input dtype. Representation error already present in caller-supplied single-precision data is the caller's data quality and is neither masked nor "corrected" (the standard backward-error stance).
✅ Sixth review — all four adopted
- Stale CHANGELOG claim — correct, and embarrassing in a useful way: the round-3 sentence "eps taken from the spectrum's own dtype" survived my round-5 revert, so the changelog described the exact mechanism the code no longer had. Now documents the fixed double-precision epsilon and the external-solver limitation.
- Expose
rtol— adopted onliouvillian_gap/oscillating_mode_gap/spectral_spread/lep_proximity. Pinned with the reviewer's own example: a displaced stationary mode at1e-7·max|λ|from an external single-precision solver yields-1e-7under the double-calibrated default and the correct0.2withrtol = 1e3·(eps32/eps64)— scale-relative, no absolute floor. - Factor multiplication overflow — adopted: an amplitude probe like
1e200made the productinfbefore_boundedever saw it, and0·infyields NaN. Models now evaluate under a suppressed overflow errstate and_boundedmaps non-finite intermediates to the saturation bound; both encode "absurdly far from the data", which is exactly what the optimiser needs to hear. - Immutable taxonomy mappings — adopted:
RESERVED_A_CLASSES.pop("A6")could silently desynchronise the taxonomy from the import-time coverage guard, which compares against the precomputedREACHABLE_A_CLASSEStuple. All four exported taxonomy mappings are nowMappingProxyType, pinned by a mutation test.
Full suite 849 passed; ruff and mypy clean.
Sources: SciPy low-level LAPACK docs, numpy.linalg.eig, numpy linalg/_linalg.py _commonType ("always double or cdouble", verified against the installed source).
Generated by Claude Code
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4ddca422a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…NotFoundError) test_unresolved_certificate_floors_the_verdict_to_undefined imported helper builders from tests.test_classifier_semantics_debt, but the tests directory is not an importable package on the CI runner (rootdir-dependent sys.path), so every matrix job failed with ModuleNotFoundError while the local run passed. The test now constructs its minimal synthetic results locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23ed0b1f3a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…nclusive matrix refutation All three findings reproduced before changing anything, then adopted. ## One zero-mode scale for certification and filtering (P1) certified_eigvals accepts a stationary residual up to the true eigensolver backward error rtol*eps*||L||_2, but every downstream filter used the spectrum-side proxy rtol*eps*max|lambda|. For a strongly non-normal trace-preserving operator the two differ by ||L||_2/max|lambda| -- measured 3.9e3 on a 4x4 example -- so a certified-RESOLVED zero mode with residual between the thresholds survived the radius filter as a spurious genuine mode: D1 reported ~1e-12 (occasionally NEGATIVE, impossible for GKSL) instead of the true gap 1, and D9 returned four eigenmodes where three exist. Reproduced on 20/20 constructions at skew<=1e-3. Fix: new shared helper operator_zero_tolerance (= the certificate bound, pinned equal by test); every consumer that holds the OPERATOR filters with it -- spectral layer (D1/D3/D4, has_complex_pairs), Mpemba layer (slowest mode + expansion via _certified_decomposition, which now carries the bound), petermann_factors, D24. Genuine slow modes inside the coarser band are not silently swallowed: the #113 ambiguity split reports resolved=False and the verdict floor fires. Radius default untouched for spectrum-only sites; caller atol still wins. Deterministic 4x4 fixture frozen as exact float64 literals with a guard-skip if a future LAPACK resolves it below the radius tolerance. ## D24 recomputation is certified (P2) compute_zhou_predictor filtered raw sla.eig output, retaining exactly the solver failure the spectral and Mpemba layers repair: on the stiff #112 fixture D24 reported gap 7.28e-6 against the certified 1.074e-5 (~30% shift of the mixing-time window; reproduced). Now routed through certified_eig with the certificate bound as filter; an applicable-but-unresolved certificate returns an honest unconverged record (inf bounds, NaN gap/K), honouring caller-supplied values exactly like the existing no-nonzero-modes branch. Supplying both gap and K still bypasses the eigensolve entirely. ## Conclusively refuted partial rungs are NOT_SUPPORTED (P2) The rungs are conjunctions: kreiss=1 refutes F1 whether or not petermann_max was ever measured. The old rule promoted any missing required key to UNEVALUABLE, which propagated into an UNEVALUABLE A12 fallback while the decision ladder deterministically returned A12 -- the matrix contradicting the decision it documents. UNEVALUABLE is now reserved for the genuinely open case (no evaluated condition false AND missing evidence could still flip the rung to supported); missing keys stay listed for the audit trail. The NaN-encoding-equivalence test now pins the open case via gns_certified=1 and the refuted case is pinned separately. Full suite 929 passed (920 + 9 new pins); anchors, ruff, mypy clean; CHANGELOG, CITATION.cff pending block and taxonomy docs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
|
Thirteenth review round: all three findings reproduced first, then adopted in d4f9004. One zero-mode scale (P1, Certify the D24 eigensolve (P2, Conclusive refutation beats UNEVALUABLE (P2, Full suite 929 passed (+9 new pins), anchors byte-identical, ruff and mypy clean. CHANGELOG, the CITATION.cff pending block and the taxonomy docs are updated. Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4f9004694
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…llback for inapplicable certificates Both findings reproduced before changing anything, then adopted. ## Petermann factors consume the certified eigendecomposition (P1) petermann_factors recomputed its own raw zgeev decomposition, so on the stiff #112 fixture it consumed exactly the solver failure the spectral and Mpemba layers repair -- and no cutoff can restore an eigenvalue that is ABSENT from the raw spectrum. Reproduced: 16 "non-zero" modes with petermann_max ~ 622.7 against the certified (dgeev-real) 15 modes with ~ 2.0, corrupting D9 and the D11 input while the spectral certificate looked resolved -- a possible false F1 signal no verdict floor would catch. Now routed through certified_eig; on an applicable-but-unresolved certificate both returned arrays are the NaN unavailable sentinel (warned), so petermann_max becomes NaN and F1 reads UNEVALUABLE. Deliberately NOT the empty-array default K_max = 1.0, which would assert perfect normality from a spectrum the solver demonstrably could not resolve. ## Radius fallback when the certificate is inapplicable (P2) Without established trace preservation no zero eigenvalue is guaranteed, so the operator-norm bound is not a valid zero-mode cutoff. Reproduced with the reviewer's construction (diag(-1,-2,-3,-4) plus a 1e16 off-diagonal): bound ~ 2.2e3 exceeds the whole spectrum, the round-13 filters discarded every eigenvalue, and D24 reported gap 0.0 / unconverged for a true gap of 1 (D1 identically through the layer with a supplied steady state). Every round-13 call site -- spectral layer, Mpemba layer, D9, D24 -- now uses the certificate bound only when certificate.applicable and falls back to the radius-based #108 tolerance otherwise. Full suite 933 passed (929 + 4 new pins); anchors byte-identical; ruff and mypy clean; CHANGELOG and CITATION.cff pending block updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
|
Fourteenth review round: both findings reproduced first, then adopted in 467ed8f. Certify the Petermann eigendecomposition (P1, Radius fallback when the certificate is inapplicable (P2, Full suite 933 passed (+4 new pins: certified Petermann count/K on the stiff fixture, NaN withholding on the unresolved fixture, and the radius fallback for D1 and D24), anchors byte-identical, ruff and mypy clean. CHANGELOG and the CITATION.cff pending block are updated. Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 467ed8f687
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…nues past ambiguity Both findings reproduced by scan (4000 random stiff four-level networks each) before changing anything, then adopted. ## certified_eig validates eigenvector residuals (P1) The certificate accepted a decomposition solely on eigenvalue evidence, while D19 and the Petermann factors consume the eigenVECTORS. Reproduced: valid stiff classical networks with certified=True/resolved=True whose slow-mode LEFT eigenvector has residual up to 3.1e-4 against a bound of 3.2e-7 -- three orders beyond, not an eigenvector in any usable sense (right vectors fine; the corruption is the Ahues-Tisseur-deflated Schur basis, invisible to every magnitude test on eigenvalues). Acceptance is now PER MODE and RELATIVE: r_j <= max(VECTOR_RESIDUAL_REL_MAX * |lambda_j|, bound), r_j the larger of the unit-normalised left/right residuals. r/|lambda| is the first-order relative error scale of anything computed from the pair; a single operator-scale cutoff does NOT separate the measured populations (healthy zgeev over 301 generators <= 2.1e-10; legitimate dgeev-real repairs of the stiff family 1.2e-2..6.5e-2; corrupt decompositions 2.2e-1..2.9e1). The boundary at 1e-1 sits between marginal-but-usable and clearly broken, with fail-closed as the failure direction; an extreme repair at spectral spread ~4e11 carrying a 79% slow-mode error is deliberately failed closed -- that is not a measurement. Calibration documented at the constant. A vector-failed candidate does not end the ladder; when no route passes, certified=False with the OFFENDING residual reported (so the downstream warning shows a number that actually exceeds the printed bound), and D19, D9 and D24 withhold. certified_eigvals is deliberately untouched: the eigenvalues of such a decomposition remain usable for D1/D3/D4, and the new tests pin exactly that division. ## The ladder continues past ambiguous candidates (P2) An ambiguous candidate ended the ladder immediately (resolved=False, D1 withheld, verdict floored) even when the next route resolves the generator cleanly. Reproduced on 3 scan hits: zgeev certified with one ambiguous mode at 3.5e-7..1.6e-6 while dgeev-real returns a machine-zero stationary mode (7.3e-17..1.3e-14) with none. Both ladders now accept only ambiguity-free candidates; the best ambiguous candidate (fewest ambiguous modes, ties by ladder order) is returned fail-closed only when every route stays ambiguous. Healthy path unchanged and still lazy (pinned by the existing Schur-counter test). Full suite 936 passed (933 + 3 new pins); anchors byte-identical; ruff and mypy clean; CHANGELOG and CITATION.cff pending block updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
|
Fifteenth review round: both findings reproduced by scan (4000 random stiff four-level networks each), then adopted in ebdd30b. Validate eigenvectors before certifying (P1, Continue past ambiguous repair candidates (P2, both ladders) — Confirmed on 3 scan hits: zgeev certified with one ambiguous mode at Full suite 936 passed (+3 new pins), anchors byte-identical, ruff and mypy clean. CHANGELOG and the CITATION.cff pending block are updated. Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebdd30b2a3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…on-matched certificates, D11 decoupling All four findings reproduced before changing anything, then adopted. ## Prony fallback seeds are grid-relative (P2) The fallback seeded (beta, omega) = (1, 1) in absolute rate units, so on a valid non-uniform grid spanning t = 1e7 the M3b fit started seven orders of magnitude off and least-squares "converged" (success=True) onto the seed itself -- reproduced: [A, beta, omega] ~ [0.006, 1, 1] for true values 5e-8 and 2e-6, corrupting the fitted rate and suppressing M3b/A8 purely because of the rate unit. Fallback rates are now 5.0/t_span and the success-path positivity floors 5e-6/t_span, both reproducing the historical values exactly at the canonical t_span = 5 (same convention as ALPHA_SEED_FLOOR_FRAC / M3A_SLOPE_SEED_FRAC). The measured non-uniform scenario is now unit-invariant across c in [1e-6, 1e6] (pinned end to end). ## D24 uses the eigenvalue certificate when only the gap is missing (P2) The recomputation required certified_eig even when the caller supplied petermann_factor -- but that path consumes no eigenvectors. Reproduced: a stiff network whose eigenvalue certificate resolves a usable gap of 3.32e-6 while only the round-15 eigenvector gate fails returned an unconverged NaN record. The certificate now matches what is consumed: certified_eigvals for the gap-only path, certified_eig when the Petermann factor is recomputed (that path stays fail-closed, pinned). ## D11 decoupled from the D9 vector gate (P2) The D9 NaN sentinel flowed into bohr_arithmetic_progression, which silently filtered it and reported the default length 1 as a measured value. Reproduced: a ladder-Hamiltonian network whose certified spectrum carries a length-3 progression while D9 is withheld reported 1. D11's input is now recomputed from the certified spectrum (D11 consumes only eigenvalues); when the eigenvalues themselves are unresolved, bohr_ap_length is NaN (field now float-typed, NaN = documented sentinel). ## Release-note correction (P2, documentation) The atol opt-in for the legacy absolute Hermiticity gate exists on the standalone is_hermitian predicate only; the builders never accepted it. The #109 CHANGELOG entry now says so explicitly instead of advertising an unavailable builder argument. Full suite 942 passed (936 + 6 new pins); anchors byte-identical; ruff and mypy clean; CHANGELOG and CITATION.cff pending block updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
|
Sixteenth review round: all four findings reproduced first, then adopted in e26df05. Grid-relative Prony fallback (P2, Eigenvalue certificate for the gap-only D24 path (P2, Decouple D11 from the D9 vector gate (P2, Builder Full suite 942 passed (+6 new pins), anchors byte-identical, ruff and mypy clean. CHANGELOG and the CITATION.cff pending block are updated. Generated by Claude Code |
…hem (CodeQL py/unused-local-variable) The gap-only recompute path (round-16) bound vl = vr = None purely to satisfy definition-on-all-paths, but the Petermann loop is the only consumer and runs exactly in the need_vectors branch. Moving the extraction there removes the dead assignments; behaviour unchanged (suite 942 passed, ruff and mypy clean). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a8ae9c09e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ambiguous = int(np.count_nonzero(in_band & (magnitudes > split))) | ||
| if ambiguous == 0: |
There was a problem hiding this comment.
Do not treat every sub-split decay mode as zero
Fresh evidence after the certificate fixes is a valid weak-dissipation qubit with H=diag(0,1) and amplitude-damping/dephasing rates 1e-15 and 1e-14: SciPy returns the exact spectrum [0, -1e-15, -2.05e-14±i], but the global-norm split is 6.66e-15, so this condition declares the genuine -1e-15 population mode an unambiguous zero mode. The certificate therefore reports resolved=True, its bound filters that mode, and D1 becomes 2.05e-14 instead of the true 1e-15, propagating a wrong gap to downstream diagnostics. Determine zero-mode membership from nullspace/stationary residual evidence rather than assuming every eigenvalue below a global eps*||L|| split is machine zero.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Zutreffend, auf dem aktuellen Stand reproduziert — und nicht behoben. Ich lasse den Thread deshalb offen und sage, was stattdessen geschehen ist.
Nachgefahren auf pr107-fix (PR #121, Stand f194c97), also auf dem neuesten Zweig, nicht nur hier:
H = diag(0, 1), Amplitudendaempfung 1e-15, Dephasierung 1e-14
SciPy-Spektrum [-5.5e-15-1j, -5.5e-15+1j, -1.0e-15+0j, 0+0j]
certified_eigvals solver=zgeev applicable=True certified=True resolved=True
Zertifikat-bound 2.220446049250313e-13
|Eigenwerte| [0, 1e-15, 1, 1]
Die echte Populationsmode bei 1e-15 liegt unter der Schranke 2.22e-13 und wird gefiltert. Genau wie beschrieben.
Warum ich es nicht in diesem PR repariere. Der Befund ist eine Instanz der bereits erfassten Decke aus #113: die spektrale Spreizung ist hier 1e15, über der dort dokumentierten Grenze von ~4.5e12. Ihr Vorschlag ist aber schärfer als das, was in #113 stand, und ich habe ihn dort eingetragen (#113, Kommentar von heute) statt ihn in einem Thread verschwinden zu lassen:
Determine zero-mode membership from nullspace/stationary residual evidence rather than assuming every eigenvalue below a global
eps*||L||split is machine zero.
Das ist der Wechsel von einer Betrags-Frage zu einer Struktur-Frage. |λ| < eps·‖L‖ misst den Eigenwert gegen eine globale Norm, die hier vom Hamilton-Anteil gesetzt wird — mit der langsamen Mode hat sie nichts zu tun. Das Nullraum-Residuum ‖L v‖/‖v‖ fragt direkt, ob der Vektor stationär ist. Das ist eine Änderung am Zertifikat-Begriff, nicht ein Wächter-Nachzug, und gehört nicht in einen PR mit diesem Zuschnitt.
Ein Punkt, den Ihr Beispiel beiträgt und der in #113 fehlte: es braucht keinen exotisch steifen Generator, um in die Decke zu laufen. Es genügt, dass der Hamilton-Anteil die Norm dominiert, während alle dissipativen Raten klein sind — eine physikalisch gewöhnliche Lage. Die Decke ist erreichbarer, als die Formulierung „above ~4.5e12 spread" vermuten liess.
In #113 steht jetzt auch die Auflage für die Umsetzung: die Diskriminierung muss an genau diesem Paar belegt werden — diese 1e-15-Mode muss überleben, eine echte Maschinen-Null muss weiterhin gefiltert werden. Ein Kriterium, das beide durchlässt, wäre keine Verbesserung, sondern das Abschalten des Filters.
There was a problem hiding this comment.
You are right, and this is now fixed on the follow-up branch.
Zero-mode membership is no longer decided by the global eps * ||L|| split. certified_nonzero_modes (src/liouscope/numerics/linalg.py:303) computes the a posteriori backward-error bound from the residuals actually attained -- max(||Lx - lam_hat x||, ||L^H y - conj(lam_hat) y||) / |y^H x| -- and any mode whose magnitude exceeds margin * bound is certified as genuinely non-stationary. refine_zero_band (linalg.py:412) then removes those certified modes from the band:
nonzero = certified_nonzero_modes(L_c, eigenvalues, magnitudes <= bound, ...)
in_band = (magnitudes <= bound) & ~nonzeroThe move is deliberately one-directional -- it can only take a mode OUT of the zero set, never put one in -- so it cannot manufacture a false resolved verdict.
Your exact counterexample is pinned as a regression test: tests/test_issue118_residual_findings.py::test_slow_mode_below_the_norm_scaled_band_survives_as_the_gap builds H = diag(0, 1) with rates [1e-15, 1e-14] and asserts layer.gap == approx(1e-15), i.e. the genuine population mode, not the 2.05e-14 the old split reported. A positive control sits next to it (test_healthy_generator_keeps_its_untouched_certificate) so the refinement is proven inert where there is nothing to rescue.
Discrimination: reverting that & ~nonzero to the bare magnitudes <= bound turns the test red; restoring it turns it green. The rescue is load-bearing, not decorative.
Evidence for this reply: full suite 995 passed (exit 0) on pr107-fix @ 9581f39, and a mutation run over all four fixes below in which every one discriminates (take the fix out -> the named test goes red; restore -> green; 4/4, none blind). The mutation harness verifies restoration by hash, so no mutated source survives the run.
There was a problem hiding this comment.
This finding is correct on this branch. Measured at 4a8ae9c: zero_mode_count=2 - the tolerance eps*||L|| swallows the genuine 1e-15 mode.
It is closed in the follow-up PR #121, which contains this branch in full: src/liouscope/numerics/linalg.py:455 (_certified_non_stationary_modes) and :531 (refine_zero_band) decide zero-mode membership from the a-posteriori backward-error bound over the residuals actually achieved, instead of from eps*||L||. At 123bd57 the same setup yields zero_mode_count=1 and D1 = 1e-15.
Deliberately left unresolved here: the fix lives on the successor branch, not on this one. Back-porting it would mean cherry-picking f3b73b4 together with its second axis and tests into a branch the successor already replaces - divergent history for no gain. Whether to close this PR as superseded is a product decision, not a technical one, and is with the maintainer.
Verified locally on the pushed branch: ruff and mypy clean, tests/test_anchors.py 21 passed. Reverse-mutation runs are recorded in an evidence ledger with one run_id per run; a run that dies by error rather than on an assertion is not counted as proof. CI on GitHub not measured here.
Review-Threads durchgesehen (2026-08-18): 38 von 54 aufgelöst, 16 bleibenDer PR war durch Aufgelöst, jeweils mit Begründung im Thread:
Nicht aufgelöst: 16 Befunde am aktuellen Stand → #118. Jeder davon ist eine Bewertung: Der PR ist nicht merge-reif, und das liegt nicht an der Formalie. Grundlinie lokal geprüft: 942 Tests grün ( |
Zuschnitt liegt bereit — zwei Zweige statt einer 17. RundeWie im Kommentar oben angekündigt ist dieser PR zugeschnitten. Die Naht liegt
Der Vollständigkeitsbeleg: Was der Schnitt gekostet hat: genau einen Test von 856. Was der Schnitt gezeigt hat: die Schichtgrenze verläuft anders, als der Die Pull Requests sind bewusst noch nicht eröffnet. Eine PR-Eröffnung gegen Dieser PR sollte geschlossen werden, sobald die beiden Teile offen sind — die #115 hängt an Teil B, nicht mehr an diesem PR: es ändert das |
Review-Fäden nachgezählt: 16 offen → 3Die 16 offenen Review-Fäden dieses Pull Requests wurden gegen den heutigen Kopf des Die vollständige Zuordnung Befund → Fundstelle steht in Issue #118. Zwölf sind im Code beantwortet, jeweils mit Datei und Zeile belegt. Bei einem Was offen bleibt
Zwei davon sind numerisch, eines ist eine Textkorrektur. Methodischer VorbehaltDie dreizehn sind durch Lesen des Codes am genannten Stand belegt, nicht durch Wer einen der aufgelösten Fäden anders sieht, kann ihn jederzeit wieder öffnen. |
Antwort auf "Do not treat every sub-split decay mode as zero"Ihr Gegenbeispiel läuft auf dem Nachfolgezweig (
Der Grund ist, dass die Zugehörigkeit zur Null-Mode nicht mehr über den globalen Rücknahme-Probe: entfernt man Der Thread hängt an diesem PR, der Code dazu liegt in PR #121 — dort ist er Teil der Sammelantwort. 🤖 Generated with Claude Code |
Status 2026-09-07 — do not merge this separately; it is strictly contained in two other PRsMeasured, not inferred ( Zero commits are unique to this PR. Everything here is already inside #121 and inside #127. Merging it on its own would not add a line of code; it would only decide which SHA Current state:
What Marco has to decideMerging #121 with a merge commit retires this PR and #108 automatically, because Read-only measurement. No push, no merge, no rebase, nothing resolved. |
Summary
Two related pieces of classifier-integrity work.
#108 — zero-mode separation is now scale-relative (CHANGES NUMERICAL RESULTS on rescaled input). The filter deciding which modes are the steady state used an absolute floor
|lambda| > 1e-10, duplicated across five modules. A Liouvillian carries rate dimension, so this broke in both directions underL -> cL, and it was found by auditing the layer the #101 conformance suite does not reach:c— every genuine mode falls below the floor, so D1 collapses to0.0(unconditionally firing the gapless F5 reach leg) and the D19 slowest-mode overlap collapses to0.0, raising a false A11/F4 Mpemba candidate on the highest-priority rung. Measured end-to-end on an amplitude-damped qubit withrho_0 = |+><+|:A12/none NOT_EXCLUDED EXPLORATIONatc = 1becameA11/F4 CANDIDATE CONFIRMATIONatc = 1e-10. The issue-D19/A11 Mpemba detector false-positives on trivially symmetric initial states (README quickstart classifies as A11 CONFIRMED/PUBLICATION_GRADE) #68 non-triviality guard could not correct it — it depends on the same_slowest_modehelper.c— the round-off zero mode (~eps * ||L||) rises above the floor and counts as genuine, producing a negative spectral gap (-1.4e-6atc = 1e10), impossible for a GKSL generator by definition.The correction is canonical and carries no free parameter — unlike the #101 F5 threshold, it needs no calibration study:
numerics.scale.spectral_zero_tolerance()derives the threshold from the spectrum itself asZERO_MODE_RTOL * max|lambda|(spectral radius: homogeneous of degree one, unitary-similarity invariant, consistent withrate_scale). At unit scale it reproduces the historical floor, so all 21 anchors and the full suite stay green byte-identically; only rescaled generators change. The legacy floor stays available as an explicitatol=opt-in on every affected function, mirroring how #99 preserved the pre-#97 steady-state tolerance.D1 deliberately does not clamp the gap at zero: after the fix a positive
Re(lambda)is no longer round-off but a genuinely unstable (non-GKSL) mode, and masking it would trade one silent failure for another.#102 — hypothesis-wise evidence matrix (additive, report-only).
ClassificationResult.hypothesis_matrixreports every hypothesis of the A1-A12 taxonomy with supporting measurements, counterevidence, missing evidence and explicit fail-closed claim floors (RESERVED/UNEVALUABLE→UNDEFINED,NOT_SUPPORTED→NOT_EXCLUDED,SUPPORTED→ the verdict it would receive as winner).support_scoreships as the honestly-named ordinal twin ofconfidence, andREACHABLE_A_CLASSESbecomes the coverage denominator with A6/A7/A9 excluded from claims. The priority chain is now declarative (_ladder_spec), so decision, shadow report and matrix derive from one source.Scope
Verification
pytest -qpasses locally — 796 passed (669 at branch point)tests/test_anchors.pyunchanged and green — the Absolute zero-mode tolerance EPS_GAP breaks D1/D3/D4/D16/D19/D20 under rate rescaling — false A11/F4 candidate and negative gap #108 fix is byte-identical at unit scale, which is the load-bearing claim and is asserted, not assumedMANIFEST_SCHEMA.json: not touched — the run-manifest contract is unchanged; report fields are additive with defaults.github/workflows/: not touchedCITATION.cfffollows the repo's own 2026-08-09 convention — unreleased capability sentences go in thePending for the next cutblock, not the abstract of the cited v0.5.0 release, and Absolute zero-mode tolerance EPS_GAP breaks D1/D3/D4/D16/D19/D20 under rate rescaling — false A11/F4 candidate and negative gap #108 is flagged there as a results-changing correction rather than an additionQuality contract
tests/test_zero_mode_scale.pyincludes a discrimination test proving theatolopt-in genuinely restores the pre-Absolute zero-mode tolerance EPS_GAP breaks D1/D3/D4/D16/D19/D20 under rate rescaling — false A11/F4 candidate and negative gap #108 defect; without it a no-op "fix" would pass the invariance assertionsTest plan
tests/test_zero_mode_scale.py(33 tests,c in {1e-10 ... 1e12}): tolerance homogeneity / zero-operator / fail-closed semantics; D1, D3, D4 scaling exactly withc; D19 overlap invariant; no false Mpemba candidate; D16 scaling; Petermann mode count invariant; gap never negative for a GKSL generator while a genuinely unstable mode still reports one; end-to-enddiagnose()verdict invariance undert -> t/c.tests/test_hypothesis_matrix.py(61 tests): taxonomy coverage, SUPPORTED ⟺ ladder-fires equivalence, claim-floor truth table, partial-evidence retention, score-only-when-supported, RFC-8259 serialisation, report-only non-influence.ruffandmypyclean.Reproducibility note
The #108 repro uses two fixed systems with no random component: an amplitude-damped qubit (
H = 0, jumpsigma_-) withrho_0 = |+><+|, and a Rabi-driven damped qubit (H = 0.7 sigma_x, jumpsigma_-, rate 0.4). End-to-end runs useseed=1,bootstrap_B=20,t_grid = linspace(0, 5/c, 64). The #102 matrix introduces no numerical change: it is derived from the evidence dict the classifier already computes.Linked issues
Closes #108. Refs #102 (partial: evidence matrix, rename option 1, reachability gate; the calibration/validation design stays open). Refs #101 (same defect class as #108, disjoint location — the F5 gate switch remains gated on slice C). Refs #70 (A5 no-EXCLUDED semantics reused for the claim floors).
🤖 Generated with Claude Code
https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q