Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
33c31c3
feat(#102): hypothesis-wise evidence matrix, support_score, reachabil…
claude Aug 14, 2026
4492b83
fix(#102): make REACHABLE_A_CLASSES load-bearing via import-time cove…
claude Aug 14, 2026
472c4b2
fix(#108): scale-relative zero-mode separation + Codex review follow-ups
claude Aug 15, 2026
f424769
fix: address Codex review — backward-error zero-mode threshold, requi…
claude Aug 15, 2026
9bc909f
fix: third Codex review — dtype-aware eps, one-sided exp cap, A12 une…
claude Aug 15, 2026
83f3d79
fix: fourth Codex review — grid-relative fit seed, NaN evidence, hone…
claude Aug 15, 2026
8008445
fix: drop redundant isinstance in _is_unavailable (mypy redundant-expr)
claude Aug 15, 2026
1038f7e
fix: revert dtype-derived eps — storage dtype is not the solver's pre…
claude Aug 15, 2026
b4ddca4
fix: enforce the double-precision solver contract + sixth review round
claude Aug 15, 2026
32a5186
fix: reject mismatched support_score/confidence (seventh review round)
claude Aug 15, 2026
3850dc0
fix: eighth review round — fit-boundary validation, NaN encoding-inde…
claude Aug 15, 2026
ccf3c08
fix: keep established catalogs JSON-serializable (ninth review round)
claude Aug 15, 2026
e9d4ff4
fix: tenth review round — A12 missing propagation, stiff-solver limit…
claude Aug 15, 2026
24886cd
fix: eleventh review round — ladder never crashes on partial evidence…
claude Aug 15, 2026
d3c5049
fix(#109,#112,#113): scale-relative Hermiticity validation + structur…
marcohost33-maker Aug 15, 2026
48c7b94
fix: patch certified_eigvals via monkeypatch string target (CodeQL py…
claude Aug 15, 2026
c74464f
fix: twelfth review round — five hardening fixes on the #112/#113 cer…
claude Aug 15, 2026
23ed0b1
fix: local synthetic builders instead of cross-test import (CI Module…
claude Aug 15, 2026
d4f9004
fix: thirteenth review round — one zero-mode scale, certified D24, co…
claude Aug 15, 2026
467ed8f
fix: fourteenth review round — certified Petermann factors, radius fa…
claude Aug 15, 2026
ebdd30b
fix: fifteenth review round — eigenvector-residual gate, ladder conti…
claude Aug 15, 2026
e26df05
fix: sixteenth review round — grid-relative Prony fallback, consumpti…
claude Aug 15, 2026
4a8ae9c
refactor: extract D24 eigenvectors only in the branch that consumes t…
claude Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,151 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- **Zero-mode separation is scale-relative: D1/D3/D4/D9/D16/D19/D20/D24 no
longer depend on the choice of rate unit, removing the ZERO-MODE-INDUCED
verdict flips (issue #108). CHANGES NUMERICAL RESULTS** for generators whose
spectral radius is far from unity.
This does **not** make the mechanism verdict unit-invariant in general: the
A10/F5 branch still gates on the rate-dimensioned `henrici_eta > 1.0`, so
rescaling can still cross that threshold. That remaining limitation is
tracked in #101 (slice C) and stated in the README; #108 removes a different,
independent source of unit dependence.
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, so D1 collapsed to
`0.0` (unconditionally firing the gapless F5 reach leg) and the D19
slowest-mode overlap collapsed to `0.0`, raising a **false A11/F4 Mpemba
candidate** on the highest-priority rung of the classifier. Measured
end-to-end on an amplitude-damped qubit with `rho_0 = |+><+|`: the verdict
moved from `A12/none NOT_EXCLUDED EXPLORATION` at `c = 1` to
`A11/F4 CANDIDATE CONFIRMATION` at `c = 1e-10` — a textbook system promoted
to a quantum-Mpemba candidate by a change of units alone. The issue-#68
non-triviality guard could not correct it, since it depends on the same
`_slowest_mode` helper.
- **large `c`** — the round-off zero mode (`~eps * ||L||`) rose *above* the
floor and was counted as genuine, yielding a **negative spectral gap**
(`-1.4e-6` at `c = 1e10` for a Rabi-driven damped qubit), which is
impossible for a GKSL generator by definition.
The correction is canonical and carries no free parameter: the new
`numerics.scale.spectral_zero_tolerance()` derives the threshold from the
spectrum itself as `ZERO_MODE_EPS_FACTOR * eps * max|lambda|` (spectral
radius — homogeneous of degree one and unitary-similarity invariant, like
`rate_scale`), with `eps` taken from the spectrum's own dtype so a
single-precision solver is not judged by double-precision round-off.
Comment thread
marcohost33-maker marked this conversation as resolved.
Outdated
**All 21 anchor regressions and the full suite stay green**, but the change
is *not* confined to rescaled generators: at unit scale the threshold is
~2.2e-13 rather than the historical 1e-10, so a mode between those two values
is now classified as genuine where it was previously discarded. That
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.
Non-finite spectra now fail closed rather
than silently reporting "no non-zero modes" — including on the legacy path,
since a compatibility switch may restore the old *threshold* but must not
restore silent acceptance of corrupted solver output. The legacy absolute
floor remains 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 scale-relative
filter 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.
The threshold is a multiple of the eigensolver BACKWARD ERROR
(`ZERO_MODE_EPS_FACTOR * eps * max|lambda|`), not a fixed fraction of the
spectral radius. A fixed fraction would impose a dynamic-range ceiling and
discard the genuine slow modes of a METASTABLE generator (A5): with
`1e-10 * max|lambda|`, two damping channels at rates `1.0` and `1e-12` (true
gap `5e-13`) reported a gap of `5e-1`. Calibration measured 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.
Pinned by `tests/test_zero_mode_scale.py` (tests over
`c in {1e-10 ... 1e12}`: dimensionless quantities invariant, rate-valued ones
scaling by `c`, genuine metastable slow modes preserved, no false Mpemba
candidate, no negative gap, and a discrimination test proving the `atol`
opt-in really restores the old defect). The suite also pins the **known
non-invariance** of the mechanism class (A10/F5 still gates on
rate-dimensioned `henrici_eta`, issue #101), so the limitation stays visible
instead of being mistaken for a passing invariance claim.
- **Fit seed rate is grid-relative, not an absolute floor (issue #108 class,
fitting path). CHANGES NUMERICAL RESULTS** for fits on time grids far from
unit span.
`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` — exactly what a slow generator
in small rate units produces — 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 then returned **the floor itself** as the measured rate:
`beta_D == beta_D_linear == 1e-3` on a system whose true rate was `5e-11`,
silently corrupting D5/D17 while every spectral quantity looked healthy.
The floor is now a dimensionless decay depth over the fitted window
(`ALPHA_SEED_FLOOR_FRAC / t_span`, the rate that decays ~0.5 % across the
grid), 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-samples fallback rate is likewise grid-relative
(~one e-folding across the window) instead of an absolute `1.0`.
Model evaluations are additionally bounded in magnitude: capping the exponent
bounds `exp`, but M3a multiplies it by `(A + B t)`, whose magnitude is
unbounded in the parameters, and least-squares then squares the product
inside its own normal equations — overflowing in SciPy's `trf` rather than in
this module.
Residual limitation, deliberately **not** claimed as fixed: at `c = 1e-10`
the least-squares convergence criteria stop tracking the rescaling, so fitted
rates are asserted invariant only over `c in [1e-6, 1e10]`. Tracked in its own
Comment thread
marcohost33-maker marked this conversation as resolved.
Comment thread
marcohost33-maker marked this conversation as resolved.
issue; non-dimensionalising the fit is the proper resolution.

### Added
- **Hypothesis-wise evidence matrix + `support_score` + reachability gate
(issue #102, additive, `claim_status: pending`, no verdict change).**
- `ClassificationResult.hypothesis_matrix` reports one entry for **every**
hypothesis of the A1-A12 taxonomy: each decision rung, the A12 fallback,
and the schema-reserved A6/A7/A9. Each entry records `status`
(`SUPPORTED`/`NOT_SUPPORTED`/`UNEVALUABLE`/`RESERVED`), the `supporting`
conditions with the evidence values they read, the failing conditions as
`counterevidence`, `missing` required evidence keys, an explicit
fail-closed `claim_floor` and the per-class ordinal `support_score`.
Claim-floor rules: `RESERVED`/`UNEVALUABLE` → `UNDEFINED` (no rule / no
evidence — no claim); `NOT_SUPPORTED` → `NOT_EXCLUDED` (a threshold that
did not fire is absence of support, not proof of absence, issue #70 A5);
`SUPPORTED` → the verdict the hypothesis would receive were it the winner
(via the unchanged `_confidence` → `_pick_verdict_tier` → A11
maximally-mixed-floor pipeline), so the winner's floor equals the
reported verdict exactly (pinned by test).
- To keep the matrix from drifting away from the decision, the priority
chain is now defined **declaratively**: `_ladder_spec()` returns rungs of
atomic `_Condition` predicates (each naming the evidence keys it reads),
and `_hypothesis_ladder` (winner + shadow report) and
`hypothesis_evidence_matrix` are both derived from that one spec.
Behaviour-preserving: conditions, order, short-circuit evaluation and all
(class, family, verdict, tier, confidence) outputs are unchanged
(669-test baseline green, anchors untouched).
- **`ClassificationResult.support_score` (issue #102 "rename" option 1):**
the honestly-named twin of `confidence`, with documented, explicitly
NON-probabilistic ordinal semantics (`0.20 < 0.50 < 0.70 < 0.85 < 0.95`
ranks rule strength; no calibration evidence exists). `confidence` stays
as the legacy alias carrying the identical value (pinned equal by test);
a *calibrated* replacement remains gated on the preregistered validation
design in issue #102. Additive + defaulted (`NaN`) so serialised older
results stay valid.
- **Reachability/ontology gate:** new `liouscope.REACHABLE_A_CLASSES`
(taxonomy minus `RESERVED_A_CLASSES`, 9 classes) is the coverage
denominator for any "n of N classes" statement; reserved classes appear
in the matrix as `RESERVED` with a permanent `UNDEFINED` floor and are
excluded from claims. `RESERVED_A_CLASSES` is now exported too.
- The per-hypothesis numerical-uncertainty and perturbation-robustness
columns from the issue-#102 wishlist are deliberately **not faked**; they
remain open in the issue. No `MANIFEST_SCHEMA` bump: the run-manifest
contract is untouched (report fields are additive with defaults).
- Docs: `docs/explanation/layers-and-taxonomy.md` gains the matrix
vocabulary and the reachable-denominator rule; README documents
`hypothesis_matrix`; the tutorial prints `support_score`.
- Tests: `tests/test_hypothesis_matrix.py` (57 tests: taxonomy coverage,
SUPPORTED ⟺ ladder-fires metamorphic equivalence, claim-floor truth
table incl. non-finite `beta_D` and the A11 ensemble override, RFC-8259
serialisation with `inf` evidence, report-only non-influence, legacy
default contract).
- **Branch-shadowing report: `ClassificationResult.triggered_hypotheses`
(issue #102 slice "assess branch shadowing", `claim_status: pending`).** The
classifier resolves by PRIORITY and returns exactly one dominant `a_class`,
Expand Down
13 changes: 12 additions & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,18 @@ abstract: >-
# mechanism hypotheses are reported alongside the dominant label with their
# priority-shadowing status (`ClassificationResult.triggered_hypotheses`,
# issue #102), and the support score is a documented deterministic heuristic
# rather than a calibrated probability.
# rather than a calibrated probability; a hypothesis-wise evidence matrix
# spanning the full A1-A12 taxonomy reports supporting measurements,
# counterevidence, missing evidence and explicit fail-closed claim floors, with
# the schema-reserved classes excluded from claims and coverage denominators
# (`ClassificationResult.hypothesis_matrix`, issue #102); and the zero-mode
# separation underlying D1/D3/D4/D9/D16/D19/D20/D24 is scale-relative, so those
# diagnostics no longer depend on the choice of rate unit (issue #108). The
# last item CHANGES NUMERICAL RESULTS for generators whose spectral radius is
# far from unity and must be described as a correction, not merely an addition,
# when the release is cut. It must NOT be described as making the mechanism
# verdict unit-invariant in general: the A10/F5 branch still gates on the
# rate-dimensioned henrici_eta threshold (open in #101).
keywords:
- open quantum systems
- Lindblad
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ Letting it drive decisions is deferred to the preregistered calibration study
in issue #102. The field is additive and defaults to `()`, so results
serialised by older versions stay valid.

`hypothesis_matrix` (issue #102) goes one step further and reports **every**
hypothesis of the taxonomy — including the ones that did *not* fire and the
schema-reserved A6/A7/A9 — each with its supporting measurements,
counterevidence, missing required evidence, an explicit fail-closed
`claim_floor` (`RESERVED`/`UNEVALUABLE` → `UNDEFINED`, `NOT_SUPPORTED` →
`NOT_EXCLUDED`, `SUPPORTED` → the verdict it would receive as winner) and its
ordinal `support_score`. Decision ladder and matrix are derived from the same
Comment thread
marcohost33-maker marked this conversation as resolved.
Outdated
declarative rung spec, so the audit surface cannot drift from the decision;
the matrix is report-only. See
`docs/explanation/layers-and-taxonomy.md` for the full vocabulary.

The same example with a 1D lattice geometry:

```python
Expand Down Expand Up @@ -271,7 +282,8 @@ require physics-domain interpretation; no claim of universality is made beyond t
covered by the V1-V5 validation systems.

Two honesty notes on the classifier surface (tracked in issues #101/#102):
`classification.confidence` is a deterministic **heuristic support score**,
`classification.support_score` (and its legacy alias `confidence`) is a
deterministic **ordinal heuristic**,
not a calibrated probability; and the A10/F5 (phantom-relaxation) verdict
path is **not yet invariant under a change of rate units** — the
scale-relative successor diagnostics (`henrici_relative`, `kreiss_scaled`,
Expand Down
59 changes: 52 additions & 7 deletions docs/explanation/layers-and-taxonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,62 @@ The design rule behind all three bullets is the same: **fail closed.**
When evidence is missing, malformed, or merely asserted, the report degrades
to the weaker claim rather than trusting the caller.

## `confidence` is a heuristic support score, not a probability
## `support_score` (né `confidence`) is ordinal, not a probability

`ClassificationResult.confidence` is a **deterministic, rule-based support
`ClassificationResult.support_score` is a **deterministic, rule-based support
score** in `[0, 1]` (fixed values such as `0.70`, `0.85`, `0.95` attached to
specific evidence combinations). It is **not** a posterior probability and it
has **not** been calibrated against held-out labelled reference families — do
not read `0.85` as "85 % probability the label is right". Calibrated,
hypothesis-wise evidence reporting is tracked in issue #102; until that
lands, treat the number as an ordinal ranking of rule strength and rely on
the *verdict/tier* vocabulary (which is evidence-graded and fail-closed) for
claims.
not read `0.85` as "85 % probability the label is right". Treat the number as
an *ordinal ranking of rule strength* (`0.20 < 0.50 < 0.70 < 0.85 < 0.95`)
and rely on the *verdict/tier* vocabulary (which is evidence-graded and
fail-closed) for claims.

`confidence` is the **legacy alias** for the same value: issue #102 offered
rename-with-honest-semantics or calibrate, and the rename shipped first
(option 1). Both fields carry identical values (pinned by test); a genuinely
*calibrated* score would have to pass the preregistered validation design in
issue #102 (family-split calibration/holdout sets, reliability curves,
adversarial negatives) before it may replace the ordinal one.

## The hypothesis evidence matrix

`ClassificationResult.hypothesis_matrix` (issue #102) reports, for **every**
hypothesis of the taxonomy — each decision rung, the A12 fallback, and the
schema-reserved classes — one entry with:
Comment thread
marcohost33-maker marked this conversation as resolved.

| key | meaning |
|---|---|
| `status` | `SUPPORTED` / `NOT_SUPPORTED` / `UNEVALUABLE` / `RESERVED` |
| `supporting` | atomic conditions that hold, with the evidence values read |
| `counterevidence` | conditions that fail, with their values |
| `missing` | required evidence keys absent from this run |
| `claim_floor` | what this run could claim about the hypothesis |
| `support_score` | the ordinal score this class would receive |

The claim floor follows explicit, fail-closed rules: `RESERVED` and
`UNEVALUABLE` floor to `UNDEFINED` (no rule / no evidence — no claim);
`NOT_SUPPORTED` floors to `NOT_EXCLUDED` (a threshold that did not fire is
absence of support, **not** proof of absence); `SUPPORTED` receives the
verdict the hypothesis would get were it the winner — so for the reported
class the floor equals the reported verdict exactly (pinned by test).

Both the decision ladder and the matrix are derived from one declarative
rung specification (`_ladder_spec`), so the audit surface cannot drift from
the decision. The matrix is **report-only**: no verdict consumes it, and the
per-hypothesis numerical-uncertainty / perturbation-robustness columns from
the issue-#102 wishlist are *not faked* — they remain open until the
underlying machinery exists.

### Reserved classes are excluded from coverage denominators

A6/A7/A9 have no code-backed decision rule (`RESERVED_A_CLASSES` records the
per-class rationale). They appear in the matrix as `RESERVED` with a
permanent `UNDEFINED` claim floor, and any "n of N classes" coverage
statement must use the reachable denominator
`liouscope.REACHABLE_A_CLASSES` (9 classes), not the full taxonomy — the
reachability contract is pinned by an AST-level test that scans the actual
decision source.

## Known limitation: the F5 decision path is not rate-unit invariant

Expand Down
3 changes: 2 additions & 1 deletion docs/tutorials/first-diagnostic-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ print(r.classification.a_class) # one of "A1".."A12"
print(r.classification.f_family) # gap-failure family "F1".."F5" or "none"
print(r.classification.verdict) # CONFIRMED / CANDIDATE / NOT_EXCLUDED / UNDEFINED
print(r.classification.tier) # e.g. EXPLORATION vs PUBLICATION_GRADE
print(r.classification.confidence) # heuristic support score 0..1 (NOT calibrated)
print(r.classification.support_score) # ordinal heuristic 0..1 (NOT calibrated;
# `confidence` is the legacy alias)
```

How to read the classification:
Expand Down
4 changes: 4 additions & 0 deletions src/liouscope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
DIAGNOSTIC_SCHEMA_VERSION,
F_FAMILIES,
F_FAMILY_DESCRIPTIONS,
REACHABLE_A_CLASSES,
RESERVED_A_CLASSES,
Comment thread
marcohost33-maker marked this conversation as resolved.
TAXONOMY_VERSION,
)
from ._diagnostics import diagnose
Expand Down Expand Up @@ -54,6 +56,8 @@
"ENSEMBLE_MPEMBA_CONFIRMED",
"F_FAMILIES",
"F_FAMILY_DESCRIPTIONS",
"REACHABLE_A_CLASSES",
"RESERVED_A_CLASSES",
"TAXONOMY_VERSION",
"ClassificationResult",
"DiagnosticReport",
Expand Down
Loading
Loading