All notable changes to LiouScope are documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
D16 was published from the spectrum for which D1/D3/D4 had just been withheld (PR #121, round-23 review, finding 13). Where the zero-mode certificate is applicable but unresolved, the spectral layer reports D1, D3, D4 as NaN and
has_complex_pairsasNone, because the candidate spectrum is explicitly untrustworthy.diagnose()nevertheless passed those same eigenvalues intolep_proximity, and the LEP layer returned a finite D16. Measured on the issue-#113 stiff fixture:lep_proximity = 0.0with 51 candidate pairs -- and0.0is not a neutral number but the coalescence limit, i.e. the strongest exceptional-point signal the diagnostic can emit, manufactured out of slow modes sitting below the eigensolver's backward error. A missing or ambiguous slow mode changes the closest eigenvalue pair directly, so the number was never a measurement.compute_lep_layergainsspectral_resolved(defaultTrue, so callers who say nothing keep measuring) and withholds D16 as NaN withlep_candidate_count=Nonewhen it isFalse;LepResult.lep_candidate_countis thereforeint | None, in parity withSpectralResult.has_complex_pairs.diagnose()derives the flag from the sameapplicable and not resolvedpredicate the spectral layer uses, so the whole run withholds on one condition rather than on two. The withheld value is deliberately distinguishable from both measured extremes:0.0(coalesced) andinf(no pair) are answers, NaN is the absence of one, and_strip_unavailablein the classifier keys on exactly that. D17 and D18 stay measured -- D17 already inherits the withheld gap, D18 is computed from the operator.The certificate floor downstream caps the classifier's verdict, which is a different guarantee from not publishing the number:
LepResultis returned to callers and persisted as audit metadata. -
A non-trace-preserving operator became certificate-applicable when it was written in small enough rate units (PR #121, round-23 review, finding 12).
trace_preservation_defectsquares its entries before summing, so fordiag([0, -1e-200, -2e-200, -3e-200])every square falls below the smallest subnormal and the pair comes back(0.0, 0.0). The applicability gate then evaluates0.0 > tp_rtol * max(0.0, tiny), which is False for every operator, and both certificate APIs reportedapplicable=True, certified=Truefor an operator whose relative trace defect is3 / sqrt(14) ~ 0.80. The same operator at1e-150was correctly refused, so a pure change of rate unit decided whether the object counted as a legal generator. Both norms are now recomputed on a copy scaled by the largest finite component whenever the reference scale has been lost.The rescue is deliberately one-directional: it repairs underflow and leaves overflow alone.
||L||_F = inffor the round-20 counterexample is not an accident to be worked around but the input to the round-21 refusal of a non-finite reference scale. That norm is mathematically about1.4e308and therefore representable, so a scaled computation would hand back a finite number, readmit the operator and silently reopen a hole that took five interpreter versions of CI to close. The two directions look symmetric and are not: only one of them has a decision behind it. A dedicated test pins the overflow refusal so that a later "symmetric" tidy-up goes red. -
The workflow hardening gate was blind to the commonest way to write a step (PR #129).
.github/scripts/check_workflow_hardening.pyenforces the SHA-pinning rule of AGENTS.md section 4 on every workflow, and nothing enforced it in turn. It reported green while four classes of unsafe workflow passed:USES_REwas^\s*uses:, which does not match the list form- uses:. Seven of 27 action references in this repository were invisible to the gate, everyactions/checkoutamong them. They are correctly pinned today by discipline, not by this check; a PR moving one to@mainpassed.docker://was exempt outright, so a mutable third-party tag (docker://org/img:latest) was waved through. The exemption now requires an immutable@sha256:digest.permissionswas checked for presence only, sopermissions: write-all-- a declaration of total access -- counted as evidence of least privilege.- The
pull_request_targetwaiver was a substring test over the raw file, so writing the waiver's name inside a#comment satisfied it.
The
github.com/exemption is removed as dead code:uses:does not accept that prefix and a GitHub owner name cannot contain a dot, so no such organisation can exist. It was the subject of a CodeQL incomplete-substring alert -- which is the only reason the file came under scrutiny. CodeQL flagged the harmless line and missed the regex two lines above it that actually broke the gate.Closing the
docker://hole initially rejected digest-pinned containers:_check_uses_pinsplits the reference at@before consulting_is_third_party_uses, so a digest test there ran on the truncated string and the digest then failed the 40-character git-SHA rule. Container digests and git SHAs are now checked as the different pin shapes they are. That regression was caught by an over-correction control, not by review. -
Two zizmor audits re-enabled whose suppression reasons no longer hold (PR #129).
impostor-commitwas disabled because the audit crashed on the private cross-repo pincoworkerz-ci; that pin is gone, and the only two occurrences of the name left in the tree are the comments explaining the suppression. It is the one audit that verifies a 40-character SHA actually exists upstream, so the entire pinning strategy was unverified without it.advanced-security: falsewas justified with "private repo without GHAS" -- this repository is public, measured rather than assumed, so zizmor findings were never uploaded to Code Scanning: absent there for want of an upload, not for want of findings. (annotationsis set tofalsealongside it; the action refuses to start with both enabled.)
- Tests for the hardening gate itself (PR #129). It had none. Each unsafe workflow is asserted next to a positive control that must still pass -- rejection alone would be satisfied by a gate that fails everything, which is exactly as useless as one that passes everything. Includes a digest-pinned container that must remain allowed, and a check that the real tree passes its own gate.
quality contractis not a required status check onmain; onlytest (3.10-3.14)andqutip-cross-check (3.11/3.12)are. Five security workflows run and none of them blocks a merge, so the comment in.github/workflows/zizmor.ymlclaiming the step "GATET die CI (rot = blockt merge)" is factually wrong. Making it required is the right follow-up, but only after this fix lands -- otherwise a blind gate becomes mandatory.- Eight gates that decided from what they never had (PR #121 round-22
review). CHANGES
StabilityReportPAYLOAD TYPES and the A10/F5 CLASSIFIER BRANCH. All eight share one shape: a value that is not a measurement is used as if it were one.- Non-finite tolerances were honoured instead of refused.
defect > tp_rtol * frois False for every operator whentp_rtolis NaN, and has an infinite right-hand side when it isinf, socertified_eigvals(diag([0,-1,-2,-3]), tp_rtol=nan)came backapplicable=True, certified=Truefor a trace defect of 3.0. Round 21 closed this shape one variable further along (the reference scalefro) and left the parameter beside it unchecked;rtolandtp_rtolare now validated incertified_eigvalsandcertified_eig. - An inapplicable certificate reported one zero mode.
zero_mode_countkept its dataclass default of1in both inapplicable branches, sodiag([1,2,3,4])-- a spectrum with no zero eigenvalue -- claimed a stationary mode into persisted audit metadata. It is now0. - The refined tolerance underflowed to zero.
sqrt(lo * hi)forms the product first, and that product scales asc^2under a uniform rate rescale while the refinement itself is scale free; belowc ~ 1e-139the tolerance became0.0and the numerical stationary residual survived the gap filter as physics.sqrt(lo) * sqrt(hi)is the same number and cannot underflow (measured:lo=1e-177, hi=1e-165gave0.0, now1e-171). - An overflowing spectral radius produced a gapless verdict.
|1.3e308 + 1.3e308j|exceeds the double range, sonp.absreturnedinfalthough every component is finite and passed the finiteness gate.spectral_zero_tolerancereturnedinf, no mode satisfied|lambda| > tol, and D1/D3/D4 all reported0.0for a spectrum with modes of order1e308. Now computed by scaling on the largest component (measured after: tolerance4.08e295, D15.0e307, D31.3e308, D48.0e307); a tolerance that is still not finite is refused. - The sparse builder inherited none of the dense overflow repair. The
round-18 fix divided each diagonal entry before summing in
build_liouvillianonly;build_sparse_liouvillianstill calleddiagonal().sum(), overflowed toinf, and accepted the very matrix its dense twin rejected on the same input (Hermiticity defect1e290). Both paths now share the overflow-safe gauge shift and the refusal of a non-finite derived scale. - The jackknife never asked whether its fits converged. The round-20 guard
protects the bootstrap replicates only;
_jackknifecopiedfit.paramsunconditionally, and a failedleast_squaresreturns its unchanged starting value --theta_hatitself. The BCa acceleration is a third moment of exactly these estimates, so the endpoints moved with no data behind them (measured: one non-fit in forty shifted one interval width to0.923x)._jackknifenow raisesRuntimeError, whichcompute_relaxation_layeralready routes tobca_ci_beta = (nan, nan). - The unresolved run was the one whose report would not serialise.
build_stability_reportcopied D1/D3/D9 through as raw floats while the layers above deliberately publish NaN when a certificate is unresolved, anddump_stability_reportwrites withallow_nan=False-- sojson.dumpsraised on precisely the run whose audit record matters most. Non-finite diagnostic values are now encoded asnull, the placeholder already used for D8b/D10b and byZeroModeCertificate.as_dict. Consumers readingdiagnostics["D1_gap"]must acceptNone. - A missing gap was read as the gapless limit.
_f5_reachdefaultedgapto0.0, which is positive evidence for the phantom-relaxation reach leg. With an unresolved certificate the spectral layer withholds D1 as NaN,_strip_unavailableremoves the key, and withhenrici_eta > 1the classifier returned A10/F5 and marked the hypothesisSUPPORTEDfor a reach it could not compute. The downstream certificate floor caps the verdict atUNDEFINEDbut does not withdraw the class, the family or the matrix status.gapis now a REQUIRED key of that condition, so the rung isUNEVALUABLEwithout it; a gap MEASURED as0.0still fires the documented gapless branch. This reverses the deliberate choice recorded intest_missing_optional_key_is_reported_without_forcing_unevaluable, whose invariant now travels ongap_to_gns_ratio-- a key that genuinely has documented semantics without a value.
- Non-finite tolerances were honoured instead of refused.
- The second repair step ended the eigenvalue ladder instead of continuing it
(PR #121 round-18 review).
certified_eigvalsguarded its primary solve but not thedgeev-realstep that runs next -- and unlike the primary, that step sits BEFOREzgees-schurandbalanced-zgeev. ALinAlgErrorthere left the generator and discarded two repair routes that could still have certified the spectrum. The sibling ladder incertified_eigalready suppressed exactly this step, so the defect was two ladders drifting apart, not an overlooked case. A structural test now fails if eitherdgeev-realroute is left outside a suppression block. - The anti-overfit gate scored fits that never converged (PR #121 round-19
review). CHANGES
HoldoutResult.acceptfor saturated fits.holdout_validateignoredfit.successand scoredfit.paramsregardless. On the saturation plateau the model is CONSTANT, so train and holdout RMSE are equally enormous and their ratio is about 1 -- measured1.0106onholdout_validate(M0, linspace(0, 1e10, 64), exp(-5t/1e10), [1, -1]), which passed the1 + deltacriterion and returnedaccept=True. The gate that exists to catch non-generalising models was satisfied by a non-model.acceptnow requiresfit.success, and the newHoldoutResult.fit_successfield keeps "did not generalise" separable from "was never fitted" -- they demand different responses. - D3, D4 and the oscillating-pair flag are withheld with D1 (PR #121
round-19 review). CHANGES
SpectralResultOUTPUTS when an applicable certificate stays uncertified. The round-17 repair withheld D1 and stopped there;oscillating_gap,spectral_spreadandhas_complex_pairswere still published from the same candidate spectrum the layer's own warning calls untrustworthy. Withholding D1 alone was not a partial fix but an inconsistent one: it taught consumers that this layer withholds what it cannot stand behind, which made the surviving finite values more credible, not less.has_complex_pairsis nowbool | None, because a bool has no NaN andFalsewould assert "no oscillating modes" rather than report an unanswered question;classificationmapsNoneto the evidence dict's NaN sentinel, where_strip_unavailableremoves it and the A8 rung does not hold. - A comparison against NaN read as "no Hermiticity violation" (PR #121
round-19 review).
np.trace(H)sums the diagonal before dividing, so it overflows toinffor a FINITEHwith large entries -- the explicit finiteness gate immediately above has already passed at that point. The gauge-fixedscalethen became NaN anddefect > EPS_HERMITICITY * scaleevaluated False, sobuild_liouvillianaccepted a two-dimensional1e308 * Icarrying a one-sided1e290entry, an order-one gauge-fixed Hermiticity defect. The gauge shift is now computed assum(diag(H) / d), which is bounded bymax|diag(H)|and cannot overflow whileHis finite; independently, a non-finite derived scale is refused outright so that any future route to one cannot silently reopen the hole. A genuinely Hermitian1e308 * Iis still accepted -- the repair is not a size limit. - One zero-mode cutoff, obtained one way (PR #121 round-17 review, four
findings). CHANGES NUMERICAL RESULTS on any generator where the a
posteriori refinement rescues a genuine slow mode. Four consumer layers kept
filtering with the certificate's raw band after the refinement had lowered
the applied tolerance, discarding the rescued mode one layer after the
certificate saved it. Measured on the two-level
omega = 1, rates1e-15 / 1e-14generator (bound = 2.22e-13,applied = 5e-16, ratio 444): D24 gap2.05e-14 -> 1e-15and the mixing-time window3.37e14 -> 6.91e15; D9 kept 2 modes withpetermann_max = 1.0and now keeps 3 with2.0; the D19 overlap moved from0.0— the false A11/F4 trigger on the highest rung — to0.141, andexpansion_alphafrom-1e-15to-33.6; the D11 fallback scanned 12 of 15 certified modes and now scans all 15. These were not four independent mistakes: the same two-branch expression was copy-written at five sites, only the spectral layer was migrated when the refinement landed, and the docstring ofoperator_zero_tolerancestill told readers to filter "equivalently, with theboundof theZeroModeCertificate" — true when written, false since. The decision now lives once, inZeroModeCertificate.zero_set_tolerance();boundis documented as report-only;_certified_decompositionhands its consumers the certificate instead of a bare number; and a guard test pins the remaining.boundreaders so a sixth site fails the suite instead of a review. - D1 is withheld when no repair route certifies the spectrum (PR #121).
With an applicable certificate and
certified=False,compute_spectral_layerstill published a finite gap read off the explicitly untrustworthy candidate spectrum. The warning does not reach a caller that consumesSpectralResult.gap, and_gather_evidencederives ratios from that value, so the number travelled further than its caveat.gapis now the NaN unavailable sentinel for both unresolved kinds, as the ambiguous case already was. D3/D4 are unchanged (see the note inspectral.py). - A saturated GLS fit is no longer selectable (PR #121). Flipping
GLSFitOutput.successchanged nothing: no consumer read the flag, the fit still carried a finite AICc, and the exact plateau case the guard detects could winchoose_modeland supply the reported decay rate. The failure is made non-selectable at the single choke point (_fit_with_modelassignsaicc = inf) rather than as four separate obligations on the consumers; with no selectable model left,aicc_modelis the existing"none"sentinel andbeta_Dis NaN instead of being read off a failed fit.parametric_bootstraprefuses a saturated base fit outright — a resample around a non-estimate is not an uncertainty — and reports the count of non-converged replicates it retained (retaining widens the interval; dropping them would narrow it). - The eigensolver repair ladder survives a primary nonconvergence (PR #121).
A
LinAlgErrorfrom the incumbentzgeevcall endedcertified_eigvals/certified_eigbefore the real-driver, Schur and balanced routes were tried — defeating the ladder in precisely the case it exists for. The error is now carried and re-raised only if no route produced a spectrum. - The
zero_mode_certificatefield documentation matched neither the code nor this changelog (PR #121). It described a report-only field whileclassify_mechanismreads it and_apply_spectral_certificate_floorcaps both the reported verdict and the tier from it. Documentation only; no behaviour change. - The zero-mode band gets a second, independent axis: an a posteriori
backward-error certificate (issue #118 finding 15). CHANGES NUMERICAL
RESULTS on generators whose slowest decay sits far below the scale their
Hamiltonian sets. The band is
1e3 * eps * ||L||_2, and||L||_2is fixed by the oscillation frequency while the decision is about decay: a two-level system withomega = 1and jump rates1e-15 / 1e-14had its genuine slowest mode (D1 = 1e-15) swallowed by the band, was certifiedresolvedwithzero_mode_count = 2, and D1 reported the next, twentyfold faster eigenvalue (2.05e-14) with full confidence. The magnitude axis cannot fix this, and theZERO_MODE_AMBIGUITY_FACTORcomment already said why: healthy round-off reaches2.38 * eps*||L||while unresolved slow modes were measured at4.87— populations a factor of two apart, which no threshold separates.certified_nonzero_modestherefore adds the per-mode bound|lambda - lambda_hat| <~ max(||L x - lambda_hat x||, ||L^H y - conj(lambda_hat) y||) / |y^H x|, which is a certificate: were the mode stationary, the bound would force|lambda_hat| <= bound, so exceeding it proves it is not. Measured over the same corpus that calibrated the existing factor (96 healthy generators / 132 in-band modes; 6 stiff generators with analytic gaps): 0 healthy modes reachedq = 1(max 0.472) and all 3 stiff members carrying a genuine in-band mode were rescued. The refinement is one-directional — it can only take a mode OUT of the zero set — so it cannot produce the false "unresolved" verdict the ambiguity split guards against. The certificate now carrieszero_tolerance, which D1/D3/D4 filter by; without a rescue it equalsboundand the healthy path is unchanged bit for bit. - A GLS fit that ends inside the model magnitude guards fails closed
(issue #118 finding 9). The guards keep an out-of-range optimiser probe
finite, which is what they are for — but the value they return is constant,
so its derivatives vanish and
least_squaresterminates on "gradient is small". Measured:fit_gls_ar1(M0, t in [0, 1e10], y = exp(-5t/1e10), p0 = [1, -1])returnedsuccess=Truewithp0unchanged and a residual norm of7.9e100. Only the FINAL evaluation is judged (probes passing through the plateau are exactly what the caps exist for);GLSFitOutputgainssaturated, naming which guard fired.
SpectralResult.zero_mode_certificateis no longer described as "report-only" (issue #118 finding 16). It was load-bearing from the commit that introduced_apply_spectral_certificate_floor, and is now load-bearing on a second path throughzero_tolerance. See the corrected entry below.
- Prony fallback seeds are grid-relative (round-16 review; the #108 class
of defect). CHANGES NUMERICAL RESULTS on grids far from unit span where
the Prony estimate falls back (non-uniform sampling, short signals,
degenerate data). The fallback seeded
(beta, omega) = (1, 1)in absolute rate units, so on a valid non-uniform grid spanningt = 1e7the M3b fit started seven orders of magnitude off and least-squares "converged" (success=True) onto the seed itself — measured[A, beta, omega] ≈ [0.006, 1, 1]for true values5e-8/2e-6— corrupting the fitted rate and suppressing the M3b/A8 hypothesis purely because of the rate unit. Fallback rates are now5.0 / t_spanand the success-path positivity floors5e-6 / t_span, both reproducing the historical values exactly at the canonicalt_span = 5(the same convention asALPHA_SEED_FLOOR_FRAC/M3A_SLOPE_SEED_FRAC); the M3b fit on the measured non-uniform scenario is now unit-invariant acrossc ∈ [1e-6, 1e6]. - D24 uses the eigenvalue certificate when only the gap is missing
(round-16 review). The recomputation branch required
certified_eigeven when the caller suppliedpetermann_factor— but that partial path consumes no eigenvectors, so a stiff network whose eigenvalue certificate resolves a usable gap (measured3.32e-6) while only the round-15 eigenvector gate fails returned an unnecessary unconverged record. The certificate now matches what is consumed:certified_eigvalswhen only the gap is recomputed, the strictercertified_eigwhen the Petermann factor is. - D11 is decoupled from the D9 vector gate (round-16 review). When D9
is withheld (round-15), the NaN sentinel array flowed into
bohr_arithmetic_progression, which silently filtered it and reported the default length1as a measured D11 value. D11 consumes only eigenvalues, and the eigenvalue certificate frequently still resolves in exactly that situation, so its input is now recomputed from the certified spectrum; only when the eigenvalues themselves are unresolved doesbohr_ap_lengthbecome NaN (the field is now float-typed with NaN as the documented unavailable sentinel). - Release-note correction (round-16 review, documentation only): the
atol=opt-in for the legacy absolute Hermiticity gate exists on the standaloneis_hermitianpredicate only; the builders never accepted it and their relative gate cannot be bypassed. The #109 entry below now says so explicitly instead of advertising an unavailable builder argument. - certified_eig validates eigenVECTOR residuals before certifying
(round-15 review). CHANGES NUMERICAL RESULTS on stiff generators.
The certificate accepted a decomposition solely because one eigenvalue was
close to zero — but D19 and the Petermann factors consume the
eigenvectors, and a small
|lambda|does not vouch for them. Measured on a valid stiff classical network:certified=True, resolved=Truewhile the slow mode's LEFT eigenvector had residual three orders of magnitude beyond the certificate bound, i.e. not an eigenvector in any usable sense (the right vectors were fine). Acceptance is now per mode and relative —r_j <= max(VECTOR_RESIDUAL_REL_MAX * |lambda_j|, bound)with the larger of the unit-normalised left/right residuals — becauser/|lambda|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<= 2.1e-10, legitimatedgeev-realrepairs a few percent, corrupt decompositions 22%–2900%; the calibration is documented at the constant). A candidate failing the gate does not end the ladder; when no route passes, the certificate reportscertified=Falsewith the offending vector residual, and every vector consumer (D19, D9, D24) withholds.certified_eigvalsis deliberately untouched by this gate: the eigenvalues of such a decomposition remain usable for D1/D3/D4. - The repair ladder continues past ambiguous candidates (round-15
review). CHANGES NUMERICAL RESULTS on stiff generators, in the
fail-open-to-correct direction. A candidate whose in-band spectrum
contained an ambiguous mode (#113) previously ended the ladder
immediately with
resolved=False— withholding D1 and flooring the verdict — even when the very next route resolves the generator cleanly (measured: zgeev certified with one ambiguous mode at6.3e-7while dgeev-real returns a2.1e-15stationary mode with none). Both ladders now accept only ambiguity-free candidates and fall back to the best ambiguous candidate (fewest ambiguous modes) only when every route stays ambiguous. The healthy path is unchanged and still lazy. - Petermann factors (D9) consume the certified eigendecomposition
(round-14 review). CHANGES NUMERICAL RESULTS on stiff generators.
petermann_factorsrecomputed its own rawzgeevdecomposition, so on a stiff trace-preserving generator it consumed exactly the solver failure the spectral and Mpemba layers repair (#112) — and no zero-mode cutoff can restore an eigenvalue that is absent from the raw spectrum. Measured on the stiff four-level fixture: 16 "non-zero" modes withpetermann_max ≈ 622.7against the certified 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. The function now usescertified_eig; when the certificate is applicable but unresolved (#112/#113) both returned arrays are the NaN unavailable sentinel, sopetermann_maxbecomes NaN and the F1 rung reads UNEVALUABLE instead of the empty-array defaultK_max = 1.0, which would assert perfect normality. - Radius fallback when the zero-mode certificate is inapplicable
(round-14 review). Without established trace preservation no zero
eigenvalue is guaranteed, so the operator-norm certificate bound is not a
valid zero-mode cutoff: on a non-trace-preserving, strongly non-normal
input the bound (measured
2.2e3) exceeded the whole spectrum{-1,-2,-3,-4}and the round-13 filters discarded every eigenvalue — D1 =0.0(gapless, firing the F5 reach leg) and an unconverged D24 for a well-separated spectrum with true gap1. All round-13 call sites (spectral layer, Mpemba layer, D9, D24) now use the certificate bound only whencertificate.applicable, and fall back to the radius-based #108 tolerance otherwise. - One zero-mode scale for certification and filtering (round-13 review of
the #112 machinery). CHANGES NUMERICAL RESULTS on strongly non-normal
generators. The
ZeroModeCertificateaccepts a stationary residual up to the true eigensolver backward errorrtol * eps * ||L||_2, but the downstream zero-mode filters (spectral_zero_tolerance, #108) only see the spectrum and used the spectral radiusmax|lambda|as a proxy. For a strongly non-normal trace-preserving operator||L||_2exceeds the radius by orders of magnitude (measured:3.9e3on a 4x4 example), so a certified-resolved zero mode with residual between the two thresholds survived the radius filter as a spurious "genuine" mode: D1 reported~1e-12— occasionally negative, impossible for a GKSL generator — instead of the true gap1, and D9 reported four eigenmodes where three exist. Every consumer that holds the operator now filters with the certificate's own bound (new shared helperliouscope.numerics.linalg.operator_zero_tolerance): the spectral layer (D1/D3/D4,has_complex_pairs), the Mpemba layer (slowest mode, mode expansion),petermann_factors(D9) and the D24 predictor. Genuine slow modes inside the coarser band are not silently swallowed — the #113 ambiguity split reports them asresolved=Falseand floors the verdict. The radius-based default ofspectral_zero_toleranceis unchanged for spectrum-only call sites, and a caller-suppliedatolstill wins. - D24 recomputation is routed through the certified eigensolve (round-13
review). CHANGES NUMERICAL RESULTS on stiff generators. When
compute_zhou_predictorrecomputes an omittedgap/petermann_factorit filtered the rawzgeevspectrum, retaining exactly the solver failure the spectral and Mpemba layers repair (#112): on the stiff four-level fixture D24 reportedgap = 7.28e-6where the certified solve recovers the physical1.074e-5, shifting the whole predicted mixing-time window by ~30%. The recomputation now usescertified_eigand the certificate's zero-mode bound; when the certificate is applicable but unresolved (#112/#113) the predictor returns an honest unconverged record (infinite bounds, NaN gap/K) instead of bounds built on a demonstrably wrong spectrum. Caller-supplied values keep bypassing the eigensolve entirely. - Hypothesis matrix: a conclusively refuted partial rung is NOT_SUPPORTED,
not UNEVALUABLE (round-13 review; #102 machinery). The rungs are
conjunctions, so one evaluated-false condition refutes a rung no matter
what a missing sibling measurement would have said (
kreiss = 1refutes F1 with or withoutpetermann_max). Previously any missing required key forced UNEVALUABLE, which also propagated into an UNEVALUABLE A12 fallback while the decision ladder deterministically returned A12 — the matrix contradicting the decision it documents.UNEVALUABLEis now reserved for the genuinely open case: no evaluated condition is false and the missing evidence could still flip the rung to supported. Absent keys stay listed inmissingfor the audit trail either way; claim floors follow the status as before (refuted →NOT_EXCLUDED, open →UNDEFINED). - D1 refuses to report a gap it cannot resolve, instead of reporting a fast
mode (issue #113, PARTIAL). CHANGES NUMERICAL RESULTS on very stiff
generators, where the previous value was wrong by orders of magnitude.
The #108 tolerance carries a safety factor of
ZERO_MODE_EPS_FACTOR = 1e3above the bare backward erroreps * ||L||. Once the spectral spread grows large enough, genuine slow modes fall inside that safety factor; the filter discards them as "zero" and D1 reads off the next surviving mode — a fast one. Measured on a four-level classical jump network:5.0e7reported for a true gap of1.07e-5, i.e. wrong by twelve orders of magnitude and in the fail-open direction.ZeroModeCertificatenow separates the two readings of an in-band mode: a degenerate stationary manifold (conserved quantity / symmetry sector) puts its extra zero modes at machine zero and is perfectly legal, whereas a swallowed slow mode sits aboveeps * ||L||, inside the safety factor. Only the latter (ambiguous_count > 0) marks the layer unresolved, and D1 is then reported as NaN — deliberately not0.0, which asserts gaplessness and fires the F5 reach leg, and deliberately not the fast mode.ZERO_MODE_AMBIGUITY_FACTOR = 30is calibrated against a measured distribution rather than chosen: across 83 healthy generators the largest genuine in-band|lambda| / (eps*||L||)reaches2.38(median0.39), while unresolved slow modes were measured at4.87and4.87e2. The two populations overlap within about2x, so the split sits an order of magnitude above the healthy maximum rather than between them: a false NaN destroys a correct analysis, a missed marginal case only leaves the previous behaviour. Bounded reach, stated explicitly: roughly one further decade of spread is now fail-closed; beyond that the slow modes sink below the backward error and the defect is undetectable by any magnitude test. Issue #113 stays open for the extended-precision / Krylov route. - Spectral layer: the computed spectrum is now checked against an exact
structural fact before D1/D3/D4 are read off it (issue #112). CHANGES
NUMERICAL RESULTS on stiff generators.
Every trace-preserving generator satisfies
vec(I)^H L = 0exactly, so0is an exact eigenvalue and a correct eigensolve must return some|lambda| <= ZERO_MODE_EPS_FACTOR * eps * ||L||. A spectrum without one is proof that the solve failed — a theorem, not a tuned threshold. This is a different defect from #108: #108 fixed the zero-mode filter, while here the spectrum being filtered is wrong, so no separation tolerance can repair it (asserted intests/test_spectral_certificate.py: the relative and the legacy absolute filter return byte-identical wrong gaps). Mechanism: LAPACKzgeevdeflates a subdiagonal when the Ahues-Tisseur test|h[i,i-1] h[i-1,i]| <= eps |h[i,i]| |h[i-1,i-1] - h[i,i]|passes. On a stiff generator the large diagonal entries inflate that bound, so the entire slow block deflates to its own diagonal. Measured on a four-level classical jump network (rate spread ~1e10, exactly trace preserving): the exact zero mode vanished from the returned spectrum, a spurious mode appeared in its place, and D1 reported7.28e-6for a generator whose true gap is1.074e-5(independently confirmed against the analytically decoupled population and coherence blocks, and against a matrix-exponential decay oracle). Note this failure is invisible to conditioning: the per-eigenvalue condition numbers of the wrong spectrum are 1–25, i.e. the solver reports the wrong answer as well conditioned, and explicit balancing does not fix it (both measured). It is also not repairable by deflatingvec(I)with a dense similarity: that destroys the block structure which made the slow modes computable in the first place, and was measured to make 12% of stiff systems worse. What works is re-solving the same matrix by a route whose deflation is not driven by the stiff diagonal.numerics.linalg.certified_eigvals()therefore trieszgeev, then (whenLis real-valued)dgeev, then the complex Schur form, then a balancedzgeev, and returns the first spectrum satisfying the certificate. The incumbent result is kept byte-identically whenever it is already correct — measured across 400 random four-level networks, stiff and well-conditioned alike,zgeevwas retained in 400/400 and no system's gap got worse. If no route is certified,compute_spectral_layeremits aRuntimeWarningand marks the layer unresolved rather than reporting a gap it cannot stand behind.SpectralResult.zero_mode_certificaterecords the outcome. The field is additive (it has a default, so the run-manifest contract is unchanged), but it is not report-only:classifyreadscertificate["resolved"]and applies_apply_spectral_certificate_floor, which caps verdict and tier. An unresolved certificate therefore changes the reported classification, by design — a certificate that could not withhold a verdict would be decoration. This entry previously called the field "report-only"; that description was wrong from the commit that introduced the floor, and the correction is recorded here rather than silently dropped. Since the a posteriori refinement below, the certificate also carrieszero_tolerance, which D1/D3/D4 filter by — a second load-bearing path. - Hermiticity validation is scale-relative:
His no longer accepted or rejected on the basis of its units (issue #109).is_hermitianand both Liouvillian builders applied an absolute1e-9tolerance to the Hamiltonian, which carries energy/rate dimension — the same defect class as #108, in input validation rather than in a diagnostic. Measured in both directions:- fail-open (the serious direction) — a genuinely non-Hermitian
Hwith a relative defect of1e-6passed validation once||H|| <~ 1e-3, producing a generator that is not GKSL at all, after which every downstream diagnostic describes dynamics that are not a quantum channel. The sparse builder shared the defect, so both paths accepted it. - false rejection — an exactly Hermitian
Hreconstructed by a unitary similarity carries round-off~eps*||H||, which exceeds1e-9once||H|| >~ 1e8, so valid input was refused.EPS_HERMITICITYis now read as a relative tolerance onmax|H|, so at unit scale — the regime of every existing fixture — the gate is unchanged; only operators far from unit scale move. The legacy absolute gate remains available as an explicitatol=opt-in on the standaloneis_hermitianpredicate only (mirroring how #99 and #108 preserved their predecessors); the buildersbuild_liouvillian/build_sparse_liouvilliandeliberately expose no such override — their gate is always relative, and pre-validating with the predicate does not bypass it.is_density_matrixkeeps its absolute reading: a density matrix is trace-normalised, so that reading is already relative to a fixed scale and the rate-dimension argument does not apply.
- fail-open (the serious direction) — a genuinely non-Hermitian
- 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 underL -> cL:- small
c— every genuine mode fell below the floor, so D1 collapsed to0.0(unconditionally firing the gapless F5 reach leg) and the D19 slowest-mode overlap collapsed to0.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 withrho_0 = |+><+|: the verdict moved fromA12/none NOT_EXCLUDED EXPLORATIONatc = 1toA11/F4 CANDIDATE CONFIRMATIONatc = 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_modehelper. - 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-6atc = 1e10for a Rabi-driven damped qubit), which is impossible for a GKSL generator by definition. The correction is canonical and carries no free parameter: the newnumerics.scale.spectral_zero_tolerance()derives the threshold from the spectrum itself asZERO_MODE_EPS_FACTOR * eps * max|lambda|(spectral radius — homogeneous of degree one and unitary-similarity invariant, likerate_scale), withepsfixed at DOUBLE-precision machine epsilon: the library's own dense solves are guaranteed double (below), so the storage dtype of a spectrum says nothing about the precision it was computed in. A spectrum from a genuinely single-precision external/GPU solver is the one case needing a coarser threshold, is indistinguishable from a downcast double result by inspection, and therefore belongs to the caller: the eigenvalue-based diagnostics (D1/D3/D4/D16) expose thertolmultiplier for exactly that, so such callers can widen the filter without reverting to an absolute floor. 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 explicitatol=opt-in on every affected function, mirroring how #99 preserved the pre-#97 steady-state tolerance. The double-precision solver contract is now enforced, not assumed:eig_nonhermitiandocuments "always LAPACKzgeev", butscipy.linalg.eigdispatches by dtype and genuinely ran single-precisioncgeevon acomplex64input (measured: ~30x the eigenvalue error of the double solve on the same stored matrix;numpy.linalgby contrast always computes in double and only casts the result back). Since every backward-error tolerance above is calibrated against the double solve, all dense eigen/Schur boundaries (eig_nonhermitian, the Mpemba layer, Petermann/Henrici, D24) now promote tocomplex128before solving. Representation error already present in caller-supplied single-precision data is the caller's data quality and is neither masked nor "corrected". This supersedes the storage-dtype-derived epsilon briefly present during review (it discarded resolved metastable modes: acomplex64two-channel generator with rates1.0/1e-4, true gap5e-5, reported5e-1). D1 deliberately does not clamp the gap at zero: after the scale-relative filter a positiveRe(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): with1e-10 * max|lambda|, two damping channels at rates1.0and1e-12(true gap5e-13) reported a gap of5e-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 atc in {1, 1e6, 1e12}: the numerical zero mode never exceeded1.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 bytests/test_zero_mode_scale.py(tests overc in {1e-10 ... 1e12}: dimensionless quantities invariant, rate-valued ones scaling byc, genuine metastable slow modes preserved, no false Mpemba candidate, no negative gap, and a discrimination test proving theatolopt-in really restores the old defect). The suite also pins the known non-invariance of the mechanism class (A10/F5 still gates on rate-dimensionedhenrici_eta, issue #101), so the limitation stays visible instead of being mistaken for a passing invariance claim.
- small
- 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_m0floored the seeded decay rate at an absolute1e-3. A rate is1/time, so on a grid spanningt = 5e10— exactly what a slow generator in small rate units produces — the seed sat1e7above the true rate, in a region whereexp(-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-3on a system whose true rate was5e-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 reproduces1e-3exactly at thet_span = 5used 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 absolute1.0. Model evaluations are additionally bounded in magnitude: capping the exponent boundsexp, 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'strfrather than in this module. Residual limitation, deliberately not claimed as fixed: atc = 1e-10the least-squares convergence criteria stop tracking the rescaling, so fitted rates are asserted invariant only overc in [1e-6, 1e10]. Tracked in its own issue; non-dimensionalising the fit is the proper resolution.
- Import convention in
AGENTS.mdcorrected to the codebase (issue #110, documentation only, no code change). The rule demanded absolute intra-package imports; the package has never followed it — measured at 135 relative import statements across 39 of 53 modules and 0 absolute ones. Relative intra-package imports are standard for asrc/layout, so the convention was corrected rather than ~30 modules rewritten. The divergence's only effect was that automated reviewers kept flagging conforming code as a violation (which is how it surfaced, during the PR #107 review).
-
Hypothesis-wise evidence matrix +
support_score+ reachability gate (issue #102, additive,claim_status: pending, no verdict change).ClassificationResult.hypothesis_matrixreports 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 recordsstatus(SUPPORTED/NOT_SUPPORTED/UNEVALUABLE/RESERVED), thesupportingconditions with the evidence values they read, the failing conditions ascounterevidence,missingrequired evidence keys, an explicit fail-closedclaim_floorand the per-class ordinalsupport_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_Conditionpredicates (each naming the evidence keys it reads), and_hypothesis_ladder(winner + shadow report) andhypothesis_evidence_matrixare 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 ofconfidence, with documented, explicitly NON-probabilistic ordinal semantics (0.20 < 0.50 < 0.70 < 0.85 < 0.95ranks rule strength; no calibration evidence exists).confidencestays 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 minusRESERVED_A_CLASSES, 9 classes) is the coverage denominator for any "n of N classes" statement; reserved classes appear in the matrix asRESERVEDwith a permanentUNDEFINEDfloor and are excluded from claims.RESERVED_A_CLASSESis 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_SCHEMAbump: the run-manifest contract is untouched (report fields are additive with defaults). - Docs:
docs/explanation/layers-and-taxonomy.mdgains the matrix vocabulary and the reachable-denominator rule; README documentshypothesis_matrix; the tutorial printssupport_score. - Tests:
tests/test_hypothesis_matrix.py(57 tests: taxonomy coverage, SUPPORTED ⟺ ladder-fires metamorphic equivalence, claim-floor truth table incl. non-finitebeta_Dand the A11 ensemble override, RFC-8259 serialisation withinfevidence, 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 dominanta_class, so a system that simultaneously shows, say, Mpemba overlap and pseudospectral phantom evidence reported only the first — the concurrently supported mechanism was silently erased. The new additive field reports every hypothesis that fires, in decision order, each as{rule_id, a_class, f_family, shadowed}.shadowedkeys on the (class, family) pair, not on ladder position: several rungs can reach the same conclusion —gap_rate_consistency < 0.05(with D17) fires the strong A1 rung and necessarily the residual< 0.20rung too — and marking the second one shadowed would report a mechanism conflict where none exists. Every firing rung is still listed, so two rules supporting one conclusion stay visible as corroboration; only a genuinely different suppressed (class, family) counts as shadowing. The tuple is rule-level and deliberately not deduplicated — one suppressed mechanism can occupy several entries (A1via both its rungs,A10/F5via the pseudospectral and the M3a rung) — so the count of suppressed mechanisms is the number of distinct(a_class, f_family)pairs among the shadowed entries, not the number of entries. Documented with the counting recipe on the public README surface. To keep the report from drifting away from the decision it describes, the priority chain was extracted into a single_hypothesis_ladder()that evaluates all rungs and returns(rule_id, a_class, f_family, fires); both_pick_a_class()(first firing rung, elseA12) andtriggered_hypotheses()are derived from that one list, so there is no second copy of the conditions to fall out of sync. No verdict behaviour changed: class, family, verdict, tier and confidence are computed exactly as before (the ladder preserves the decision order; only the earlyreturns became eager evaluation, and every directly indexed evidence key is unconditionally populated by_gather_evidence). The report is deliberately not consumed by any decision — letting it drive verdicts would be a classifier change and belongs behind the preregistered calibration study of issue #102. Theconfidencefield is re-documented in_types.pyas a heuristic support score, not a posterior probability; theconfidence→support_scorerename remains open. Serialised older results stay valid (default()). -
D14 transient amplitude: projector baseline and norm geometry separated (issue #103,
claim_status: pending). The legacy D14trans_amplitude_ratio = sup_t ||e^{tL}||_2conflated three questions. It is preserved byte-identically and re-documented precisely as an unstructured Hilbert-Schmidt semigroup norm estimate over the full complex Liouville space — not a state-amplitude ratio. Three additive, advisory fields split the confounds apart:steady_projector(L)builds the asymptotic Riesz projectorP_inffrom an ordered Schur decomposition plus a Sylvester solve, not from a single arbitrary null vector, so a degenerate stationary manifold yields the correct rank-kconditional expectation. Semisimplicity of every peripheral mode is verified via rank deficiency ofL - λI; a defective zero mode is fail-closed (semisimple=False, downstream valueNaN). The peripheral tolerance is relative to||L||_F, so the split is rate-unit invariant (consistent with issue #101).- D14b
centered_transient_amplitude=sup_t ||e^{tL} − P_inf||_2, and D14ddecaying_transient_amplitude=sup_t ||e^{tL}|_decay||_2on the decaying invariant subspace in an orthonormal basis. These are not equivalent, contrary to the wording of issue #103: att = 0the centred form is||I − P_inf||, and for a non-trivial oblique projector||I − P|| = ||P||, so centring alone still carries exactly the baseline it was meant to remove. Only the restricted semigroup starts at1. Both are reported so the difference stays visible; a regression test pins the identity. - D14c
operational_trace_amplitudeevaluates trace-norm amplification on traceless-Hermitian differences of density matrices, recorded as an explicit lower bound on the induced 1→1 norm (state family, seed and time grid are reported). For CPTP dynamics it must not exceed 1, which makes it its own contractivity control. No classifier change: the F2 branch keeps consuming the legacy D14 until the preregistered calibration study in issue #102.
Hardened after cross-family review of PR #105: the peripheral cutoff is the Schur backward error
n·eps·||L||_F, notsqrt(eps)(the latter absorbed genuinely resolved slow modes — a generator with rates 1 and 1e-8 reported rank 4 instead of 1); only genuinely zero modes count as stationary, and an oscillatory peripheral mode fails closed becausee^{tL}has no time-independent limit then; the new diagnostics validate their time grids (finite, non-negative, strictly increasing — a backward-time grid evaluates the non-CPTP inverse and would fake a contractivity violation); their default grids includet = 0; the run seed is threaded into D14c and recorded inTransientResult.transient_seed; andcompute_transient_layercomputes the propagator sweep once and shares it across the variants. -
Scale-relative non-normality/pseudospectrum diagnostics (issue #101 slice A,
claim_status: pending). One shared operator rate scaleliouscope.numerics.scale.rate_scale(L) = ||L||_F(documented zero-operator semantics, fail-closed on non-finite input) now underpins additive, dimensionless variants of the rate-dimensioned legacy diagnostics: D8bhenrici_relative = η_N/||L||_Fin[0, 1](clip-tolerance fail-closed), D10bkreiss_grid_lower_bound(dimensionless grid, local refinement, edge- maximizer + convergence metadata in the newKreissGridEstimate), scale- relative D11b/D12 (resolvent_peak_scaled,ridge_fwhm_rel), D13 witheps_abs = eps_rel · rate_scale(pseudospectral_radius_rel) and the new gap-directed intrusion diagnosticpseudospectral_abscissa(_rel)vianumerics.pseudospec.pseudospectrum_extent(single-sweep radius+abscissa, NaN "under-resolved" marker instead of a fake0.0). All are exactly invariant under a positive unit rescaleL → cLforc ∈ {1e-10 … 1e10}— pinned by the new slice-B conformance suitetests/test_scale_conformance.py(invariance, rate-valued~cscaling, D14t → t/cmetamorphic agreement, zero/normal/gapless-normal/Jordan oracles, unitary-basis invariance, fail-closed guards). The new fields are additive with NaN/False defaults onNonNormalityResult/ResolventResult(older callers and serialised results stay valid), surfaced as advisory evidence keys (ADVISORY_EVIDENCE_KEYSextended; the pinned metamorphic non-influence test covers them) and as pending-stampedD8b_henrici_relative/D10b_kreiss_scaledentries in the stability report. No classifier/verdict behaviour changed: per #101, the F5 gate switch is deferred to the preregistered calibration study + independent physics review. No manifest-contract change (run manifest fields unchanged, schema stays 1.5.0).
- Estimator labelling for D10 (issue #101 re-audit). The docstrings of
kreiss_constantand the non-normality module no longer describe the legacy grid search as "Mitchell 2020": the value is a finite-grid lower bound without globality certificate. Values are byte-identical; docs only. - Docs honesty (issues #101/#102 release policy).
confidenceis now documented as a deterministic heuristic support score (NOT calibrated; the tutorial's "calibrated 0..1" claim is fixed) and the docs state explicitly that the A10/F5 verdict path is not yet rate-unit invariant, with the new scale-relative diagnostics listed as pending advisory evidence (docs/explanation/layers-and-taxonomy.md). MANIFEST_SCHEMA_VERSION1.4.0 → 1.5.0 — injective input-hash encoding (issue #97 item 4).compute_input_hashnow absorbs each input object as a length-framed, type-tagged field (tag || len(payload) || payload) instead of a barerepr/byte concatenation. The old encoding was not injective: distinct input tuples could collide when their serialised forms concatenated to the same byte stream — e.g.compute_input_hash(12, 3)andcompute_input_hash(1, 23)both hashed"123". Withindiagnose()(fixed arity/types) the collision was practically unreachable, butcompute_input_hashis exported public API, so the derivation is hardened and the schema stepped. Migration: input hashes and run IDs are, as always, comparable only within oneschema_version; 1.4.0 manifests remain valid historical records but do not re-derive under 1.5.0. Pinned intests/test_manifest.py(test_input_hash_framing_is_injective). Docs (README,docs/CANON_STATUS,docs/DEVELOPMENT_MIGRATION_0.6.0.dev0, reproducibility tutorial/how-to/ explanation) updated to1.5.0.
steady_state/sparse_steady_statetolerance is now scale-relative (issue #97 item 5). The dense null-space tolerance wasmax(atol, n2·eps·s[0])with a default absolute flooratol = 1e-9in arbitrary rate units; a Liouvillian has rate dimension, so a pure change of unitsL → c·Lflipped the uniqueness diagnosis. Symptom:1e-10 · Lfor amplitude damping (unique steady state|0⟩⟨0|) raisedDegenerateSteadyStateErrorwith a wrong "null space has dimension 4" diagnosis, and withallow_degenerate=Truereturned a wrong state plus a warning asserting non-uniqueness as fact. The tolerance is nowmax(atol, max(rtol, n2·eps) · s[0])withrtol = 1e-9(relative, new keyword) andatol = 0.0(absolute floor now opt-in); the near-zero-trace check inside the normaliser is decoupled fromatol(the null-vector candidate has unit 2-norm, so that check is dimensionless). The sparse guard insparse_steady_stategets the same semantics: guard thresholdmax(tol·scale, |sigma_shift|)withscale = sqrt(‖L‖₁‖L‖∞)(cheap upper bound ons_max) and a defaultsigma_shiftchosen relative to that scale (None→1e-8·scale) instead of the fixed absolute1e-8. Fail-closed direction preserved: genuine degeneracy is detected at every scale. Pinned intests/test_lindblad.py(…_invariant_under_rate_rescaling,…_degeneracy_still_detected_at_small_scale,…_atol_is_an_opt_in_absolute_floor) andtests/test_sparse.py(test_sparse_steady_state_diagnosis_invariant_under_rate_rescaling). API note:steady_state(..., atol=…)keeps its absolute-floor meaning but no longer doubles as the trace threshold;sparse_steady_state(..., sigma_shift=…)keeps its meaning when passed explicitly. Anchors unaffected (verified 2026-07-13:pytest tests/test_anchors.pygreen, all canonical fixtures are O(1)-scaled where relative ≈ old absolute tolerance).- D7b
entanglement_asymmetrynow computes the Rylands charge-sector measure (issue #97 item 1). The previous implementation applied a full single-qubit Pauli twirl, which maximally mixes the twirled qubit and yields an entropy deficitS(rho_1) + ln2 − S(rho)rather than the published Ares–Murciano–Calabrese / Rylands entanglement asymmetryΔS_A = S(Σ_q Π_q ρ Π_q) − S(ρ)with U(1) charge-sector projectors. The symptom: a Bell state(|01⟩+|10⟩)/√2, which lives entirely in the single charge sectorq=1and is therefore exactly symmetric (ΔS_A = 0), was reported as2 ln2 ≈ 1.386. D7b is now the charge-block-dephasing measure (total magnetisationQ = n_1 + n_2for the supportedd=4block). D7b is advisory-only — it never feeds a classifier verdict — and is not an anchor fixture, so this changes notests/test_anchors.pybehaviour and no gate outcome; it corrects a mislabelled report value. Pinned intests/test_relaxation.py::test_d7b_entanglement_asymmetry_is_rylands_charge_measure.CITATION.cffstays pinned to released v0.5.0 per its own policy (the citable diagnostic surface — that D7b exists — is unchanged); the corrected methodology is recorded here for the next release's citation. - AR(1) bias-correction docstrings corrected to match measured behaviour
(issue #97 item 3, §6 Reality-Anchor).
fitting/neff.pyclaimed the raw lag-1 estimator is biased by−(1+3ρ)/nand that the frozen first-order correction(ρ̂(n−1)+1)/(n−3)"removes the leading O(1/n) bias term". Monte-Carlo (40k reps) shows the raw bias is empirically closer to−(1+4ρ)/nand the correction leaves a residualO(1/n)downward bias of order−2ρ/n(e.g.−0.013atn=80, ρ=0.5). The formula is unchanged — it stays the audit-frozen first-order closed form — but the docstrings now state the true cancelled/residual terms and tie the residual to the existing small-nwarning, so the documented claim matches the code. No numerical/result change. - Fail-closed hardening batch (2026-07-12 full-repo deep review). Three
independent review passes (numerical core, classifier gates, IO/manifest/CI)
found paths where malformed or non-finite input could silently upgrade a
verdict, bypass a gate or fabricate a result. All fixes below are pinned in
tests/test_failclosed_hardening.py(25 tests) plus additions totests/test_sparse.py/tests/test_lindblad.py/tests/test_export.py; methodology-gated findings that need physics decisions are tracked in issue #97, not blind-fixed here.- A11 floor override now type-gated (
diagnostics/api.py). Any duck-typed object exposingpermits_claim_floor_override=Truecould lift the single-state maximally-mixed A11 floor, bypassing all provenance validation inensemble.py; non-EnsembleEvidenceinput now raisesTypeErrorfail-closed. - Non-finite symmetrised gaps no longer grant the issue-#80 A1
certificate (
diagnostics/classification.py).kms_gap=inf(ratio collapses to 0.0 <= 1.2) orgns_gap=inf(passes the>=floor test) silently grantedsym_gap_corroboratedand upgraded A1 to CONFIRMED/PUBLICATION_GRADE 0.95; both certificate legs now require finite gaps. - F5 gapless phantom limit fires when the GNS gap is also floored
(
diagnostics/classification.py). Withgap=0ANDgns_gap=0(the realistic gapless case) the oldinf > infcomparison was False and the documented gapless-phantom contract never fired, falling through to A12. - Malformed steady-state shapes fail closed in the maximally-mixed floor
(
diagnostics/classification.py). A still-vectorised(d^2,)steady state read as "not maximally mixed" and silently disabled the A11 floor; uninterpretable shapes now raise. The documentedd < 2placeholder behaviour is unchanged. EnsembleEvidencerejects duplicateinput_hashes(ensemble.py). Two paired runs with identical input hashes are the same system + initial state (no ordering-parameter variation) and are structurally not a reference-family comparison.build_liouvillian/build_sparse_liouvillianinput gates (core/lindblad.py,sparse/build.py). NaN/inf rates passed the sign test (NaN < 0is False), an all-real ±inf diagonal H passed the Hermiticity gate, and the sparse builder skipped Hermiticity/rate/shape validation entirely (accepting non-GKSL generators the dense twin rejects). Both builders now share the same fail-closed gates;examples.v4_thermal_two_leveladditionally rejects non-finitebeta/omega.sparse_steady_statedegeneracy guard (sparse/dense parity, S1 audit anchor) (sparse/arnoldi.py). Pure-dephasing-like degenerate NESS manifolds returned an arbitrary (not even PSD) ARPACK vector silently while the dense path raisedDegenerateSteadyStateError; the sparse path now checks the two smallest-magnitude eigenvalues and fails closed, with the sameallow_degenerate=Trueescape hatch.steady_stateno-null-vector fallback is no longer silent (core/lindblad.py). An invertible superoperator (no steady state at all) returned the smallest-singular-value direction with residual||L rho|| = O(1)and no signal; the fallback now emits aRuntimeWarningcarrying the residual, the docstring describes the actual mechanism (SVD direction, not "smallest-real-part eigenvector"), and degenerate(0, 0)/ non-2-D inputs get structuredValueErrors instead of a bareIndexError.diagnose()validatest_gridlike every other boundary input (_diagnostics.py). A NaN-contaminated grid propagated throughexpm(L t)and still produced a finite, confident-lookingbeta_D; non-finite, negative, unordered or non-1-D grids now raise.- Zhou predictor early return honours caller-supplied arguments
(
_zhou.py). The all-zero-modes return hard-codedgap=0.0/petermann_factor=nan, overwriting explicitly passed values so the manifest-grade record contradicted its own call. dump_reportwrites RFC 8259-valid JSON (io/export.py). The defaultallow_nan=Trueemitted bareInfinity/NaNtokens (evidence ratios are legitimately inf), which strict consumers (JavaScriptJSON.parse, Postgresjsonb, serde) reject; Python's lenient parser hid it from the round-trip tests. Non-finite floats are now tagged{"__nonfinite__": "inf" | "-inf" | "nan"}(mirroring the__complex__tagging) and all three JSON writers (dump_report,dump_manifest,dump_stability_report) enforceallow_nan=False.- CI/tooling drift. The pre-commit ruff hook (v0.12.1) linted with a
materially different rule engine than the CI gate (ruff==0.15.20) — now
pinned together with a co-bump note.
docs/requirements.txtwas lower-bound-only while RTD builds withfail_on_warning: true; the RTD toolchain is now pinned exactly (sphinx 9.0.4 / furo 2025.12.19 / myst-parser 5.1.0, verified clean undersphinx-build -W).
- A11 floor override now type-gated (
- Contributor-docs drift.
CONTRIBUTING.mdstill pointed the clone URL at the defunctcoworker-researchorg (the same drift the packaging metadata fix already removed frompyproject.toml); it now points atmarcohost33-maker/Liouscope. The lint command was aligned with AGENTS.md (ruff check src tests benchmarks—benchmarks/is part of the CI lint gate and was missing from the contributor instructions). - Support / governance statement in the README (issue #72 item 3, JOSS
community gate). The Contributing section now states explicitly where to get
help (GitHub issues, no separate channel) and who makes maintainer decisions
(repository owner; methodology changes via
CHANGELOG.md/ ADRs).
- Docs slice 2: Diátaxis sections written out (issue #72 item 2,
follow-up slice). The Tutorials, How-to and Explanation stubs are replaced
with hand-written content: two tutorials (first diagnostic run;
reproducible runs with SPEC 7
rngand manifests), four how-to guides (manifest export/validation, QuTiP cross-checks, A11EnsembleEvidence, D24 Zhou predictor) and three explanation pages (why "no single number", D1–D24 layers + A1–A12 taxonomy + verdict vocabulary, auditable reproducibility). All code snippets were executed against the current API as part of the change;sphinx-build -Wstays clean (MySTdollarmathenabled for the math blocks). - QuTiP dynamics differential oracle (
tests/test_qutip_dynamics_oracle.py, 007acc portfolio-audit 2026-07-07 ask). The spectral oracle suite never exercised the time-domain propagation that the relaxation layer (D5–D7) fits against. New coverage: closed-form trajectory oracles for amplitude damping and pure dephasing, CPTP invariants (trace/Hermiticity/positivity) at every propagated time point, and QuTiP differentials —_evolvetrajectories againstqutip.mesolve(independent adaptive-ODE integrator) andexpm(L t)against the exponential of QuTiP's independently built Liouvillian. 8 tests; the QuTiP half runs under the existingqutipmarker /ci-qutip.ymljob. - FAIR / registry badges in the README (issue #72 item 5, "Kleinvieh").
PyPI version badge (the package is live on PyPI), Zenodo version-DOI badge
(10.5281/zenodo.21246109, verified 2026-07-08) and the fair-software.eu
badge at its honestly measured 4/5 (●●●●○: public repository, license,
registry, citation; the checklist dot requires an OpenSSF Best Practices
badge, which is future work). Badge state follows the
howfairischecker semantics (Zenodo does not count as registry there — PyPI does). PEP 735[dependency-groups]was evaluated and deliberately not added: per the issue's own criterion it only pays off once CI moves to uv, and mirroring the extras into groups today would create a second place for the dependency list to drift. - Sphinx + Read the Docs documentation skeleton (issue #72 item 2).
A
docs/Sphinx site (furo theme, MyST Markdown,autodoc+napoleonAPI reference) organised along the Diátaxis structure (tutorials / how-to / reference / explanation), plus a.readthedocs.yaml(v2) anddocs/requirements.txt/ adocsoptional-dependency group. Builds clean undersphinx-build -W(warnings-as-errors); RTD mirrors that gate viafail_on_warning: true. The Reference section is auto-generated from the publicliouscope.__all__surface; tutorials / how-to / explanation are intentional stubs for a follow-up slice. - JOSS paper skeleton (issue #72 item 3).
paper/paper.md+paper/paper.bibfollowing the JOSS 2026 section requirements (Summary, Statement of need, State of the field, Software design, Research impact, AI usage disclosure). All six bibliography DOIs are resolver-verified; author identities/ORCIDs and research-impact evidence are flagged TODO (not fabricated) and must be completed before submission. - SPEC 7
rngkeyword, additive phase (a) (issue #72 item 1).diagnose(),seed_everything()and the D18 surface (compute_lep_layer/initial_state_sensitivity) now accept a SPEC 7rngargument (int,numpy.random.SeedSequence,GeneratororBitGenerator) alongside the legacyseed; supplying both raisesValueError.rnginputs are normalised to a derived integer seed by the new public bridgeliouscope.derive_seed(re-exported fromliouscope.io.seedwith theSeedLike/RNGLikealiases), and that derived value is what the run manifest records -- the manifestseedfield,make_run_idderivation and schema version are unchanged, so a manifest alone still reproduces the run. Passing aGeneratorconsumes one draw from the caller's generator (SPEC 7 consumption semantics). Legacyseed-only and no-argument calls remain byte-identical (defaults 42 / 7 preserved).seedstays fully supported in this phase; deprecation is a later, separate SPEC 7 phase. Contract pinned intests/test_spec7_rng.py.
- Coverage ratchet 80 → 90 (issue #72 item 4). Measured branch coverage on
the baseline (Python 3.14) was 94.61%. The
--cov-fail-undergate in CI and the new[tool.coverage.report] fail_underinpyproject.tomlare both set conservatively to 90 (up from the previous fixed 80), leaving headroom for cross-version variation on the 3.10–3.14 matrix.CONTRIBUTING.mdupdated to match. - Release identity and provenance hardening. Released
v0.5.0remains immutable; default-branch VCS installs now report0.6.0.dev0. Manifest schema1.4.0adds the canonical structured-ensemble-evidence digest to the input-hash domain when evidence participates; hashes are comparable only within one schema version. - A11 typed evidence gate. A single state on
rho_ss = I/dremainsUNDEFINED / EXPLORATION. Publicensemble_confirmation=Trueis rejected fail-closed. Only validated immutableEnsembleEvidencewithPASSand reasonENSEMBLE_MPEMBA_CONFIRMEDmay suppress the floor. The evidence binds manifest/run/input digests, family ordering, comparison and uncertainty methods, software version, and distinct producer/reviewer attestations; its digest enters the run hash and its payload remains inDiagnosticReport.extras. - PyPI publication privilege-separated. Manual dispatch is build/QA only
without OIDC. The upload job is restricted to a published release, explicit
tag checkout, the protected
pypienvironment, the enable flag, and repeated source/artifact/tag/commit identity checks. External PyPI/Zenodo/SWHID gates in issue #50 remain open.
- Classifier semantics debt B3/B4 made explicit contracts (issue #70,
completes the sub-findings PR #81 left open; A5/A6/A8/A9 already on main).
Both changes are behaviour-preserving -- no real-input classification
result changes; the sacred anchor suite (
tests/test_anchors.py) and the V1-V5 golden classification assertions stay byte-identical green.- B3 -- reserved A-classes A6/A7/A9. The taxonomy
A1-A12-v3.1defines twelve classes but_pick_a_classemits only nine; A6 (accelerated-decay), A7 (weak-dissipation singular, Mori 2024) and A9 (prethermalization/ETH) have no decision branch yet. They are now recorded in_consts.RESERVED_A_CLASSESas a discoverable code-level contract analogous toRESERVED_DIAGNOSTIC_SLOTS(D21-D23), so the "12 classes" name stays honest instead of silently unreachable. A static-AST reachability test forces the contract to stay in lock-step with the code (wiring A6/A7/A9 later must update the reserved set). - B4 -- advisory (unused) evidence named.
lep_proximity(D16),bohr_ap_length(D11) andmpemba_expansion_alpha(D20) are surfaced in theevidencedict for audit but deliberately do NOT influence class/verdict/confidence.classification.ADVISORY_EVIDENCE_KEYSnames that contract; a metamorphic test proves the non-influence (perturbing each key across{0, ±1e9, ±inf, nan}leaves the decision invariant). Wiring any of them (e.g. D18initial_state_sensitivityas an A1 confidence dampener) is a class-influencing design decision with false-positive risk and is left to a dedicated PR with anchor + FP coverage -- not blind-hooked here. Pinned bytests/test_classifier_semantics_debt.py(8 tests, executed).
- B3 -- reserved A-classes A6/A7/A9. The taxonomy
- Anti-overfit gates wired into the claim vocabulary (issue #71 B2). The
residual-whiteness (Ljung-Box,
fitting/whiteness.py) and temporal-holdout (fitting/holdout.py) gates were implemented and unit-tested but exported by nothing and connected to no claim-level concept. They are now exported fromliouscope.fitting, and a newfitting/claim_gate.py::assess_relaxation_claimmaps their verdicts onto the StabilityReportSAFE/REVIEW/BLOCKvocabulary fail-closed (worst-wins): a rejected holdout ->BLOCK, non-white residuals or an un-run gate ->REVIEW, both gates run and passing ->SAFE. The result feeds straight intobuild_stability_report(claim_level=...). This is purely additive --diagnose()behaviour is unchanged; callers opt in. Newtests/test_claim_gate.pypins the full truth table (10 tests, executed). - D21-D23 reserved-slot contract (issue #71 C2). The
D1-D24schema name spans slots that are defined in the Drive-side canon but not implemented in this repository._consts.RESERVED_DIAGNOSTIC_SLOTSnow records D21-D23 as an explicit, discoverable code-level contract next toDIAGNOSTIC_SCHEMA_VERSION, each marked "reserved ... not implemented", so the "24 diagnostics" name stays honest. Pinned bytests/test_reserved_slots.py.
MANIFEST_SCHEMA_VERSIONbumped1.2.0->1.3.0(provenance-derivation contract change; MIGRATION NOTE). Therun_id/input_hashfix below widens the set of hashed inputs, so for the same physical input the derivedinput_hashandrun_idVALUES now differ from v0.5.0/main (empirically: input_hash1ac0e089…->20a2f552…, run_idface535e…->43016c65…on the reference small system). This is intentional (it fixes the collision described under Fixed), but it is backward-incompatible for provenance: arun_idarchived under schema1.2.0(v0.5.0 or earlier) does NOT re-derive under1.3.0— the old key is stable and valid within1.2.0, it is simply not comparable across the bump. The JSON manifest STRUCTURE (field list, types,additionalProperties:false) is unchanged, so a1.2.0manifest still parses; only the hash-derivation domain changed, which is exactly what theschema_versionbump signals. Migration: consumers that key archives or reproducibility checks onrun_id/input_hashmust (a) treat hashes as comparable only within a singleschema_version, and (b) re-emit manifests under1.3.0if a stable cross-version key is required — there is no value-level back-migration (the pre-bump inputs were never hashed). TheMANIFEST_SCHEMA.jsonrun_id/input_hashdescriptions now state the schema_version-scoping explicitly so the constraint is machine-discoverable. (AGENTS.md Definition-of-Done #6: run-manifest contract touched -> schema bump + migration note.)- D17 gap-rate consistency is now dimension-coherent (issue #69). D17 was
|beta_D - Delta| / Delta, comparing the relative-entropy fit ratebeta_Ddirectly with the spectral gapDelta. Relative entropy near the steady statepiis quadratic in(rho - pi)whenpiis faithful (full-rank), so it decays at2*Delta; whenpiis rank-deficient the null-space-leakage term is linear, so it decays at1*Delta. That metric multiplierm in {1, 2}(verified empirically across V1-V5:m ~ 2.0for faithfulpiV1/V2/V4,m ~ 1.05for rank-deficientpiV3/V5) inflated D17 (dephasing~3.3, amp-damp~1.1) and made the A1 "gap-controlled" label unreachable for every system. D17 now usesbeta_D_linear, the dominant decay rate of the linear trace-distance curve (LIOU-F-018), which decays at the bare mode rate and is therefore dimension-coherent withDelta. The relative-entropy ratebeta_Dis unchanged (still the headline relaxation rate; the fit/bootstrap/anchor pipeline is untouched). The classifier now exposesbeta_D,beta_D_linear,gapand the implied multiplierd17_metric_multiplierin the evidence dict (the factor is explicit and auditable, not hidden), and a genuine single-mode gap-controlled system can now earn A1 "gap-controlled" (family "none" -- see issue #70 A6 below, which corrected the A1 family from F1). The A1 early-branch is ordered after the F1-F5 gap-failure families and only before the relative-entropy-shape branches (M2/M3a/M3b):gap_rate_consistency+linear_fit_modelare initial-state-dependent, so a strongly non-normal phantom/skin operator with anrho_0that excites only the slow gap mode must NOT shadow its true (operator-intrinsic) F5/F2 mechanism -- it stays A10/F5 (resp. A4/F2), not A1 (Equalita #79 review). No V1-V5 mechanism label changes (golden-pinned end-to-end against baseline3fef6a1); only the D17 number is corrected. New end-to-end regressiontests/test_validation_systems/test_d17_gap_coherence.py(20 tests, incl. V1-V5 a_class golden + an adversarial non-normal-phantom shadow test) plus synthetic ordering regressions intests/test_classification.py. Additive fieldsRelaxationResult.beta_D_linear/linear_fit_modelandLepResult.beta_D_linearare defaulted, so serialised reports stay valid;compute_lep_layer(beta_D=...)->compute_lep_layer(beta_D_linear=...)and thegap_rate_consistency(beta_D=...)param ->rate=...(both internal). - U1 solver uncertainty now reads from a named nominal floor
(issue #71 B5).
compute_uncertainty_layerreported a bare1e-10magic number for U1 when nosolver_residualwas supplied; it now reads_consts.U1_NOMINAL_FLOORand the docstring states plainly that U1 is a conservative placeholder (not a measured residual) unless the caller runs an ODE-tolerance sweep -- whichdiagnose()does not. Same numeric value, no behaviour change; the semantics are now explicit in code. - Classifier semantics debt cleared: A1 family, F5 dimensional coherence,
LEP degeneracy, dead EXCLUDED verdict (issue #70, A5/A6/A8/A9). Four
semantic corrections to the A1-A12 classifier, each with a physics rationale:
- A6 -- A1 now maps to family
"none", notF1.F1is the Mori-Shirai overlap gap-FAILURE mechanism (PRL 125, 230604); A1 (asymptotic-gap- controlled, primitive QMS) is precisely the no-gap-failure case. Tagging the healthy gap-controlled label with a gap-FAILURE family was a category error."none"= "No gap-failure mechanism flagged" is A1's correct family. (tests/test_classification.py+test_d17_gap_coherence.pyupdated to pin A1/"none"; the #69 dimension-coherence logic is untouched.) - A8 -- the F5 phantom-relaxation rule is now dimension-coherent and
scale-invariant. The old rule
pseudospectral_radius > 2 * gap_to_gns_ratiocompared a RATE (the D13 radiusmax{|z|: z in sigma_eps(L)}) against a dimensionless ratio, so rescaling the LiouvillianL -> cL(a pure change of time unit) flipped the A10/F5 verdict. The rule now compares the dimensionless pseudospectral reachradius / gap(how far the eps-pseudospectrum extends relative to the asymptotic decay rateDelta, the physical phantom signature, Znidaric 2023) against2 * gap_to_gns_ratio-- both sides dimensionless, scale-invariant to leading order. A vanishing gap is treated as infinite reach (the gapless/critical phantom limit). New metamorphic rescale tests overc in [1e-3, 1e3]pin no verdict flip, plus a regression proving the old bare-radius rule would have flipped. - A9 --
lep_proximity(D16) no longer blind to exact degeneracies. The min-separation scan skipped every pair withsep <= atol, so an exactly degenerate eigenvalue pair -- the STRONGEST exceptional-point signal (two eigenvalues coalescing) -- was invisible, and a fully degenerate spectrum returnedinf("maximally far from an EP"), the exact inverse of the physics. Coalescence now yields proximity0.0; the candidate-count loop uses the same data and amax(10*min_sep, atol)window, so the two loops are mutually consistent. (D16 measures eigenvalue proximity only; genuine defective EP vs semisimple degeneracy is disambiguated by the Petermann factor D9, as before.) New edge tests cover exact/full/sub-atol degeneracy. - A5 -- the unreachable
EXCLUDEDverdict was removed from theVerdictLiteral (and_consts.VERDICT_EXCLUDED). A single-pass, maximum-evidence classifier reports the best-fit A-class with its support and never reports a class it is simultaneously ruling out, so a per-class active-rejection verdict is not expressible in this architecture -- the value was permanently unreachable throughdiagnose(). The oldconfidence < 0.30 -> EXCLUDEDbranch was also semantically wrong (low confidence = epistemic "unresolved" =NOT_EXCLUDED, not counter-evidence); low confidence now correctly yieldsNOT_EXCLUDED. API note:Verdictnarrows from 5 to 4 members; this is a type-surface narrowing only (runtimediagnose()output is unchanged -- it never emittedEXCLUDED). Genuine active exclusion is deferred to a future per-hypothesis scoring mode. B3 (unreachable classes A6/A7/A9) and B4 (unused D16/D18/D11/D12/D20 evidence) are analysed but NOT wired in this PR (see PR body) -- both are class/verdict-influencing design decisions that can introduce false positives and are deferred to dedicated PRs with anchor coverage.
- A6 -- A1 now maps to family
- A1 PUBLICATION_GRADE now requires a positive symmetrised-gap certificate,
not threshold exhaustiveness (issue #80; classification-semantics change in
a dedicated PR per AGENTS.md §3; the twin of the #88 fix). The A1 early
branch awarded CONFIRMED/PUBLICATION_GRADE (confidence 0.95) whenever
gap_rate_consistency < 0.05+ single-exp held and none of the F1-F5 thresholds fired — so the publication-grade claim rested on the unprovable exhaustiveness of the F1-F5 threshold set (both #79 reviewers' residual): a hypothetical weakly-non-normal gap failure below all thresholds with a single-exp-at-gap trajectory would self-certify. The burden of proof is now reversed (issue #80 to-do 2): a new evidence keysym_gap_corroborated(1.0 iff a measured symmetrised gap shows no F3-grade reduction — certified GNSgap_to_gns_ratio <= 1.2, or KMSgap_to_kms_ratio <= 1.2) gates the 0.95 score; uncorroborated A1 caps at 0.70 → CANDIDATE/CONFIRMATION (honest grade: the single-exp-at-gap observable is measured, operator-intrinsic gap control is not).gap_to_kms_ratiothereby graduates from advisory (#89) to class-influencing; using it as an F3 veto stays deferred. Anchor-preserving: the gap-controlled thermal reference hasDelta_GNS = Delta_KMS = Deltaexactly (corroborated → 0.95 unchanged); V1-V5 golden labels are A5/A11 and never take the A1 confidence path; the sacred anchor suite stays green. Synthetic adversary + KMS-certificate + double-floored fail-closed tests added.claim_status: pendinguntil cross-family review confirms the semantics. - A2/F3 no longer fires off the
gns_gapconservative-floor sentinel (issue #88; classification-semantics change in a dedicated PR per AGENTS.md §3).diagnostics.spectral.gns_gapdeliberately floorsDelta_GNSto ~0 when the GNS symmetrisation certifies no contraction (the documented 2026-07 audit-A1 behaviour for non-detailed-balance steady states carrying coherences; that floor is unchanged). But_gather_evidenceturned the sentinel intogap_to_gns_ratio ~ 1e10..inf, and the F3 branch plus_confidencepromoted the exploded ratio straight to A2/F3 CONFIRMED / PUBLICATION_GRADE — a publication-grade Mori-Shirai-2023 mechanism claim keyed on the absence of a certificate, on inputs as tame as a textbook Rabi-driven amplitude-damped qubit withDelta_KMS == Delta(provably no real symmetrised-gap reduction). Fix (issue #88 option 1: positive-evidence burden): the evidence dict now carriesgns_certified(1.0 iffgns_gap >= GNS_CERTIFIED_RTOL * gap, new named constant1e-8in_consts), and both the F3 branch and the A2 high-confidence rule require it — the uncertified sentinel falls through to the state-dependent shape branches (honest floor: absence of evidence).gap_to_kms_ratiois now surfaced as advisory audit context (issue #88 option 2 — wiring it as an F3 veto is deferred to its own FP study); the advisory metamorphic contract covers it. Verdict flips (uncovered input class only): the #88 repro family (Rabi qubit, drives 0.3/0.7/1.5) flips A2/F3 CONFIRMED/ PUBLICATION_GRADE (0.85) → A8 or A10-via-M3a CANDIDATE/CONFIRMATION (≤0.70). Anchor-preserving: V1/V3/V4 have diagonal steady states (finite, certifiedgns_gap), V5 is caught by the Mpemba branch first; the sacred anchor suite and all V1-V5 golden assertions stay green. A certified reduction still fires A2/F3 at 0.85 (positive-control test). New e2e guardtests/test_classifier_f3_sentinel.py;claim_status: pendinguntil cross-family review confirms the semantics. - Hermiticity / normalisation validation gates are now absolute, not
~10^4x looser than advertised.
numerics.linalg.is_hermitian(and thereforeis_density_matrix), the Hamiltonian Hermiticity check incore.lindblad.build_liouvillian("H must be Hermitian within 1e-9 atol"), and the unit-norm check incore.jumps.engineered_target_jumpsall callednp.allclose/np.isclosewith onlyatol=set. NumPy's defaultrtol=1e-5silently widened each gate toatol + 1e-5*|entry|(~1e-5 for O(1) density matrices / Hamiltonians), so a matrix non-Hermitian at 1e-6 — a thousand times the documented 1e-9 tolerance — passed as valid. All three now passrtol=0.0so theatolmeans exactly what the docstring/error says. Behaviour-preserving for every physical operator (Hermitian to machine precision); only genuinely malformed input near the old blind spot is now rejected. New regressiontests/test_numerics.py:: test_is_hermitian_atol_is_absolute_not_relative. - GLS AR(1) log-likelihood is now the exact Prais-Winsten likelihood.
fitting.gls.fit_gls_ar1whitens with the exact AR(1) transform (keeping observation 0 scaled bysqrt(1-rho^2), allnpoints), but reportedgaussian_log_likelihood(whitened)— the iid-Gaussian likelihood of the whitened residuals, which omits the transform's log-Jacobian+0.5*log(1-rho^2). The reported value was therefore a hybrid of the exact and conditional likelihoods. Because each model M0..M3b fits its ownrhoand its per-modellog_likelihoodfeedsaicc()/choose_model(which drives mechanism classification), the missing rho-dependent term biased cross-model AICc toward under-fitting high-rhofits. The Jacobian is now added. Anchor-preserving: the sacredtests/test_anchors.pysuite and the V1-V5 golden classification assertions stay green (the correction does not flip the selected model on any canonical fixture); it only affects near-tied AICc comparisons on other data. No manifest-contract or citable-claim change (the run manifest does not record per-model likelihoods;input_hashis derived from inputs, not outputs). New regressiontests/test_fitting.py:: test_gls_ar1_log_likelihood_is_exact_prais_winsten. run_id/input_hashnow cover every output-affecting input. Both provenance keys were derived only from(L_super, rho_initial)(plusseedandframework_versionforrun_id), so twodiagnose()calls that differed only in an analysis knob —bootstrap_B,t_grid,include_mpemba,solver_path, or an explicitly-suppliedrho_steady_state— produced byte-different reports carrying an identicalrun_id. That is a collision: theMANIFEST_SCHEMAdocumentsrun_idas "deterministically derived from input parameters", but a largerbootstrap_B(wider BCa CI) reused the same key, breaking archival keyed onrun_id.compute_input_hashnow folds all output-affecting arguments; theMANIFEST_SCHEMArun_id/input_hashdescriptions are made accurate. Determinism for identical inputs is unchanged (repeated runs still collide, as required). New regressiontests/test_manifest.py::test_run_id_distinguishes_analysis_config.seed_everythingfails closed on out-of-range seeds before mutating any global state. Legacynp.random.seedonly accepts0 <= seed < 2**32; a seed of2**32passed the previous non-negative-int guard, then raised mid-function afterPYTHONHASHSEEDandrandom.seedhad already been changed (partial, inconsistent state). The bound is now checked up front. New regression intests/test_input_guards.py.- D14/D15 time grid stays strictly ascending under extreme non-normality.
_physics_time_gridbuilt its log-spaced early segment asgeomspace(t_decay * 1e-6, t_early, …); when the numerical abscissa exceeds the gap by more than six decades the start overtakest_earlyand the segment ran backwards, clustering the fine sampling after the transient peak instead of before it. The start is now clamped belowt_earlyin that regime (a no-op for all normal spectra). New regression intests/test_transient.py. - D13
pseudospectral_radiusfails closed on non-finite operators. The grid search fedLstraight intoeigvals/svdvalswith no finite/square guard, unlike the rest ofnumerics(linalg.py,cptp.py); a NaN/inf operator surfaced as an opaque LAPACK error deep in the svd loop instead of a located, argument-namedValueError. Addedrequire_finite_square_2dat the entry point. New regression intests/test_resolvent.py. - Metadata / provenance consistency.
codemeta.jsonno longer overclaims "D21-D24 implemented" (it now matchesCITATION.cff: D1-D20 + D2b/D7b/D11b + the opt-in D24, with D21-D23 as reserved schema slots);MANIFEST_SCHEMA.json$idpoints at the canonicalmarcohost33-maker/Liouscoperepo instead of a stale org; andbuild_stability_reportcastscp_choi_min_eigthroughfloat()like every other numeric it stores, so anp.float32/np.longdoubleinput cannot break JSON serialization. - Removed a dead classifier branch. The A1
gap_rate_consistency < 0.05 and aicc_model == "M0"pre-check returned the identical("A1", "none")that the following< 0.20line already returns, and the A1 confidence keys ongap_rate_consistencyalone — so it changed neither label nor score. Deleting it removes an implied distinction that does not exist (no behaviour change). - D16
lep_proximitynow fails closed on non-finite eigenvalues (issue #82, part 2). NaN / +-inf eigenvalues were silently swallowed by the pairwise comparisons and yielded a finite(proximity, count)result -- e.g. a NaN input returned(1.0, 1), a spurious LEP classification instead of an error.lep_proximity(and thereforecompute_lep_layer) now raisesValueError, listing the offending indices, before the scan. This closes a Silent-Failure- Gate violation; the exact/full/sub-atol degeneracy behaviour is unchanged. Pinned by new negative-path tests intests/test_lep.py. - A8/F5 scale-invariance test no longer overclaims (issue #82, part 1). The
test
test_a8_f5_rule_is_scale_invariant_under_rescalehand-scaled both the gap and the pseudospectral radius by the same factor, makingradius/gaptrivially constant, yet its name claimed full scale-invariance. It is renamed totest_a8_f5_decision_rule_invariant_under_exactly_scaled_evidence(it pins the decision rule on idealised evidence only) and a new end-to-end metamorphic test recomputes the D13 pseudospectral radius from a genuinely rescaled operatorc*M. Because D13 uses a fixedeps = 1e-3, the reachradius/gapis scale-invariant to leading order only (measured drift ~1.34x over six decades, converging to the true spectral-radius/gap at large scale); the classifier docstring already said "to leading order" -- only the test/PR wording overclaimed. Relative-eps rescaling for exact invariance is a physics-design follow-up, out of scope here. - D19/A11 Mpemba detector no longer false-positives on trivially symmetric
initial states (issue #68). Three coupled defects were corrected:
diagnostics/mpemba.py::overlap_c1normalised the slowest-mode overlap by||l_1||, so only its zero test was meaningful. It now returns the true biorthogonal expansion coefficient|<l_1, rho_0>| / |<l_1, r_1>|(denominator floored atEPS_DIV), the physical weight of the slow mode.- A non-triviality guard (
is_trivial_overlap, wired throughdiagnose()via the newrho_steady_stateargument ofcompute_mpemba_layer) now distinguishes a symmetry-protected zero overlap from an anomalous skip. In the sector decomposition set by the eigenprojectors ofrho_ss(robust to a degenerate / maximally mixed steady state, unlike an eigenvector basis), arho_0whose active blocks are disjoint from the slowest mode's can never populate it — trivially fast relaxation, not Mpemba. Amplitude damping with the defaultrho_0 = I/2(and the excited state|1><1|) previously returnedA11 F4 CONFIRMED PUBLICATION_GRADE; both now fall through the A11 branch.MpembaResultgains atrivial_overlapfield. - A single initial state skipping the slowest mode is now CANDIDATE-grade,
not PUBLICATION_GRADE:
A11confidence is capped at 0.70 because confirmation needs a reference family (e.g. thermal states across temperatures) the single-state pipeline does not provide. The README dephasing example (rho_ss = I/2, which collapses to a single eigenprojector so the triviality guard cannot fire) staysA11but asCANDIDATE/CONFIRMATION, no longer self-certifying. - New/updated coverage in
tests/test_mpemba.py(biorthogonal coefficient, guard truth table, a fine-tuned qutrit true-positive oracle, canonical false-positive regressions) andtests/test_classification.py.
- CI: pinned
ruff==0.15.20and reformatted the_diagnosticsimport block. Ruff's isortI001heuristic forlines-after-importsbefore a module-level constant changed between 0.15.14 and 0.15.20, flagging a byte-identical, previously-green import block and turning the whole test matrix red on a tool release rather than a code defect. Ruff is now pinned exactly in bothpyproject(devextra) and the reusableci-python-local.ymlinstall step; a bump is now a deliberate, reformat-in-one-PR event. diagnose()fails closed on the solver path. An unknownsolver_pathraisesValueError; the reservedsparse_arpackpath raisesNotImplementedErrorinstead of silently falling back to the dense solver. Now covered bytests/test_solver_path_contract.py(2 tests, executed).- D2/D2b symmetrised gaps (
numerics.adjoint,diagnostics.spectral) are now genuine Gram-adjoint constructions (2026-07 audit A1). Three coupled defects were fixed:alicki_adjointbuilt the pi-weighted adjoint as(rho^-1 (x) I) L (rho (x) I)while the GNS similarity transform used the anchor-B GramG = rho.T (x) I. The two coincide only for real-diagonal steady states; off that manifold the "symmetrised" generator was not G-Hermitian, its silent Hermitisation masked the defect, andgns_gap()returned unphysical negative values (repro: generic d=3 GKSL with steady-state coherences gaveDelta_s = -4.4atDelta = 0.63). The Kron factors are now transposed — i.e. exactlyG^{-1} L G. No-op for the real-diagonal anchor fixtures (all 21 anchors unchanged).kms_gapmixed pictures: it symmetrised with the GNS adjoint but conjugated with the KMS Gram. It now uses the KMS-Gram adjoint via the newnumerics.adjoint.gram_adjoint(L, G)helper.- Gap extraction (
_real_gap_from_symmetric) now deflates exactly ONE steady-state zero mode instead of filtering all near-zero eigenvalues. A degenerate Hermitian-part kernel means there is no certified exponential GNS contraction (Delta_s = 0); the old filtering over-certified the bound (Delta_s > Delta, contradicting the Kadison-Schwarz contraction property). Consequence: driven pure dephasing withrho_ss = I/2now honestly reportsgns_gap = 0(the sigma_z mode has zero instantaneous GNS decay rate); detailed-balance systems keepDelta_s = Delta. New contraction-bound regression tests (0 <= Delta_s <= Delta,0 <= Delta_KMS <= Delta) pin the fix on non-diagonal steady states (40/40 random systems verified).
sparse.chi1.chi1_lower_boundreturnedK^(1/4)instead of the documentedK^(1/2)(2026-07 audit A4): the ratio||r||*||l|| / |<l,r>|is already the square root of the Petermann factor; the code took a second square root, quadratically weakening the ARPACK-reliability certificate. A regression test now pins the certificate against the densepetermann_factorsoracle (the old test only assertedchi > 0).- Silent zero-width confidence interval on bootstrap failure
(
diagnostics.relaxation.compute_relaxation_layer, 2026-07 audit A7): a swallowedparametric_bootstrap/bca_ciexception used to keep the degenerate initialisation(beta_D, beta_D), which the uncertainty layer read asfit_uncertainty = 0.0— perfect certainty as the failure mode. The CI is now(nan, nan)plus aRuntimeWarning;compute_uncertainty_layeralready maps that tofit_uncertainty = nan. - Robustness guards (2026-07 audit A10/A11):
core.lindblad.steady_statecasts integer/bool input instead of crashing innp.finfo;examples.v4_thermal_two_levelrejectsbeta*omega <= 0(divide-by-zero);io.seed.seed_everythingrejectsboolseeds (silent 0/1 seeding);core.jumps.engineered_target_jumpsrejects a zero target vector and its docstring now matches the two-operator return. - Docstring/comment drift fixed against implementations: D4 spread comment in
_types.py, pseudospectrum grid location, D18 sensitivity ensemble, D7b fallback value,diagnostics/__init__layer count. liouscope.numerics.cptp: fail-closed input validation now rejects non-finite generator/channel entries (L_super,channel_super) and invalid (negative / non-finite) tolerances before the matrix exponential, Choi construction, or eigensolvers run. This replaces opaque LAPACK failures with a clearValueErrorand prevents a bad caller tolerance from silently inverting the CP/TP verdict.
- Property-based test suite (
tests/test_property_based.py, Hypothesis): ground-truth-free invariants on randomly drawn GKSL systems — gap scalinggap(c*L) = c*gap(L), spectrum invariance under unitary similarity, trace-distance axioms + CPTP contractivity, the GKSL semigroup lawPhi_{t1+t2} = Phi_{t2} Phi_{t1}, and no-false-alarm coverage for the Choi CPTP gate.derandomize=Truekeeps CI deterministic (thehypothesisdev dependency was previously declared but unused). - CodeQL SAST workflow (
.github/workflows/codeql.yml): weekly + PR/pushsecurity-and-qualityPython analysis, SHA-pinned, minimal permissions (OpenSSF Scorecard SAST check; ruff is a linter, not SAST). - pre-commit configuration (
.pre-commit-config.yaml): ruff-check, zizmor (regular persona), CITATION.cff validation (cffconvert) and standard hygiene hooks. No formatter (history churn) — CI remains the authoritative gate. numerics.adjoint.gram_adjoint(L_super, G): adjoint of the Heisenberg-picture generator w.r.t. an arbitrary Gram matrix (GNS and KMS are the two instantiations).jsonschema>=4.18added to thedevextra so CI actually exercises the fullDraft202012Validatorstructural-conformance path for run manifests. It was previously absent from the dev/CI environment, so_compiled_validator()returnedNoneand the manifest schema-contract test was silently skipped on every CI run (the built-in required-field fallback still ran). No runtime change —jsonschemaremains an optional runtime extra with graceful fallback.tests/test_cptp_choi.py: regression coverage for the new non-finite / invalid tolerance seams, plus an independent partial-trace oracle that pins thechoi_matrixtensor-leg convention (the identity channel's Choi matrix is the unnormalised maximally entangled operator, noteye(d**2)) and its CP-boundary PSD-ness acrossd in {2, 4, 8, 16}.
ci.yml/ci-qutip.yml:concurrencygroups auto-cancel superseded runs on non-main refs (sp-repo-review GH102).pypi.yml: fail-closed release QA gate (twine check --strict+check-wheel-contents) between build and Trusted-Publishing upload.scorecard.yml:publish_results: truenow that the repo is public (badge-capable, externally auditable score); stale private-repo comment refreshed.pyproject.tomlstrictness batch (sp-repo-review): pytest gains--strict-config,xfail_strict,log_levelandfilterwarnings = ["error", ...](unexpected warnings — including deprecations from dependency drift — now fail CI; the documentedResolventConvergenceWarninglower-bound path is allowlisted); mypy gainswarn_unreachable+enable_error_code = [ignore-without-code, redundant-expr, truthy-bool];wheelremoved frombuild-system.requires(setuptools adds it itself when needed); redundant rufftarget-versiondropped (inferred fromrequires-python).pypi.yml: the Trusted-Publishing step now setsprint-hash: trueso each uploaded sdist/wheel's SHA-256 is logged for the release-evidence lock. No second attestation step is added — the PyPA action already uploads a PEP 740 attestation by default under Trusted Publishing, and PyPI rejects duplicate predicates / more than two attestations per file.docs/RELEASE_AUDIT_v0.5.0.md§5 documents the evidence flow; the stale "private repo" comment was refreshed to "public".
- README: the diagnostic layer table now uses the code-backed D-numbering (module docstrings / StabilityReport keys as source of truth) — the previous table circulated a second, contradictory numbering scheme. The "24 diagnostics" claim is qualified honestly: D1-D20 (+D2b/D7b/D11b) and D24 are code-backed; D21-D23 are schema-defined but not yet implemented.
- README/AGENTS.md reproducibility claims corrected to what the code does: the
manifest does not (yet) record lattice geometry / dissipator family / full
result graph;
seed_everythingdoes not control SciPy/BLAS threading. - README quickstart no longer suggests the dephased-qubit example classifies as "A1" (see the tracked Mpemba false-positive issue).
- Synchronized
docs/CANON_STATUS.mdandAGENTS.mdwith the v0.5.0 runtime canon: public-repo status, Python 3.14 CI coverage, the dedicated QuTiP cross-check checks, and theStabilityReport v2.1additive projection. - Added an Architecture Decision Record directory (
docs/adr/) with ADR 0001 — Scientific-Python support policy (SPEC 0). ADR 0001 records the decision to follow SPEC 0 and the post-v0.5 target (requires-python >=3.12, CI 3.12/3.13/3.14, drop 3.10/3.11 in a dedicated support-policy release). It is a decision record only —pyproject.toml, the CI matrix, and classifiers are unchanged in this PR. - Consolidated the duplicate support-policy ADR: the governance detail from
docs/ADR_SUPPORT_POLICY.md(release classification, evidence gate, the required-steps checklist) is merged intodocs/adr/0001-python-support-policy.md, and the old path is reduced to a pointer stub so existing links still resolve. Single numbered ADR going forward.
0.5.0 - 2026-06-25
Release cut: the Canon v0.5 diagnostics & contracts wave plus the cross-family
CPTP-Choi hardening (PR #55, B1/B2) and the repo-wide mypy gate fix that
shipped on main after v0.4.1. MINOR bump per SemVer: backward-compatible,
additive API surface (8 validated formelbuch entries LIOU-A-011/A-012/A-013,
F-018/F-019/F-020, RPT-001, NG-003; new StabilityReport v2.1 projection and
cptp Choi gate). No diagnose() / DiagnosticReport break, no manifest
schema bump (the new StabilityReport is a separate, additive projection).
- CPTP Choi gate (
liouscope.numerics.cptp) hardened against non-GKSL / corrupted input (cross-family math review of PR #55, B1/B2). The gate now fail-closes on two seams a naive Choi test waved through:- B1 — trace preservation is checked at the propagator, not the generator.
The TP residual was
||<<I| M_L||(the generator), which is dt-/scale-blind: a sub-tolerance generator violation amplified by a largedtis a gross propagator trace violation that an absolute-tolerance generator check passes. Repro: generator residual7.07e-10(< the old1e-9) but propagator trace scaled ~148x -> real TP residual~208; the old gate reportedis_tp=True. Now TP is||<<I| Phi - <<I||onPhi = exp(dt*L)->is_tp=False. - B2 — Choi Hermiticity is checked before the PSD test. A non-Hermiticity-
preserving map has a non-Hermitian Choi matrix; Hermitising it (
(J+J^dag)/2) before taking the minimum eigenvalue masked the defect. Repro: a depolarizing channel plus a small non-HP term has Hermitisedmin_eig > 0(old PSD check "CP") yet||J - J^dag|| > 0; the old gate reportedis_cp=True. Now a non-Hermitian Choi forcesis_cp=False. - Absolute
1e-9tolerances replaced by relative ones (scaled by||J||/sqrt(d)) so the verdict is scale-invariant.ChoiGateResultgainschoi_herm_residualandis_hp. The gate's CP claim therefore now also fail-closes on non-GKSL / corrupted input, not only on physical channels. Regression tests added for both repros intests/test_cptp_choi.py.
- B1 — trace preservation is checked at the propagator, not the generator.
The TP residual was
liouscope.fitting.holdout.train_holdout_splitnow rejects a non-strictly- increasing (or non-finite) time grid, preventing future-sample leakage into training from an unordered series (cross-family review c3).liouscope.fitting.whitenessdocstring: precise Ljung-Box (1978) citation (Biometrika 65(2), 297-303) and an explicit statement of thedof = m - n_paramschoice (defaultn_params=0, the conservative fail-closed dof; cross-family review c2). No behaviour change.- Repo-wide mypy gate failure on the Python 3.12-3.14 CI matrix. NumPy >=2.5
ships type stubs that use the PEP 695
typestatement, which mypy rejects while parsingnumpy/__init__.pyiunless its target is >=3.12. Raised[tool.mypy] python_versionfrom3.10to3.12. Runtime 3.10/3.11 support is still guarded by the test matrix andruff target-version = py310; the change only affects type-checking semantics, not packaged code. (Preferred over cappingnumpy<2.5so v0.5 development stays on the current NumPy.)
-
Canon v0.5 diagnostics & contracts (additive, backward-compatible; validated formelbuch entries LIOU-A-011/A-012/A-013, F-018/F-019/F-020, RPT-001, NG-003). Each new diagnostic is pinned to an independent oracle (closed form, QuTiP, or analytic soll-value), never to its own machinery:
- CPTP Choi-PSD gate (
liouscope.numerics.cptp, LIOU-A-011): verifies complete positivity ofexp(dt*L)via the Choi-matrix minimum eigenvalue (>= -tol) plus the trace-preservation residual|| <<I| M_L ||. Oracles: the transpose map (positive but not CP) has Choimin_eig = -1; a dephasing channel sits on the CP boundary atmin_eig = 0(entry beleg); and an Euler stepI + dt*Lis shown non-CP whereexp(dt*L)stays CP (entry NR-002: Euler positivity is not a CP proof). Dense-only CP proof by design. - Trace distance D_tr (
liouscope.diagnostics.relaxation.trace_distance, LIOU-F-018):(1/2)||rho-sigma||_1, the observable relaxation metric beside D5/D6/D7, plus an additiveRelaxationResult.trace_distance_curve. Oracles: orthogonal pure states -> 1, diagonal states -> total variation, Fuchs-van de Graaf bounds vs the Uhlmann fidelity, CPTP contractivity, andqutip.tracedist. - Temporal holdout split (
liouscope.fitting.holdout, LIOU-A-012): an out-of-sample anti-overfit gate for the M0-M3b hierarchy (time-ordered tail, no shuffling). - Residual-whiteness gate (
liouscope.fitting.whiteness, LIOU-A-013): Ljung-Box Q with the chi-squared reference fromscipy.stats(white noise passes, AR(1) is rejected). - Metamorphic spectral oracles (
tests/test_metamorphic_spectral.py, LIOU-F-020):gap(c*L)=c*gap(L)andspec(U L U^dag)=spec(L)— ground-truth- free invariants. - Gap-invariant reproduction (
tests/test_gap_invariants_canon.py, LIOU-F-019): reproduces the pack mini-oracle parametrisation (amplitude dampinggap=gamma/2; thermalg_down=0.9, g_up=0.2, omega=1.3 -> gap=0.55). NOTE: the generalgamma/2and(g_up+g_down)/2oracles already exist intests/test_qutip_spectral_oracle.py(PR#51); this adds the exact pack parametrisation. Dephasing2*gammais intentionally NOT re-added (canon). - StabilityReport v2.1 contract (
liouscope.io.stability_report+ packagedSTABILITY_REPORT_SCHEMA.json, LIOU-RPT-001): a claim-safe, machine-auditable projection of aDiagnosticReportaddingclaim_level(SAFE/REVIEW/BLOCK),direction,cp_evidence_level, independently recomputed invariant residuals, anevidence_bundleandprovenance. New diagnostics carryclaim_status="pending". Purely additive — it does NOT modify the existing run manifest,MANIFEST_SCHEMA.jsonorDiagnosticReport(no schema-version bump; older artefacts stay valid). - Petermann interpretation caveat (D9 docstring, LIOU-NG-003): a large
Petermann factor is necessary-but-not-sufficient for transient amplification;
sup_t ||e^{tL}||is bounded by the Kreiss constant (D10) / numerical abscissa (D15), not by the Petermann factor alone.
Every new public boundary ships negative/edge-input gates (negative
dt, NaN, non-square dim, badholdout_frac,m>=N, non-positive dof, out-of-enum verdict fields, tampered schema fields). Verified locally on CPython 3.14 via the CI command chain:ruff check src tests benchmarks(exit 0),mypy src/liouscopeclean at--python-version 3.12(the pyproject-default invocation aborts on an unrelated numpy-stub/toolchain skew in the local sandbox), full suitepytest --cov-fail-under=80(375 passed, coverage 93.9%), anchorspytest tests/test_anchors.py(21 passed),pytest -m qutip(8 passed). - CPTP Choi-PSD gate (
-
Independent-oracle cross-checks for the non-normality layer D8-D11 (
tests/test_nonnormality_oracle.py). The previous tests (tests/test_nonnormality.py) only asserted signs and self-consistency (eta > 0,K > 0,length >= 1), so a wrong normalisation or eigenvalue filter would pass. The new module pins each diagnostic to a closed-form or an independent numerical oracle, never to the library's own machinery: D8 Henrici against the unitarily-invariantsqrt(||A||_F^2 - sum|lambda|^2)(Henrici 1962; closed form|b|for[[0,b],[0,-1]]); D9 Petermann against the 2x2 adjugate condition-numbertr(B0^H B0)/|li-lj|^2(e.g. Phys. Rev. Research 5, 033042 (2023)) plus the normality floor (Petermann = 1 for a pure-dephasing Liouvillian); D10 Kreiss against the continuous-time reference facts (K = 1 for a normal Hurwitz matrix; K >= 1 always; closed formsqrt(1+b^2)for[[0,b],[0,-1]]) and an independent 2D Nelder-Mead resolvent-norm maximisation (Kreiss matrix theorem; Mitchell, SIAM J. Matrix Anal. Appl. 41(4), 2020); D11 Bohr AP against hand-constructed spectra with a known longest arithmetic progression and thelog_2(d)Pauli bound (Basso, arXiv:2510.07267, 2025). Each oracle includes a non-vacuous negative control (wrong-zero Henrici, missing-denominator Petermann via eigenvalue gap != 1, grid-cannot-exceed-oracle Kreiss, no-three-term-AP Bohr). Test-only; no production code, anchor, orDiagnosticReportoutput changes. Verified locally via the CI command chain on CPython 3.14:ruff check src tests(exit 0),mypy src/liouscope(exit 0),pytest -qfull suite (307 passed, 1 skipped — pre-existing localjsonschema-extra skip),pytest -m qutip(6 passed), new moduletests/test_nonnormality_oracle.py(26 passed). -
Independent-oracle cross-checks for the spectral diagnostic layer (
tests/test_qutip_spectral_oracle.py). The previous QuTiP cross-checks only validated the Liouvillian builder (matrix construction); the diagnostic outputs (D1 gap, D3 oscillating gap, steady state, GNS/KMS symmetrised gap) were only asserted for self-consistency and loose magnitudes (abs(gap - 0.2) < 0.05 or abs(gap - 0.4) < 0.05). The new module pins them on three canonical GKSL systems (amplitude damping, coherently driven dephasing, detailed-balance thermal qubit) against two independent oracles: closed-form analytic spectra (always runs) andqutip.liouvillian(...).eigenenergies()/qutip.steadystate(...)(@qutip_required). Includes the Mori-Shirai prediction that the symmetrised gap coincides with the standard gap at equilibrium (PRL 130, 230404 / arXiv:2212.06317). Test-only; no production code, anchor, orDiagnosticReportoutput changes. Verified locally via the CI command chain on CPython 3.14.4:ruff check src tests(exit 0),mypy src/liouscope(exit 0), anchors (21 passed), full suite with coverage (281 passed, 1 skipped — pre-existing localjsonschema-extra skip, coverage 93.54% ≥ 80% gate),pytest -m qutip(6 passed). -
CI test matrix extended to Python 3.14 (
.github/workflows/ci.yml) and3.14added to thepyproject.tomlTrove classifiers. Verified locally via the exact CI command chain on CPython 3.14.4:pip install -e .[dev,qutip](qutip 5.3.0 ships cp314 wheels),ruff check src tests benchmarks(exit 0),mypy src/liouscope(exit 0), anchor regressions (21 passed), full suite (274 passed, 1 skipped, coverage 93.54% ≥ 80% gate). Closes the freshness gap where the library ran clean on 3.14 but CI never exercised it.
- Added
docs/RELEASE_AUDIT_v0.4.1.md, the post-v0.4.1-tag public/citable-release readiness audit (archive/provenance gates + post-v0.4.1 CI hardening). It is documentation-only and is not part of the taggedv0.4.1source snapshot (thev0.4.1tag at commit1965f2bpredates this file); it lives in[Unreleased]as accompanying post-release documentation. Updateddocs/CANON_STATUS.md§5 to point public/citable-release work at this v0.4.1 audit instead of the supersededdocs/RELEASE_AUDIT_v0.4.0.md§5.
codemeta.jsonwas stale: it reportedversion: "0.2.0", described only "twenty diagnostics D1-D20", and setcodeRepositoryto the non-existentgithub.com/coworker-research/liouscope. Synchronized to the repo canon:version→0.4.1(matchingsrc/liouscope/_version.pyandCITATION.cff), diagnostic description → 24 diagnostics D1-D24 (D1-D20 original submission set, D21-D24 post-submission; schemaD1-D24-Übersicht-v3, per_consts.py/MANIFEST_SCHEMA.json), andcodeRepository→github.com/marcohost33-maker/Liouscope. Metadata-only; no runtime/API change.pyproject.toml[project.urls](Homepage/Repository/Issues) pointed togithub.com/coworker-research/liouscope, which does not exist (HTTP 404 — thecoworker-researchorg has no such repo). The published wheel/sdist metadata therefore carried dead project links. Corrected to the canonical repositorygithub.com/marcohost33-maker/Liouscope, matchingCITATION.cff,AGENTS.md("Visibility: PRIVATE (marcohost33-maker/Liouscope)"), and theCHANGELOG.mdversion-compare links. Metadata-only; no runtime/API change.
0.4.1 — 2026-06-16
Release cut: numerics correctness + production-hardening that shipped on main
after v0.4.0, PRs #43–#45 (resolvent conjugate-transpose fix, resolvent
hardening + large-matrix scaling, classifier taxonomy doc fix, and coverage
lifts across the fit, Liouvillian, sparse, classification, and numerics
layers). PATCH bump per SemVer: the change set is a numerics correctness fix
plus tests and documentation corrections. The one new public symbol
(numerics.resolvent.ResolventConvergenceWarning) is a diagnostic warning on a
numerics utility, not a new feature on the diagnose() / DiagnosticReport
API surface, and no anchor or report output changes — see the per-entry
"anchors unaffected" notes below.
numerics.resolvent.resolvent_normlarge-matrix branch (n > 128): the SuperLU power-iteration computed the wrong conjugate-transpose for the resolvent.lu.solve(y.conj()).conj()evaluatesconj(A)^{-1} y, which equals the required(A^H)^{-1} yonly for symmetricA; on the non-normal Liouvillians this kernel targets the returned||(zI - L)^{-1}||_2was wrong by ~50% (e.g. 0.550 vs the dense reference 1.181 on a random non-symmetric matrix). Fixed to solve the LU's conjugate-transpose system directly vialu.solve(y, trans="H"); the power-iteration estimate now matches a dense SVD reference to < 1e-6 relative across seeds. The function is a publicnumericsutility and is not on thediagnose()report path, so no anchor orDiagnosticReportoutput changes (the small-matrix dense branch, used for Hilbert dimensions up to 128, was already correct). (The power-iteration convergence handling is hardened further under "Changed" below.)
- Regression tests for the fit-model layer (
tests/test_models.py): the closed-form M0–M3b evaluations and all initial-guess seeds, including the log-linear M0 regression with its too-few-positive-samples fallback and the FFT-based M3b dominant-frequency pick (supplied-omega and short-signal branches). Raisesfitting/models.pycoverage from 69% to 97% (the only remaining line is a defensiveelseunreachable for equal-length inputs). - Branch coverage for the foundational Liouvillian builder and steady-state
solver (
tests/test_lindblad.py): thejump_ops=Nonedefault, theorder != "F"guard, zero-rate-jump skipping, the rate-length guard (distinct from the jump-shape guard), the non-square-superoperator rejection, the no-exact-null-space smallest-singular-vector fallback, and the traceless-null-space "cannot normalise" RuntimeError. Raisescore/lindblad.pycoverage from 81% to 99%. - Fallback-path tests for the Prony M3b seed (
tests/test_fitting.py): short-signal, non-uniform-sampling, and too-few-samples-for-model-order fallbacks — pinning the robustness guards that letprony_seeddegrade gracefully instead of raising. Raisesfitting/prony.pycoverage to 88%. - Edge-branch tests for
sparse.build.build_sparse_liouvillian(tests/test_sparse.py): the order/shape/rate-length validation errors, thejump_ops=Noneandrates=Nonedefaults, and zero-rate-jump skipping. Raisessparse/build.pycoverage from 71% to 100%. - Regression tests pinning the A1–A12 mechanism classifier decision tree
(
tests/test_classification.py): synthetic-input coverage of every_pick_a_classbranch, the_pick_verdict_tierthresholds (including theEXCLUDEDandUNDEFINEDpaths unreachable through natural confidence values), and the_confidencescoring rules. Raisesclassification.pycoverage from 71% to 100%. - Tests for the previously-untested large-matrix branches of
numerics.resolvent(tests/test_numerics.py): the SuperLU sparse solve (n > 256) and the power-iteration resolvent norm (n > 128, the regression guard for the fix above), plus a clustered-singular-value case proving the norm stays accurate to < 1e-3 when the top two singular values nearly coincide.
diagnostics.classificationdocumentation corrected to match the authoritative_conststaxonomy. The module docstring previously described an unrelated "evidence families" scheme (F1=spectral, F4=resolvent, …) that contradicted_consts.F_FAMILY_DESCRIPTIONS, where F1–F5 denote the literature-anchored gap-failure mechanisms (F1 Mori-Shirai overlap PRL 125 230604; F2 skin effect PRL 127 070402; F3 symmetrised gap PRL 130 230404; F4 quantum Mpemba PRL 127 060401; F5 phantom relaxation arXiv:2306.07876 — all references web-validated). The misleading inline comment on the A3 branch ("F4 resolvent-amplified non-normality") was fixed: A3 = overlap/eigenvector- amplified (Mori-Shirai 2020) correctly maps to family F1, which is what the code already returned — a comment/doc defect, not a behaviour change. Added a guard test (test_family_citations_consistent_between_docstring_and_consts) pinning the docstring and_conststo the same citations so they cannot drift apart again. Also removed a deadf_familyparameter from the private_pick_verdict_tierhelper (it never influenced the verdict/tier). No classifier output changes; anchors unaffected.- D11b/D12 resolvent diagnostics now scale to large Liouvillians. The inline
per-frequency dense inverse + SVD in
diagnostics.resolvent.resolvent_peak_curve(201 denseO(n^3)solves, intractable for larger lattices and a duplicate of the numerics utility) is replaced by a delegation tonumerics.resolvent.resolvent_norm. Forn <= 128— which includes every anchor/example system — this is bit-identical (same dense inverse + SVD); forn > 128it uses the SuperLU shift-and-invert power iteration, so the resolvent peak (D11b) and ridge FWHM (D12) become computable where the dense path previously could not finish. The peak matches the dense reference to machine precision; low-norm tail points of the profile inherit the power iteration's documented ~1e-3 lower-bound behaviour in the clustered regime. numerics.resolvent.resolvent_normhardened for production: the power iteration now emits aResolventConvergenceWarning(new, exported fromnumerics.resolvent) when it exhausts its budget on tightly-clustered top singular values (the returned value is then a documented lower bound), the iteration budget was raised 80 → 200 (moderately clustered spectra now converge; tight clusters improve from ~2e-4 to ~1e-6 relative error), and the size cutoffs / iteration constants are now named module constants instead of magic numbers. New tests cover the warning path and a convergent clustered case.numerics.resolvent.resolvent_normdocstring now documents the method as the standard pseudospectra shift-and-invert approach (Trefethen, Pseudospectra of Linear Operators, SIAM Rev. 1997) and records why plain power iteration is sufficient for the dominant value (value-convergence is robust to top singular-value clustering, unlike eigenvector-convergence — verified empirically), so Lanczos/svdsis intentionally not used.- Packaging metadata modernised to PEP 639:
pyproject.tomlnow declares the license as the SPDX expressionlicense = "Apache-2.0"withlicense-files = ["LICENSE"], and the deprecatedLicense ::trove classifier is removed (the SPDX expression is the single source). Thesetuptoolsbuild requirement is raised to>=77.0(first release with PEP 639 support). This clears the deprecation that setuptools enforces after 2026-02-18 for the oldlicense = {text = ...}table, and is the same Metadata-2.4 machinery behind thetwine checklicense-filenote recorded for this release. No runtime, API, or dependency change.
0.4.0 — 2026-06-07
Release cut: everything below shipped on main between v0.3.0 (2026-05-28)
and this tag (18 commits, PRs #34–#41 — audit waves 2026-06-04/06-06 + D14
physics-scaling). MINOR bump per SemVer: backward-compatible API additions
(gap= forwarding, TransientGridWarning, SOURCE_DATE_EPOCH).
- D14 transient time-grid physics-scaling (audit 2026-06-06, P2-1):
diagnostics.transient.trans_amplitude_ratio(sup_t ||e^{tL}||_2) previously used a fixed coarse gridlinspace(0.01, 5.0, 30). For systems whose relaxation timescale1/Deltafalls outside[0.01, 5]— i.e. the small-gap, strongly non-normal regime the diagnostic is meant to detect — the supremum was silently underestimated (measured 27.8% too low on an amplitude-damping channel with gap 0.01: fixed grid 1.0209 vs the true sqrt(2) = 1.4142, recovered by a dense oracle). The grid is now physics-scaled to the spectral gap (D1) and the numerical abscissa: a two-scale window[0, ~8/Delta]with a log-spaced early segment (to resolve sharp growth peaks) plus a linear late segment. Across damping rates spanning three orders of magnitude the auto-scaled D14 now matches a dense reference to < 1e-3 relative. An explicitt_grid=still overrides the scaling (backward compatible); when no gap and no grid are given, the legacy coarse grid is kept as a fallback. ATransientGridWarningis emitted when the propagator norm is still rising non-negligibly at the right grid edge (the returned sup is then a lower bound).compute_transient_layerforwards the gap sodiagnose()benefits automatically. New tests intests/test_transient.pypin the behaviour against a dense brute-force oracle; the QuTiP physics-kernel parity cross-checks remain green (the change does not touch the Lindblad builder, steady state, or spectrum). - Version single-source (audit 2026-06-06, P0):
pyproject.tomlno longer carries a second hard-coded version literal.[project]now declaresdynamic = ["version"]and[tool.setuptools.dynamic]reads the version fromsrc/liouscope/_version.py(the documented single source). Previouslypyproject.tomlsaid0.3.0while_version.py(= runtimeliouscope.__version__and every manifest'sframework_version) said0.2.0, so a built wheel reported the wrong version and every run manifest recorded a wrongframework_version— a provenance bug for a tool that claims paper-grade reproducibility._version.pyis bumped to0.3.0; a new test (test_version_single_source) assertsimportlib.metadata.version("liouscope") == liouscope.__version__. - Reproducibility claim precision (audit 2026-06-06, P1): the README claim that
two runs produce "byte-identical manifests" was empirically false — the
embedded wall-clock
timestampvaries, so the manifest SHA differed each run. README now states the true property (byte-identical except for the recordedtimestamp;run_id/input_hashare run-invariant) and both properties are gated by tests. - Diagnostic-count consistency (audit 2026-06-06, P1): README headline said
"twenty diagnostics D1-D20" while its own table and the code constant
DIAGNOSTIC_SCHEMA_VERSION = "D1-D24-Übersicht-v3"run to D24. README and AGENTS.md now consistently say "24 diagnostics D1-D24 (D1-D20 submission set; D21-D24 post-submission)".
SOURCE_DATE_EPOCHsupport in the manifest writer (audit 2026-06-06, P1): when this reproducible-builds standard env var is set to a fixed Unix timestamp, the manifest'stimestampfield uses it instead of the wall clock, making the manifest fully byte-identical across runs. An unparseable/negative value is rejected fail-closed (no silent fallback to the wall clock). Gated bytest_manifests_byte_identical_with_source_date_epoch.- Boundary-guard hardening wave (audit 2026-06-06): fail-closed input
validation and structured IO errors at the public surface. No API breaks, no
new mandatory dependencies, anchor regressions unchanged.
numerics.linalg.require_finite_square_2d: a reusable boundary validator that rejects non-finite (NaN/inf), non-square, or empty operators with a structured, argument-namedValueError(e.g."L_super contains non-finite entries (1 NaN, 0 inf)"). Previously such inputs flowed intoscipy.linalg.expm/svdand surfaced as opaque, location-blind LAPACK messages ("array must not contain infs or NaNs"/"SVD did not converge") that named neither the offending argument nor the real defect.diagnose()now validatesL_superand any caller-suppliedrho_initial/rho_steady_state(finiteness + shape match againstd) at the entry boundary, before any numerics run. The previously uncaught path was supplyingrho_steady_state(which bypasses the steady-state SVD), where an inf/NaN inL_superwas only caught deep insideexpm.io.export.load_reportfails closed with structured, path-bearing errors: a missing path raisesFileNotFoundError("report file not found: ..."); malformed JSON or a non-object top level raisesValueErrornaming the file. Replaces the rawjson.loads(Path(path).read_text(...))that gave callers no context (the "exists()-then-read-raw" fail-open class).io.export.dump_reportandio.manifest.dump_manifestnow create missing parent directories (parents=True, exist_ok=True) so nested artefact paths do not fail with a bareFileNotFoundErroron first write.tests/test_input_guards.py(12 tests) andtests/test_export.py(8 tests): negative/edge inputs (NaN, inf, empty, wrong shape, missing file, malformed/non-object JSON) fail before the happy path; valid input is not rejected (no false positives). Closes the prior coverage gap forio.export.
- Statistics-hardening wave (audit 2026-06-04, findings S1-S6):
core.lindblad.DegenerateSteadyStateError+steady_state(allow_degenerate=)(S1): a multi-dimensional Liouvillian null space (non-unique NESS / decoherence-free subspace) now fails closed instead of silently returning an arbitrary representative; opt in viaallow_degenerate=Truefor one representative with aRuntimeWarning.fitting.neff.ar1_correlation_corrected(S2): small-sample bias-corrected lag-1 autocorrelation(rho*(n-1)+1)/(n-3)(Marriott-Pope/Kendall; arXiv:2010.05870) +RuntimeWarningatn <= 40. Wired intofit_gls_ar1so AR(1)-whitened CIs are no longer over-confident at smalln.- Two exact analytic anchors in
tests/test_anchors.py(anti-circularity): pure-dephasing Liouvillian gap =2*gamma, amplitude-damping coherence decay rate =gamma/2, both asserted toatol=1e-12. .github/workflows/ci-qutip.yml: dedicated job that installs the QuTiP extra and runs the QuTiP cross-checks without skipping (pytest -m qutip), plus a guard against a vacuous 0-collected run. Closes the gap where cross-family validation only ever skipped in CI. (Required-check registration is done separately by a maintainer.)_zhou.CLAIM_STATUS/_zhou.CLAIM_REFERENCE(S6): the D24 Zhou predictor is markedpending/unverified because its cited reference (arXiv:2601.06256) could not be independently verified at audit time. README + module docstring annotated accordingly.
_consts.EPS_DIV: canonical division-by-zero floor (1.0e-300) shared by the Petermann inner-product guard indiagnostics.nonnormality.petermann_factorsand_zhou. Replaces the previously hard-coded1.0e-300magic numbers. Deliberately distinct from the physics-scaleEPS_GAP/EPS_SUPP.tests/test_zhou.py: a closed-form anchor for the D24 Zhou predictor (single-qubit pure dephasing, gap = 1, K = 1 -> both bounds = log(1/eps)) plus a defective-mode guard test (a near-defective mode must not poison the finite upper bound).fitting.prony._default_seedand guardedprony_seed: the Prony seed now catchesLinAlgError/ValueErrorfromlstsq/np.rootson near-singular Hankel data (e.g. all-NaN/inf signals), emits aRuntimeWarning, and falls back to a safe default seed with a strictly positive amplitude. New regression tests intests/test_fitting.py(fails-before on the pre-guard code, which raisedLinAlgErroron non-finite input).
fitting.bootstrap.bca_ci(S3, S4): the BCa bias-correctionz0now uses the Efron-1987 half-correction for ties (mean(<) + 0.5*mean(==)) instead of a strict<(which drovez0 -> -infwhen bootstrap replicates equalledtheta_hat); CI endpoints now use linear quantile interpolation (np.quantile) instead of granular nearest-rank.- CI
mypy src/liouscopeis now an enforcing gate (continue-on-errorremoved); the previously type-blind 18mypyfindings were fixed (annotated numpy return locals,is_dataclassinstance narrowing). No behaviour change. _zhou: zero-eigenvalue and division-by-zero thresholds now use the canonicalEPS_GAP/EPS_DIVconstants instead of inline1.0e-10/1.0e-300._zhou.CLAIM_STATUS(S6 re-audit 2026-06-04):pending/unverified ->reference-verified-bound-coarser. The cited reference was independently verified against the arXiv PDF: Yi-Neng Zhou, "Universal Predictors for Mixing Time more than Liouvillian Gap", arXiv:2601.06256 (v3 2026-05-20, University of Geneva). The placeholder title andUNVERIFIEDmarker are replaced with the real title/author/version. The implemented upper bound is in the same family as Zhou's central result Eq.(16) and exact in the normal-mode limit (pure-dephasing anchor), but is a related, generally coarser surrogate: it uses the Petermann (Schatten-2) factorsqrt(K)rather than Zhou's per-mode trace-norm factorC_j = ||rho_j||_1 * ||sigma_j||_op, a single globalgap/K_maxinstead of a per-mode maximum of(1/lambda_j) log(N_mode C_j), and omits theN_modefactor. Differences documented exactly in the_zhoumodule docstring;CLAIM_REFERENCE, README, and the status-lock test updated accordingly. (No formula change.)fitting.neff.ar1_correlation_correcteddocstring hardened (no formula change): added a validity note (first-order Kendall / Marriott-Pope correction; reliable up torho ~ 0.85, residual bias beyond that set by the truncation order) and full references (Marriott & Pope 1954; Kendall 1954; arXiv:2010.05870; Dou et al. 2026, Br. J. Math. Stat. Psychol.). Documents that a higher-order Kendall variant tested marginally better at highrho/nbut was deliberately not adopted to keep the audit formula bit-stable.
0.3.0 - 2026-05-28
liouscope._zhou: Zhou universal mixing-time predictor (D24) as an opt-in diagnostic. FrozenZhouPredictorResultdataclass.
MANIFEST_SCHEMA.jsonmoved from the repo root intosrc/liouscope/MANIFEST_SCHEMA.json.pyproject.tomlalready listed it under[tool.setuptools.package-data], but the file was not actually at that path, so wheels built from PyPI shipped without it. The schema is now correctly bundled (verified by inspecting the wheel) and loaded viaimportlib.resourcesso the lookup works under editable, wheel and zipfile installs.validate_manifestnow uses a cachedjsonschema.Draft202012Validator(per the python-jsonschema performance guidance) instead of the autodetectingjsonschema.validateconvenience wrapper. The bundled schema declaresdraft/2020-12, so this is the matching validator class._utc_now_iso()uses the idiomaticisoformat(timespec="microseconds").replace("+00:00", "Z")pattern. Pinningtimespecguarantees a fixed-width 27-character timestamp string on every call, eliminating the case where zero-microsecond timestamps would lose the fractional component.liouscope.io.manifest_payload: schema-compliant projection of aDiagnosticReportthat includesschema_version,taxonomy_version, anddiagnostic_schema_versionas documented inMANIFEST_SCHEMA.json.liouscope.io.dump_manifest: writes the manifest payload to a JSON file.liouscope.io.validate_manifest: validates a manifest dict againstMANIFEST_SCHEMA.json. Usesjsonschemawhen available, falls back to a built-in subset check otherwise.
liouscope.io.build_manifestnow uses timezone-awaredatetime.datetime.now(UTC). The previousdatetime.utcnow()call was deprecated in Python 3.12 and slated for removal in 3.14.liouscope._zhou.mixing_time_upper_boundrescaling formula. The previous version contained a no-op (epsilon / epsilon) and dropped the1/gapfactor, returning incorrect mixing-time estimates for anyepsother than the original one.ZhouPredictorResultnow carries the spectral gap and Petermann factor used to build it so rescaling is well-defined.- README quickstart code now uses the actual public API
(
one_d_chain,heisenberg_xxz_hamiltonian,boundary_dephasing_jumps,diagnose(L, rho_initial=...),report.relaxation.beta_D,report.relaxation.bca_ci_beta) — the previous snippet referenced symbols that did not exist (Chain1D,XXZ,tau_eff,ci95).
zizmorworkflow security audit added (SHA-pinnedzizmorcore/zizmor-action@v0.5.6via5f14fd08...).- All Actions SHA-pinned (
actions/checkout@v4.2.2,setup-python@v6.2.0, etc) — Welle G Gold-Standard pattern. - Dependabot configured (weekly grouped pip + github-actions, 7-day cooldown against Shai-Hulud-style supply-chain attacks, May 2026).
- OpenSSF Scorecard workflow (private-repo SARIF guard + workflow_dispatch).
- Tier-2.5 Branch Protection:
enforce_admins=true,required_conversation_resolution=true,dismiss_stale_reviews=true(solo-dev pattern,required_approving_review_count=0). delete_branch_on_merge=true..gitattributes(eol=lf) for cross-platform consistency.
0.2.0 -- 2026-04-17
- Twenty diagnostics D1-D20 organised in six layers S/N/R/U/C/G.
- Twelve-class mechanism taxonomy A1-A12 (
TAXONOMY_VERSION = "A1-A12-v3.1"). - Fit hierarchy M0/M1/M2/M3a/M3b with Prony-seed initialisation for M3b.
- Statistical pipeline: GLS with AR(1) residuals, N_eff via Geyer 1992 IPS estimator, AICc with N_eff correction, parametric bootstrap with BCa confidence intervals.
- Sparse path (
liouscope.sparse) with ARPACK shift-invert for d up to 128. - Run manifest with SHA-256 run-id and JSON export. Schema version 1.2.0.
- Four lattice geometries (1D chain, 2D square, honeycomb, triangular) and four benchmark Hamiltonians (Ising, XY, Heisenberg-XXZ, Bose-Hubbard).
- Three dissipator families (bulk, boundary, engineered).
- Five validation systems V1-V5 as library functions in
liouscope.examples. - Paper figure pipeline.
- FIX-1: GNS Gram matrix is
rho_ss^T (x) I(not the KMS form). - FIX-2: Column-stacking via
flatten(order='F')is enforced everywhere. - FIX-3: LEP detection includes complex-conjugate eigenvalue pairs.
- FIX-4: Pauli-sector rate is labelled distinctly from
Delta_s. - FIX-5: M0 fit uses log-linear regression on D(rho||pi) as baseline.
- FIX-6: Henrici eta_N via Schur decomposition.
- E1-E10: All ten normative patches from the v3 audit applied.
- A. Column-stacking
order='F'. - B. GNS Gram
rho_ss^T (x) I. - C. Alicki adjoint direction
L_tilde* = (rho^{-1} (x) I) L (rho (x) I). - D. zgeev (
scipy.linalg.eig) for non-Hermitian Liouvillian. - E. SuperLU for resolvent computations.
- F. AICc-only model comparison (M1/M2 are not nested).
- G. Parametric bootstrap on GLS-AR(1) residuals with BCa-CI.
- H. N_eff via Geyer 1992 IPS in AICc small-sample correction.
- I. Conjugate-pair inclusion in LEP proximity.
- J. supp(rho_0) subset supp(rho_ss) check with eps = 1e-12 regularisation.
- K. HS adjoint distinct from pi-weighted adjoint.
- L.
TAXONOMY_VERSIONstamped on everyClassificationResult. - M. D11 = Bohr-AP (Basso 2025), D11b = resolvent peak.
- N. D24 = Zhou (universal mixing-time predictor), not Lee-Bound.