Skip to content

research: find what kills MGE multi-start lanes (62% value-NaN) #128

Description

@Jammy2211

Overview

The trapped-lane counter (PyAutoFit#1475 004f798, PyAutoGalaxy#572 695b27c) ran on the real imaging/mge cell and cleared the ell_comps plateau as a suspect — and surfaced a much larger effect nobody has characterised. On a production dataset/model/analysis via build_for_cell (16 starts x 150 steps, cloud CPU, 352s):

counter lane-steps share
n_value_nan_lane_steps 1498 62.42%
n_grad_nan_lane_steps 9 0.38%
n_constrained_lane_steps 0 0.00%
n_resurrections 0

The population fell from alive 16/16 to alive 2/16. Roughly seven of every eight starts die, and the best fit is being found by two survivors.

This contradicts a documented assumption: AbstractMultiStartGradient's resurrect docstring justifies the resurrect=False default by asserting the parametric MGE-class cell "has only the measure-zero singularity, so the apply_if_finite guard suffices". The measurement says otherwise, and the dominant mode is value-NaN, which apply_if_finite does not rescue — it only zeroes the step, so the lane stays dead and resurrect=False never redraws it. n_resurrections: 0 confirms that mechanism.

This task finds the cause. It does not change any search behaviour — see Boundary.

Plan

  • Find where the non-finite likelihood comes from — which parameters/regions and which operation. This is the open question and it does not need a GPU.
  • Measure whether resurrect=True recovers the budget on this cell, and what it costs.
  • Confirm the effect at production budget, on GPU, across seeds — graded on the alive-versus-step curve, not on reproducing the scalar "62%".
  • Record the finding in this repo's hazard index.
Detailed implementation plan

Affected repositories

  • autolens_profiling (primary) — investigation, measurement, hazard-index entry
  • PyAutoFit / PyAutoGalaxyread only here; any fix is a separate task

Branch survey

Repository Current branch Dirty?
autolens_profiling main (a34d6191) clean
PyAutoMind claude/mge-lane-death-ih8q92 clean

Suggested branch: research/mge-lane-death

Implementation steps, ordered by information per unit time

This ordering is deliberate and differs from the order the questions were first written down. The cause is the unanswered question and it is the cheap one; the GPU reproduction confirms an effect already measured once. Do not queue for a GPU before step 1 has been attempted.

  1. Where the NaNs come from. Candidates: the mask edge, a Gaussian sigma collapsing, the linear inversion / NNLS solve, the sqrt at r=0. The existing ~6-minute 16x150 CPU run already produces all 1498 deaths — instrument it to dump each lane's parameter vector at the step its value first goes non-finite, and the distribution of those vectors is the answer. Minutes-long iteration, not a queued job.
  2. Whether resurrect=True recovers the budget on this cell, and what it costs. The docstring says resurrection is for pixelized sources; this evidence suggests the parametric cell may need it too. Measurement of the candidate remedy, not adoption of it.
  3. Whether it holds at production budget, on GPU, across seeds. One seed at reduced budget on CPU so far, and the float64 GPU path is different numerics. Log alive N/16 per step so curves can be overlaid across budgets.

Grade step 3 on the curve, not the scalar

62% is a survival integral, not a hazard rate: a value-NaN lane is frozen by apply_if_finite and never redrawn, so it keeps counting a value-NaN lane-step on every subsequent step. Hold the death curve fixed and run 300 steps instead of 150 and the same physics reports ~75%. A production run coming back above 62% is therefore not automatically a worse result. Compare alive-versus-step curves; use the scalar only within a fixed budget.

"When they happen" is already partly answered

Inverting the 1498 over 14 dead lanes with no resurrections gives sum(k_i) = 14*150 - 1498 = 602, i.e. a mean death step of ~43 of 150. Deaths concentrate in the first third — trajectories walking into a wall mid-descent, not bad initial draws that _broad_starts failed to filter. Step 1's per-lane death steps should confirm or overturn this; a disagreement is itself a finding, since it would imply lanes recovering and re-dying, which resurrect=False says they cannot.

Key files

  • scripts/misc/searches/_setup.pybuild_for_cell, the production dataset/model/analysis. Note it writes into dataset/ (rewrites the HST FITS, adds positions.json, emits results/simulators/*); it is not read-only.
  • results/searches/multi_start_nan_accounting/ — where the existing NaN-accounting artefact lives; the new results belong alongside it.
  • The hazard index in this repo — where the cause gets recorded.

Environment

  • Python 3.12+ (autonerves); pip install jaxnnls is required for the JAX NNLS solver path and is not pulled in by default.
  • Install autolens with --no-deps if using editable local autofit/autogalaxy, or the released wheels clobber them.
  • Cell scripts honour SEARCHES_N_STARTS / SEARCHES_N_STEPS / SEARCHES_BATCH_SIZE / SEARCHES_DISABLE_VIZ.
  • imaging/mge runs on CPU in ~6 min at 16x150. The pixelized mesh cells do not — that is JIT compile, not memory (batch_size=1 clears the OOM), and they need the GPU. Shrinking the source mesh is the wrong cost lever; the image-plane grid at mask_radius 3.5 dominates.
  • On A100 set jax_enable_x64 explicitly — it is not inherited under sbatch, and float32 would understate the quantity under test.

A trap worth knowing before re-running

validate_zero.py (in the prompt) is the positive control that makes the 0 meaningful — without it, a zero cannot be distinguished from "nothing was watching". Two notes:

  • It must probe with jnp arrays. Concrete Python floats trip validate_ell_comps and raise before the constraint is ever reached.
  • Its four checks call discovery/evaluation/the predicate directly, so none of them exercises the search loop that accumulates the counter. What closes that gap is in the result JSON: the counters come back as integer 0/1498, not null, and the driver reads them via si.get(k) — which would have given null had _fit never written the key. A null in the counters block on a re-run means broken plumbing, not a clean cell. The two look identical in a summary table.

Original prompt

The full prompt — including the verbatim result JSON and both self-contained driver scripts (rerun_cell.py, validate_zero.py) — lives in PyAutoMind and travels with the task:

draft/research/autolens_profiling/mge_lane_death.md (permalink, 0b8895ae)

The scripts are deliberately not committed to this repo: they are throwaway measurement drivers, not part of the script tiers, and they hardcode a workspace root and a scratchpad output path. Copy them out to run.

Background on the counter itself: complete/2026/08/frozen-lane-counter.md — the full run, the detector-design finding, and the positive control.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions