Repository navigation
lmax_signal resolves to one ceiling on Fisher, Spectra and PICSLike - #57
Conversation
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.
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 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()sobasis={"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 resolvedlmax_signal(setter → params →4*nside) without masking propertyAttributeErrors. - Adds regression tests in
qubeandpicslike, and documents the behavior change in the changelog (including theSpectra.lmax_signalpost-constructionRuntimeError).
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.
| # 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() |
| 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.
What changed
There was no way to set a consistent signal-cov ceiling on a
Spectra. The tworoutes failed in opposite directions, both silently. Measured at nside=4/T,
harmonic basis:
Spectra.lmax_signalbasis_manager.lmax_signalbasis={"lmax_signal": 8}s.lmax_signal = 8The 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).
PICSLikedropped the keyoutright, 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 threeconstructors, replacing the block that lived in
Fisheralone. The threeclasses share
_resolve_basis_config, so putting the alias in one of them wasthe whole defect.
8752a38then follows the same defect class one level up. Thelmax_signalproperty itself moves to
Core:Fisher,SpectraandPICSLikeeach carrieda byte-identical getter, which is how
Spectracame to be missing theparams.lmax_signalrung in the first place.Spectrakeeps only its refusingsetter, via
@Core.lmax_signal.setter. Net 37 lines removed, and the next rungadded to that resolution order gets added once.
Second of three PRs splitting the referee round. The results-unchanged leg
landed already; the
qml_vs_pseudoclfigure rework follows separately.Public surface
Breaking:
Spectra.lmax_signalnow raisesRuntimeErrorwhen assigned afterconstruction, instead of accepting the value and ignoring it.
Spectrabuilds and runs its internal Fisher inside
__init__(unlikeFisher,which runs only on an explicit
run()), so a later assignment cannot changethe ceiling anything was computed at. The message names the two routes that
do work. Scripts that set it and quietly got the
4*nsideanswer now failloudly and need
basis={"lmax_signal": N}orlmax_signal:in the config.Spectra(fisher=…)now adopts that Fisher'slmax_signaland raisesValueErrorif the constructor also asks for a different one. A suppliedFisher owns the ceiling: its Cls, beams and basis are already built and
run()reuses them, so a conflicting request cannot be honoured. Matches howfisher=already refusesmask=,noise_cov1=,cls_data=andbeam=.Adopting it also fixes the plain case, where
Spectraresolved fromparams.lmax_signal/4*nsideindependently of the Fisher it was handed andcould 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 almax_signalsetter alias on all threeof
Fisher,SpectraandPICSLike. OnSpectrathis changes computedoutput, since the old behaviour ran one analysis at two ceilings. On
PICSLikethe key previously did nothing at all.Spectra.lmax_signalhonoursparams.lmax_signalbefore falling back to4*nside, matchingFisherandPICSLike. A standaloneSpectra(nofisher=) previously ignored the config key.fisher.lmax_signal/picslike.lmax_signalbeforerun()nowapplies to the computation basis too.
Internal, no surface:
Spectra._lmax_signalmoves to the top of__init__. It was initialisednear the end, after
_get_fisher()had already run, so reading the propertyduring Fisher construction raised
AttributeErroronce the internal Fisherwas pinned to it.
Core's fallback stops usinggetattr(self, "lmax_signal", None), whichswallows an
AttributeErrorraised inside a subclass property and degradesto
4*nside, the exact silent failure this resolution order exists toremove. It dispatches on the class instead.
_resolve_basis_configdocstring no longer claims a passed dict is"used as-is"; it is copied.
lmax_signalis now defined once, onCore.FisherandPICSLikeinheritgetter and setter;
Spectrainherits the getter and overrides only thesetter. No behaviour change, asserted by the same test set passing before and
after.
_absorb_basis_lmax_signalno longer describes itself as a setter alias. Itwrites
_lmax_signaldirectly, and now says so and why: absorb runs duringconstruction, which is exactly the window a setter may legitimately refuse,
as
Spectra's does. Routing it through the property was considered andrejected on those grounds.
Checklist
docs/source/changelog.rstupdated under Unreleased, with theRuntimeErrorunder Breaking changes and theSpectracase worded aswrong results rather than an inconsistent attribute.
already governs the alias.
CONTEXT.md: no domain term introduced or sharpened..lmax_signal =assignment in scripts,benchmarks and notebooks is on a
Fisher, never aSpectra, so the newRuntimeErrorreaches none of them.picslike 103 passed / 2 skipped.
getters, applied in
8752a38.fisher=tests were mutation-checked: removing the adoptionfails both, so they are not decoration.
the dict route failed
lmax_signal == 8(got 16), the setter route failedbasis_manager.lmax_signal == 8(got 16).Note on coverage
test_spectra_ceiling_changes_the_answerasserts the ceiling reaches thenumbers, not just the attribute. That is the assertion the attribute checks
cannot make: the inert setter passed
obj.lmax_signal == 8while changing noresult. 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.