Skip to content

fix(#135): make Gaussian profile likelihood scale-safe - #147

Merged
marcohost33-maker merged 9 commits into
chatgpt/issue-130-scaled-normsfrom
chatgpt/issue-135-scale-safe-likelihood
Sep 8, 2026
Merged

fix(#135): make Gaussian profile likelihood scale-safe#147
marcohost33-maker merged 9 commits into
chatgpt/issue-130-scaled-normsfrom
chatgpt/issue-135-scale-safe-likelihood

Conversation

@marcohost33-maker

@marcohost33-maker marcohost33-maker commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Scope

Focused statistical/numerical fix for #135, stacked only on the fully exact-head-verified #136 (59f861a571c5ee06d3c5aa2917079fe10eb22641) so the xLASSQ-style scaling primitive is reused rather than duplicated.

Statistical contract

  • remove the absolute 1e-30 variance floor from Gaussian profile likelihood;
  • evaluate unknown-variance Gaussian profile likelihood directly in log(RSS) space;
  • under r -> c r, each non-degenerate model log-likelihood shifts by the common -n log|c|, preserving pairwise delta-logL and AIC/AICc ordering;
  • exact-zero RSS with estimated variance is an explicit unavailable/degenerate likelihood state, not fabricated finite certainty;
  • explicitly supplied finite positive sigma keeps exact-zero residuals valid with finite likelihood.

Numerical contract

scaled_log_sum_squares() reuses #136's exact power-of-two scaled accumulation and never materializes RSS. This avoids both underflow and overflow even when sum(r**2) is outside the float64 range.

Downstream fail-closed propagation

GLSFitOutput.likelihood_degenerate is propagated into FitResult; such fits have success=False, AICc is non-selectable, and parametric bootstrap refuses to resample around the non-estimate.

Regression/discrimination suite

tests/test_issue135_likelihood_scale.py covers:

  • log-RSS across underflow/overflow regimes;
  • delta-logL and AICc-winner invariance across 1e-150 .. 1e150 observable scaling;
  • true RSS above float64 range;
  • exact-zero unknown-sigma degeneracy;
  • exact-zero known-sigma positive control;
  • GLS degeneracy + bootstrap refusal;
  • ordinary noisy positive control;
  • propagation to FitResult and non-selectability.

Change boundary

Exactly seven files differ from #136; no classifier, spectral threshold, N_eff/AICc correction, or #128 anchor calibration is changed here. #128 remains a separate estimand/information-definition problem.

Exact-head verification

Final scientific head: 561c2bb7bf8653e229f552dfea945a3a03e10c6d (one commit directly above #136).

Verification-only PR #148 targeted main because repository PR workflows are configured for main only. On this exact head all evidence families completed successfully:

  • CI: Python 3.10, 3.11, 3.12, 3.13, 3.14 — success (lint, mypy, anchors, full suite; coverage leg on 3.12);
  • Quality Contract — success;
  • CI (QuTiP cross-checks) — success;
  • CodeQL — success;
  • reusable CI pilot — success, including tree-clean.

The first candidate head failed before tests solely on Ruff I001 import ordering; the import block was minimally corrected, the POSIX EOF newline restored, and the final tree was re-squashed to the single commit above before this exact-head matrix was run.

This is slice-level evidence for #135; it is not evidence for unrelated stacked branches.

Add signed build-provenance attestations and verify release artifacts before PyPI Trusted Publishing. Keep permissions least-privilege and action pins immutable.
* docs(#133): record enforced required-check contract and negative proof

* docs(#133): add point-in-time governance evidence lock
@marcohost33-maker
marcohost33-maker force-pushed the chatgpt/issue-135-scale-safe-likelihood branch from fc32bd7 to 561c2bb Compare September 1, 2026 21:50

@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: ba4785f2e3

ℹ️ 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/fitting/aicc.py
Comment thread src/liouscope/fitting/gls.py Outdated
Comment thread src/liouscope/fitting/aicc.py
marcohost33-maker and others added 2 commits September 3, 2026 00:40
…underflowed

Three findings of the round-1 external review on PR #147.

[gls.py:227] When the whitened residuals carry a finite, non-zero RMS below
the smallest positive float64, ``log_rss`` and ``log_sigma`` are both finite
and the profile likelihood is computable -- but ``exp(log_sigma)`` underflows
to 0.0, and this branch returned success=False, log_likelihood=nan,
likelihood_degenerate=True. ``_fit_with_model`` then scored the model inf and
dropped it from selection. That is the ABSOLUTE SCALE BOUNDARY issue #135 set
out to remove, put back one layer down: the same curve in different rate units
either is or is not an AICc candidate.

Reproduced at 6ddd053 before any change, on the reviewer's construction: one
minimum-subnormal residual (5e-324) among 64 otherwise-zero points gives
log_rss = -1488.8801438427624, log_sigma = -746.519513463061 and a profile
log-likelihood of +47686.4367955108, while exp(-746.52) underflows because the
smallest subnormal is exp(-744.44).

The fit now keeps its finite log-space likelihood and stays selectable; only
the scale-dependent evidence is withheld. GLSFitOutput gains
``scale_unavailable`` (additive, default False). ``sigma`` is NaN there,
deliberately NOT 0.0: ``_ar1_resample`` consumes it as the innovation standard
deviation, and 0.0 would draw identical replicates -- a zero-width confidence
interval, which is the failure mode of an uncertainty pipeline rather than a
conservative one. ``parametric_bootstrap`` refuses on the new flag rather than
on ``success``, so "no interval" and "no estimate" stay distinguishable, and
``compute_relaxation_layer`` reports the CI as NaN through its existing
RuntimeError handler.

[aicc.py:40, twice] The likelihood replacement is a methodology change that
can reorder AICc, change the selected model and change the reported rate, and
the commit recorded it in neither place AGENTS.md requires. CITATION.cff now
carries the pending correction (AGENTS.md:121-123) and CHANGELOG.md an
[Unreleased] "Changed" entry for the likelihood itself plus a "Fixed" entry
for the underflow repair above (AGENTS.md:195-197). Both state what the change
is NOT: only the likelihood path was corrected, model selection is not
scale-free in general, and per-model likelihoods are not manifest fields, so
the behavioural change is invisible in input_hash -- archived analyses whose
selected model matters must be re-run.

Discrimination proven per GUARD LINE, 3/3, run DK-20260902T224741-cc2ae3b913bd
(ledger Vero/Data/diskriminierung_runs.jsonl): the underflow detection, the
NaN sigma and the bootstrap refusal each turn their test red at an assertion
when removed, green again after restore. The first attempt scored ROT DURCH
ABSTURZ because the test asserted the warning via ``pytest.warns``, whose
failure is not attributable to a guard line; it now records warnings and
asserts on them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKDUXBV4qXpWX8ZtpqgAZ

@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: cc2cd2c7cb

ℹ️ 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
Comment thread src/liouscope/fitting/aicc.py Outdated
marcohost33-maker and others added 2 commits September 3, 2026 02:33
…refused by arithmetic

Round-2 external review of PR #147.

- _types.py / relaxation.py: FitResult carries `scale_unavailable` and
  `_fit_with_model` copies it. Without it the persisted report showed only
  `bca_ci_beta = (nan, nan)` -- the value ANY bootstrap or jackknife failure
  produces -- and the reason lived in a warning the artefact drops.
- aicc.py: the standardised RSS enters as `-0.5 * RSS`, so its representable
  range reaches `2 * float64.max`. The guard compared against `log(max)` and
  returned -inf for the octave above it; the bound now carries `+ log(2)` and
  the exponential is taken after subtracting `log(2)` where that is needed.
  Below that octave the arithmetic is untouched, so no existing likelihood
  moves by even one ulp (pinned by a bit-identity test).

Beleglauf folgt (volle Suite, rund 5 min).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKDUXBV4qXpWX8ZtpqgAZ
…OverflowError

The mutation run reported ROT DURCH ABSTURZ:OverflowError for this control:
without the guard, math.exp raises before any assertion runs, so the death was
not attributable and the case did not count as a proof. The outcome is captured
and its TYPE checked with isinstance instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKDUXBV4qXpWX8ZtpqgAZ

@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: cee633f9a4

ℹ️ 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/fitting/aicc.py Outdated
marcohost33-maker and others added 2 commits September 3, 2026 09:46
…n two logs

Thread PRRT_kwDOSXoNQ86evTEG, src/liouscope/fitting/aicc.py:90.

Round 2 widened the bound to log_max + log(2) and the boundary was still
decided in LOG SPACE, where at this magnitude it cannot be decided.

Measured, sigma=1 with a single residual sqrt(float_max)*sqrt(2):
  log_rss          = 710.475860073944
  log_max + log(2) = 710.4758600739439   -- exactly ONE ulp lower
so the guard returned -inf, although the quantity the formula actually forms
is 1.7976931348623155e+308 = 0.9999999999999999 * float_max, finite.

The obvious smaller repair does not work either, and the code and a test now
say so: exp(log_rss - log(2)) raises OverflowError on this same input,
because one ulp of a logarithm near 710 is a factor of ~1e-16 in the value --
exactly the width of the decision being made.

So the halved sum is MATERIALISED from the residuals: halving each term
before accumulating keeps every partial sum inside float64 whenever the true
half-RSS is, and float64 addition itself decides the boundary. There is no
rounded threshold left to be one ulp wrong about, and an overflow is now a
measurement rather than a prediction.

Verified after the change:
  reviewer's fixture      -> -1.7976931348623155e+308 (was -inf)
  4x float_max            -> -inf
  1e3x float_max          -> -inf
  64 residuals overflowing only in the SUM -> -inf   (a per-term bound would
                                                      have missed this one)
  ordinary/sigma=None/empty/nan/inf/all-zero inputs unchanged
The lower octave keeps 0.5 * exp(...) untouched, so no previously computable
likelihood moves by one ulp -- the promise round 2 made.

Baseline pr147-work @ cee633f: 1094 passed, exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKDUXBV4qXpWX8ZtpqgAZ
…teness

Follow-up inside the same finding, found by the mutation run
DK-20260903T074723-246b5d512bf2, which reported the finiteness check BLIND.

Measuring why exposed a latent state collapse rather than dead code:
  half = inf -> without the check the subtraction already yields -inf
                (measured), so the check is provably equivalent there
  half = nan -> without the check the function returns nan; WITH a blanket
                isfinite() check it would return -inf
and those are different answers. This function returns NaN by contract for
"model-selection likelihood unavailable"; -inf means "dropped from
selection". A guard that silently turns the first into the second is the
same substitution this whole review wave is about.

NaN is unreachable on this path -- a NaN residual is caught by the
isnan(log_rss) gate above, and every term 0.5*s*s is non-negative -- so the
condition now states the invariant it actually means.

M7 stays BLIND after the change and now provably so, rather than by
accident. 19/19 in tests/test_issue135_likelihood_scale.py, ruff clean,
mypy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKDUXBV4qXpWX8ZtpqgAZ
@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Status 2026-09-07 — the three green checks here are not test coverage, and one widely-held belief about this PR is wrong

Two separate things, and it matters that they are separated.

1. The head has never been tested. Confirmed.

head 77b3acea -> check-runs: 3
                 zizmor | zizmor workflow audit | Reject invalid UTF-8 in .github
                 test-matrix jobs: 0

Three green checks, none of which runs a test. Cause, from .github/workflows/ci.yml:

on:
  push:         { branches: [main, "claude/**"] }
  pull_request: { branches: [main] }

This branch is chatgpt/* (not claude/**) and targets chatgpt/issue-130-scaled-norms (not main). It falls through both triggers. That is structural, not a flake, and it applies identically to every chatgpt/* PR in this stack that does not target main.

The last real matrix on this branch ran at 561c2bb7, five commits back. Everything since — cc2cd2c, 9808a9a, cee633f, 6507277, 77b3ace — has never been through ruff, mypy or pytest on a runner.

2. Correcting a claim that has been circulating: the matrix at 561c2bb7 is GREEN, not red.

The story going round is that the exact-head verification PR #148 is red on a single ruff I001 in fitting/gls.py, and that the invariance test file therefore never executed. Measured, that is wrong.

561c2bb7 (head of the closed verification PR #148): 13 check-runs, 0 non-success
  test (ubuntu-latest, 3.10 / 3.11 / 3.12 / 3.13 / 3.14)   success
  qutip-cross-check (3.11 / 3.12)                          success
  quality contract | CodeQL | reusable gate                success

The red run was real but earlier and short-lived — 15f3eaa2 and ba4785f2 at 21:47 / 21:49 on 2026-09-01, and the failure was indeed ruff check (Found 1 error, exit 1, lint runs before pytest). It was superseded 65 seconds later by 561c2bb7, which is green across the full matrix. So the invariance tests did execute in CI, once, at 561c2bb7.

ruff at the current head is clean as well:

ruff 0.15.20 (the exact pin from pyproject.toml)
  77b3ace  -> All checks passed!   exit 0
  561c2bb7 -> All checks passed!   exit 0

That is an absence claim, so it carries a positive control: injecting a misordered import into src/liouscope/fitting/gls.py in a scratch worktree makes the same invocation report I001 Import block is un-sorted or un-formatted --> src/liouscope/fitting/gls.py:13:1. The check can see the defect; the defect is not there.

What Marco has to decide

The lint is not the problem. The trigger is. Nothing needs fixing on this branch for ruff. What is missing is a test run on 77b3ace, and no amount of reading this PR's rollup will produce one. Two ways out, both his call:

Measured locally in read-only worktrees at 77b3ace and 561c2bb7. No push, no re-run, no workflow_dispatch, nothing resolved.

@marcohost33-maker
marcohost33-maker merged commit bf6b33b into chatgpt/issue-130-scaled-norms Sep 8, 2026
3 checks passed
@marcohost33-maker
marcohost33-maker deleted the chatgpt/issue-135-scale-safe-likelihood branch September 8, 2026 12:21
marcohost33-maker added a commit that referenced this pull request Sep 11, 2026
… representable

The #124 rescaling divides the optimiser residuals by max|y|. SciPy's
finite-difference probes step in absolute parameter units, so for a tiny
scale the rescaled residual, Jacobian or cost leaves float64; least_squares
then raised or failed and the fit was reported unsuccessful. That broke the
#147 contract (an unrepresentable MLE scale withholds only the CI, not the
fit): tests/test_issue135_likelihood_scale.py failed 2/21 after main was
merged in, and _fit_with_model("M0") regressed vs main at 1e-150 and 1e-310.

The rescaled solve now runs under np.errstate(call=...); any FP exception or
non-finite cost/fun/jac sends the iteration to the raw residuals (exactly
main's problem) with a RuntimeWarning. A finite rescaled solve that did not
converge is still a failure. Adds a Jacobian-level regression test.

Full-suite evidence run follows this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186oBqQsooNFqonGmyBd9Mp
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.

1 participant