Skip to content

lmax_signal resolves to one ceiling on Fisher, Spectra and PICSLike - #57

Merged
ggalloni merged 2 commits into
masterfrom
fix/lmax-signal-one-ceiling
Sep 8, 2026
Merged

ggalloni merged 2 commits into
masterfrom
fix/lmax-signal-one-ceiling

Conversation

@ggalloni

@ggalloni ggalloni commented Sep 8, 2026 •

Copy link
Copy Markdown
Owner

What changed

There was no way to set a consistent signal-cov ceiling on a Spectra. The two
routes failed in opposite directions, both silently. Measured at nside=4/T,
harmonic basis:

route Spectra.lmax_signal basis_manager.lmax_signal
basis={"lmax_signal": 8} 16 8
s.lmax_signal = 8 8 16
neither 16 16

The dict route shifted the deconvolved Cl by ~0.6% at the lowest bandpower
against the coherent answer; the setter produced output bit-identical to
setting nothing at all (max|diff| exactly 0.0). PICSLike dropped the key
outright, it being absent from that class's basis-key filter with no setter
alias behind it.

The dict alias moves to Core._absorb_basis_lmax_signal, called from all three
constructors, replacing the block that lived in Fisher alone. The three
classes share _resolve_basis_config, so putting the alias in one of them was
the whole defect.

8752a38 then follows the same defect class one level up. The lmax_signal
property itself moves to Core: Fisher, Spectra and PICSLike each carried
a byte-identical getter, which is how Spectra came to be missing the
params.lmax_signal rung in the first place. Spectra keeps only its refusing
setter, via @Core.lmax_signal.setter. Net 37 lines removed, and the next rung
added to that resolution order gets added once.

Second of three PRs splitting the referee round. The results-unchanged leg
landed already; the qml_vs_pseudocl figure rework follows separately.

Public surface

Breaking:

  • Spectra.lmax_signal now raises RuntimeError when assigned after
    construction, instead of accepting the value and ignoring it. Spectra
    builds and runs its internal Fisher inside __init__ (unlike Fisher,
    which runs only on an explicit run()), so a later assignment cannot change
    the ceiling anything was computed at. The message names the two routes that
    do work. Scripts that set it and quietly got the 4*nside answer now fail
    loudly and need basis={"lmax_signal": N} or lmax_signal: in the config.

  • Spectra(fisher=…) now adopts that Fisher's lmax_signal and raises
    ValueError if the constructor also asks for a different one. A supplied
    Fisher owns the ceiling: its Cls, beams and basis are already built and
    run() reuses them, so a conflicting request cannot be honoured. Matches how
    fisher= already refuses mask=, noise_cov1=, cls_data= and beam=.
    Adopting it also fixes the plain case, where Spectra resolved from
    params.lmax_signal/4*nside independently of the Fisher it was handed and
    could report a ceiling that was not in effect. This path was untouched by the
    first commit and had the same two-ceiling split.

Changed behaviour, results move:

  • basis={"lmax_signal": N} is now a lmax_signal setter alias on all three
    of Fisher, Spectra and PICSLike. On Spectra this changes computed
    output, since the old behaviour ran one analysis at two ceilings. On
    PICSLike the key previously did nothing at all.
  • Spectra.lmax_signal honours params.lmax_signal before falling back to
    4*nside, matching Fisher and PICSLike. A standalone Spectra (no
    fisher=) previously ignored the config key.
  • Setting fisher.lmax_signal / picslike.lmax_signal before run() now
    applies to the computation basis too.

Internal, no surface:

  • Spectra._lmax_signal moves to the top of __init__. It was initialised
    near the end, after _get_fisher() had already run, so reading the property
    during Fisher construction raised AttributeError once the internal Fisher
    was pinned to it.
  • Core's fallback stops using getattr(self, "lmax_signal", None), which
    swallows an AttributeError raised inside a subclass property and degrades
    to 4*nside, the exact silent failure this resolution order exists to
    remove. It dispatches on the class instead.
  • The _resolve_basis_config docstring no longer claims a passed dict is
    "used as-is"; it is copied.
  • lmax_signal is now defined once, on Core. Fisher and PICSLike inherit
    getter and setter; Spectra inherits the getter and overrides only the
    setter. No behaviour change, asserted by the same test set passing before and
    after.
  • _absorb_basis_lmax_signal no longer describes itself as a setter alias. It
    writes _lmax_signal directly, and now says so and why: absorb runs during
    construction, which is exactly the window a setter may legitimately refuse,
    as Spectra's does. Routing it through the property was considered and
    rejected on those grounds.

