The blast radius is wider than the prompt assumed. It says this "only bites when zeroed_pixels > 0" and is therefore "opt-in per mesh" — but RectangularRTUAdaptDensity.zeroed_pixels returns the
entire edge ring unconditionally, with no opt-in and no count to set. For the
Rectangular*AdaptDensity family it is always on, and that is the mesh family every real-fit
measurement in the prompt used (108 zeroed of 784 on a 28x28 is exactly 4*28 - 4).
Click to expand starting prompt
The reconstruction noise map describes a different estimator than the default solver
Type: bug
Target: autoarray
Repos:
- @PyAutoArray
- @autolens_workspace
Difficulty: medium
Autonomy: human-required
Priority: medium
Status: in-progress
Filed: 2026-08-22 (backfilled from git)
Why this exists
Found 2026-08-22 while researching
draft/bug/autoarray/reconstruction_noise_map_covariance_sqrt.md (the numerics
and semantics half). The question that started it: why has the noise map on
source reconstructions not always been reliable?
The answer is not the elementwise-sqrt bug — that only touches off-diagonals and
provably cannot reach the 1D noise map. It is this: the noise map is computed
from a formula that describes an estimator PyAutoArray no longer uses by
default. Autonomy: human-required because the primary fix is a statistical
decision about what the reported uncertainty means, not a code repair.
DECISION MADE 2026-08-22 — posterior, not diagnostic
The human was asked whether this noise map is a posterior (a calibrated uncertainty on the
reconstructed flux, used for error bars and S/N) or a diagnostic (a relative readout of how
well-constrained each pixel is). Answer: posterior.
That settles the direction and closes the diagnostic-only escape hatch offered under Defect 1
("rewrite the docstring rather than the maths"). Defects 1 and 2 are real defects, not documentation
drift. What remains open is the design of the fix, not whether one is needed.
Defect 1 — the covariance formula assumes an unconstrained solve; the default is NNLS
abstract.py:859 computes C = inv(curvature_reg_matrix) = [F + λH]^-1. That
is the posterior covariance of the classical semi-linear inversion — the
unconstrained linear-Gaussian solution of Warren & Dye (2003) eq. 12, which is
what reconstruction_positive_negative_from computes.
But config/general.yaml ships:
use_positive_only_solver: true # DEFAULT
So the default reconstruction is fnnls_cholesky — a non-negative least
squares solve, minimising ||Zs - x||² subject to s >= 0. That is a
constrained estimator with an active set: fnnls maintains a passive set P
and solves slg.solve(ZTZ[P][:,P], ..., assume_a="pos") on the free pixels only,
pinning the rest at exactly zero.
Imposing s >= 0 is equivalent to truncating the Gaussian prior to the
non-negative orthant. The posterior is then a truncated multivariate Gaussian,
whose covariance is not [F + λH]^-1:
- For pixels well inside the positive region, the constraint is inactive and
[F + λH]^-1 is a good approximation.
- For pixels near the boundary, truncation reduces the variance, so the
reported noise is systematically overstated.
- For pixels pinned at exactly zero, the marginal posterior is not Gaussian at
all — it piles up at the boundary. The reported number is meaningless.
Why this bites source reconstructions hardest. A lensed source is compact; most
of the mesh is empty sky. So NNLS pins a large fraction of pixels at zero, the
active set is large, and the unconstrained formula is worst exactly where it is
most used. That matches the reported symptom precisely.
The docstring makes an uncertainty claim, not a diagnostic one — "the RMS standard
deviation of the noise in every pixel ... should be used for any scientific
analysis (e.g. source reconstructions of strong lenses)" — so this matters.
The honest counter-argument, which a human should weigh: if the noise map is
meant only as a diagnostic of how well each pixel is constrained by data plus
regularization, [F + λH]^-1 is defensible for any solver, and the fix is to
rewrite the docstring rather than the maths. Decide which of the two it is before
writing code. That decision is the point of this prompt.
Defect 2 — the noise map ignores edge-zeroed pixels the reconstruction excluded
config/general.yaml also ships use_edge_zeroed_pixels: true. Under it, the
reconstruction (abstract.py:511-539) subsets the system:
curvature_reg_matrix = self.curvature_reg_matrix[self.zeroed_ids_to_keep][:, self.zeroed_ids_to_keep]
solves the reduced problem, then scatters back with exact zeros at the zeroed
pixels. reconstruction_noise_map_with_covariance inverts self.curvature_reg_matrix
— the full matrix, respecting neither this reduction nor the separate
mapper_indices reduction that curvature_reg_matrix_reduced applies for the
log-det.
Those excluded rows are, by the Delaunay mesh's own docstring, the
"poorly constrained boundary vertices" whose zeroing exists to "stabilize the
linear inversion" and "prevent poorly constrained boundary vertices from absorbing
flux". The noise map re-admits into an explicit inverse precisely the degenerate
rows the reconstruction deliberately dropped to stay stable.
The user-visible result: a pixel whose reconstruction reads exactly 0 (meaning
"not solved for") gets a noise value computed as though it had been solved.
Reconstruction and noise map disagree about what those pixels mean.
Scope: only bites when zeroed_pixels > 0. Delaunay.__init__ defaults it to 0,
so this is opt-in per mesh — check rectangular_rtu_adapt_density and any
workspace configs before sizing the blast radius.
Defect 3 — use_edge_zeroed_pixels is silently ignored unless the positive-only solver is on
This is the "does the edge-pixel handling make sense next to positive-only?"
question, and the answer is no. The control flow (abstract.py:509-554):
if self.settings.use_positive_only_solver: # default True
if self.settings.use_edge_zeroed_pixels and self.has(cls=Mapper):
...subset, fnnls, scatter back...
else:
return reconstruction_positive_only_from(FULL matrix)
return reconstruction_positive_negative_from(FULL matrix) # edge-zeroing never consulted
use_edge_zeroed_pixels is nested inside the positive-only branch. Setting
use_positive_only_solver: false — a reasonable thing to do, for speed or to
permit negative values — silently disables edge-zeroing too, with no warning.
The poorly-constrained boundary vertices come straight back into the solve and
results change for a reason the config does not express.
These are orthogonal concerns. Which parameters are solvable (edge-zeroing) is a
statement about the mesh; which solver walks them is a separate choice. Edge-zeroing
should apply to both branches, or the coupling should be made explicit and
documented.
Suggested direction (interpretation now settled; design still open)
If the noise map is to describe the estimator actually used, the covariance should
be formed on the same index set the reconstruction solved, and scattered back:
- Determine the kept set exactly as
reconstruction does — zeroed_ids_to_keep
under edge-zeroing, and, for the NNLS answer to Defect 1, further restricted to
the free set (pixels with reconstruction > 0).
- Cholesky-invert that submatrix (per the sibling prompt's
cho_factor /
cho_solve fix).
- Scatter back into full shape. Decide what the excluded pixels report —
0
matches the reconstruction's own convention and keeps plots working; NaN is
more honest ("never estimated") but breaks colourbars and the CSV. Recommend
0 with an explicit docstring statement, since the reconstruction already
reports 0 there and consumers handle it.
Restricting to the NNLS free set gives the covariance conditional on the active
set — standard practice for constrained least squares, and a defensible,
documentable choice. It is still an approximation: it ignores the uncertainty in
the active set itself. Say so in the docstring rather than implying exactness.
Downstream evidence for why this matters (added 2026-08-22)
autolens_workspace uses the 1D noise map directly in user-facing science scripts —
scripts/{imaging,interferometer,group,multi_galaxy}/features/pixelization/source_science.py
compute:
reconstruction_noise_map = inversion.reconstruction_noise_map
signal_to_noise_map = reconstruction / reconstruction_noise_map
So the quantity this prompt argues is computed for the wrong estimator is divided into the
reconstruction to produce a signal-to-noise map on a source reconstruction — the number
that ends up in papers. If the NNLS/unconstrained mismatch is real, it propagates straight
into published S/N. That raises the stakes on the Defect 1 decision and is the concrete
reason to instrument a real fit rather than reason about it further.
SUPERSEDED — synthetic proxy, badly underestimated the effect (kept as a caution)
This prompt's load-bearing claim was that NNLS pins a large fraction of a compact source's mesh,
flagged as reasoned-not-measured. Now measured with PyAutoArray's real solver
(autoarray.util.fnnls.fnnls_cholesky) on a problem shaped like a source-plane inversion — compact
Gaussian source, 4-neighbour gradient regularization, mesh mostly covering empty sky.
CONFIRMED — the pinned fraction is large, and scales with source compactness:
| mesh |
pixels |
source area |
pinned at 0 |
| 20x20 |
400 |
2% |
46.0% |
| 20x20 |
400 |
5% |
24.8% |
| 20x20 |
400 |
15% |
1.2% |
| 30x30 |
900 |
2% |
47.8% |
| 30x30 |
900 |
5% |
26.4% |
| 30x30 |
900 |
15% |
0.4% |
| 40x40 |
1600 |
2% |
50.0% |
| 40x40 |
1600 |
5% |
26.8% |
| 40x40 |
1600 |
15% |
0.5% |
A quarter to a half of the mesh is pinned for a genuinely compact source; the effect vanishes for an
extended one. The premise holds.
But the numerical consequence is far smaller than this prompt implied. Shipped full-matrix noise
map vs the active-set-conditional one (covariance restricted to the free set), on the 900-pixel /
5%-source case (226 pinned):
| quantity |
value |
| median noise, shipped (full matrix) |
0.002226 |
| median noise, active-set-conditional |
0.002159 |
| ratio shipped / conditional, free pixels |
median 1.025, min 1.002, max 1.123 |
The shipped map overstates uncertainty on the free pixels by ~2.5% median, up to ~12% worst case.
The bias is systematic and one-directional (min 1.002 — it never understates), exactly as the
truncation argument predicts. But it is a few percent, not the order-of-magnitude error the
Priority: high grading assumed.
The pinned pixels are less alarming than feared: their reconstruction is exactly 0.0, so
signal_to_noise_map = reconstruction / reconstruction_noise_map yields 0 / 0.00228 = 0. Zero S/N
for an unlit pixel is defensible — though the reported noise value itself is still meaningless, the
posterior there being a spike at the boundary rather than a Gaussian.
Re-graded Priority: high -> medium. A systematic, always-one-direction ~2.5% (up to 12%)
overstatement of source-plane error bars is worth correcting for a quantity now confirmed to be a
posterior and which feeds published S/N maps. It is not an emergency.
Caveat, and a real one. This is a structural proxy, not a lens fit: the mapping matrix is random
rather than produced by ray tracing, so neighbouring image pixels do not map to neighbouring source
pixels as they do in reality. That structure affects conditioning and could move the magnitude either
way. The pinned fraction is robust to it (it follows from source compactness, not mapping
structure); the 2.5% / 12% figures are indicative only. Re-measure on a real Delaunay fit before
quoting them anywhere.
Defect 3 (the config coupling) is untouched by all of this — it is an unambiguous bug at any priority,
but it sits on the reconstruction path and so changes fit results. It needs its own sign-off.
MEASURED ON A REAL LENS FIT 2026-08-22 — the proxy was wrong; re-graded back to high
The synthetic measurement above was re-run on a real ray-traced fit, reproducing
autolens_workspace/scripts/imaging/features/pixelization/source_science.py: Isothermal
einstein_radius=1.6 + shear, RectangularBilinearAdaptDensity(28, 28), Constant regularization,
r=3.0" mask, over_sample_size_pixelization=4, compact Sersic source, PSF and Poisson noise.
Defaults confirmed live: use_positive_only_solver=True, use_edge_zeroed_pixels=True,
mesh.zeroed_pixels = 108 of 784.
The proxy understated the effect by an order of magnitude, and its caveat was the reason. A random
mapping matrix spreads data support across the whole mesh; real ray tracing concentrates it in the
arc, so far more of the mesh is unconstrained.
Sensitivity sweep (noise x med = median shipped/active-set-conditional on free pixels; flux % =
change in source flux passing the workspace's S/N >= 5 cut):
| r_eff |
reg coeff |
mesh |
params |
pinned % |
noise x med |
max |
flux % |
| 0.05 |
1.0 |
28 |
784 |
97.1% |
2.837 |
9.34 |
-24.1% |
| 0.10 |
1.0 |
28 |
784 |
88.6% |
1.692 |
10.60 |
-11.1% |
| 0.30 |
1.0 |
28 |
784 |
53.2% |
1.208 |
8.02 |
-3.9% |
| 0.60 |
1.0 |
28 |
784 |
21.0% |
1.025 |
4.06 |
-1.1% |
| 0.10 |
0.1 |
28 |
784 |
87.1% |
1.713 |
10.39 |
-29.8% |
| 0.10 |
10.0 |
28 |
784 |
82.3% |
1.117 |
1.90 |
-2.4% |
| 0.10 |
100.0 |
28 |
784 |
83.2% |
1.009 |
1.57 |
-0.2% |
| 0.10 |
1.0 |
20 |
400 |
91.0% |
1.481 |
4.93 |
0.0% |
| 0.10 |
1.0 |
40 |
1600 |
90.4% |
1.421 |
10.36 |
-48.9% |
Two clean trends: the gap grows with source compactness and shrinks with regularization
strength. At coeff=100 it essentially vanishes (1.009); at realistic coeff~1 with a compact
source it is a factor of 1.7-2.8, with individual pixels up to 10x.
The framing that matters — these are two bounds, not right-vs-wrong
Do not read "1.8x" as "the shipped noise map is wrong by 1.8x". The two quantities bracket the
truth:
- Shipped (full-matrix) ignores the
s >= 0 constraint entirely, so it overstates — an upper
bound.
- Active-set-conditional treats the active set as known, ignoring uncertainty about which
pixels are pinned, so it understates — a lower bound.
- The true truncated-Gaussian posterior lies between them.
So the finding is: the source-plane noise map is ambiguous at the factor-of-2 level for compact
sources, and the shipped value sits at the pessimistic end of that range. That is still a serious
problem for a published error bar — but "switch to the conditional covariance" is not the
correct fix, it just swaps one bound for the other. Designing the real fix means either computing the
truncated posterior properly, or documenting the bracket honestly.
Downstream consequence is a threshold effect, which amplifies it
source_science.py does not merely display the noise map:
signal_to_noise_map = reconstruction / reconstruction_noise_map
mesh_pixel_mask = signal_to_noise_map < 5.0
reconstruction_masked[mesh_pixel_mask] = 0.0
Source flux and magnification are computed from the surviving pixels. A hard cut turns a smooth noise
bias into a discrete one: pixels near S/N = 5 flip in or out. Measured flux shifts of -24%,
-30% and -49% in the table above come from a handful of pixels crossing that line.
Re-graded Priority: medium -> high. The earlier downgrade was made on the synthetic proxy and
was wrong.
What is still not established
- One lens configuration, one mass model, one noise realization per row. No error bars on these numbers.
reg coeff was fixed by hand. In a real model-fit λ is a free parameter the sampler optimises,
and the coeff=100 row shows the effect nearly vanishes when λ is large. Where fitted λ actually
lands for these datasets decides whether this bites in practice — measure that before acting.
RectangularBilinearAdaptDensity, not Delaunay. Delaunay is the other common source mesh and was
not tested.
Scripts: scratchpad/real_fit_measure.py, scratchpad/sensitivity.py (session artefacts).
SETTLED 2026-08-22 — at the FITTED lambda the effect nearly vanishes. Re-graded to low.
The open question from the sweep above was: those numbers used a hand-set regularization
coefficient, but in a real fit Constant.coefficient is a free parameter under
LogUniform(1e-6, 1e6) (workspace config/priors/regularization/constant.yaml), and the figure of
merit for a pixelized fit is the Bayesian log evidence. So argmax_lambda log_evidence is what
the sampler converges on — computed deterministically over a 17-point grid rather than by running
Nautilus.
| r_eff |
lambda* |
log evidence |
pinned % |
noise x med |
max |
S/N>=5 ship |
cond |
flux % |
| 0.05 |
10 |
27605.4 |
96.6% |
1.263 |
1.91 |
12 |
12 |
0.0% |
| 0.10 |
10 |
26932.8 |
87.1% |
1.055 |
2.45 |
29 |
30 |
-1.3% |
| 0.30 |
10 |
24635.2 |
42.3% |
1.007 |
2.10 |
138 |
138 |
0.0% |
lambda* = 10 in all three cases, comfortably inside the scanned grid (1e-3 .. 1e5) — not an edge
artefact.
The Bayesian evidence self-selects away from the problematic regime. The factor-of-2.8 gap found
above occurs at lambda ~ 0.1-1, i.e. under-regularized solutions the evidence penalises. At the
lambda a real fit chooses, the median gap is 1.007-1.263 and the downstream science outputs —
source flux and magnification through the S/N >= 5 cut — move by 0.0%, -1.3%, 0.0%. The pixel
counts either side of the cut are all but identical (12/12, 29/30, 138/138).
The pinned fraction stays large (42-97%), so the mechanism in Defect 1 is real and confirmed. It
simply does not have a large numerical consequence at the operating point.
Re-graded Priority: high -> low. Honest accounting: this prompt has been graded high ->
medium -> high -> low across three measurements. The swings came from measuring progressively less
wrong things — synthetic proxy, then a real fit at hand-set lambda, then a real fit at fitted
lambda. Only the last is the operating point, and it is the one that governs.
What remains true and worth doing
- For a very compact source (
r_eff = 0.05) the shipped noise map still overstates by ~26%
median, up to ~2x on individual pixels. That is a real bias on a published error bar even though
it moves no flux. Anyone quoting per-pixel source uncertainties on a compact source should know.
- The bracket framing stands: full-matrix overstates, active-set-conditional understates, and the
truncated-Gaussian posterior lies between. A fix that simply swaps to the conditional covariance
would be wrong at any lambda.
- Recommended immediate action is documentation, not code: state in
reconstruction_noise_map's docstring that the covariance is that of the unconstrained solve,
that the default solver is NNLS, and that for compact sources this overstates per-pixel noise by a
few tens of percent at most. Cheap, honest, no API change.
- A proper truncated-posterior implementation is only worth it if someone needs calibrated per-pixel
error bars on very compact sources. Not now.
Caveats on this result
- The lens mass was fixed at truth. In a real model-fit the mass is free too, and a poor mass
model may need a lower lambda to absorb residuals — which is the regime where the gap opens. This
is the most likely way the conclusion could be wrong.
- One noise realization per row; no error bars on lambda*.
RectangularBilinearAdaptDensity only; Delaunay untested.
- Evidence was maximised on a grid, not sampled — Nautilus explores a posterior over lambda, so some
posterior mass sits at lower lambda where the gap is larger.
Script: scratchpad/fitted_lambda.py (session artefact).
PARTIALLY ADDRESSED 2026-08-22 — the docstring caveat shipped
The recommended immediate action was taken: PyAutoArray#472
documents on reconstruction_noise_map that the covariance is that of the unconstrained solve
while the default solver is NNLS, quantifies the overstatement at the evidence-optimal coefficient
(x1.01 to x1.26 median, up to ~2x per pixel) and below it (~2.8x median, ~10x per pixel), and records
that restricting to the free set is not the correction because the two bracket the truth.
Documentation only — no behaviour change, no API change.
What this prompt still owns
- Defect 1's actual maths. Computing the truncated-Gaussian posterior properly. Graded
low:
worth it only if someone needs calibrated per-pixel error bars on a very compact source.
- Defect 2 — the covariance ignores
zeroed_ids_to_keep while the reconstruction subsets by it.
Never measured in isolation; the real fits here had 108 zeroed pixels of 784 and they are folded
into the "pinned" counts throughout, so its separate contribution is unknown.
- Defect 3 —
use_edge_zeroed_pixels nested inside the use_positive_only_solver branch, so
turning the positive-only solver off silently disables edge-zeroing. Untouched by any of the
measurement above and unambiguous at any priority. It sits on the reconstruction path, so it
changes fit results and needs its own sign-off. This is the most likely next piece of real work
here.
- The open measurement: re-run the evidence-optimal lambda with the lens mass free rather
than fixed at truth. That is the single result most likely to overturn the low grading.
SCOPED FOR DEV 2026-08-27 — Defect 2 only; Defect 3 is closed
/start_dev re-read this prompt against PyAutoArray main @ da5ec9a0. Three of the four open
items are not live code work, so this prompt now owns exactly one:
| Item |
State on main @ da5ec9a0 |
| Defect 1 — unconstrained covariance vs NNLS |
Docstring caveat shipped (#472). The truncated-posterior maths stays deferred at low — "worth it only if someone needs calibrated per-pixel error bars on a very compact source". |
Defect 2 — covariance ignores zeroed_ids_to_keep |
LIVE. This prompt's task. reconstruction_covariance_matrix (abstract.py:844) inverts the full curvature_reg_matrix; reconstruction (abstract.py:516-544) solves the zeroed_ids_to_keep submatrix and scatters back exact zeros. |
Defect 3 — use_edge_zeroed_pixels nested inside the positive-only branch |
CLOSED — deliberate, not a bug. 84b9ed4 (2026-08-22): "the author confirmed the scoping is deliberate". Documented in three places — config/general.yaml:6, Settings.use_edge_zeroed_pixels, and a comment at the nesting site itself saying explicitly not to hoist the check. The text above calling this "the most likely next piece of real work" is stale — do not act on it. |
| The open measurement — evidence-optimal lambda with the lens mass free |
Deferred. Not code work; belongs in its own research/ prompt when someone wants it. |
Blast radius is wider than this prompt assumed
The scope note above says Defect 2 "only bites when zeroed_pixels > 0" and that
Delaunay.__init__ defaults it to 0, "so this is opt-in per mesh". That understates it.
RectangularRTUAdaptDensity.zeroed_pixels (rectangular_rtu_adapt_density.py:161) returns the
entire edge ring unconditionally — there is no opt-in and no count to set. For the
Rectangular*AdaptDensity family it is always on. That is the mesh family every real-fit
measurement in this prompt used: 108 zeroed of 784 on a 28x28 is exactly the edge ring, 4*28 - 4.
The three index sets are not three inconsistencies — two of them are unrelated
The Verification section below asks whether curvature_reg_matrix_reduced's mapper_indices
reduction should apply to the noise map too, and calls the inconsistency between all three "itself a
finding". It is not. mapper_indices drops linear objects that carry no regularization (light
profiles and the like) from the reduced matrices; it has nothing to do with edge zeroing. The
comment # ids of values which are on edge so zero-d and not solved for at abstract.py:349,
:387 and :574 is simply wrong, and is what makes the two look like the same concern. Correct
the comment as part of this task. Only zeroed_ids_to_keep vs the noise map was ever a defect.
DECISION 2026-08-27 — excluded pixels report NaN
The prompt recommended 0 ("matches the reconstruction's own convention and keeps plots
working"). The human chose NaN, and the two consumer paths were checked before deciding —
neither breaks, and NaN is the safer of the two:
- Plots.
norm_from (plot/utils.py:270) is the single colour-scale implementation behind
plot_array and plot_inversion_reconstruction. It derives vmax with np.nanmax and its
docstring already names "an all-NaN array" as a handled fallback; the linear branch returns
None and lets matplotlib autoscale, which ignores NaN. Unsolved pixels render blank, which is
what they are.
- CSV.
save_reconstruction_csv (inversion_plots.py:395) already writes nan into the
noise_map column when the covariance raises. NaN extends an established convention rather
than inventing one.
0 was the worse option, not the safer one. source_science.py computes
reconstruction / reconstruction_noise_map, and the reconstruction at these pixels is exactly
0.0. Under 0 that is 0/0 -> NaN plus a RuntimeWarning on every call; under NaN the
NaN propagates silently. Both give NaN < 5.0 == False, so the pixels survive the S/N >= 5
cut carrying a reconstruction of 0.0 and measured flux is unchanged either way.
Document the convention in the reconstruction_noise_map docstring and in the workspace
scripts that consume it — which is why @autolens_workspace is now on this prompt.
This is a value change on the pixels that ARE solved
Restricting the inverse is not a re-scaling of the excluded rows. For an SPD matrix
[A^-1]_keep >= (A_keep)^-1 in the positive-semidefinite ordering, so every kept pixel's noise
drops, on every Rectangular*AdaptDensity fit. Needs a measured before/after on a real fit and a
release note.
The Defect 1 bracket framing does not apply here. These pixels are fixed at zero by
construction, deterministically, before the solver runs — not selected by the data the way the NNLS
active set is. Conditioning on them is exactly right, and there is no upper/lower bracket to
straddle. Do not let the "restricting to the free set is not the correction" caveat in
reconstruction_noise_map's shipped docstring be read as applying to this change; it is about the
NNLS active set only.
Structural fix, not a local patch
The reason this defect exists is that the predicate deciding whether the solve subsets the system
lives inline in reconstruction and nowhere else, so the covariance could never have known about
it. Give the inversion one property for "which indices did the solve actually solve", have both
reconstruction and reconstruction_covariance_matrix read it, and the two cannot drift apart
again.
Verification
- Reproduce the symptom first. Take a real Delaunay source fit, compute the
noise map under the current code and under the free-set-restricted covariance,
and compare. Quantify how many mesh pixels are pinned at zero by NNLS — the
claim that this fraction is large for compact sources is reasoned, not
measured, and the whole prompt rests on it. If the fraction turns out small,
Defect 1 is a much smaller problem than stated here and should be re-graded.
- Confirm reconstruction and noise map agree on which pixels were solved: every
pixel the reconstruction reports as an exact structural zero should be
identifiable in the noise map by the documented convention.
- With
zeroed_pixels > 0, assert the covariance is formed on the reduced matrix
— regression-test the shape and the scatter-back, not just values.
- For Defect 3, assert
use_edge_zeroed_pixels: true + use_positive_only_solver: false either applies edge-zeroing or raises/warns. It must not silently ignore
the setting.
- Check whether
curvature_reg_matrix_reduced's mapper_indices reduction should
apply to the noise map too. The log-det uses it; the noise map does not. Decide
deliberately — this is a third, separate index set and the inconsistency between
all three is itself a finding.
Prior art — read before starting
complete/2026/08/numerical-inversion-failures.md — this cluster's refutation.
complete/2026/07/pix-inversion-not-positive-definite.md — an earlier
non-positive-definite hypothesis, also refuted; documents the GaussianKernel
PD-guarantee f1817af0.
autoarray/util/cholesky_funcs.py:50-80 — near-coincident mesh vertices make the
Schur pivot's sign depend on BLAS thread count. The degeneracy is real and
documented; this prompt is about not feeding it into an explicit inverse.
abstract.py:805 — the repo already documents ~1e-6 evidence round-off from
"factorizing the explicitly formed inverse" at cond(C) ~ 1e9 on clustered
traced mesh vertices.
Provenance
- Found during: research for the sibling prompt, 2026-08-22.
- Do the sibling first — it is small, needs no science decision, and its Cholesky
covariance helper is the building block this prompt reuses.
Overview
AbstractInversion.reconstructionsolves only thezeroed_ids_to_keepsubmatrix and scatters theresult back with exact zeros at the pixels it excluded.
reconstruction_covariance_matrixinverts the full
curvature_reg_matrix. So a pixel whose reconstruction reads exactly0(meaning "never solved for") gets a noise value computed as though it had been solved — the
reconstruction and its noise map disagree about what those pixels mean.
This is Defect 2 of
draft/bug/autoarray/reconstruction_noise_map_solver_mismatch.md, and afterre-reading that prompt against
main@da5ec9a0it is the only live code work left in it.Defect 1's docstring caveat shipped in #472 and its remaining maths stays deferred at
low;Defect 3 was closed as deliberate by
84b9ed4(2026-08-22), so the prompt text calling it "themost likely next piece of real work" is stale.
The blast radius is wider than the prompt assumed. It says this "only bites when
zeroed_pixels > 0" and is therefore "opt-in per mesh" — butRectangularRTUAdaptDensity.zeroed_pixelsreturns theentire edge ring unconditionally, with no opt-in and no count to set. For the
Rectangular*AdaptDensityfamily it is always on, and that is the mesh family every real-fitmeasurement in the prompt used (108 zeroed of 784 on a 28x28 is exactly
4*28 - 4).Plan
reconstructionsolves only the
zeroed_ids_to_keepsubmatrix and scatters back exact zeros;reconstruction_covariance_matrixinverts the full matrix, so a pixel whose reconstruction readsexactly
0still gets a noise value computed as though it had been solved.both the reconstruction and the covariance read it, so the two cannot drift apart again. That the
predicate lived inline in one method and nowhere else is why this defect was possible.
NaNatthe excluded pixels (human decision, 2026-08-27) — matching the convention the CSV writer already
uses for a noise map it could not compute.
NaNconvention in thereconstruction_noise_mapdocstring and in theautolens_workspacescripts that consume the noise map.mapper_indicesas edge-zeroing; it dropsunregularized linear objects and is an unrelated index set. That wrong comment is why the prompt
read three index sets as one unexplained inconsistency.
value change on a real fit for the release note.
Detailed implementation plan
Work Classification
Both — library (@PyAutoArray) with a workspace documentation leg (@autolens_workspace) behind
the library-first merge gate.
Affected Repositories
Branch Survey
main@da5ec9a0Existing PyAutoArray branches:
main,dev_Q,feature/delaunay-jax-find-simplex,claude/autonerves-floor-regime-stamp— none related.worktree_check_conflict→ 0 (no conflict).Suggested branch:
feature/reconstruction-noise-map-zeroed-pixelsWorktree root:
~/Code/PyAutoLabs-wt/reconstruction-noise-map-zeroed-pixels/Scope
Defect 2 only. Verified live on PyAutoArray
main@da5ec9a0.low.84b9ed4(2026-08-22).
config/general.yaml:6,Settings.use_edge_zeroed_pixelsand a comment at thenesting site itself all now state the scoping is intended. Do not "fix" it.
Implementation Steps
autoarray/inversion/inversion/abstract.py— new propertysolve_ids_to_keep, returningzeroed_ids_to_keepiffsettings.use_positive_only_solver and settings.use_edge_zeroed_pixels and self.has(cls=Mapper)(the exact predicate currently inline at
:509-516), elseNone. Rewritereconstruction(
:493-557) to consume it, so there is one source of truth.reconstruction_covariance_matrix(:844) — whensolve_ids_to_keepis notNone,Cholesky-invert
curvature_reg_matrix[keep][:, keep]and scatter into a full[total_params, total_params]matrix whose excluded rows/cols areNaN. Keep the existingfiniteness guard, input/output symmetrisation and
LinAlgErrorcontract — they apply unchangedto the submatrix. A principal submatrix of an SPD matrix is SPD, so
cho_factorstays valid andis better-conditioned than the full matrix. The guard must run on the submatrix, before the
NaNscatter-back, or it will trip on theNaNs this change deliberately introduces.reconstruction_noise_map(:939) — expression unchanged (sqrt(diag(C))), inherits thefix. Docstring gains the exclusion convention: pixels the solve zeroed report
NaN, meaning"never estimated", and are the same pixels whose reconstruction is exactly
0.0.Comment fix at
:349,:387,:574.# ids of values which are on edge so zero-d and not solved fordescribesmapper_indices, which drops linear objects carrying noregularization — not edge zeroing. This wrong comment is the reason the prompt read the three
index sets as one unexplained inconsistency; two of them are unrelated concerns.
Blast radius.
RectangularRTUAdaptDensity.zeroed_pixels(
rectangular_rtu_adapt_density.py:161) returns the whole edge ring unconditionally, so this isalways on for the
Rectangular*AdaptDensityfamily — not opt-in as the prompt's scope noteclaims.
Delaunay.zeroed_pixelsis a count defaulting to0and stays opt-in.Value change on the pixels that ARE solved.
[A^-1]_keep >= (A_keep)^-1in the PSDordering, so every kept pixel's noise drops, on every
Rectangular*AdaptDensityfit.Measure before/after on a real fit and release-note it.
This is not the Defect 1 bracket. These pixels are fixed at zero by construction, before
the solver runs — not selected by the data the way the NNLS active set is. Conditioning on them
is exactly correct and there is no upper/lower bound to straddle. Do not let the "restricting to
the free set is not the correction" caveat in the shipped
reconstruction_noise_mapdocstringbe read as applying here; it is about the NNLS active set only.
Consumer paths — both checked, both safe under
NaN.norm_from(plot/utils.py:270) is the single colour-scale implementation behindplot_arrayandplot_inversion_reconstruction. It derivesvmaxwithnp.nanmaxand itsdocstring already names "an all-
NaNarray" as a handled fallback; the linear branch returnsNoneand lets matplotlib autoscale, which ignoresNaN. Unsolved pixels render blank.save_reconstruction_csv(inversion_plots.py:395) already writesnaninto thenoise_mapcolumn when the covariance raises, so this extends an established convention.
source_science.py,reconstruction / reconstruction_noise_mapgives0.0 / NaN = NaNsilently;
NaN < 5.0isFalse, so the pixel survives theS/N >= 5cut carrying areconstruction of
0.0and measured flux is unchanged. The rejected0convention would havegiven
0/0plus aRuntimeWarningon every call.Tests
test_autoarray/inversion/inversion/test_abstract.py, alongside the 8 existing cases at:681-812:[total_params, total_params]; excluded rows/cols areNaN; the kept block equalsinv(curvature_reg_matrix[keep][:, keep])reconstruction == 0.0exactly at an excluded pixel iffreconstruction_noise_mapis
NaNthere<=the old full-matrix value (one-directional)use_edge_zeroed_pixels=False, whenuse_positive_only_solver=False, and when there is no mapperLinAlgErrorcontract still holds on a non-PD or non-finite submatrix —inversion_plots.py:169and:397catch onlyLinAlgErrorWorkspace leg (separate PR, library-first gate)
Document the
NaNconvention in the fourscripts/{imaging,interferometer,group,multi_galaxy}/features/pixelization/source_science.pyscripts and their notebooks.
Downstream check
Grep
reconstruction_noise_mapin PyAutoGalaxy, PyAutoLens, autolens_workspace,autogalaxy_workspace. The sibling record
(
complete/2026/08/reconstruction-noise-map-covariance-sqrt.md) found the 4source_science.pyscripts and nothing in the two libraries.
Key Files
autoarray/inversion/inversion/abstract.py—reconstruction(:493),zeroed_ids_to_keep(
:393),mapper_indices(:241),curvature_reg_matrix_reduced(:370),reconstruction_covariance_matrix(:844),reconstruction_noise_map(:939)autoarray/inversion/mesh/mesh/rectangular_rtu_adapt_density.py:161— unconditional edge ringautoarray/inversion/mesh/mesh/delaunay.py:94— opt-in countautoarray/inversion/plot/inversion_plots.py:169,395— the twoLinAlgErrorguardsautoarray/plot/utils.py:270—norm_from, the NaN-safe colour scaletest_autoarray/inversion/inversion/test_abstract.py:681-812— existing coveragePrior art
PyAutoMind/complete/2026/08/reconstruction-noise-map-covariance-sqrt.md— the shipped sibling(fix: form the reconstruction covariance via Cholesky, not an elementwise sqrt #469): the Cholesky covariance helper this builds on, and the review traps it hit
(
check_finite, upper-triangle-onlycho_factor).PyAutoMind/complete/2026/08/numerical-inversion-failures.md— the refutednon-positive-definite hypothesis in this cluster.
Original Prompt
Click to expand starting prompt
The reconstruction noise map describes a different estimator than the default solver
Type: bug
Target: autoarray
Repos:
Difficulty: medium
Autonomy: human-required
Priority: medium
Status: in-progress
Filed: 2026-08-22 (backfilled from git)
Why this exists
Found 2026-08-22 while researching
draft/bug/autoarray/reconstruction_noise_map_covariance_sqrt.md(the numericsand semantics half). The question that started it: why has the noise map on
source reconstructions not always been reliable?
The answer is not the elementwise-sqrt bug — that only touches off-diagonals and
provably cannot reach the 1D noise map. It is this: the noise map is computed
from a formula that describes an estimator PyAutoArray no longer uses by
default.
Autonomy: human-requiredbecause the primary fix is a statisticaldecision about what the reported uncertainty means, not a code repair.
DECISION MADE 2026-08-22 — posterior, not diagnostic
The human was asked whether this noise map is a posterior (a calibrated uncertainty on the
reconstructed flux, used for error bars and S/N) or a diagnostic (a relative readout of how
well-constrained each pixel is). Answer: posterior.
That settles the direction and closes the diagnostic-only escape hatch offered under Defect 1
("rewrite the docstring rather than the maths"). Defects 1 and 2 are real defects, not documentation
drift. What remains open is the design of the fix, not whether one is needed.
Defect 1 — the covariance formula assumes an unconstrained solve; the default is NNLS
abstract.py:859computesC = inv(curvature_reg_matrix)=[F + λH]^-1. Thatis the posterior covariance of the classical semi-linear inversion — the
unconstrained linear-Gaussian solution of Warren & Dye (2003) eq. 12, which is
what
reconstruction_positive_negative_fromcomputes.But
config/general.yamlships:So the default reconstruction is
fnnls_cholesky— a non-negative leastsquares solve, minimising
||Zs - x||²subject tos >= 0. That is aconstrained estimator with an active set:
fnnlsmaintains a passive setPand solves
slg.solve(ZTZ[P][:,P], ..., assume_a="pos")on the free pixels only,pinning the rest at exactly zero.
Imposing
s >= 0is equivalent to truncating the Gaussian prior to thenon-negative orthant. The posterior is then a truncated multivariate Gaussian,
whose covariance is not
[F + λH]^-1:[F + λH]^-1is a good approximation.reported noise is systematically overstated.
all — it piles up at the boundary. The reported number is meaningless.
Why this bites source reconstructions hardest. A lensed source is compact; most
of the mesh is empty sky. So NNLS pins a large fraction of pixels at zero, the
active set is large, and the unconstrained formula is worst exactly where it is
most used. That matches the reported symptom precisely.
The docstring makes an uncertainty claim, not a diagnostic one — "the RMS standard
deviation of the noise in every pixel ... should be used for any scientific
analysis (e.g. source reconstructions of strong lenses)" — so this matters.
The honest counter-argument, which a human should weigh: if the noise map is
meant only as a diagnostic of how well each pixel is constrained by data plus
regularization,
[F + λH]^-1is defensible for any solver, and the fix is torewrite the docstring rather than the maths. Decide which of the two it is before
writing code. That decision is the point of this prompt.
Defect 2 — the noise map ignores edge-zeroed pixels the reconstruction excluded
config/general.yamlalso shipsuse_edge_zeroed_pixels: true. Under it, thereconstruction (
abstract.py:511-539) subsets the system:solves the reduced problem, then scatters back with exact zeros at the zeroed
pixels.
reconstruction_noise_map_with_covarianceinvertsself.curvature_reg_matrix— the full matrix, respecting neither this reduction nor the separate
mapper_indicesreduction thatcurvature_reg_matrix_reducedapplies for thelog-det.
Those excluded rows are, by the Delaunay mesh's own docstring, the
"poorly constrained boundary vertices" whose zeroing exists to "stabilize the
linear inversion" and "prevent poorly constrained boundary vertices from absorbing
flux". The noise map re-admits into an explicit inverse precisely the degenerate
rows the reconstruction deliberately dropped to stay stable.
The user-visible result: a pixel whose reconstruction reads exactly
0(meaning"not solved for") gets a noise value computed as though it had been solved.
Reconstruction and noise map disagree about what those pixels mean.
Scope: only bites when
zeroed_pixels > 0.Delaunay.__init__defaults it to0,so this is opt-in per mesh — check
rectangular_rtu_adapt_densityand anyworkspace configs before sizing the blast radius.
Defect 3 —
use_edge_zeroed_pixelsis silently ignored unless the positive-only solver is onThis is the "does the edge-pixel handling make sense next to positive-only?"
question, and the answer is no. The control flow (
abstract.py:509-554):use_edge_zeroed_pixelsis nested inside the positive-only branch. Settinguse_positive_only_solver: false— a reasonable thing to do, for speed or topermit negative values — silently disables edge-zeroing too, with no warning.
The poorly-constrained boundary vertices come straight back into the solve and
results change for a reason the config does not express.
These are orthogonal concerns. Which parameters are solvable (edge-zeroing) is a
statement about the mesh; which solver walks them is a separate choice. Edge-zeroing
should apply to both branches, or the coupling should be made explicit and
documented.
Suggested direction (interpretation now settled; design still open)
If the noise map is to describe the estimator actually used, the covariance should
be formed on the same index set the reconstruction solved, and scattered back:
reconstructiondoes —zeroed_ids_to_keepunder edge-zeroing, and, for the NNLS answer to Defect 1, further restricted to
the free set (pixels with
reconstruction > 0).cho_factor/cho_solvefix).0matches the reconstruction's own convention and keeps plots working;
NaNismore honest ("never estimated") but breaks colourbars and the CSV. Recommend
0with an explicit docstring statement, since the reconstruction alreadyreports
0there and consumers handle it.Restricting to the NNLS free set gives the covariance conditional on the active
set — standard practice for constrained least squares, and a defensible,
documentable choice. It is still an approximation: it ignores the uncertainty in
the active set itself. Say so in the docstring rather than implying exactness.
Downstream evidence for why this matters (added 2026-08-22)
autolens_workspaceuses the 1D noise map directly in user-facing science scripts —scripts/{imaging,interferometer,group,multi_galaxy}/features/pixelization/source_science.pycompute:
So the quantity this prompt argues is computed for the wrong estimator is divided into the
reconstruction to produce a signal-to-noise map on a source reconstruction — the number
that ends up in papers. If the NNLS/unconstrained mismatch is real, it propagates straight
into published S/N. That raises the stakes on the Defect 1 decision and is the concrete
reason to instrument a real fit rather than reason about it further.
SUPERSEDED — synthetic proxy, badly underestimated the effect (kept as a caution)
This prompt's load-bearing claim was that NNLS pins a large fraction of a compact source's mesh,
flagged as reasoned-not-measured. Now measured with PyAutoArray's real solver
(
autoarray.util.fnnls.fnnls_cholesky) on a problem shaped like a source-plane inversion — compactGaussian source, 4-neighbour gradient regularization, mesh mostly covering empty sky.
CONFIRMED — the pinned fraction is large, and scales with source compactness:
A quarter to a half of the mesh is pinned for a genuinely compact source; the effect vanishes for an
extended one. The premise holds.
But the numerical consequence is far smaller than this prompt implied. Shipped full-matrix noise
map vs the active-set-conditional one (covariance restricted to the free set), on the 900-pixel /
5%-source case (226 pinned):
The shipped map overstates uncertainty on the free pixels by ~2.5% median, up to ~12% worst case.
The bias is systematic and one-directional (
min 1.002— it never understates), exactly as thetruncation argument predicts. But it is a few percent, not the order-of-magnitude error the
Priority: highgrading assumed.The pinned pixels are less alarming than feared: their reconstruction is exactly
0.0, sosignal_to_noise_map = reconstruction / reconstruction_noise_mapyields0 / 0.00228 = 0. Zero S/Nfor an unlit pixel is defensible — though the reported noise value itself is still meaningless, the
posterior there being a spike at the boundary rather than a Gaussian.
Re-graded
Priority: high->medium. A systematic, always-one-direction ~2.5% (up to 12%)overstatement of source-plane error bars is worth correcting for a quantity now confirmed to be a
posterior and which feeds published S/N maps. It is not an emergency.
Caveat, and a real one. This is a structural proxy, not a lens fit: the mapping matrix is random
rather than produced by ray tracing, so neighbouring image pixels do not map to neighbouring source
pixels as they do in reality. That structure affects conditioning and could move the magnitude either
way. The pinned fraction is robust to it (it follows from source compactness, not mapping
structure); the 2.5% / 12% figures are indicative only. Re-measure on a real Delaunay fit before
quoting them anywhere.
Defect 3 (the config coupling) is untouched by all of this — it is an unambiguous bug at any priority,
but it sits on the reconstruction path and so changes fit results. It needs its own sign-off.
MEASURED ON A REAL LENS FIT 2026-08-22 — the proxy was wrong; re-graded back to high
The synthetic measurement above was re-run on a real ray-traced fit, reproducing
autolens_workspace/scripts/imaging/features/pixelization/source_science.py: Isothermaleinstein_radius=1.6+ shear,RectangularBilinearAdaptDensity(28, 28),Constantregularization,r=3.0"mask,over_sample_size_pixelization=4, compact Sersic source, PSF and Poisson noise.Defaults confirmed live:
use_positive_only_solver=True,use_edge_zeroed_pixels=True,mesh.zeroed_pixels = 108of 784.The proxy understated the effect by an order of magnitude, and its caveat was the reason. A random
mapping matrix spreads data support across the whole mesh; real ray tracing concentrates it in the
arc, so far more of the mesh is unconstrained.
Sensitivity sweep (
noise x med= median shipped/active-set-conditional on free pixels;flux %=change in source flux passing the workspace's
S/N >= 5cut):Two clean trends: the gap grows with source compactness and shrinks with regularization
strength. At
coeff=100it essentially vanishes (1.009); at realisticcoeff~1with a compactsource it is a factor of 1.7-2.8, with individual pixels up to 10x.
The framing that matters — these are two bounds, not right-vs-wrong
Do not read "1.8x" as "the shipped noise map is wrong by 1.8x". The two quantities bracket the
truth:
s >= 0constraint entirely, so it overstates — an upperbound.
pixels are pinned, so it understates — a lower bound.
So the finding is: the source-plane noise map is ambiguous at the factor-of-2 level for compact
sources, and the shipped value sits at the pessimistic end of that range. That is still a serious
problem for a published error bar — but "switch to the conditional covariance" is not the
correct fix, it just swaps one bound for the other. Designing the real fix means either computing the
truncated posterior properly, or documenting the bracket honestly.
Downstream consequence is a threshold effect, which amplifies it
source_science.pydoes not merely display the noise map:Source flux and magnification are computed from the surviving pixels. A hard cut turns a smooth noise
bias into a discrete one: pixels near
S/N = 5flip in or out. Measured flux shifts of -24%,-30% and -49% in the table above come from a handful of pixels crossing that line.
Re-graded
Priority: medium->high. The earlier downgrade was made on the synthetic proxy andwas wrong.
What is still not established
reg coeffwas fixed by hand. In a real model-fit λ is a free parameter the sampler optimises,and the
coeff=100row shows the effect nearly vanishes when λ is large. Where fitted λ actuallylands for these datasets decides whether this bites in practice — measure that before acting.
RectangularBilinearAdaptDensity, not Delaunay. Delaunay is the other common source mesh and wasnot tested.
Scripts:
scratchpad/real_fit_measure.py,scratchpad/sensitivity.py(session artefacts).SETTLED 2026-08-22 — at the FITTED lambda the effect nearly vanishes. Re-graded to low.
The open question from the sweep above was: those numbers used a hand-set regularization
coefficient, but in a real fit
Constant.coefficientis a free parameter underLogUniform(1e-6, 1e6)(workspaceconfig/priors/regularization/constant.yaml), and the figure ofmerit for a pixelized fit is the Bayesian log evidence. So
argmax_lambda log_evidenceis whatthe sampler converges on — computed deterministically over a 17-point grid rather than by running
Nautilus.
lambda* = 10in all three cases, comfortably inside the scanned grid (1e-3 .. 1e5) — not an edgeartefact.
The Bayesian evidence self-selects away from the problematic regime. The factor-of-2.8 gap found
above occurs at
lambda ~ 0.1-1, i.e. under-regularized solutions the evidence penalises. At thelambda a real fit chooses, the median gap is 1.007-1.263 and the downstream science outputs —
source flux and magnification through the
S/N >= 5cut — move by 0.0%, -1.3%, 0.0%. The pixelcounts either side of the cut are all but identical (12/12, 29/30, 138/138).
The pinned fraction stays large (42-97%), so the mechanism in Defect 1 is real and confirmed. It
simply does not have a large numerical consequence at the operating point.
Re-graded
Priority: high->low. Honest accounting: this prompt has been graded high ->medium -> high -> low across three measurements. The swings came from measuring progressively less
wrong things — synthetic proxy, then a real fit at hand-set lambda, then a real fit at fitted
lambda. Only the last is the operating point, and it is the one that governs.
What remains true and worth doing
r_eff = 0.05) the shipped noise map still overstates by ~26%median, up to ~2x on individual pixels. That is a real bias on a published error bar even though
it moves no flux. Anyone quoting per-pixel source uncertainties on a compact source should know.
truncated-Gaussian posterior lies between. A fix that simply swaps to the conditional covariance
would be wrong at any lambda.
reconstruction_noise_map's docstring that the covariance is that of the unconstrained solve,that the default solver is NNLS, and that for compact sources this overstates per-pixel noise by a
few tens of percent at most. Cheap, honest, no API change.
error bars on very compact sources. Not now.
Caveats on this result
model may need a lower lambda to absorb residuals — which is the regime where the gap opens. This
is the most likely way the conclusion could be wrong.
RectangularBilinearAdaptDensityonly; Delaunay untested.posterior mass sits at lower lambda where the gap is larger.
Script:
scratchpad/fitted_lambda.py(session artefact).PARTIALLY ADDRESSED 2026-08-22 — the docstring caveat shipped
The recommended immediate action was taken: PyAutoArray#472
documents on
reconstruction_noise_mapthat the covariance is that of the unconstrained solvewhile the default solver is NNLS, quantifies the overstatement at the evidence-optimal coefficient
(x1.01 to x1.26 median, up to ~2x per pixel) and below it (~2.8x median, ~10x per pixel), and records
that restricting to the free set is not the correction because the two bracket the truth.
Documentation only — no behaviour change, no API change.
What this prompt still owns
low:worth it only if someone needs calibrated per-pixel error bars on a very compact source.
zeroed_ids_to_keepwhile the reconstruction subsets by it.Never measured in isolation; the real fits here had 108 zeroed pixels of 784 and they are folded
into the "pinned" counts throughout, so its separate contribution is unknown.
use_edge_zeroed_pixelsnested inside theuse_positive_only_solverbranch, soturning the positive-only solver off silently disables edge-zeroing. Untouched by any of the
measurement above and unambiguous at any priority. It sits on the reconstruction path, so it
changes fit results and needs its own sign-off. This is the most likely next piece of real work
here.
than fixed at truth. That is the single result most likely to overturn the
lowgrading.SCOPED FOR DEV 2026-08-27 — Defect 2 only; Defect 3 is closed
/start_devre-read this prompt against PyAutoArraymain@da5ec9a0. Three of the four openitems are not live code work, so this prompt now owns exactly one:
main@da5ec9a0low— "worth it only if someone needs calibrated per-pixel error bars on a very compact source".zeroed_ids_to_keepreconstruction_covariance_matrix(abstract.py:844) inverts the fullcurvature_reg_matrix;reconstruction(abstract.py:516-544) solves thezeroed_ids_to_keepsubmatrix and scatters back exact zeros.use_edge_zeroed_pixelsnested inside the positive-only branch84b9ed4(2026-08-22): "the author confirmed the scoping is deliberate". Documented in three places —config/general.yaml:6,Settings.use_edge_zeroed_pixels, and a comment at the nesting site itself saying explicitly not to hoist the check. The text above calling this "the most likely next piece of real work" is stale — do not act on it.research/prompt when someone wants it.Blast radius is wider than this prompt assumed
The scope note above says Defect 2 "only bites when
zeroed_pixels > 0" and thatDelaunay.__init__defaults it to0, "so this is opt-in per mesh". That understates it.RectangularRTUAdaptDensity.zeroed_pixels(rectangular_rtu_adapt_density.py:161) returns theentire edge ring unconditionally — there is no opt-in and no count to set. For the
Rectangular*AdaptDensityfamily it is always on. That is the mesh family every real-fitmeasurement in this prompt used: 108 zeroed of 784 on a 28x28 is exactly the edge ring,
4*28 - 4.The three index sets are not three inconsistencies — two of them are unrelated
The Verification section below asks whether
curvature_reg_matrix_reduced'smapper_indicesreduction should apply to the noise map too, and calls the inconsistency between all three "itself a
finding". It is not.
mapper_indicesdrops linear objects that carry no regularization (lightprofiles and the like) from the reduced matrices; it has nothing to do with edge zeroing. The
comment
# ids of values which are on edge so zero-d and not solved foratabstract.py:349,:387and:574is simply wrong, and is what makes the two look like the same concern. Correctthe comment as part of this task. Only
zeroed_ids_to_keepvs the noise map was ever a defect.DECISION 2026-08-27 — excluded pixels report
NaNThe prompt recommended
0("matches the reconstruction's own convention and keeps plotsworking"). The human chose
NaN, and the two consumer paths were checked before deciding —neither breaks, and
NaNis the safer of the two:norm_from(plot/utils.py:270) is the single colour-scale implementation behindplot_arrayandplot_inversion_reconstruction. It derivesvmaxwithnp.nanmaxand itsdocstring already names "an all-
NaNarray" as a handled fallback; the linear branch returnsNoneand lets matplotlib autoscale, which ignoresNaN. Unsolved pixels render blank, which iswhat they are.
save_reconstruction_csv(inversion_plots.py:395) already writesnaninto thenoise_mapcolumn when the covariance raises.NaNextends an established convention ratherthan inventing one.
0was the worse option, not the safer one.source_science.pycomputesreconstruction / reconstruction_noise_map, and the reconstruction at these pixels is exactly0.0. Under0that is0/0->NaNplus aRuntimeWarningon every call; underNaNtheNaNpropagates silently. Both giveNaN < 5.0 == False, so the pixels survive theS/N >= 5cut carrying a reconstruction of
0.0and measured flux is unchanged either way.Document the convention in the
reconstruction_noise_mapdocstring and in the workspacescripts that consume it — which is why
@autolens_workspaceis now on this prompt.This is a value change on the pixels that ARE solved
Restricting the inverse is not a re-scaling of the excluded rows. For an SPD matrix
[A^-1]_keep >= (A_keep)^-1in the positive-semidefinite ordering, so every kept pixel's noisedrops, on every
Rectangular*AdaptDensityfit. Needs a measured before/after on a real fit and arelease note.
The Defect 1 bracket framing does not apply here. These pixels are fixed at zero by
construction, deterministically, before the solver runs — not selected by the data the way the NNLS
active set is. Conditioning on them is exactly right, and there is no upper/lower bracket to
straddle. Do not let the "restricting to the free set is not the correction" caveat in
reconstruction_noise_map's shipped docstring be read as applying to this change; it is about theNNLS active set only.
Structural fix, not a local patch
The reason this defect exists is that the predicate deciding whether the solve subsets the system
lives inline in
reconstructionand nowhere else, so the covariance could never have known aboutit. Give the inversion one property for "which indices did the solve actually solve", have both
reconstructionandreconstruction_covariance_matrixread it, and the two cannot drift apartagain.
Verification
noise map under the current code and under the free-set-restricted covariance,
and compare. Quantify how many mesh pixels are pinned at zero by NNLS — the
claim that this fraction is large for compact sources is reasoned, not
measured, and the whole prompt rests on it. If the fraction turns out small,
Defect 1 is a much smaller problem than stated here and should be re-graded.
pixel the reconstruction reports as an exact structural zero should be
identifiable in the noise map by the documented convention.
zeroed_pixels > 0, assert the covariance is formed on the reduced matrix— regression-test the shape and the scatter-back, not just values.
use_edge_zeroed_pixels: true+use_positive_only_solver: falseeither applies edge-zeroing or raises/warns. It must not silently ignorethe setting.
curvature_reg_matrix_reduced'smapper_indicesreduction shouldapply to the noise map too. The log-det uses it; the noise map does not. Decide
deliberately — this is a third, separate index set and the inconsistency between
all three is itself a finding.
Prior art — read before starting
complete/2026/08/numerical-inversion-failures.md— this cluster's refutation.complete/2026/07/pix-inversion-not-positive-definite.md— an earliernon-positive-definite hypothesis, also refuted; documents the
GaussianKernelPD-guarantee
f1817af0.autoarray/util/cholesky_funcs.py:50-80— near-coincident mesh vertices make theSchur pivot's sign depend on BLAS thread count. The degeneracy is real and
documented; this prompt is about not feeding it into an explicit inverse.
abstract.py:805— the repo already documents~1e-6evidence round-off from"factorizing the explicitly formed inverse" at
cond(C) ~ 1e9on clusteredtraced mesh vertices.
Provenance
covariance helper is the building block this prompt reuses.