Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
url: https://pypi.org/p/liouscope
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand Down Expand Up @@ -103,6 +104,18 @@ jobs:
test "$SOURCE_VERSION" = "$EXPECTED_VERSION"
test "$HEAD_SHA" = "$TAG_SHA"
test "$EVENT_SHA" = "$TAG_SHA"
- name: Generate build provenance attestation
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: "dist/*"
- name: Verify build provenance attestation
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
for artifact in dist/*; do
gh attestation verify "$artifact" --repo "${{ github.repository }}"
done
- name: Publish via Trusted Publishing
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,60 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **The Gaussian likelihood behind AICc is evaluated in log-RSS space, and an
exact-zero RSS is now an explicit abstention (issue #135).** This is a
METHODOLOGY change with user-visible consequences: it can reorder AICc,
change which of M0..M3b is selected, and therefore change the reported decay
rate for a given run. Three parts:
- `gaussian_log_likelihood` evaluates the profile likelihood at the Gaussian
MLE directly from `log(RSS)` via the new
`numerics.norms.scaled_log_sum_squares`, instead of forming RSS in ordinary
units. Residual series whose true sum of squares lies outside the float64
range — a rescaled curve is enough — previously produced `0` or `inf` RSS
and an unusable likelihood. The value is now scale-covariant, and
likelihood *differences* between models are invariant under a common
rescaling of the residuals, which is the quantity AICc actually consumes.
- An exact-zero RSS has no finite interior MLE for the positive scale
parameter, so the profile likelihood is `NaN` rather than an invented
absolute epsilon variance. `fit_gls_ar1` reports this as
`likelihood_degenerate=True` with `success=False`, `aicc()` then scores the
model `inf`, and `parametric_bootstrap` refuses: a perfect fit yields no
interval rather than a zero-width one. Passing an explicit positive `sigma`
still gives a finite likelihood for zero residuals, which is the
well-posed case.
- Runs whose reported model or rate changes are those where the previous
absolute-RSS path had over- or underflowed, or where the winner was chosen
against a floored likelihood. Re-run any archived analysis whose selected
model matters; the run manifest does not record per-model likelihoods, so
the change is not visible in `input_hash`.

### Fixed
- **A fit was withheld entirely because a number its likelihood never uses
could not be materialised (PR #147, round-1 review).** When the whitened
residuals have 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 `fit_gls_ar1`
then returned `success=False`, `log_likelihood=NaN` and
`likelihood_degenerate=True`. `_fit_with_model` scored that model `inf` and
dropped it from selection, which reintroduces exactly the ABSOLUTE SCALE
BOUNDARY into model selection that the change above removed: the same curve
in different rate units either is or is not a candidate. Measured on one
minimum-subnormal residual (`5e-324`) among 64 otherwise-zero points:
`log_rss = -1488.88`, `log_sigma = -746.52`, profile log-likelihood
`+47686.44`, `exp(log_sigma) = 0.0`.

The fit now stays selectable with its finite log-space likelihood, and only
the scale-dependent evidence is withheld. `GLSFitOutput` gains
`scale_unavailable` (additive, default `False`); `sigma` is `NaN` there,
deliberately not `0.0`, because `_ar1_resample` consumes it as the
innovation standard deviation and `0.0` would generate identical replicates
— a zero-width confidence interval, which is the failure mode of an
uncertainty pipeline, not 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 handler. Three mutations, one per guard line, are
proven to discriminate.
- **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,
Expand Down
17 changes: 17 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@ abstract: >-
# likewise scale-relative rather than absolute (issue #109), which closes a
# fail-open path that admitted non-GKSL generators at small ||H||; this changes
# which inputs are ACCEPTED, not the numerics of accepted ones.
# Also pending for the next cut: the Gaussian likelihood behind AICc is
# evaluated in log-RSS space rather than by forming the residual sum of squares
# in ordinary units, and an exact-zero RSS is an explicit abstention rather than
# a floored variance (issue #135). This CHANGES NUMERICAL RESULTS and is a
# METHODOLOGY correction, not an addition: AICc ordering, the selected model
# M0..M3b and therefore the reported decay rate can differ for runs whose
# residual scale previously over- or underflowed, and a perfect fit now yields
# no confidence interval instead of a zero-width one. The likelihood value is
# scale-covariant and likelihood DIFFERENCES between models are invariant under
# a common rescaling of the residuals, which is the quantity AICc consumes. It
# must NOT be described as making model selection scale-free in general: only
# the likelihood path was corrected, and the residual-scale parameter itself
# still has a float64 representation limit -- a fit whose MLE scale underflows
# keeps its log-space likelihood and stays selectable, but its bootstrap
# interval is withheld (round-1 review of PR #147). Archived analyses whose
# selected model matters should be re-run; per-model likelihoods are not run-
# manifest fields, so this change is not visible in input_hash.
keywords:
- open quantum systems
- Lindblad
Expand Down
48 changes: 48 additions & 0 deletions docs/GOVERNANCE_AUDIT_2026-09-01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# LiouScope governance audit — 2026-09-01

Repository: `marcohost33-maker/Liouscope`
Observed `main`: `386041b4072a776a985beb34af5815fb344c74f9`

## Scope

This is a point-in-time repository-governance evidence lock. It does not amend the historical scientific or packaging claims of `RELEASE_AUDIT_v0.5.0.md` and does not certify settings that the available integration cannot read.

## Required checks observed on `main`

GitHub branch metadata reports `main` as protected, enforcement level `everyone`, with these required status checks:

- `test (ubuntu-latest, 3.10)`
- `test (ubuntu-latest, 3.11)`
- `test (ubuntu-latest, 3.12)`
- `test (ubuntu-latest, 3.13)`
- `test (ubuntu-latest, 3.14)`
- `qutip-cross-check (3.11)`
- `qutip-cross-check (3.12)`
- `quality contract`

## Negative-control proof

Draft PR #141 intentionally adds one inert `workflow_dispatch`-only workflow containing a mutable `actions/checkout@main` reference. The `Quality Contract` workflow completed with `failure`; its `quality contract` job failed specifically at `Check workflow hardening`.

This demonstrates that the aggregate required check receives and propagates a repository-policy failure. The control workflow is deliberately non-mergeable test material and must be removed after the evidence is captured.

## Positive / non-workflow proof

This audit and the accompanying `QUALITY_WORKFLOW_OS.md` update are documentation-only. Their PR is used to verify that the required `quality contract` is emitted for a non-workflow change and reaches a terminal result rather than remaining indefinitely `Expected`.

## Explicitly unverified protection settings

The integration can read the branch summary but receives HTTP 403 for the detailed branch-protection endpoint. Therefore this audit does not claim independently verified values for:

- required approving-review count;
- force-push allowance;
- branch deletion allowance;
- administrator bypass;
- linear-history or signed-commit requirements;
- conversation-resolution requirements.

These settings remain a repository-settings verification item if a release or governance claim depends on them.

## Verdict

`quality contract` is observed as a required status check and its fail-closed path is load-bearing. Issue #133 can be closed once the documentation-only positive-control PR is green and the negative-control PR has been closed/reset without merging its deliberate violation.
22 changes: 22 additions & 0 deletions docs/QUALITY_WORKFLOW_OS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ Quality claims must include at least one success metric and one counter-metric:
| release frequency | failed release recovery time |
| coverage | escaped defect rate |

### Current enforced `main` contract — observed 2026-09-01

This is an **observed governance snapshot**, not a timeless claim. At `main@386041b4072a776a985beb34af5815fb344c74f9`, GitHub's branch metadata reports `main` as protected with required-status-check enforcement level `everyone` and the following required checks:

- `test (ubuntu-latest, 3.10)`
- `test (ubuntu-latest, 3.11)`
- `test (ubuntu-latest, 3.12)`
- `test (ubuntu-latest, 3.13)`
- `test (ubuntu-latest, 3.14)`
- `qutip-cross-check (3.11)`
- `qutip-cross-check (3.12)`
- `quality contract`

The `quality contract` is therefore merge-boundary evidence, not merely advisory workflow output, for this observed configuration.

The enforcement path is tested in both directions:

- ordinary PR heads have produced `quality contract = success`;
- controlled negative PR #141 added an inert, `workflow_dispatch`-only workflow with an intentionally mutable `actions/checkout@main` reference; its `quality contract` failed specifically at `Check workflow hardening`. The violating workflow is test evidence only and must never be merged.

A connector-access limitation remains explicit: the repository branch endpoint exposes the required-check set above, but the integration receives HTTP 403 for the full branch-protection detail endpoint. This audit therefore does **not** infer or certify review-count, force-push, deletion, administrator-bypass, or other protection settings that were not independently observable. Those settings must be verified in GitHub repository settings before any release process depends on them.

## 3. Repository Quality Delta Score (RQDS v0.2)

`RQDS = 0.20*CI_Reliability + 0.20*Security_Posture + 0.15*Evidence_Coverage + 0.15*Maintainability + 0.15*Delivery_Stability + 0.10*Observability + 0.05*Cost_Discipline - Penalty`
Expand Down
1 change: 1 addition & 0 deletions src/liouscope/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class FitResult:
n_eff: float
residual_ar1_rho: float
success: bool
likelihood_degenerate: bool = False


@dataclass(frozen=True, slots=True, kw_only=True)
Expand Down
1 change: 1 addition & 0 deletions src/liouscope/diagnostics/relaxation.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def _fit_with_model(
n_eff=n_eff,
residual_ar1_rho=fit.rho_ar1,
success=fit.success,
likelihood_degenerate=fit.likelihood_degenerate,
Comment thread
marcohost33-maker marked this conversation as resolved.
)
return fit_result, fit.params

Expand Down
45 changes: 39 additions & 6 deletions src/liouscope/fitting/aicc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@

from __future__ import annotations

import math
from collections.abc import Mapping

import numpy as np

from ..numerics.norms import scaled_log_sum_squares


def aicc(log_likelihood: float, k: int, n_eff: float) -> float:
"""Return ``AICc`` for given log-likelihood, parameter count, and N_eff."""
Expand All @@ -29,18 +32,48 @@ def gaussian_log_likelihood(
*,
sigma: float | None = None,
) -> float:
"""Gaussian log-likelihood for ``y - y_hat``.
"""Gaussian log-likelihood for ``y - y_hat`` without absolute RSS floors.

Uses MLE sigma if ``sigma`` is omitted.
When ``sigma`` is omitted, evaluate the profile likelihood at the Gaussian
MLE ``sigma_hat**2 = RSS / n`` directly in log-RSS space. Exact zero RSS has
no finite interior MLE for the positive scale parameter and therefore
returns NaN (model-selection likelihood unavailable) instead of inventing
Comment thread
marcohost33-maker marked this conversation as resolved.
Comment thread
marcohost33-maker marked this conversation as resolved.
an absolute epsilon variance. With an explicitly supplied finite positive
``sigma``, zero residuals remain a valid finite likelihood.
"""
residuals = np.asarray(residuals, dtype=float)
n = residuals.size
rss = float(np.dot(residuals, residuals))
if n == 0:
return float("nan")

log_rss = scaled_log_sum_squares(residuals)
if math.isnan(log_rss):
return float("nan")

log_2pi = math.log(2.0 * math.pi)
if sigma is None:
sigma_sq = max(rss / n, 1.0e-30)
if log_rss == float("-inf"):
return float("nan")
if log_rss == float("inf"):
return float("-inf")
return float(-0.5 * n * (log_2pi + 1.0 + log_rss - math.log(n)))

sigma = float(sigma)
if not math.isfinite(sigma) or sigma <= 0.0:
return float("nan")
log_sigma = math.log(sigma)
if log_rss == float("-inf"):
standardised_rss = 0.0
elif log_rss == float("inf"):
return float("-inf")
else:
sigma_sq = sigma * sigma
return float(-0.5 * n * (np.log(2.0 * np.pi * sigma_sq) + rss / (n * sigma_sq)))
log_standardised_rss = log_rss - 2.0 * log_sigma
if log_standardised_rss > math.log(np.finfo(float).max):
return float("-inf")
Comment thread
marcohost33-maker marked this conversation as resolved.
Outdated
standardised_rss = math.exp(log_standardised_rss)
return float(
-0.5 * n * log_2pi - n * log_sigma - 0.5 * standardised_rss
)


def choose_model(aiccs: Mapping[str, float]) -> str:
Expand Down
17 changes: 17 additions & 0 deletions src/liouscope/fitting/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ def parametric_bootstrap(
if rng is None:
rng = np.random.default_rng(0)
base = fit_gls_ar1(model, t, y, p0, bounds=bounds)
if base.scale_unavailable:
# Round-1 review (PR #147). The base fit is a legitimate AICc
# candidate -- its log-space likelihood is finite -- but its innovation
# scale is not representable, so there is nothing to draw replicates
# from. ``sigma`` is NaN there by construction, and ``rng.normal(0, nan)``
# returns NaN rather than raising, so every replicate would be fitted to
# NaN data and the resulting interval would be an artefact. Refusing
# here routes the case into the caller's existing handler, which reports
# the CI as NaN -- "fit uncertainty UNKNOWN", which is the true state.
raise RuntimeError(
"parametric_bootstrap: the base fit has no representable residual "
"scale (issue #135), so replicates cannot be simulated; the fit "
"itself remains a valid model-selection candidate, only its "
"interval is unavailable"
)
if not base.success:
# Round-17 review (PR #121). Every replicate is simulated AROUND
# ``theta_hat``; if the base fit ended on the model's magnitude
Expand All @@ -65,6 +80,8 @@ def parametric_bootstrap(
+ (f" (saturated: {', '.join(base.saturated)})" if base.saturated else "")
+ (" (the curve carries no resolvable variation, issue #123)"
if base.degenerate else "")
+ (" (the residual likelihood scale is degenerate, issue #135)"
if base.likelihood_degenerate else "")
+ "; a bootstrap around a non-estimate has no meaning"
)
theta_hat = base.params
Expand Down
Loading
Loading