Checklist

  • docs/source/changelog.rst updated under Unreleased, with the
    RuntimeError under Breaking changes and the Spectra case worded as
    wrong results rather than an inconsistent attribute.
  • ADR: none needed. No decision here is hard to reverse, and ADR-0018
    already governs the alias.
  • CONTEXT.md: no domain term introduced or sharpened.
  • Doc-drift sweep done. Every .lmax_signal = assignment in scripts,
    benchmarks and notebooks is on a Fisher, never a Spectra, so the new
    RuntimeError reaches none of them.
  • Tests pass per package: cosmocore 547 passed, qube 201 passed / 8 skipped,
    picslike 103 passed / 2 skipped.
  • Over-engineering pass run on the diff. It found the three duplicated
    getters, applied in 8752a38.
  • The two fisher= tests were mutation-checked: removing the adoption
    fails both, so they are not decoration.
  • Tests written red-first and confirmed failing for the predicted reasons:
    the dict route failed lmax_signal == 8 (got 16), the setter route failed
    basis_manager.lmax_signal == 8 (got 16).

Note on coverage

test_spectra_ceiling_changes_the_answer asserts the ceiling reaches the
numbers, not just the attribute. That is the assertion the attribute checks
cannot make: the inert setter passed obj.lmax_signal == 8 while changing no
result. It does not pin what the coherent answer is, because the pre-fix
values were the buggy two-ceiling ones and are not worth a golden baseline.

There was no way to set a consistent signal-cov ceiling on a Spectra. The
two routes failed in opposite directions, both silently. Measured at
nside=4/T, harmonic basis: basis={"lmax_signal": 8} left the basis at 8
while Spectra's own property, and so the Cls and beams, stayed at 16,
shifting the deconvolved Cl by ~0.6% at the lowest bandpower; while
s.lmax_signal = 8 moved the reported attribute and nothing else, producing
output bit-identical to setting nothing at all (max|diff| exactly 0.0).
PICSLike dropped the key outright, it being absent from that class's
basis-key filter with no setter alias behind it.

The dict alias moves to Core._absorb_basis_lmax_signal, called from all
three constructors, replacing the block that lived in Fisher alone. The
three classes share _resolve_basis_config, so putting the alias in one of
them was the whole defect.

Spectra needed two further changes. Its internal Fisher is now pinned to
the resolved ceiling before it runs, so the pair agrees by construction
rather than by which spelling the caller used. And _lmax_signal moves to
the top of __init__, beside the basis config: it was initialised near the
end, after _get_fisher() had already run, so reading the property during
Fisher construction raised AttributeError.

The Spectra setter now raises RuntimeError instead of accepting a value it
cannot honour. Spectra builds and runs its Fisher inside __init__, unlike
Fisher which runs only on an explicit run(), so an assignment afterwards
cannot change the ceiling anything was computed at. The message names the
two routes that do work. This is breaking: scripts that set it and quietly
got the 4*nside answer now fail loudly.

Core's fallback also stops using getattr(self, "lmax_signal", None), which
swallows an AttributeError raised inside a subclass property and degrades
to 4*nside, the exact silent failure this resolution order exists to
remove. It dispatches on the class instead. The _resolve_basis_config
docstring no longer claims a passed dict is "used as-is"; it is copied.
Copilot AI lite review requested due to automatic review settings September 8, 2026 09:24
@codecov

codecov Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.14286% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.66%. Comparing base (7152e35) to head (8752a38).

Files with missing lines Patch % Lines
src/cosmoforge.cosmocore/cosmocore/core.py 29.41% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #57      +/-   ##
==========================================
- Coverage   92.75%   92.66%   -0.09%     
==========================================
  Files          40       40              
  Lines        5091     5088       -3     
