Skip to content

W1 · Phase 4 Stage 1: PositionsLH plumbing in the searches framework + hazard transects (#159) - #168

Merged
Jammy2211 merged 7 commits into
mainfrom
feature/phase4-positions-stage1
Aug 24, 2026
Merged

W1 · Phase 4 Stage 1: PositionsLH plumbing in the searches framework + hazard transects (#159)#168
Jammy2211 merged 7 commits into
mainfrom
feature/phase4-positions-stage1

Conversation

@Jammy2211

@Jammy2211 Jammy2211 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 4 Stage 1 of the inference programme (PROGRAMME §4 Phase 4). Closes #159. Unblocks Gate B pt 2 / W2 (#160) and Phase 5.

  • PlumbingSEARCHES_POSITIONS=off|on, SEARCHES_POSITIONS_THRESHOLD=<float>|auto, SEARCHES_POSITIONS_FACTOR=<float> thread al.PositionsLH into AnalysisImaging/AnalysisInterferometer for single-plane imaging/interferometer cells; group/cluster/datacube/point_source raise when on. The pixelized raise_inversion_positions_likelihood_exception short-circuit now applies only with positions off.
  • Artifact isolation — the autofit identifier hashes [search, model, unique_tag] only (the Analysis is not hashed), so a positions arm tag (pos_t0.3_f1e8, pos_tauto0.2_f1e5) is composed into unique_tag for every sampler builder (new arm_unique_tag) and into config_name. Found + fixed a real collision on the way: resolve_output_paths used the raw CLI name, so on/off pairs overwrote the same results JSON. 16 tests incl. assert_disjoint_output_paths.
  • Truth positionsdataset/imaging/hst/ had no positions.json; derived once from the committed tracer.json with the simulator's own PointSolver recipe (re-simulating would change data.fits) and committed (4 quad positions). Idealisation recorded (target_class: 3).
  • Stage-1 transectspositions_transects.py: θ_E ∈ [0,3] × {0.3, auto} × {1e5, 1e8} + off; fine sweep across the threshold; ell_comps/shear sweeps. Results + PNG/JSON under results/notes/inference/phase_04_positions/.
  • Hazards ledgerlikelihood/positions_penalty check with threshold-hinge, interior-plateau, argmax-switch findings.

Key findings (RESULTS.md)

  • auto threshold = max(3 × 7.7e-4, 0.2)collapses to the 0.2 floor on truth positions; the "auto" arm is "0.2 fixed" here.
  • Threshold crossings at θ_E ≈ 1.451 / 1.750 (0.3) and 1.501 / 1.700 (0.2); hinge gradient jump 2.0e5 vs 2.0e8 — exactly linear in factor.
  • Interior plateau: exact-zero value and gradient on all four arms.
  • Argmax-switch kinks exist but sit below every Stage-1 threshold (outside the active-penalty region).
  • Penalty/likelihood gradient-norm ratio (t0.3 f1e8): 0 inside → ~1.0 outside — comparable to, not overwhelming, the base gradient.
  • Flagged, not investigated: an anomalous 1e18 base-likelihood gradient norm at ell_comps_0 = 0 (unrelated to positions).

Verification

pytest scripts/misc/test: 108 passed, 1 failed — test_hazards_prior_exit.py::test__records_the_clipper_as_what_blocks_it passes on main; it fails in a task worktree only because hazards/_anchor.py looks for sibling repos under the worktree root (PyAutoLabs-wt/<task>/PyAutoFit does not exist), so code_exists resolves False — a layout artifact, not a regression; CI checks out the library mains as siblings. ruff check scripts clean. On/off smoke pair on multi_start_prodigy_autoconv/mge confirmed distinct output/ dirs and results JSONs. build_readme --check clean. Two OOM/laziness bugs in the transect harness found and fixed during the run (chunked vmap at 64; jit trace ordering).

Note: one merge commit (69bd8d2) was recorded with conflict markers by mistake and resolved in the immediately following commit — history left intact rather than rewritten.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6

Jammy2211 and others added 7 commits August 24, 2026 15:22
Phase 4 Stage 1 (issue #159). Adds SEARCHES_POSITIONS=off|on,
SEARCHES_POSITIONS_THRESHOLD=<float>|auto, SEARCHES_POSITIONS_FACTOR=<float>
env vars, threaded through _setup.build_for_cell/_build_analysis into
al.AnalysisImaging/AnalysisInterferometer's positions_likelihood_list, for
single-plane imaging/interferometer cells only — group/cluster/datacube/
point_source raise NotImplementedError rather than silently ignoring the
request.

Truth positions are loaded from dataset/<class>/<instrument>/positions.json
when present, else (imaging only) derived once from tracer.json via the
simulator's own PointSolver recipe and committed
(dataset/imaging/hst/positions.json, 4 quad-image positions). The 'auto'
threshold mode replicates SLaM's result.positions_threshold_from(factor=3.0,
minimum_threshold=0.2) and empirically collapses to the 0.2 floor for this
truth-positions-through-truth-tracer idealisation.

Correctness guard: PyAutoFit's identifier hashes only [search, model,
unique_tag] -- the Analysis object (and therefore whether a positions
penalty is attached) is not hashed. positions_arm_tag() composes into
multi_start_unique_tag and is passed as unique_tag to build_nautilus/
build_nss via the new arm_unique_tag() helper, and into run_search's
config_name, so a positions-on run can never silently share an output
directory/identifier with the positions-off run of the "same" cell.
_sampler_config_dict/multi_start_settings/_build_summary always record a
"positions" block ({"enabled": false} when off).

Adds scripts/misc/test/test_searches_positions.py (16 tests) covering env
parsing/validation, arm-tag composition, the auto-threshold floor collapse,
the dataset_class support guard, and assert_disjoint_output_paths (new
_samplers.py helper) proving positions-on/off and differing-factor arms
never collide on output_path or identifier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
…argmax-switch)

