Skip to content

Commit e463de7

Browse files
authored
Merge pull request #264 from PyAutoLabs/claude/numerical-inversion-failures-7xsp1k
prompt: close numerical-inversion-failures (refuted); ship + scope the autoarray noise-map cluster
2 parents 8a91d10 + fdbff3c commit e463de7

8 files changed

Lines changed: 1033 additions & 92 deletions
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# numerical-inversion-failures — release-run non-positive-definite inversion failures
2+
3+
**Date:** 2026-08-22
4+
**Issue:** [PyAutoArray#467](https://github.com/PyAutoLabs/PyAutoArray/issues/467) (closed)
5+
**PRs:** none — **no code was changed in any repo**
6+
**Outcome:** investigated to a definitive verdict; no defect exists to fix.
7+
8+
## What this task was
9+
10+
Release run `28784914443` (PyAutoHeart#27, the first real release-profile validation) reported 42
11+
script failures, split across seven prompts in `PyAutoMind/draft/bug/health_fixes/`. This task owned
12+
two, both alleging `LinAlgError`-class failures from non-positive-definite matrices in inversion
13+
paths:
14+
15+
- `autolens_workspace_test/scripts/interferometer/model_fit.py`
16+
- `autogalaxy_workspace/scripts/interferometer/features/pixelization/galaxy_reconstruction.py`
17+
18+
The prompt asserted the autolens leg "reproduces on current `main`" and prescribed a five-step
19+
repair: capture curvature/regularization matrix properties at failure, localise the defect between
20+
sampled parameters, regularization construction, numerical stabilization or an underdetermined
21+
script model, then fix the owning library.
22+
23+
Brain sized it `too-large` (score 16), fix-locus "library source", strategy "split into phases".
24+
25+
## What was actually done
26+
27+
Not started as the prescribed repair. The premise was gated first, for the same reason its sibling
28+
`autofit_sampler_database` was: the claim was six weeks old, and the cluster around it had already
29+
produced two independent findings that it had aged out.
30+
31+
Both scripts were re-run on current `main` from a **cleared** `output/`, under each workspace's
32+
`config/build/profile_release.yaml`, env resolved by `autohands.env_config.build_env_for_script` at
33+
workspace CWD, 1800s `mode=release` cap. Libraries at `main`: PyAutoFit `248ca971f`, PyAutoArray
34+
`b808a9b1`, PyAutoGalaxy `7e3856dd`, PyAutoLens `d8f6bb3df`, PyAutoNerves `f6d6d52`. Three workspace
35+
checkouts were behind `origin/main` and were synced first.
36+
37+
## Result: 0 / 2 reproduce
38+
39+
| Script | Result | Secs |
40+
|---|---|--:|
41+
| `autolens_workspace_test/scripts/interferometer/model_fit.py` | PASS | 78 |
42+
| `autogalaxy_workspace/scripts/interferometer/features/pixelization/galaxy_reconstruction.py` | PASS | 70 |
43+
44+
No non-positive-definite failure and no `LinAlgError` in either. The prompt's specific claim that the
45+
autolens leg reproduces on `main` is false.
46+
47+
## Why this verdict is strong: two prior independent refutations of the same hypothesis
48+
49+
This is not a lone green run. The same hypothesis — that PyAuto's inversion path produces
50+
non-positive-definite curvature/regularization matrices — had already been tested and refuted twice:
51+
52+
1. **`complete/2026/07/pix-inversion-not-positive-definite.md`** (2026-07-21) investigated a
53+
six-marker `LinAlgError: matrix not positive definite / singular` cluster across
54+
autogalaxy_workspace + HowToGalaxy. Outcome **inverted**: all six markers were stale and **no code
55+
was fixed**. The pix `LinAlgError` had been cured on 2026-04-10 — the same day the markers were
56+
filed — by PyAutoArray's `GaussianKernel` PD-guarantee `f1817af0` (symmetrise + trace-scaled
57+
diagonal jitter). Evidence was a 40-draw numpy inversion A/B across the full `GaussianKernel`
58+
LogUniform prior (coeff/scale `1e-6`..`~5e5`): **0 raises / 0 non-finite** on both `cholesky` and
59+
`slogdet`.
60+
2. **`complete/2026/08/autofit-sampler-database.md`** (PyAutoFit#1508, 2026-08-21) — the sibling from
61+
this same release run, **0/9 reproduce**, closed with no code change.
62+
63+
A PD-guarantee landed in the owning library four months ago, was independently verified by direct
64+
matrix probing, and both of this prompt's scripts now pass. The defect described here does not exist
65+
on `main`.
66+
67+
## Why the issue was closed rather than parked open
68+
69+
The same test the sibling applied, and it passes here: **neither script is parked.** Checked against
70+
`main` on 2026-08-22 —
71+
72+
- `autogalaxy_workspace/config/build/no_run.yaml` — 8 entries, none matching
73+
`interferometer/features/pixelization/galaxy_reconstruction` (GUI scripts, fits/png_make,
74+
search-viz, and one SLOW shapelets entry).
75+
- `autolens_workspace_test/config/build/no_run.yaml` — no `interferometer/model_fit` entry. It
76+
appears only as a *consumer* on line 62, where `interferometer/simulator/with_lens_light.py` is
77+
marked `BOOTSTRAP-TARGET` because it produces `model_fit`'s dataset — which confirms `model_fit`
78+
itself runs.
79+
80+
So both scripts re-execute under exactly this profile in **every** `mode=release` pass. Re-validation
81+
is automatic; a surviving defect fails the next release run loudly and earns a fresh issue with fresh
82+
evidence. There is no human reminder to lose — the same reasoning that closed PyAutoFit#1508, and the
83+
reason this prompt closes while `samples_parameter_paths` (#1327) stays parked.
84+
85+
This is now the **fourth** independent finding that this cluster aged out. The consistent explanation
86+
across all of them is the one #1327 reached: stale cached `output/` in the 2026-07 release run,
87+
against libraries that have since absorbed dozens of fixes.
88+
89+
## What this does NOT establish
90+
91+
1. **The autolens leg ran on numpy, not JAX.** `autolens_workspace_test`'s release profile *defaults*
92+
`PYAUTO_DISABLE_JAX="1"`; scripts opt back in with an in-file `ENV: jax` declaration, and
93+
`model_fit.py` carries none. That is release-faithful — it is what the release run itself executes
94+
— but JAX-on and JAX-off are different numerical code paths, and this refutation covers only the
95+
numpy one. A JAX-only inversion conditioning defect would not appear here.
96+
2. **These were source-tree runs**, not the TestPyPI wheels the release run installed. A wheel-only
97+
packaging defect would not show.
98+
99+
## Incidental finding — filed as its own prompt
100+
101+
`galaxy_reconstruction.py` passes while emitting 4x `RuntimeWarning: invalid value encountered in
102+
sqrt` from `PyAutoArray/autoarray/inversion/inversion/abstract.py:859`. **This looks exactly like
103+
evidence for the prompt's hypothesis and is not** — it is a separate, unconditional defect:
104+
105+
```python
106+
@property
107+
def reconstruction_noise_map_with_covariance(self) -> np.ndarray:
108+
return np.sqrt(np.linalg.inv(self.curvature_reg_matrix))
109+
```
110+
111+
`sqrt` is applied **elementwise to the whole inverse matrix**, whose off-diagonal entries are
112+
covariances and are generally negative — so those entries are NaN *by construction*, for any input
113+
matrix, however well-conditioned. It is not a conditioning symptom and does not rescue the prompt.
114+
115+
Confirmed still present on PyAutoArray `main` @ `a6b07cd` (2026-08-22). The 1D
116+
`reconstruction_noise_map` is unaffected — it takes the diagonal, and elementwise-sqrt commutes with
117+
taking the diagonal — so the science path is correct; only the covariance-aware consumer and the
118+
warning spam are hit.
119+
120+
Filed as **`draft/bug/autoarray/reconstruction_noise_map_covariance_sqrt.md`**, not fixed here: the
121+
correct off-diagonal semantics are an API/science decision, and the fix has a real trap (the 1D
122+
science path is derived from this property's diagonal, so a naive change silently converts a
123+
standard-deviation into a variance).
124+
125+
126+
**2026-08-22 follow-up — the incidental finding got bigger.** Research into *why* source-reconstruction
127+
noise maps have been unreliable found the sqrt bug is **not** the cause: `np.sqrt` is elementwise, so it
128+
commutes with taking the diagonal and provably cannot reach the 1D noise map. Three deeper defects were
129+
found in the same property, and split into a second prompt,
130+
`draft/bug/autoarray/reconstruction_noise_map_solver_mismatch.md`:
131+
132+
1. **Estimator mismatch (the big one).** `inv(curvature_reg_matrix)` is the posterior covariance of the
133+
*unconstrained* Warren & Dye solve. But `use_positive_only_solver: true` is the shipped default, so the
134+
reconstruction is an NNLS active-set solve. Imposing `s >= 0` truncates the posterior, so the reported
135+
noise is overstated near the boundary and meaningless for pinned pixels — and a compact lensed source
136+
pins a large fraction of the mesh at zero, so the formula is worst exactly where it is most used.
137+
2. **Edge-zeroed pixels ignored.** `use_edge_zeroed_pixels: true` is also default; the reconstruction
138+
solves on `zeroed_ids_to_keep` and scatters back zeros, while the noise map inverts the *full* matrix —
139+
re-admitting the poorly-constrained boundary vertices the zeroing exists to remove.
140+
3. **`use_edge_zeroed_pixels` is nested inside the positive-only branch**, so turning the positive-only
141+
solver off silently disables edge-zeroing with no warning. Two orthogonal settings, silently coupled.
142+
143+
Corroboration for the numerics half: `abstract.py:805` already documents `~1e-6` evidence round-off from
144+
"factorizing the explicitly formed inverse" at `cond(C) ~ 1e9` on clustered traced mesh vertices — applied
145+
to the log-det, never to the noise map. And `inversion_plots.py:395` already wraps the noise map in
146+
`except np.linalg.LinAlgError`, writing NaN to the CSV: a guard that exists because this fails in practice.
147+
148+
## Follow-on state of the cluster
149+
150+
`draft/bug/health_fixes/README.md` row struck through. Of the original seven prompts: two shipped or
151+
closed with code (`aggregator_output_contracts`), two closed as refuted with no code change
152+
(`autofit_sampler_database`, this one), one parked not-reproducing (`samples_parameter_paths`,
153+
#1327), and three remain in `draft/` with dated gate annotations
154+
(`jax_runtime_and_parity`, `jit_visualization_outputs`, `release_timeout_policy`) — those are *not*
155+
complete, because their SLOW/NEEDS_FIX parkings describe *intermittent* failures that a single green
156+
run cannot clear.
157+
158+
## Original prompt
159+
160+
# Fix release-profile numerical inversion failures
161+
162+
Type: bug
163+
Target: health_fixes
164+
Difficulty: too-large
165+
Autonomy: supervised
166+
Priority: high
167+
Status: formalised
168+
169+
## Context
170+
171+
Two interferometer scripts fail in inversion paths with non-positive-definite matrices.
172+
The Autolens test failure reproduces on current `main`; the Autogalaxy script passed in a
173+
stateful local checkout and needs a clean confirmation.
174+
175+
Owners: @PyAutoArray, @PyAutoGalaxy, @PyAutoLens, @autogalaxy_workspace, and
176+
@autolens_workspace_test.
177+
178+
## Scripts
179+
180+
- `autogalaxy_workspace/scripts/interferometer/features/pixelization/galaxy_reconstruction.py`
181+
- `autolens_workspace_test/scripts/interferometer/model_fit.py`
182+
183+
## Required work
184+
185+
1. Reproduce in clean output/worktrees with deterministic seeds and release settings.
186+
2. Capture the curvature and regularization matrix properties at failure: symmetry,
187+
conditioning, eigenvalue range, dtype, backend, and mapper configuration.
188+
3. Identify whether the defect is invalid sampled parameters, regularization construction,
189+
numerical stabilization, or a script model that permits an undefined inversion.
190+
4. Fix the owning library for valid inputs. Do not catch `LinAlgError` or alter the script
191+
to hide a genuine inversion failure.
192+
5. Add numerical regression tests and rerun both scripts repeatedly under the profile.
193+
194+
<!-- formalised retroactively by the Intake (Conception) Agent on 2026-07-08 -->
195+
196+
## 2026-08-21 — REPRODUCTION GATE RUN: **2/2 PASS — prompt refuted**
197+
198+
Method (identical to the gate that closed the sibling `autofit_sampler_database`, PyAutoFit#1508):
199+
every script run from a **cleared** `output/`, under its workspace's
200+
`config/build/profile_release.yaml`, env resolved by `autohands.env_config.build_env_for_script`
201+
at workspace CWD, 1800s `mode=release` cap. Libraries at `main`: PyAutoFit `248ca971f`,
202+
PyAutoArray `b808a9b1`, PyAutoGalaxy `7e3856dd`, PyAutoLens `d8f6bb3df`, PyAutoNerves `f6d6d52`.
203+
Three workspace checkouts were **behind `origin/main`** and were synced first.
204+
205+
| Script | Result | Secs |
206+
|---|---|--:|
207+
| `autolens_workspace_test/scripts/interferometer/model_fit.py` | PASS | 78 |
208+
| `autogalaxy_workspace/scripts/interferometer/features/pixelization/galaxy_reconstruction.py` | PASS | 70 |
209+
210+
The prompt states the autolens leg "reproduces on current `main`". It does not. No
211+
non-positive-definite failure, no `LinAlgError`, in either.
212+
213+
**Note which numerical path each took.** `autolens_workspace_test`'s release profile *defaults*
214+
`PYAUTO_DISABLE_JAX="1"`, and scripts opt back in with an in-file `ENV: jax` declaration.
215+
`model_fit.py` has no such declaration, so it ran on **numpy** — release-faithful, but worth
216+
knowing for a claim about inversion numerics, since JAX-on and JAX-off are different code paths.
217+
218+
### Incidental finding — a real defect, but NOT this prompt's
219+
220+
`galaxy_reconstruction.py` passes while emitting 4x
221+
`RuntimeWarning: invalid value encountered in sqrt` from
222+
`PyAutoArray/autoarray/inversion/inversion/abstract.py:859`:
223+
224+
```python
225+
def reconstruction_noise_map_with_covariance(self):
226+
return np.sqrt(np.linalg.inv(self.curvature_reg_matrix))
227+
```
228+
229+
`sqrt` is applied **elementwise to the whole inverse matrix**, whose off-diagonal entries are
230+
covariances and are generally negative — so those entries are NaN *by construction*, for any
231+
matrix, however well-conditioned.
232+
233+
**This is not evidence of a non-positive-definite matrix** and does not rescue the prompt's
234+
hypothesis, despite looking exactly like it would. It is a separate defect: a property whose
235+
docstring promises a matrix that "accounts for the covariance of the noise between pixels" returns
236+
NaN wherever that covariance is negative. The 1D `reconstruction_noise_map` is unaffected — it
237+
takes the diagonal, and `diag(sqrt(M)) == sqrt(diag(M))` — so the science path is correct; only
238+
the covariance-aware consumer and the warning spam are hit. Worth its own PyAutoArray prompt.

0 commit comments

Comments
 (0)