Skip to content

fix(#108): scale-relative zero-mode separation + feat(#102): hypothesis evidence matrix - #107

Merged
marcohost33-maker merged 23 commits into
mainfrom
claude/liouscope-repo-analysis-xgztfd
Sep 8, 2026
Merged

fix(#108): scale-relative zero-mode separation + feat(#102): hypothesis evidence matrix#107
marcohost33-maker merged 23 commits into
mainfrom
claude/liouscope-repo-analysis-xgztfd

Conversation

@marcohost33-maker

@marcohost33-maker marcohost33-maker commented Aug 14, 2026

Copy link
Copy Markdown
Owner

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 under L -> cL, and it was found by auditing the layer the #101 conformance suite does not reach:

  • small c — every genuine mode falls below the floor, so D1 collapses to 0.0 (unconditionally firing the gapless F5 reach leg) and the D19 slowest-mode overlap collapses to 0.0, raising a false A11/F4 Mpemba candidate on the highest-priority rung. Measured end-to-end on an amplitude-damped qubit with rho_0 = |+><+|: A12/none NOT_EXCLUDED EXPLORATION at c = 1 became A11/F4 CANDIDATE CONFIRMATION at c = 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_mode helper.
  • large c — the round-off zero mode (~eps * ||L||) rises above the floor and counts as genuine, producing a negative spectral gap (-1.4e-6 at c = 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 as ZERO_MODE_RTOL * max|lambda| (spectral radius: homogeneous of degree one, unitary-similarity invariant, consistent with rate_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 explicit atol= 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_matrix reports every hypothesis of the A1-A12 taxonomy with supporting measurements, counterevidence, missing evidence and explicit fail-closed claim floors (RESERVED/UNEVALUABLEUNDEFINED, NOT_SUPPORTEDNOT_EXCLUDED, SUPPORTED → the verdict it would receive as winner). support_score ships as the honestly-named ordinal twin of confidence, and REACHABLE_A_CLASSES becomes 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

Quality contract

Test 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 with c; 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-end diagnose() verdict invariance under t -> 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.
  • Full suite 796 passed; ruff and mypy clean.

Reproducibility note

The #108 repro uses two fixed systems with no random component: an amplitude-damped qubit (H = 0, jump sigma_-) with rho_0 = |+><+|, and a Rabi-driven damped qubit (H = 0.7 sigma_x, jump sigma_-, rate 0.4). End-to-end runs use seed=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

…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
Comment thread src/liouscope/_consts.py Dismissed
…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
@marcohost33-maker
marcohost33-maker marked this pull request as ready for review August 14, 2026 23:53

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/diagnostics/classification.py
Comment thread docs/explanation/layers-and-taxonomy.md
Comment thread src/liouscope/diagnostics/classification.py Outdated
Comment thread src/liouscope/diagnostics/classification.py
## #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
@marcohost33-maker marcohost33-maker changed the title feat(#102): hypothesis-wise evidence matrix, support_score, A6/A7/A9 reachability gate fix(#108): scale-relative zero-mode separation + feat(#102): hypothesis evidence matrix Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Response to the Codex review (commit 472c4b2)

Two findings adopted, one adopted in the form the repo's own convention requires, one declined with reasoning.

✅ P2 "Avoid assigning winner-only scores to failed hypotheses" — adopted

Correct and well-spotted. _confidence answers "what grade would this class get as the winner" and keys on a subset of each firing rule, so kreiss = 11 with petermann_max = 1 failed F1 while the matrix still printed A3 at 0.85 next to that rung's own counterevidence. A score without support is not a weaker claim, it is a misleading one. support_score is now None unless the hypothesis is SUPPORTED (matching the existing RESERVED treatment), pinned by test_unsupported_hypotheses_carry_no_support_score, which asserts the invariant across the whole matrix rather than just the one example.

✅ P2 "Preserve available evidence when a rung is partially missing" — adopted

Also correct, and it cut against the matrix's own purpose: a run with a measured kreiss but no petermann_max discarded the genuine Kreiss support. Conditions whose own inputs are present are now evaluated even when a sibling is unevaluable; the rung status still degrades to UNEVALUABLE, so partial evidence never promotes a hypothesis. Pinned by test_partially_missing_rung_keeps_its_evaluable_evidence.

✅ P1 "Update citation metadata" — adopted, but in the Pending block, not the abstract

The underlying point is right and now matters more than when the review was written, because this PR also carries a results-changing correction (#108). But the change belongs in a different place than suggested: CITATION.cff carries an explicit convention added 2026-08-09 —

abstract describes the surface of the CITED release only […] Capability sentences for work that is merged but not yet released do NOT belong here; they would credit the published artifact with results it cannot produce.

Editing the abstract would credit the published, DOI-frozen v0.5.0 artifact with capabilities it cannot produce. The Pending for the next cut block is the convention-sanctioned location, and it now records the evidence matrix and flags #108 explicitly as a correction that changes numerical results, so the next release cut cannot describe it as a mere addition.

❌ P1 "Validate ensemble evidence before lifting the A11 floor" — declined, with a documentation fix

The AGENTS.md contract cited is real and is already enforced — at the API boundary, not in the report layer. liouscope.diagnose() calls reject_legacy_ensemble_confirmation(), so a bare ensemble_confirmation=True already raises fail-closed; lifting the floor requires a typed EnsembleEvidence with its digests, gate status and dual attestations.

hypothesis_evidence_matrix reads the value that classify_mechanism itself writes into the evidence dict from that already-validated argument. It introduces no new path: classify_mechanism and _apply_single_state_maxmix_floor likewise take the override as a plain argument, so a hand-built dict passed directly to any of them carries exactly the trust its caller gives it. Requiring an EnsembleEvidence object in the report function alone would make it stricter than the decision function it describes — and decoupling the two is precisely what this PR set out to prevent, since the matrix's value depends on mirroring the decision exactly.

The test the review points at documents that forwarding (floor applied without the override, suppressed with it); it does not demonstrate a bypass. What was genuinely missing was a stated trust boundary, so hypothesis_evidence_matrix's docstring now names it: the matrix reports the supplied ev, it does not re-validate it, and the typed check lives at the diagnose() boundary.

If the intent is to harden the internal boundary as well — making classify_mechanism itself accept only EnsembleEvidence — that is a worthwhile but separate, API-breaking change, and it should be its own PR against the A11 contract rather than a side effect of a reporting slice.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/numerics/scale.py Outdated
Comment thread tests/test_zero_mode_scale.py Outdated
Comment thread src/liouscope/diagnostics/classification.py
Comment thread src/liouscope/numerics/scale.py Outdated
Comment thread CHANGELOG.md Outdated
Comment thread src/liouscope/diagnostics/spectral.py
…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

Copy link
Copy Markdown
Owner Author

Response to the second Codex review (commit f424769)

Four adopted — one of them a genuine defect in my own fix — one already resolved, one declined.

✅ P1 "Preserve genuine slow modes below the relative cutoff" — adopted; this was a real regression

Correct, and the most valuable finding in either review. My first cut used tol = 1e-10 * max|lambda|, which silently imposes a fixed dynamic-range ceiling of 1e10 — and a wide separation of physical rates is not an artefact but the defining feature of the metastable (A5) systems this library exists to study. Reproduced exactly as described:

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.

M0M3b 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/numerics/scale.py
Comment thread CHANGELOG.md Outdated
Comment thread src/liouscope/fitting/models.py Outdated
Comment thread src/liouscope/diagnostics/classification.py Outdated
Comment thread src/liouscope/_types.py Outdated
…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

Copy link
Copy Markdown
Owner Author

Response to the third Codex review (commit 9bc909f)

All five adopted. Two were genuine defects introduced by the previous commit — both reproduced before fixing.

✅ P1 "Match zero-mode epsilon to the eigensolver dtype"

Correct. The threshold is deliberately a multiple of the eigensolver backward error, but eps was hard-coded to float64 — so for a single-precision spectrum (round-off ~1e-7 relative) the threshold sat about nine decades below that solver's own noise, and the numerical steady-state eigenvalue survived as physical. That is exactly the negative-gap failure the function exists to prevent, reintroduced through the back door.

eps now comes from the real dtype underlying the spectrum (complex64 → float32), with a float64 fallback for integer/object input. Pinned by test_tolerance_tracks_the_spectrum_dtype (asserts the ratio equals eps32/eps64 rather than just "larger") and test_single_precision_zero_mode_is_still_filtered for the end-to-end consequence.

Worth noting for the record: on this machine np.linalg.eigvals promotes a complex64 input to double internally, so the defect did not reproduce through that particular entry point. The finding is right regardless — spectral_zero_tolerance is public and takes a spectrum from any source, and correctness must not rest on one solver's promotion behaviour.

✅ P2 "Preserve the true decaying tail when clipping exponentials"

Correct, and the more damaging of the two. Measured:

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread tests/test_zero_mode_scale.py Outdated
Comment thread src/liouscope/diagnostics/classification.py
claude added 2 commits August 15, 2026 01:17
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/numerics/scale.py
…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

Copy link
Copy Markdown
Owner Author

Response to the fifth Codex review (commit 1038f7e)

Adopted — and it caught a defect that I introduced by acting on an earlier review finding I had been unable to reproduce. Worth recording plainly, because the sequence is the lesson.

✅ P1 "Preserve resolvable single-precision slow modes" — adopted, by reverting the dtype branch

Reproduced exactly as described:

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/diagnostics/spectral.py Outdated
Comment thread CHANGELOG.md Outdated
Comment thread src/liouscope/fitting/models.py Outdated
Comment thread src/liouscope/__init__.py
## 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

Copy link
Copy Markdown
Owner Author

Correction to my fifth-round comment + response to the sixth review (commit b4ddca4)

⚠️ Correction of my own claim

In the fifth-round response I wrote: "NumPy/SciPy solve eigenproblems in double regardless of the input dtype." That statement is half wrong, and the wrong half is the half this library uses. Measured on a generic 40×40 matrix whose entries are not exactly representable in float32 (so single-precision round-off is forced to show):

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

  1. 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.
  2. Expose rtol — adopted on liouvillian_gap / oscillating_mode_gap / spectral_spread / lep_proximity. Pinned with the reviewer's own example: a displaced stationary mode at 1e-7·max|λ| from an external single-precision solver yields -1e-7 under the double-calibrated default and the correct 0.2 with rtol = 1e3·(eps32/eps64) — scale-relative, no absolute floor.
  3. Factor multiplication overflow — adopted: an amplitude probe like 1e200 made the product inf before _bounded ever saw it, and 0·inf yields 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 exactly what the optimiser needs to hear.
  4. Immutable taxonomy mappings — adopted: RESERVED_A_CLASSES.pop("A6") could silently desynchronise the taxonomy from the import-time coverage guard, which compares against the precomputed REACHABLE_A_CLASSES tuple. All four exported taxonomy mappings are now MappingProxyType, 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/_types.py
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/_zhou.py Outdated
Comment thread src/liouscope/numerics/linalg.py
Comment thread src/liouscope/diagnostics/classification.py Outdated
…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

Copy link
Copy Markdown
Owner Author

Thirteenth review round: all three findings reproduced first, then adopted in d4f9004.

One zero-mode scale (P1, numerics/linalg.py) — Confirmed exactly as described: on 4×4 trace-preserving constructions with ||L||₂/max|λ| ≥ ~1e3, the certified-resolved stationary residual sits above the radius-based spectral_zero_tolerance and survived the downstream filter (20/20 reproductions at skew ≤ 1e-3; D1 ≈ 1e-12 and occasionally negative against a true gap of 1). Fix: new shared helper operator_zero_tolerance (pinned equal to the certificate bound by test); every consumer that holds the operator now filters with it — spectral layer (D1/D3/D4, has_complex_pairs), Mpemba layer (_certified_decomposition carries the bound), petermann_factors (which was returning four eigenmodes where three exist), and D24. Genuine slow modes inside the coarser band are not silently swallowed — the #113 ambiguity split floors them as resolved=False. The radius default stays for spectrum-only call sites; caller atol still wins. A deterministic mismatch fixture is frozen as exact float64 literals with a guard-skip should a future LAPACK resolve it below the radius tolerance.

Certify the D24 eigensolve (P2, _zhou.py) — Reproduced: gap = 7.28e-6 raw vs 1.074e-5 certified on the stiff fixture. The recomputation is now routed through certified_eig with the certificate bound as filter; an applicable-but-unresolved certificate returns an unconverged record (inf bounds, NaN gap/K), honouring caller-supplied values exactly like the existing no-nonzero-modes branch.

Conclusive refutation beats UNEVALUABLE (P2, classification.py) — Adopted with the suggested rule verbatim: UNEVALUABLE only when no evaluated condition is false and missing evidence could still flip the rung to supported. kreiss = 1 with petermann_max missing now reads NOT_SUPPORTED with the counterevidence recorded and the absent key kept in missing for the audit trail; when every partially collected rung is conclusively refuted, the A12 fallback is SUPPORTED — matching the ladder's deterministic A12, restoring the matrix/decision equivalence. The NaN-encoding-equivalence test now pins the genuinely open case via gns_certified = 1.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/diagnostics/spectral.py Outdated
Comment thread src/liouscope/diagnostics/nonnormality.py Outdated
…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

Copy link
Copy Markdown
Owner Author

Fourteenth review round: both findings reproduced first, then adopted in 467ed8f.

Certify the Petermann eigendecomposition (P1, nonnormality.py) — Reproduced exactly: on the stiff #112 fixture the raw zgeev path reported 16 "non-zero" modes with petermann_max ≈ 622.69, while the certified route (dgeev-real) returns the physical 15 modes with ≈ 2.0 — and indeed no cutoff can restore an eigenvalue that is absent from the raw spectrum. petermann_factors now consumes certified_eig. On an applicable-but-unresolved certificate both returned arrays are the NaN unavailable sentinel (with a RuntimeWarning), so petermann_max becomes NaN and the F1 rung 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, spectral.py + _zhou.py) — Reproduced with your construction (diag(-1,-2,-3,-4) plus a 1e16 off-diagonal): certificate.bound ≈ 2.2e3 exceeds the whole spectrum, D24 reported gap 0.0 / unconverged, and D1 through the layer (with a supplied steady state) likewise 0.0 for a true gap of 1. Every round-13 call site — spectral layer (D1/D3/D4, has_complex_pairs), Mpemba layer, D9, and D24 — now uses the certificate bound only when certificate.applicable and falls back to the radius-based #108 tolerance otherwise.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/numerics/linalg.py
Comment thread src/liouscope/numerics/linalg.py
…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

Copy link
Copy Markdown
Owner Author

Fifteenth review round: both findings reproduced by scan (4000 random stiff four-level networks each), then adopted in ebdd30b.

Validate eigenvectors before certifying (P1, numerics/linalg.py) — Confirmed: valid stiff classical networks reach certified=True, resolved=True while the slow mode's left eigenvector has residual up to 3.1e-4 against a bound of 3.2e-7 (right vectors fine). The gate is per mode and relative — r_j ≤ max(VECTOR_RESIDUAL_REL_MAX · |λ_j|, bound) with the larger of the unit-normalised left/right residuals — because r/|λ| is the first-order relative error scale of anything computed from the pair, and a single operator-scale cutoff does not separate the measured populations (healthy zgeev over 301 generators ≤ 2.1e-10; legitimate dgeev-real repairs 1.2e-2…6.5e-2; corrupt decompositions 2.2e-1…2.9e1). The boundary at 1e-1 is calibrated and documented at the constant; the extreme repair at spectral spread ~4e11 carrying a 79% slow-mode error deliberately fails closed. A vector-failed candidate continues the ladder; when no route passes, certified=False with the offending residual reported, and D19/D9/D24 withhold. certified_eigvals is deliberately untouched — the eigenvalues of such a decomposition remain usable for D1/D3/D4, and a new test pins exactly that division.

Continue past ambiguous repair candidates (P2, both ladders) — Confirmed 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. The healthy path is unchanged and still lazy (pinned by the existing Schur-counter test), and certified_eig follows the same rule.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread CHANGELOG.md
Comment thread src/liouscope/_zhou.py Outdated
Comment thread CHANGELOG.md Outdated
Comment thread src/liouscope/diagnostics/nonnormality.py
…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

Copy link
Copy Markdown
Owner Author

Sixteenth review round: all four findings reproduced first, then adopted in e26df05.

Grid-relative Prony fallback (P2, fitting/prony.py) — Reproduced exactly: on a valid non-uniform grid spanning t = 1e7, the absolute (beta, omega) = (1, 1) fallback made the M3b fit "converge" (success=True) onto the seed itself — [A, beta, omega] ≈ [0.006, 1, 1] for true values 5e-8 / 2e-6. 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 (the ALPHA_SEED_FLOOR_FRAC / M3A_SLOPE_SEED_FRAC convention). The measured non-uniform scenario is now pinned unit-invariant across c ∈ [1e-6, 1e6] end to end.

Eigenvalue certificate for the gap-only D24 path (P2, _zhou.py) — Reproduced: with petermann_factor=2.0 supplied, the fixture's eigenvalue certificate resolves a usable gap of 3.32e-6 while only the round-15 eigenvector gate fails, yet D24 returned an unconverged NaN record. The certificate now matches what is consumed: certified_eigvals when only the gap is recomputed, the stricter certified_eig when the Petermann factor is (that path stays fail-closed, pinned).

Decouple D11 from the D9 vector gate (P2, nonnormality.py) — Reproduced with a ladder-Hamiltonian variant of the bad-vector network: the certified spectrum carries a length-3 imaginary-part progression, D9 is withheld, and D11 reported the fabricated default 1. D11's input is now recomputed from the certified spectrum (it consumes only eigenvalues); when the eigenvalues themselves are unresolved, bohr_ap_length is NaN — the field is now float-typed with NaN as the documented unavailable sentinel.

Builder atol release note (P2, CHANGELOG.md) — Correct: the opt-in exists on the standalone is_hermitian predicate only. The #109 entry now says so explicitly and notes that the builders' relative gate cannot be bypassed, instead of advertising an unavailable builder argument.

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

Comment thread src/liouscope/_zhou.py Fixed
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +351 to +352
ambiguous = int(np.count_nonzero(in_band & (magnitudes > split)))
if ambiguous == 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) & ~nonzero

The 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread CHANGELOG.md
@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Review-Threads durchgesehen (2026-08-18): 38 von 54 aufgelöst, 16 bleiben

Der PR war durch required_conversation_resolution blockiert — bei 52 offenen
Threads, von denen keiner einzige eine Antwort trug. 16 Review-Runden hatten
Code geändert, aber nie auf die Befunde geantwortet.

Aufgelöst, jeweils mit Begründung im Thread:

Nicht aufgelöst: 16 Befunde am aktuellen Stand#118. Jeder davon ist eine
inhaltliche Entscheidung (NaN-Semantik in Pflicht-Evidenz, Beschränkung negativer
Zerfallsraten, mehrdeutige Reparatur-Routen), keine Nachpflege. Sie im Stapel zu
schliessen hiesse, sie zu raten.

Bewertung: Der PR ist nicht merge-reif, und das liegt nicht an der Formalie.
Mit +5541/−175 über 16 Runden ist er zu gross geworden, um in einem Zug beurteilt
zu werden — ein Zuschnitt entlang der 16 Punkte wäre der geradere Weg als eine
17. Runde. #115 hängt daran und kann erst danach.

Grundlinie lokal geprüft: 942 Tests grün (.venv, 4 min).

@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Zuschnitt liegt bereit — zwei Zweige statt einer 17. Runde

Wie im Kommentar oben angekündigt ist dieser PR zugeschnitten. Die Naht liegt
dort, wo sie schon im Titel steht: #108 (Zero-Mode-Skala) gegen #102
(Evidenz-Matrix)
.

Teil Zweig Inhalt lokal
A split/a-zero-mode-zertifikat Nullmode-Zertifikat, skalenrelative Trennung, Hermitizität, Diagnostik darauf (#108/#109/#112/#113) + AGENTS.md (#110) 855 Tests grün
B split/b-hypothesen-matrix Hypothesenweise Evidenz-Matrix (#102), baut auf A 942 Tests grün

Der Vollständigkeitsbeleg: git diff split/b-hypothesen-matrix <dieser Zweig>
ist leer. Beide Teile zusammen ergeben exakt den Stand dieses PRs — nichts
ist unterwegs verlorengegangen, nichts hinzugekommen. Und 942 ist dieselbe
Testzahl wie hier.

Was der Schnitt gekostet hat: genau einen Test von 856.
test_unresolved_certificate_floors_the_verdict_to_undefined prüft die Wirkung
des Zertifikats auf den Verdikt und braucht dafür die Klassifikations-Schicht.
Er ist in Teil A nicht gelöscht, sondern an seiner Stelle mit Begründung
vermerkt, und in Teil B wieder scharf.

Was der Schnitt gezeigt hat: die Schichtgrenze verläuft anders, als der
PR-Titel vermuten lässt. _types.py und _consts.py tragen Felder für beide
Themen, und die Fundament-Tests laufen durch die Diagnostik — ein Zuschnitt
allein entlang numerics/ scheitert mit 23 roten Tests. Das ist kein
Nebenbefund, sondern der Grund, warum dieser PR so gross werden konnte.

Die Pull Requests sind bewusst noch nicht eröffnet. Eine PR-Eröffnung gegen
main startet hier fünf Workflows über fünf Python-Versionen, und der
Budget-Freeze steht. Die Zweige sind gepusht und lokal geprüft; der Klick
gehört Marco.

Dieser PR sollte geschlossen werden, sobald die beiden Teile offen sind — die
38 hier aufgelösten Threads und ihre Begründungen bleiben im Verlauf erhalten.
Die 16 offenen Sachfragen stehen in #118 und wandern nicht mit.

#115 hängt an Teil B, nicht mehr an diesem PR: es ändert das
Relaxations-Fenster und braucht die _safe_exp/_bounded-Wächter aus dem
Zuschnitt.

@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Review-Fäden nachgezählt: 16 offen → 3

Die 16 offenen Review-Fäden dieses Pull Requests wurden gegen den heutigen Kopf des
Zweigs (4a8ae9c09e2d682d9da0ad69b8390e6f9adf92f5) durchgesehen. 13 davon treffen
dort nicht mehr zu und sind aufgelöst worden
— sie waren offen geblieben, weil
niemand sie geschlossen hatte, nicht weil sie unbeantwortet waren.

Die vollständige Zuordnung Befund → Fundstelle steht in Issue #118.

Zwölf sind im Code beantwortet, jeweils mit Datei und Zeile belegt. Bei einem
weiteren trägt die Prämisse nicht: der Befund begründet sich mit
„Because ClassificationResult is not keyword-only" — die Klasse ist
kw_only=True (_types.py:277), und war es bereits vor dem Befund. Die
beschriebene Verschiebung der Konstruktor-Argumente kann daher nicht eintreten.

Was offen bleibt

Befund Stand am Kopf
Constrain decay rates instead of flattening negative probes fitting/models.py:53 — der einseitige Exponenten-Deckel steht unverändert, und die produktiven Aufrufe fitten weiterhin ohne Schranken
Do not treat every sub-split decay mode as zero numerics/linalg.py:342 und :531 — die Zugehörigkeit wird weiterhin rein über den globalen Betragsschnitt entschieden
Disclose that the certificate changes verdicts CHANGELOG.md:215 nennt das Feld „report-only", während classify_mechanism es auswertet und ein unaufgelöstes Zertifikat auf UNDEFINED zwingt

Zwei davon sind numerisch, eines ist eine Textkorrektur.

Methodischer Vorbehalt

Die dreizehn sind durch Lesen des Codes am genannten Stand belegt, nicht durch
erneutes Ausführen der jeweiligen Reproduktionen. Vier wurden zusätzlich unabhängig
an der Quelle nachgeprüft (_consts.py:126, fitting/models.py:211-219,
fitting/gls.py:82-88, _types.py:277); zur Kontrolle wurde derselbe Weg auf einen
der noch offenen Punkte angewandt, der erwartungsgemäss nicht als erledigt
erschien. Für die beiden Invarianz-Befunde zu den Startwerten ist belegt, dass die
verlangte gitter-relative Skalierung im Code steht — der numerische Nachweis über das
behauptete Intervall steht aus.

Wer einen der aufgelösten Fäden anders sieht, kann ihn jederzeit wieder öffnen.

@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Antwort auf "Do not treat every sub-split decay mode as zero"

Ihr Gegenbeispiel läuft auf dem Nachfolgezweig (pr107-fix, PR #121) korrekt durch — wir haben es wörtlich nachgefahren, nicht nur die Änderung gelesen.

H = diag(0,1), Raten 1e-15 / 1e-14, zero_tolerance = 5e-16: die echte Populationsmode bei 1e-15 überlebt, D1 = 1.0000e-15 statt zuvor 2.05e-14.

Der Grund ist, dass die Zugehörigkeit zur Null-Mode nicht mehr über den globalen eps*||L||-Split entschieden wird: certified_nonzero_modes zertifiziert Moden aus dem a-posteriori-Rückwärtsfehler, und refine_zero_band nimmt sie aus dem Band heraus — src/liouscope/numerics/linalg.py:478, in_band = (magnitudes <= bound) & ~nonzero.

Rücknahme-Probe: entfernt man & ~nonzero, wird test_slow_mode_below_the_norm_scaled_band_survives_as_the_gap rot; wiederhergestellt grün (protokolliert, Stand d7086ca).

Der Thread hängt an diesem PR, der Code dazu liegt in PR #121 — dort ist er Teil der Sammelantwort.

🤖 Generated with Claude Code

@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Status 2026-09-07 — do not merge this separately; it is strictly contained in two other PRs

Measured, not inferred (git merge-base --is-ancestor, git rev-list --left-right --count):

#107 head 4a8ae9c is an ancestor of  #121 head bac5fe0d   -> true
#107 head 4a8ae9c is an ancestor of  #127 head e0a6479    -> true
commits unique to #107 vs #121 head                       -> 0

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 main records.

Current state: mergeStateStatus = DIRTY, mergeable = CONFLICTING against main. The green rollup this PR is known for — 20 check-runs, 0 non-success at 4a8ae9c — is real and still there, but it describes a tree that is 39 commits behind pr107-fix.

Closes #108 is correctly set and verified via closingIssuesReferences. That band is the reason the merge method matters:

#107's head SHA must remain reachable from main for GitHub to close this PR and fire Closes #108. A merge commit does that. A squash or a rebase rewrites the SHA, and then #107 stays open as a zombie with #108 still open behind it — even though every line of it landed.

What Marco has to decide

Merging #121 with a merge commit retires this PR and #108 automatically, because 4a8ae9c becomes reachable. That is the cheapest path and it needs no work here. Resolving the conflicts on this branch would be effort spent on a tree that is superseded.

Read-only measurement. No push, no merge, no rebase, nothing resolved.

@marcohost33-maker
marcohost33-maker merged commit 4a8ae9c into main Sep 8, 2026
20 checks passed
marcohost33-maker added a commit that referenced this pull request Sep 8, 2026
fix(#118): close the three review findings that survived PR #107
@marcohost33-maker
marcohost33-maker deleted the claude/liouscope-repo-analysis-xgztfd branch September 8, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:claude PR authored by Claude Code methodology

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Absolute zero-mode tolerance EPS_GAP breaks D1/D3/D4/D16/D19/D20 under rate rescaling — false A11/F4 candidate and negative gap

3 participants