|
| 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