Phase 4 Stage 1 (issue #159, [H4.2]). New likelihood-tier hazard check
scripts/misc/hazards/checks/positions_penalty.py exercising the real
autolens.analysis.positions.PositionsLH.log_likelihood_penalty_from through
a cheap synthetic single-plane Isothermal tracer (the truth quad-image
positions and mass/shear from dataset/imaging/hst/{positions,tracer}.json,
no imaging dataset needed -- point-tracing only), producing three Findings:

- likelihood.positions-penalty.threshold-hinge: C0-continuous value,
  discontinuous jax-autodiff gradient at each max_sep==threshold crossing
  (tight +/-1e-6 local probe, not the coarse sweep grid -- see
  _local_hinge_probe's docstring for why the coarse grid alone
  over/under-states the value-continuity gap at factor=1e8).
- likelihood.positions-penalty.interior-plateau: exact-zero penalty value
  AND gradient throughout the region where max_sep < threshold.
- likelihood.positions-penalty.argmax-switch: which pair of the 4 truth
  positions realises max_sep changes discontinuously as einstein_radius
  sweeps [0, 3] -- a second, threshold-independent gradient kink source.

Registered in _registry.py (new likelihood:positions_penalty SubjectSpec)
and checks/__init__.py. Ran scan.py --subject likelihood (--check then a
real write); the unrelated prior_support/active_set/conditioning_floor
records that the same command also touches were reverted to origin's
committed values (float-noise / a pre-existing environment-flaky
PriorExitCheck failure, both unrelated to this change -- see the commit
history for the plumbing commit's verification that the PriorExitCheck
failure reproduces identically on an unmodified checkout).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
…paths

Found by the smoke on/off verification for issue #159. run_search's
output/searches/... PyAutoFit search directory was already correctly
disambiguated by positions_arm_tag() (see the earlier plumbing commit), but
resolve_output_paths derives its basename from cli.config_name -- the raw
--config-name CLI flag -- not the positions-tagged config_name local
computed later in run_search. A positions-on and positions-off run of the
same --config-name therefore silently overwrote the same
results/searches/.../<name>.json summary artifact, even though their
PyAutoFit output directories never collided.

Fix: pass resolve_output_paths a dataclasses.replace(cli,
config_name=config_name) carrying the arm-tagged name, so the results-JSON
path picks it up too. Verified with a live smoke pair (multi_start_prodigy_
autoconv/imaging/mge, n_starts=4, n_steps=20, seed=0): before the fix both
runs wrote results/searches/.../smoke_smoke.json; after, the positions-on
run writes smoke_smoke_pos_t0.3_f1e8.json and positions-off keeps
smoke_smoke.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
…SULTS.md

Phase 4 Stage 1 (issue #159). New scripts/misc/searches/positions_transects.py:
pure-evaluation eager jax.value_and_grad transects on imaging/mge/hst
(positions off at construction, analysis.positions_likelihood_list swapped
per arm), reusing build_for_cell + the model.instance_from_vector(...,
xp=jnp) / analysis.log_likelihood_function idiom already used by
clipper_campaign.py / multi_start_nan_accounting_overhead.py.

- Transect A: einstein_radius in [0,3] (n=601), per arm logl/dlogl/penalty/
  dpenalty, plus arm-independent max_sep(theta_E) + argmax-pair geometry.
- Transect B: 1e-4-spacing fine windows around each arm's threshold
  crossing -- the one-sided gradient jump (C0 hinge), an interior-plateau
  check windowed inside each arm's OWN crossings (not a fixed range for
  every arm -- the tighter auto threshold's crossings sit right at a naive
  [1.5,1.7] window's edge), and a coarse+fine argmax-switch scan.
- Transect C: mass ell_comps_0 / shear gamma_1 sweeps at fixed truth
  theta_E, plus a full 15-dim gradient-norm ratio (penalty vs likelihood)
  at 5 points, one representative arm per sweep.

Two real bugs found and fixed during development (both would have silently
zeroed every "on" arm's numbers):
- jax.jit traces LAZILY on first call, not at wrap time: the original
  get_scalar_fns/get_full_grad_fns cache helpers set then immediately reset
  analysis.positions_likelihood_list around building the jit object, so the
  eventual first (real) call always traced with positions off. Fixed by
  setting it unconditionally right before every return, cache hit or miss.
- Transect B derived the hinge probe location from the COARSE grid's
  interpolated crossing, imprecise enough (spacing ~0.075) that both probed
  fine-grid neighbours could land on the same (interior) side, silently
  reporting a zero jump. Fixed by re-locating the crossing from the fine
  grid's own precise max_sep values.

A third issue is a memory/compute lesson, not a correctness bug: a single
unchunked jax.vmap over Transect A's full n=601 grid OOMs (~30 GB
requested) -- the real MGE+inversion likelihood over 15361 masked pixels.
Added chunked_call, batching every vmap'd evaluation at 64 (matching
vram.vmap_batch_for("imaging","mge","hst")'s A100-probed cap for this
exact cell) with fixed padding, which also collapses compile count to one
XLA compile per (idx, arm) regardless of how many distinct grid shapes
Transects A/B/C throw at it.

results/notes/inference/phase_04_positions/{RESULTS.md,transects/*} record
the write-up and full numeric arrays from both a --quick (n=41) validation
run and the full (n=601) run; the two runs' crossing locations and
qualitative behaviour agree. README.md documents the SEARCHES_POSITIONS*
env vars and this script's usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
…list + log_det_method)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDABYoH6giHXhFJUks8yd6
@Jammy2211
Jammy2211 merged commit 04a2cd6 into main Aug 24, 2026
1 check passed
@Jammy2211
Jammy2211 deleted the feature/phase4-positions-stage1 branch August 24, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

W1 · Phase 4 Stage 1: PositionsLH plumbing in the searches framework + hazard transects

1 participant