==========================================
- Hits         4722     4715       -7     
- Misses        369      373       +4     
Flag Coverage Δ
cosmocore 92.00% <29.41%> (-0.28%) ⬇️
mpi 92.59% <57.14%> (-0.09%) ⬇️
nompi 91.43% <57.14%> (-0.09%) ⬇️
picslike 97.24% <100.00%> (+0.38%) ⬆️
qube 93.03% <100.00%> (+0.40%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/cosmoforge.picslike/picslike/picslike.py 96.90% <100.00%> (+0.71%) ⬆️
src/cosmoforge.qube/qube/fisher.py 97.22% <100.00%> (+0.46%) ⬆️
src/cosmoforge.qube/qube/spectra.py 88.43% <100.00%> (+0.58%) ⬆️
src/cosmoforge.cosmocore/cosmocore/core.py 85.80% <29.41%> (-2.78%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI 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.

🟡 Changes recommended

Spectra can accept and report an overridden lmax_signal even when reusing a pre-computed fisher=..., which can misrepresent the effective ceiling unless it is validated/synced to the provided Fisher.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR unifies how lmax_signal is resolved and applied across the QML stack so the signal-covariance ceiling is consistent between Cls/beams, computation basis construction, and downstream consumers in Fisher, Spectra, and PICSLike.

Changes:

  • Introduces a shared Core._absorb_basis_lmax_signal() so basis={"lmax_signal": N} is consistently routed into the instance ceiling across constructors.
  • Fixes ceiling resolution in Core.setup_computation_basis() to prefer the instance’s resolved lmax_signal (setter → params → 4*nside) without masking property AttributeErrors.
  • Adds regression tests in qube and picslike, and documents the behavior change in the changelog (including the Spectra.lmax_signal post-construction RuntimeError).
File summaries
File Description
src/cosmoforge.qube/tests/test_basis_kwarg.py Adds regression tests ensuring lmax_signal reaches both the object and the built basis; asserts the ceiling impacts results.
src/cosmoforge.qube/qube/spectra.py Resolves lmax_signal earlier, refuses late setter updates, and pins the internal Fisher ceiling to the resolved value.
src/cosmoforge.qube/qube/fisher.py Absorbs basis["lmax_signal"] at construction time and removes it from basis-forwarded kwargs.
src/cosmoforge.picslike/tests/test_picslike.py Adds a regression test verifying basis["lmax_signal"] is not silently dropped.
src/cosmoforge.picslike/picslike/picslike.py Absorbs basis["lmax_signal"] into the instance ceiling during initialization.
src/cosmoforge.cosmocore/cosmocore/core.py Adds _absorb_basis_lmax_signal() and adjusts basis setup resolution logic and documentation.
docs/source/changelog.rst Records the breaking change and behavior fixes under Unreleased.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +272 to +276
# lmax for signal matrix computation (matches the Fortran convention of
# 4*nside). Resolved here, ahead of _get_fisher(), because the internal
# Fisher is pinned to this ceiling and the property must already read.
self._lmax_signal = None
self._absorb_basis_lmax_signal()
Comment on lines +172 to +173
if self._basis_config and "lmax_signal" in self._basis_config:
self._lmax_signal = self._basis_config.pop("lmax_signal")
Two review findings on the same defect class the parent commit fixes:
one ceiling, resolved in one place.

The lmax_signal property moves to Core. Fisher, Spectra and PICSLike each
carried a byte-identical getter, which is how Spectra came to be missing
the params.lmax_signal rung in the first place. Spectra keeps only its
refusing setter, via @Core.lmax_signal.setter, so it inherits the
resolution order it used to restate. Net 37 lines removed across the four
files, and the next rung added to that order gets added once.

Spectra(fisher=...) now adopts that Fisher's ceiling, and raises
ValueError when the constructor asks for a different one. This path was
untouched by the parent commit and had the same split: a supplied Fisher's
Cls, beams and basis are already built and run() reuses them, yet Spectra
resolved from params/4*nside independently, so a Fisher built at 8 was
reported as 16 while its 8-ceiling components were used. Refusing a
conflict matches how fisher= already refuses mask=, noise_cov1=,
cls_data= and beam=. Both new tests were mutation-checked: removing the
adoption fails each of them.

_absorb_basis_lmax_signal no longer describes itself as a setter alias.
It writes _lmax_signal directly, and the docstring now says so and why:
absorb runs during construction, which is exactly the window a setter may
legitimately refuse, as Spectra's does. Routing it through the property
was considered and rejected on those grounds.
@ggalloni
ggalloni merged commit 46edb14 into master Sep 8, 2026
21 checks passed
@ggalloni
ggalloni deleted the fix/lmax-signal-one-ceiling branch September 8, 2026 12:17
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