Skip to content

fix: scale the default relaxation window to the system's own relaxation time - #115

Open
marcohost33-maker wants to merge 5 commits into
mainfrom
claude/liouscope-repo-analysis-m99u5i
Open

fix: scale the default relaxation window to the system's own relaxation time#115
marcohost33-maker wants to merge 5 commits into
mainfrom
claude/liouscope-repo-analysis-m99u5i

Conversation

@marcohost33-maker

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

Copy link
Copy Markdown
Owner

Stacked on #107. Base is claude/liouscope-repo-analysis-xgztfd, not main, so this diff shows only this branch's commits. #107 is a hard prerequisite, not a convenience: on main this change makes 7 tests fail with RuntimeWarning: overflow encountered in exp, because a physics-scaled window on a small-gap system (V5 near the EP) produces exactly the long grids that #107's _safe_exp / _bounded guards were added for. GitHub will retarget this to main when #107 merges. This PR must not merge before #107.

Summary

The relaxation layer fits every rate it reports on a time grid, and a decay rate carries dimension 1/time — so the absolute default linspace(0.0, 10.0, 80) was an unstated claim about the caller's unit of time. This replaces it with a window measured in the system's own relaxation time, [0, RELAXATION_HORIZON / Δ], and discloses the cases a uniform window cannot cover.

The defect

Measured on an amplitude-damped qubit under the pure rescale L → cL (identical physics, different unit of time), on #107's head:

c beta_D / c beta_D_linear / c AICc winner A-class
1e+02 1.025 0.254 M0 A10
1e+00 1.029 0.482 M2 A5
1e-02 1.085 0.403 M0 A1
1e-04 1.167 0.965 M0 A12
1e-06 1.251 109.99 M0 A12

A 22 % drift in beta_D, a factor ~430 error in the D17 linear rate at c = 1e-6, a model-selection flip between M0 and M2, and four different mechanism classes for one system. Since physical rates are MHz or GHz rather than O(1), this was the common regime, not a corner case. It affects D5, D6, D7, the M0..M3b AICc comparison, beta_D, its BCa interval and the D17 gap-rate check.

After

beta_D / c and beta_D_linear / c invariant to ≤1.2e-3 relative over c ∈ [1e-6, 1e6] (twelve decades) on both an amplitude-damped and a driven dephasing qubit; AICc winner stable; A-class stable over c ∈ [1e-6, 1].

