|
| 1 | +# PyAutoFit mock scaffolding fills every parameter with 1.0, which is now an invalid `ell_comps` |
| 2 | + |
| 3 | +Type: bug |
| 4 | +Target: PyAutoFit |
| 5 | +Repos: |
| 6 | +- PyAutoFit |
| 7 | +- autogalaxy_workspace_test |
| 8 | +- autolens_workspace_test |
| 9 | +Difficulty: medium |
| 10 | +Autonomy: supervised |
| 11 | +Priority: high |
| 12 | +Status: formalised — NOT started. Root cause is narrowed to PyAutoFit's mock |
| 13 | + helpers but the exact call site is **not** pinned; see "What is not yet |
| 14 | + known". Requires an environment that can run the full autogalaxy stack. |
| 15 | + |
| 16 | +## Symptom |
| 17 | + |
| 18 | +Seven aggregator integration scripts across the two `*_workspace_test` repos fail |
| 19 | +with the same exception: |
| 20 | + |
| 21 | + ValueError: ell_comps must satisfy ell_comps[0]**2 + ell_comps[1]**2 < 1; |
| 22 | + got (1.0, 1.0), whose magnitude is np.float64(1.4142135623730951) |
| 23 | + |
| 24 | +Observed in PyAutoHeart Workspace Smoke run 31356506626 (2026-08-10), legs |
| 25 | +`autogalaxy_test / misc` and `autolens_test / misc`. |
| 26 | + |
| 27 | +Failing scripts (all `scripts/misc/aggregator/`): |
| 28 | + |
| 29 | +- `autogalaxy_workspace_test`: `ellipse.py`, `fit_imaging.py`, |
| 30 | + `fit_interferometer.py`, `galaxies.py` |
| 31 | +- `autolens_workspace_test`: `tracer.py`, `fit_imaging.py`, |
| 32 | + `fit_interferometer.py` |
| 33 | + |
| 34 | +## Why this is a real bug and not a bad shipped value |
| 35 | + |
| 36 | +The guard is correct and correctly placed. `validate_ell_comps` sits on |
| 37 | +`EllProfile`, the single base every elliptical profile inherits (`ag.Ellipse` |
| 38 | +included), and enforces `f = sqrt(e_y**2 + e_x**2) < 1` because the axis ratio is |
| 39 | +`q = (1 - f) / (1 + f)` — at `f >= 1` the ellipse degenerates to `q <= 0` and has |
| 40 | +no geometric meaning. `(1.0, 1.0)` gives `f = 1.414`, `q = -0.17`. Nothing should |
| 41 | +ever construct a profile with it. |
| 42 | + |
| 43 | +This is **not** the same failure as the sampler-draw legs (`guides`, etc.), which |
| 44 | +were fixed by PyAutoGalaxy#568 making `ModelParameterException` a |
| 45 | +`FitException` so searches resample. That fix does not help here: the aggregator |
| 46 | +rebuilds instances from stored samples outside any likelihood call, so there is no |
| 47 | +resample path to take. |
| 48 | + |
| 49 | +## Evidence |
| 50 | + |
| 51 | +1. **The value is a hardcoded fill, not a sampled value.** It prints as plain |
| 52 | + `(1.0, 1.0)` — Python floats. The genuine sampler-draw failures in the same run |
| 53 | + print as `np.float64(-0.7446446619131553)`. Different provenance. |
| 54 | + |
| 55 | +2. **Two places in PyAutoFit hardcode exactly this shape:** |
| 56 | + - `autofit/non_linear/mock/mock_samples.py` — `MockSamples.default_sample_list` |
| 57 | + builds `kwargs={path: 1.0 for path in self.model.paths}`. |
| 58 | + - `autofit/non_linear/mock/mock_samples_summary.py` — `MockSamplesSummary.__init__` |
| 59 | + sets `self._kwargs = {path: 1.0 for path in self.model.paths}`, which backs |
| 60 | + both `max_log_likelihood_sample` and `median_pdf_sample`. |
| 61 | + |
| 62 | + A blanket `1.0` is a safe placeholder for most parameters and an invalid value |
| 63 | + for any `ell_comps`. `_make_samples` in `mock_search.py` already does the right |
| 64 | + thing (`prior.value_for(0.5)`), so the fix idiom exists in the same package. |
| 65 | + |
| 66 | +3. **All 7 scripts construct `MockSearch` identically and never pass |
| 67 | + `samples_summary`:** |
| 68 | + |
| 69 | + ```python |
| 70 | + search = ag.m.MockSearch( |
| 71 | + samples=samples, |
| 72 | + result=af.m.MockResult(model=model, samples=samples, |
| 73 | + samples_summary=samples.summary()), |
| 74 | + ) |
| 75 | + ``` |
| 76 | + |
| 77 | + `MockSearch.__init__` therefore falls back to `MockSamplesSummary.default()`. |
| 78 | + Whether that asymmetry (a real model in `MockResult`, a default summary on the |
| 79 | + search) is the trigger or a red herring is the open question. |
| 80 | + |
| 81 | +## Hypothesis already tested and DISPROVEN — do not re-tread |
| 82 | + |
| 83 | +The obvious suspect is the helper copy-pasted into all 7 scripts: |
| 84 | + |
| 85 | +```python |
| 86 | +def parameter_list_with_physical_ell_comps(value): |
| 87 | + parameter_list = model.prior_count * [value] |
| 88 | + for index, path_tuple in enumerate(model.all_paths): |
| 89 | + if "ell_comps" in path_tuple[0]: |
| 90 | + parameter_list[index] = 0.1 |
| 91 | + return parameter_list |
| 92 | +``` |
| 93 | + |
| 94 | +It looks broken — `all_paths` returns a tuple of `Path`s per prior and |
| 95 | +`Path = Tuple[str, ...]`, so `path_tuple[0]` is a path tuple and `in` is |
| 96 | +exact-element membership, which would not match a leaf named `ell_comps_0`. |
| 97 | + |
| 98 | +**It is not broken.** `ell_comps` has a tuple default, so PyAutoFit builds a |
| 99 | +`TuplePrior` attribute named `ell_comps`, and the path is |
| 100 | +`('ellipses', '0', 'ell_comps', 'ell_comps_0')` — it contains a bare `'ell_comps'` |
| 101 | +element, so the check matches. Reproduced by rebuilding `ellipse.py`'s exact model |
| 102 | +(two `Ellipse` models with fixed `major_axis`, plus the nested multipole |
| 103 | +collection) against installed autofit and running the real helper: |
| 104 | + |
| 105 | +``` |
| 106 | +[2] ('ellipses', '0', 'ell_comps', 'ell_comps_0') -> 0.1 |
| 107 | +[3] ('ellipses', '0', 'ell_comps', 'ell_comps_1') -> 0.1 |
| 108 | +ellipses[0].ell_comps = (0.1, 0.1) |
| 109 | +``` |
| 110 | + |
| 111 | +Index alignment is also fine: `all_paths` and `instance_from_vector` |
| 112 | +(`prior_tuples_ordered_by_id`) both order by prior id. **Changing this helper is a |
| 113 | +no-op — do not "fix" it.** |
| 114 | + |
| 115 | +## What is not yet known |
| 116 | + |
| 117 | +Which call site actually feeds the all-ones instance to the aggregator. Two |
| 118 | +candidates were checked and neither fits cleanly: |
| 119 | + |
| 120 | +- `MockSamplesSummary.default()` uses an empty `Collection()`, so its `_kwargs` |
| 121 | + is `{}`, not a dict of 1.0s. |
| 122 | +- `MockSearch._fit_fast` evaluates at `[prior.mean for prior in |
| 123 | + model.priors_ordered_by_id]`, which is `0.0` for `ell_comps` — valid. |
| 124 | + |
| 125 | +So the path runs through serialization into the database and back out through the |
| 126 | +aggregator, which is where it needs to be traced. |
| 127 | + |
| 128 | +## Suggested approach |
| 129 | + |
| 130 | +1. Run one failing script (`autogalaxy_workspace_test/scripts/misc/aggregator/ellipse.py`) |
| 131 | + against the full stack with a breakpoint or traceback on the guard, and record |
| 132 | + the actual construction stack. **This needs a real autogalaxy environment** — |
| 133 | + it could not be done from a cloud session (autoarray/jax/numba would not |
| 134 | + install there). |
| 135 | +2. Fix at the PyAutoFit mock layer: replace the blanket `{path: 1.0 ...}` with |
| 136 | + prior-median values (`{path: prior.value_for(0.5) for path, prior in |
| 137 | + model.path_priors_tuples}`), matching `_make_samples`. |
| 138 | +3. Mind the blast radius: `MockSamples`, `MockSamplesSummary` and `MockSearch` |
| 139 | + have roughly 55 call sites inside PyAutoFit alone, plus the PyAutoGalaxy and |
| 140 | + PyAutoLens suites. Run all three suites, not just PyAutoFit's. |
| 141 | +4. Consider whether `MockSearch` should inherit the `samples_summary` from a |
| 142 | + passed-in `result` rather than silently defaulting. |
| 143 | + |
| 144 | +## Notes |
| 145 | + |
| 146 | +- Do not relax or move the `ell_comps` guard. It is correct. |
| 147 | +- Do not chase the `workspace-validation-report` artifact from a cloud session |
| 148 | + (blocked at the egress proxy). Per-job logs via the Actions API carry the same |
| 149 | + failures. |
| 150 | +- Sibling work already shipped: the one genuinely unphysical shipped literal, |
| 151 | + `ell_comps=(0.5, 0.9)` in HowToGalaxy `tutorial_3_fitting`, was corrected |
| 152 | + separately. An AST scan of 454 `ell_comps` literals across |
| 153 | + autogalaxy_workspace, autolens_workspace, HowToGalaxy, HowToLens and both |
| 154 | + `*_workspace_test` repos found no other violating literal, so this ticket is |
| 155 | + the whole remaining `ell_comps` surface. |
| 156 | +- PyAutoHeart#27 is a different family (release-profile timeouts and a JAX |
| 157 | + exception, 2026-07-06); it is not related. |
0 commit comments