Design notes

  • Uniform by requirement, not convenience. The GLS layer whitens residuals with a single AR(1) coefficient, which presumes a constant sample interval. The transient layer's two-scale grid is right there (a sup_t search with no noise model) but would make the lag-1 correlation position-dependent here and quietly invalidate the whitening, the AR(1) bootstrap and N_eff.
  • HORIZON = 10 is e^-10 ≈ 4.5e-5 of the initial deviation — deep enough to identify the rate, shallow enough that the tail is not almost entirely numerical floor.
  • One D1, two entry points. compute_relaxation_layer gains an optional gap=. diagnose() forwards the D1 it already computed; a direct caller who omits it gets it from compute_spectral_layer, not a local re-derivation — so the certified zero-mode tolerance (Stiff generators: dense eigensolver displaces the whole slow spectrum beyond any threshold's reach — structural deflation needed #112) and the ambiguity rule (Spectral dynamic-range ceiling: above ~4.5e12 spread the zero-mode filter discards the genuine slow modes and D1 reports a fast one #113) cannot drift between the paths. An explicit t_grid remains authoritative.
  • What a uniform window cannot do, it says. samples_per_fast_efolding = 1 / (r_max · max(dt, t[0])) ≈ 7.9 · Δ / r_max — one uniform 80-point window straddles at most about an eightfold spread of timescales. Below one sample per fast e-folding the layer emits an UnderResolvedTransientWarning and records the value. A disclosure, not a repair: widening the window is strictly worse for the reported quantity (measured below), and log spacing is unavailable for the AR(1) reason above. The measure is itself rate-unit invariant, pinned by test.

Backward compatibility

HORIZON = 10.0 is chosen so the grid is bit-identical to the legacy linspace(0.0, 10.0, 80) at Δ = 1, and an unresolved gap (Δ ≤ 0, or NaN per #113) still returns the historical absolute window. No anchor changes: tests/test_anchors.py is untouched and green.

RelaxationResult gains four additive, defaulted fields — t_grid_source ("caller" / "gap_scaled" / "legacy_fixed"), t_grid_span, t_grid (the sampling itself) and samples_per_fast_efolding — so which window produced a given set of rates, and whether it resolved the dynamics, is recorded rather than inferred. The run-manifest contract is unchanged; older serialised reports stay valid.

Review rounds (Codex)

Three rounds, all reproduced before acting; two changed the code, one changed only the claim.

  1. P1, separated decay modes. Confirmed — with rates 1e-6 and 1 the fast mode decays to exactly 0.0 within one step. But not a regression: on that same system the old absolute window put beta_D_linear 3.5e4 relative from the true gap against 0.58 for the gap-scaled one. Both windows fail; they fail at opposite ends, and the new one fails on the quantity that is not this layer's headline output. The suggested fix (keep unit-scaled early coverage, adapt the whitening) is a change to the inference layer, not the grid — out of scope here. Addressed by disclosing instead.
  2. P2, span does not identify the grid. Accepted — [0, 1, 10] and [0, 9, 10] share a span of 10. Sharper than stated: the report already serialised three 80-point curves whose shared abscissa was missing, so the gap predates this PR. t_grid is now stored as a snapshot copy.
  3. P2, blind lead-in. A real hole in the guard added in round 1, and worse than reported: on linspace(100, 101, 101) a rate-1 mode was called "100 samples per e-folding" with no warning, while the entire relative-entropy curve measures identically zero and the fit still returned beta_D = 1.0 off its seed. The denominator is now max(dt, t[0]) — exactly dt when the grid starts at zero, so the default path is bit-for-bit unchanged, pinned by test.

Scope — what this does NOT fix

This closes the time-grid unit dependence only. Open and deliberately neither fixed nor asserted away:

Scope

  • Bug fix (no new feature)
  • New diagnostic or layer (please link the motivating reference)
  • Performance / sparse-path / numerics improvement
  • Docs / examples / packaging only
  • CI / workflow / security
  • Release / packaging / publish evidence

Verification

  • pytest -q passes locally — 973 passed (31 new); all 7 required checks green on CI at 72b7da3
  • tests/test_anchors.py unchanged and green (21 passed)
  • ruff check src tests benchmarks clean; mypy src/liouscope clean (53 files)
  • .github/scripts/check_claim_safety.py passed (21 public-facing markdown files)
  • Methodology/results touched → CITATION.cff updated (DoD item 5): recorded in the documented pending for the next cut block, described as a correction rather than an addition, and explicitly bounded — it states that this does NOT make the fitted rates invariant under an arbitrary change of rate units, since Least-squares convergence is not rate-unit invariant at extreme time-grid spans #111 and F5 evidence path: scale-relative D8/D10/D11b/D13 and mechanism-valid calibration #101 remain open
  • If touching MANIFEST_SCHEMA.json: n/a — the manifest contract is untouched
  • If new external dependency: n/a — none added
  • If touching .github/workflows/: n/a
  • If touching claims/docs/release wording: the new section in docs/explanation/layers-and-taxonomy.md states the measured invariance range, the eightfold resolution ceiling, and the dependences that remain open
  • If touching branch protection / required checks: n/a

Quality contract

  • This PR does not introduce unsupported production-ready, externally certified, PyPI-published, DOI/Zenodo-archived, or release-complete wording
  • This PR preserves the research / pre-clinical disclaimer unless a separate evidence-locked release audit proves a status change
  • False-pass and rework risk considered, not only green CI — the invariance tolerance (RTOL_RATE = 1e-2) sits ~8× above the measured floating-point noise and ~20× below the regression it guards, so it can neither flake nor pass a revert. The new disclosure is pinned in both directions (fires on real separation, silent on single-timescale systems at three rate units), so it cannot decay into noise that reviewers filter wholesale.

Reproducibility note

seed=1, bootstrap_B=10. Tested on V3 (amplitude-damped qubit, rho_0 = |+><+|, gap = c/2) and a driven dephasing qubit (gap = 0.3c) across c ∈ [1e-6, 1e6], plus a two-timescale pair of independent damped qubits for the disclosure. This CHANGES NUMERICAL RESULTS for any diagnose() call that omits t_grid and whose gap is not Δ = 1; documented under [Unreleased] / Fixed in CHANGELOG.md with the before/after table.

V5 (Jaynes-Cummings near the EP, measured r_max/Δ = 396) legitimately trips the new disclosure; those tests acknowledge it by message filter with the reason stated inline, and the disclosure is pinned positively elsewhere rather than only silenced.

Canon-impact note

None of the canon-gated surfaces are touched: no diagnostic IDs or schema change, no A-class taxonomy semantics change, no manifest fields or hashes, no StabilityReport fields, no version metadata, no D24 wording, no branch-protection or visibility change. RelaxationResult gains four additive defaulted fields, the same purely-additive pattern as beta_D_linear / trace_distance_curve. The citation metadata change is confined to the unreleased pending-notes comment block; version, date-released and the DOI are untouched.

Linked issues

Refs #101, #111, #116 (all scoped out above; the #101 boundary is now pinned by test). Depends on #107.

…on time

The relaxation layer fits every rate it reports on a time grid, and a decay
rate carries dimension 1/time, so the absolute default `linspace(0.0, 10.0, 80)`
was an unstated claim about the caller's unit of time.

Measured on an amplitude-damped qubit under the pure rescale `L -> cL`
(identical physics, different unit of time):

  c       beta_D/c   beta_D_linear/c   AICc    A-class
  1e+02   1.025      0.254             M0      A10
  1e+00   1.029      0.482             M2      A5
  1e-02   1.085      0.403             M0      A1
  1e-04   1.167      0.965             M0      A12
  1e-06   1.251      109.99            M0      A12

A 22% drift in beta_D, a factor ~430 error in the D17 linear rate, a
model-selection flip, and four different mechanism classes for one system.
Physical rates are MHz or GHz rather than O(1), so this was the common regime.

The default window is now [0, RELAXATION_HORIZON / Delta] at 80 uniform
samples: a fixed number of e-foldings of the slowest mode, the only window
carried along by the rescaling. After the fix beta_D/c and beta_D_linear/c are
invariant to <=1.2e-3 relative over twelve decades on both an amplitude-damped
and a driven dephasing qubit, the AICc winner is stable, and the A-class is
stable over c in [1e-6, 1].

The grid is uniform by requirement: the GLS layer whitens with a single AR(1)
coefficient, which presumes a constant sample interval, so the transient
layer's two-scale grid must not be reused here.

Backward compatibility: HORIZON = 10 makes the grid bit-identical to the
legacy linspace(0.0, 10.0, 80) at Delta = 1, and an unresolved gap
(Delta <= 0 or NaN per #113) still returns the historical absolute window.
`compute_relaxation_layer` gains an optional `gap=`; diagnose() forwards the
D1 it already computed, and a direct caller who omits it gets it from
`compute_spectral_layer` rather than a local re-derivation, so the certified
zero-mode tolerance (#112) and the ambiguity rule (#113) cannot drift between
the two entry points. An explicit t_grid remains authoritative.

Audit trail: RelaxationResult gains additive, defaulted `t_grid_source`
("caller" / "gap_scaled" / "legacy_fixed") and `t_grid_span`. The run-manifest
contract is unchanged and older serialised reports stay valid.

Scope: this closes the time-grid unit dependence only. The solver's own
convergence controls (#111) and the rate-dimensioned henrici_eta /
resolvent_peak the classifier consumes (#101) remain open and are neither
fixed nor asserted away; the latter is pinned as a measured boundary
(henrici_eta == c exactly, flipping A5 -> A10 between c = 1 and c = 3).

Test plan: 21 new tests in tests/test_relaxation_grid_scale.py; full suite
963 passed; anchors green; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c8r6BNjBTkpN9KemEG6up
claude added 2 commits August 16, 2026 16:39
The scaling tests interrogate different fields of the SAME sweeps, so each
assertion was re-running an identical pipeline call. Caching per (system, rate
unit) cuts the module from 71.5s to 41.6s locally without changing coverage --
worth it on a five-version CI matrix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c8r6BNjBTkpN9KemEG6up
…r the next cut

CITATION.cff's documented convention: unreleased changes go in the
pending-for-next-cut block, and result-changing ones must be described as
corrections rather than additions. This change alters numerical results for
any run without an explicit t_grid whose gap is not 1, so it owes an entry
(Definition-of-Done item 5).

Bounded to what was measured, and explicitly NOT claiming general rate-unit
invariance of the fitted rates: #111 (solver convergence controls) and #101
(rate-dimensioned henrici_eta gate) remain open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c8r6BNjBTkpN9KemEG6up
@marcohost33-maker
marcohost33-maker marked this pull request as ready for review August 16, 2026 17:06

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86442a07d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"""
if not np.isfinite(gap) or gap <= 0.0:
return np.linspace(0.0, _LEGACY_T_MAX, n_points)
return np.linspace(0.0, horizon / float(gap), n_points)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve early-time sampling for separated decay modes

When a system has widely separated rates, scaling the entire 80-point uniform grid by the smallest global gap makes the first nonzero sample occur after all faster dynamics have vanished. For example, with independent slow and fast channels of rates 1e-6 and 1, and an initial state that already equilibrates the slow subsystem, this grid samples the fast-only trajectory at t=0 and then around 2.5e5 onward, so its rate is unidentifiable; with both modes populated, the fast component is similarly lost. This can corrupt beta_D, beta_D_linear, M2/AICc selection, and therefore the mechanism classification precisely for multiscale/metastable systems. Retain unit-scaled early-time coverage as well as the 1/gap horizon, with whitening performed in a way that remains valid for the resulting sampling scheme.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reproduced, and the mechanism is exactly as described — addressed in f132bea, though not by the suggested route. Detail on both parts.

The failure is real. Two independent amplitude-damped qubits at rates 1e-6 and 1: Δ = 5e-7, grid step 2.53e5, and the fast mode's amplitude at the first non-zero sample is exactly 0.0 (underflow, not merely small). It is unsampled, as you say.

But it is not a regression this PR introduced. On that same system, comparing the two windows directly:

window beta_D_linear vs true Δ = 5e-7
old absolute [0, 10] 0.0177 3.5e4 relative
new gap-scaled [0, 2e7] 7.9e-7 0.58 relative

The absolute window did resolve the fast mode — and missed the relaxation entirely, reporting a rate four to five orders of magnitude off the gap. Both windows fail on a system like this; they fail at opposite ends, and the new one fails on the quantity that is not the layer's headline output.

On the suggested fix. "Retain unit-scaled early-time coverage as well as the 1/gap horizon" is the right instinct, but the parenthetical — "with whitening performed in a way that remains valid for the resulting sampling scheme" — is where it runs aground here. fitting/gls.py::_whiten forms r_k − ρ·r_{k−1} with a single AR(1) coefficient, so a constant sample interval is not a convenience, it is an assumption of the noise model that also carries the parametric bootstrap and N_eff. A two-scale or log grid makes the lag-1 correlation a function of position, and the CI and AICc silently stop meaning what they claim. Making the whitening interval-aware is a genuine change to the inference layer, not a grid tweak, and does not belong in a PR scoped to the window.

There is also a hard limit worth naming: samples_per_fast_efolding = 1/(r_max·dt) ≈ 7.9·Δ/r_max on this grid, so one uniform 80-point window straddles at most about an eightfold spread of timescales. Six decades is not reachable by any choice of uniform window — only by more points (~1e6 here) or by giving up uniformity.

What I did instead — disclose rather than repair. The layer now measures samples_per_fast_efolding from the spectrum and emits an UnderResolvedTransientWarning below 1 sample per fast e-folding, recording the value on the result. Fitting a component that was never sampled is precisely what this project's fail-loud convention exists to prevent; quietly reporting it as if measured was the actual defect your finding points at.

Two details that mattered:

  • I first tried a curve-shape heuristic (share of decay in the first interval) and rejected it: it measures the fast component's amplitude, not whether it was resolved. Measured 0.49 for the fully-unsampled 1e-6/1 case against 0.46 for a mild 0.1/1 one — no discrimination. The spectral measure answers the actual question.
  • The measure is itself rate-unit invariant (identical 3.95 at c ∈ {1e-4, 1, 1e4}, pinned by test). A guard that fired on one choice of time unit and not another would reintroduce, inside the guard, the very defect this PR removes.

V5 (Jaynes-Cummings near the EP, measured r_max/Δ = 396) legitimately trips it — correct signal. Those tests acknowledge the disclosure by message filter with the reason stated inline, and the disclosure is pinned positively in tests/test_relaxation_grid_scale.py (fires on separation, silent on single-timescale systems at three rate units) rather than only silenced.

Full suite 971 passed. Leaving this thread open for a maintainer: if the preference is to make the AR(1) whitening interval-aware and adopt a genuinely two-scale grid, that is a defensible larger change and I'm happy to open it as its own issue — say the word.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Nachtrag zum Angebot von oben: die vorgeschlagene grössere Änderung ist jetzt als #122 erfasst statt als Absatz in einem Thread stehen zu bleiben — „Whitening assumes a uniform grid, so the sampling scheme cannot be improved on its own".

Es hält die Kopplung fest, die den Vorschlag hier scheitern liess (AR(1) mit einem Koeffizienten ⇒ konstantes Intervall ist Annahme des Rauschmodells, nicht Bequemlichkeit — und trägt Bootstrap und N_eff mit), die gemessene harte Grenze (≈ 7.9·Δ/r_max, also höchstens rund achtfacher Zeitskalen-Spreizung pro uniformem 80-Punkte-Fenster) und die beiden Messreihen, in denen altes und neues Fenster an entgegengesetzten Enden scheitern.

Aufgenommen ist auch eine Diskriminierungs-Auflage: ein Test auf dem 1e-6/1-System, in dem die jetzige Umsetzung fällt und die neue beide Raten zurückholt — nicht bloss ein Test, dass das neue Gitter läuft.

Dieser Thread bleibt offen: die Entscheidung, ob die Inferenz-Schicht angefasst wird, gehört dem Maintainer, nicht diesem PR.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This finding stands, and the answer it received was too weak. Thank you for it.

The branch responded by disclosing the problem (UnderResolvedTransientWarning) rather than repairing it, on the stated grounds that a non-uniform grid "would make that lag-1 correlation a function of position on the grid and quietly invalidate the whitening". We measured that claim instead of trusting it, and it does not hold.

Setup: independent channels at rates 1e-6 and 1, OU-correlated noise generated from the exact transition density (Euler would be worthless across eleven decades of step size), 40 trials, fit start values deliberately detuned by 5x with a proof that the parameter actually moved -- otherwise an unmoved seed reads as a perfect fit, which is the same trap #123 describes.

uniform linspace(0, 10/gap, 80) two-scale grid
fast rate identified 0 / 40 40 / 40 (rel. err 3.8e-2)
whitened residual |lag-1|, single rho -- 0.4231
whitened residual |lag-1|, rho_k = exp(-theta*dt_k) -- 0.0945

Positive control (a nearly-uniform grid): CAR(1) collapses back onto AR(1) exactly, 0.0728 vs 0.0728 -- so the correction is inert where it should be.

Both grounds for the disclosure therefore fall. The "constant sample interval" requirement is a property of the discrete parameterisation, not of the process: the continuous-time OU/CAR(1) process has Corr(t, t+d) = exp(-theta*d), so whitening with a per-step rho_k (and rescaling by sqrt(1 - rho_k^2) to keep the innovations homoscedastic) is valid on ANY grid.

A repair is now in progress rather than a disclosure: early-time coverage retained alongside the 1/gap horizon, with the fast scale taken from the spectrum (max |Re lambda|) instead of a guessed parameter, and the consequences carried through the GLS fit, the AR(1) bootstrap, N_eff and M2/AICc selection rather than stopping at the grid call. We will report back with the same mutation evidence used for the other findings on this stack, including which cases -- if any -- still need the warning.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The repair is up: #127.

Following on from my earlier reply, where I said a repair was in progress rather than a disclosure. It is built and pushed.

What changed against what I wrote before: the whitening figures in that message (0.4231 vs 0.0945) came from my own reproduction on a hand-built two-scale grid. The delivered version measures on the grid the layer actually constructs, and reports 0.3738 with one constant rho against 0.0728 with the per-step rho_k — with the constant-rho case taken at the median step, i.e. that scheme's best case. The uniform positive control gives 0.0728 for both schemes, which is what shows the contrast comes from the grid rather than from the comparison. Different grids, both measurements valid; the delivered numbers are the ones that describe this code.

On your specific concern — that the fast scale would have to come from somewhere: it comes from the spectrum, max(-Re lambda), not from a supplied parameter, and the two-scale grid is built only when the uniform one fails to resolve it. On your case the early-time coverage goes from 1 support point to 21, and the AICc-selected model recovers the fast rate as 1.10004 against a true 1.0. The old window reported 2.173286e-05 for that same component — a "successful" two-exponential fit of a mode that was never sampled.

Three things the PR explicitly does not close, since the previous answer on this thread claimed more than it had: intermediate timescales are still under-resolved (the warning fires and names the mode), there is no small-sample bias correction on the CAR(1) path, and Geyer's N_eff is 2-4x optimistic on the uniform path — measured, filed as #128, and deliberately not touched here because swapping it would move every existing anchor.

So the warning stays, but it now means something narrower: caller-supplied grids that do not resolve their own system, and intermediate scales. It no longer fires on the case you reported.

Evidence re-run independently rather than taken from the build: 1019 passed, and 16/16 mutation cases discriminating against the delivered HEAD — that last one mattered, because the final commits were labelled docs: but changed source, so the original proof covered a state that was not being shipped.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This finding is correct on this branch. Measured: default_relaxation_grid(1e-6) puts the first non-zero sample at t=126582, where exp(-1*t) is exactly 0.0 - 7.9e-06 samples per fast e-folding.

It is closed in PR #127, which contains this branch: src/liouscope/diagnostics/relaxation.py:116 takes fast_rate and makes the grid two-scale, and :652 passes the rate through from the pipeline. The same request there yields t1 = 0.25 and 4.0 samples per fast e-folding, with a uniform head.

Deliberately left unresolved here, for the same reason as #107: the fix is on the successor branch. Closing this PR as superseded is a maintainer decision.


Verified locally on the pushed branch: ruff and mypy clean, tests/test_anchors.py 21 passed. Reverse-mutation runs are recorded in an evidence ledger with one run_id per run; a run that dies by error rather than on an assertion is not counted as proof. CI on GitHub not measured here.

Comment thread src/liouscope/diagnostics/relaxation.py
Two findings from the Codex review.

P1 -- widely separated rates. A uniform window scaled to 1/Delta cannot also
resolve a much faster mode: the window must reach ~1/Delta to see the slowest
mode relax while the step must stay under ~1/r_max to see the fastest at all,
and 80 uniform samples straddle only ~8x separation. Confirmed: with rates 1e-6
and 1 the fast mode decays to exactly 0.0 within one step.

Not a regression from the new window -- on that same system the previous
absolute window put beta_D_linear 3.5e4 relative from the true gap against 0.58
for the gap-scaled one -- but silently fitting a component that was never
sampled is what the fail-loud convention exists to prevent. The layer now
measures samples_per_fast_efolding = 1/(r_max*dt) and warns below 1, recording
the value. A disclosure, not a repair: widening the window is strictly worse
for the reported quantity and log spacing would invalidate the AR(1) whitening.

A curve-shape heuristic was tried first and rejected -- the share of decay in
the first interval measures the fast component's AMPLITUDE, not whether it was
RESOLVED (0.49 for a fully unsampled 1e-6/1 separation vs 0.46 for a mild
0.1/1 one). The spectral measure is also itself rate-unit invariant, pinned by
test, so the guard cannot fire on one choice of time unit and not another.

V5 (JC near the EP, r_max/Delta = 396) legitimately trips it; those tests
acknowledge the disclosure by message filter with the reason stated inline.

P2 -- the span does not identify the sampling. [0, 1, 10] and [0, 9, 10] share
a span of 10 while describing different trajectories, and the report already
serialised three 80-point curves whose abscissa was missing. RelaxationResult
now stores the grid itself as a snapshot copy. Additive, defaulted, no
manifest-schema change.

Test plan: 8 new tests (fires on separation, silent on single-timescale at
three rate units, invariant guard, grid identifies sampling, snapshot not
alias, length matches the curves); full suite 971 passed; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c8r6BNjBTkpN9KemEG6up

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f132bea3c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/liouscope/diagnostics/relaxation.py
Second-round Codex finding, against the guard added in the first round.

samples_per_fast_efolding used only dt = t[1] - t[0]. diagnose() permits any
non-negative grid start, so a late-starting grid leaves a lead-in from t=0
that no step size compensates for. Measured on linspace(100, 101, 101) with a
rate-1 mode: reported 100 samples per fast e-folding and emitted no warning,
while the amplitude at the first sample is e^-100 -- the entire
relative-entropy curve measures identically zero and the fit still returned a
confident beta_D = 1.0.

The denominator is now max(dt, t[0]), the largest interval the grid leaves
unsampled. That is exactly dt for any grid starting at zero, so the default
path is bit-for-bit unchanged (verified: 3.95 samples per e-folding at
c in {1e-4, 1, 1e4}, as before). The warning text now names which gap
dominates rather than always printing the step.

Test plan: 2 new tests (late-starting grid warns and reports 0.01; lead-in
inert when t[0] == 0); full suite 973 passed; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c8r6BNjBTkpN9KemEG6up
@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Antwort auf "Preserve early-time sampling for separated decay modes"

Der Befund ist inzwischen repariert, nicht mehr nur offengelegt — allerdings in PR #127, nicht auf diesem Zweig. Dieser Zweig trägt weiterhin nur die Offenlegung.

Auf fix/115-zweiskalen-car1:

  • default_relaxation_grid baut ein Zwei-Skalen-Gitter, sobald das uniforme Gitter die schnelle Mode nicht auflöst — diagnostics/relaxation.py:165-176
  • die schnelle Skala stammt aus dem Spektrum, nicht aus einer Heuristik — fastest_decay_rate, :651-652
  • die Whitening-Kopplung, an der Ihr Vorschlag zuerst scheiterte, ist mit dem per-Schritt-rho_k = exp(-theta*dt_k) aufgelöst — fitting/gls.py:117,159,183

Damit fällt unser Einwand aus der ersten Antwort („ein konstantes Intervall ist Annahme des Rauschmodells"): er galt für die diskrete Parametrisierung, nicht für den zugrundeliegenden Prozess. Das war unser Fehler in der Zuordnung, nicht Ihrer im Befund.

🤖 Generated with Claude Code

@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Status 2026-09-07 — contained in #127, and a Closes band here would be inert

Measured:

#115 head 72b7da3 is an ancestor of #127 head e0a6479   -> true
#115 head 72b7da3 is an ancestor of #121 head bac5fe0d  -> FALSE
base branch: claude/liouscope-repo-analysis-xgztfd      (not main)
check-runs at 72b7da3: 7, of which 5 are test-matrix jobs, 0 non-success

Two consequences.

1. No issue band belongs here. GitHub only honours Closes #N when the PR targets the repository's default branch. This one targets claude/liouscope-repo-analysis-xgztfd, so any keyword written into this body would be structurally inert — it would read as a promise and close nothing. I have deliberately not added one. The same applies to #131, #134, #136, #139, #143, #145 and #147.

2. It must ride with #127, not merge separately. This work is fully inside #127. If #127 is merged with a merge commit, 72b7da3 becomes reachable from main and this PR closes by itself. A squash or rebase rewrites the SHA and leaves this PR open as a zombie whose content is nonetheless already in main — the worst of the three outcomes, because the backlog then shows work that no longer exists anywhere else.

Note also that #115 is not in #121: the two scientific branches diverge here, and only #127 carries it.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants