You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The JAX PointSolver keeps at most MAX_CONTAINING_SIZE (15) containing triangles per refinement step and silently truncates anything beyond. The phase-4a sweep (RAL 356367) found an uncapped step-0 maximum of 17 (prior draw 12), and step 1 reaches exactly 15 under ±2.5/0.4, so the headroom is zero. Phase 4c of epic point-source-cpu-speed measures MCS 18/20/24 against the MCS-15 control and raises the constant to the smallest value that clears the observed maximum with margin (expected ~20), at measured cost.
Plan
Measure first, with no library change yet. Add MCS 18, 20 and 24 rows to the phase-4a/4b sweep harness and run an interleaved A/B against the MCS-15 control, on the laptop and on the quotable RAL 8490H. Report cost, completeness and uncapped counts.
Checkpoint with you: pick N, the smallest value that clears max 17 with margin (expected 20), from the cost table.
PyAutoArray: change the constant 15 → N and add a test that a 16–17-triangle containing set is no longer truncated.
PyAutoLens: make the docstrings and the ShapeSolver rejection message read the constant rather than a hard-coded "(15)", and update the test that pins that message.
Pins: re-check the fiducial log L and the workspace_test point-source and grad pins. Re-pin deliberately only if the padded shape changes a value, and explain why.
Ship library-first: PyAutoArray → PyAutoLens → autolens_profiling data PR, with an A100 no-regression row.
The existing max_containing_size() patch (~l.402) already rewrites ArrayTriangles.__init__ / for_limits_and_scale__defaults__ and the module constant, and the max_containing_size_proof gate asserts the traced (mcs, 2) shape. Reuse both; add nothing new.
Add a --configs selection, or a --mcs-only preset, so the run covers control plus mcs15/18/20/24 and skips the full 26-config grid.
Per row: median + bootstrap 90 % CI, the paired-round ratio, compile, FLOPs, XLA temp memory, vmap 1/4/16.
Completeness: 200 prior + 200 stress draws against the default and the fine reference.
Extend uncapped_containing_counts to report, per MCS, draws_exceeding_cap (already present, ~l.1493) for every refinement step, not only step 0.
List each draw whose image set differs from MCS 15 and explain it (expected: draw 12 and any other truncating draws).
Runs: laptop witness (not quotable), then the RAL 8490H euclid-ral-compute-10-4. Use the phase-4b submit pattern: branch-clone provenance asserts, and WALL-BASIS cell: point_source_image/solver_config_sweep/simple with a measured-wall source.
Run check_submits.py --check locally first.
Prefer a quiet node; record the loadavg.
Step 2 — decision checkpoint (you)
The rule: choose the smallest N ≥ 18 whose uncapped max over all steps and draws is ≤ N − 3. It must add no new image-set change beyond the explained truncation fixes, keep compile within +20 %, and add no worse than about 5 % to the scalar median.
I bring you the table. You choose N. No default change without your call (this is an epic rule).
Step 3 — PyAutoArray (array/PyAutoArray)
autoarray/structures/triangles/array.py:13MAX_CONTAINING_SIZE = N. The defaults at l.65 and l.110 bind the constant at import, so a source change propagates.
Check that CoordinateArrayTriangles.with_vertices → ArrayTriangles(...) inherits it; it uses the default.
Grep for any hard-coded 15 in autoarray/structures/triangles/.
Tests in test_autoarray/structures/triangles/test_coordinate_jax.py:
Build a lattice and point where the containing set has 16–17 members; draw 12's geometry, or a vertex-on-fold construction. Assert that all of them are kept under jit, where MCS 15 would drop some.
Assert the padded output shape is (N,).
test_np.py:25 passes max_containing_size=5 explicitly and is unaffected.
The 4b _STEP0_CONTAINMENT routes pad to self.max_containing_size, and the 4b fuzz/HLO tests stay green. The HLO guard checks the triangle-array size, not MCS.
Step 4 — PyAutoLens (lens/PyAutoLens)
autolens/point/solver/shape_solver.py:585,600: turn the "(15)" docstring and message into text built from aa...array.MAX_CONTAINING_SIZE. It stays a module-level plain string, so the "same message whether or not JAX is installed" property holds.
test_autolens/point/triangles/test_shape_solver.py:578,601,608: update the pinned message to use the constant.
point_solver.py:15,61,263 and implicit_diff.py:35,159 name the constant without a number, so no change.
Run the full PyAutoLens suite against the PyAutoArray branch.
Step 5 — pins and downstream
Fiducial 7.743201200876812: padded rows are inf sentinels. Check whether the log L is bit-identical at N. If not, trace the reason (a log-sum-exp over padded pairs) and re-pin FIDUCIAL_SOLVED_LOG_L_BY_BACKEND deliberately, with an explanation.
autolens_workspace_test scripts/point_source/jax_likelihood/* (the exact −83.38049778 asserts) and jax_grad/gradient.py: run them against the branch. Any changed pin goes into a workspace_test PR with its reason. Positions shape (MCS, 2): grep consumers.
Ship with /ship_library for PyAutoArray and PyAutoLens (Heart gate), then /ship_workspace for the autolens_profiling data plus the ledger "Phase 4c" section in results/notes/point_source_cpu_campaign.md. Add workspace_test only if a pin changed.
Explicitly not in scope
A runtime overflow counter in the library. Any on-path count costs something on the JAX default, and the harness's uncapped NumPy count already keeps truncation visible for profiling. I'd file it as a draft if you want it.
PyAutoLens is claimed by workspace-config-cleanup (Lens#751 merged, awaiting release). This task touches only autolens/point/solver/shape_solver.py and its test, so it is disjoint.
autolens_profiling is claimed by point-source-source-plane-p2c. That task touches scripts/point_source_source/… and the source-plane ledger; this one touches scripts/point_source_image/…, the new hpc submits, results/breakdown/point_source_image/ and the CPU ledger. Disjoint.
Verification
Laptop and RAL 8490H A/B JSONs with all_gates_pass, the per-draw explanations of every image-set change, and the uncapped max per step.
The new PyAutoArray truncation test fails at MCS 15 and passes at N.
Full PyAutoArray and PyAutoLens suites.
workspace_test point-source jax_likelihood + jax_grad, and the 31-script targeted smoke.
check_submits.py --check and build_readme.py --check clean.
Raise PyAutoArray's MAX_CONTAINING_SIZE from 15 to about 20. Measure 18, 20 and 24 and pick
the smallest that clears the observed maximum with margin. The cap is the static per-step capacity
of containing triangles in the JAX PointSolver, and it silently truncates when exceeded. Human
decision (2026-09-26): accepted as a phase-4a follow-up.
Source: lens/autolens_profiling/results/breakdown/point_source_image/solver_config_sweep_hpc_ral_cpu_fp64.json,
key uncapped_containing_counts, counted in NumPy with no cap on 200 prior draws. Ledger: campaign
note, "Phase 4a".
Default geometry (±9.9″ / 0.2 / 1e-3): step 0 has median 9, p99 15 and max 17, on prior draw
12.
23 draws exceed 12, and 1 exceeds 15.
Steps 1–7 peak at 13 / 11 / 9 / 7 / 5 / 5 / 5.
Draw 12 still passed completeness, because the truncated entries were the spurious fold-line
candidates. That is luck, not a guarantee.
Under ±2.5/0.4, step 1 reaches exactly 15 (p99 15). The headroom is zero there too, and a smaller
workspace grid (see draft/feature/autolens_workspace/pointsolver_grid_extent_per_package.md)
pushes counts up.
The cap is load-bearing. MCS 8 and MCS 10 lose images (prior multiplicity 62 % and 86.5 %), at
1.11× and 1.06× speed.
Report the cost of each as its own interleaved row: median, CI, compile, FLOPs and temp memory. The
cap sets the refinement grid size (720 rows per step at MCS 15: 240 kept triangles × 3) and the f64[60] neighbourhood
sorts, so expect a modest cost, and measure it.
Prove no image-set change on the 200 prior + 200 stress draws against both the default and the
reference, except where the old cap was truncating (draw 12 and any others the uncapped count
flags). Those draws must be explained one by one.
Fiducial log L 7.743201200876812 must stay bit-identical, or, if the padded shape changes it,
explain why and re-pin deliberately. Check autolens_workspace_test point_source/jax_likelihood/*
and jax_grad/gradient.py pins, and positions shape (MCS, 2).
Consider a debug-mode overflow counter or warning: a non-JAX or jax.debug-gated count of how
often the uncapped containing set exceeds the cap, so truncation is never silent again. It must not
cost anything on the default JAX path.
A100 no-regression row if the default changes.
Traps
The __defaults__ binding trap.MAX_CONTAINING_SIZE is a module constant, but ArrayTriangles.__init__ and for_limits_and_scale bind it as a default argument at import.
Patching the module constant alone does nothing. The 4a cell had to patch those functions' __defaults__ too, and restore them by value. In the library change, make sure every default
reads the new value (or switch to a None sentinel resolved at call time), and grep PyAutoLens for
any copy of the constant.
PyAutoArray is currently claimed by interferometer-transform-real-scatter. Check at start_dev.
Coordinate with phase 4b (shipped 2026-09-27, record complete/2026/09/pointsolver-step0-gather.md, PyAutoArray#580), which also touches the
triangles code, and prefer landing 4b first.
Overview
The JAX
PointSolverkeeps at mostMAX_CONTAINING_SIZE(15) containing triangles per refinement step and silently truncates anything beyond. The phase-4a sweep (RAL 356367) found an uncapped step-0 maximum of 17 (prior draw 12), and step 1 reaches exactly 15 under ±2.5/0.4, so the headroom is zero. Phase 4c of epicpoint-source-cpu-speedmeasures MCS 18/20/24 against the MCS-15 control and raises the constant to the smallest value that clears the observed maximum with margin (expected ~20), at measured cost.Plan
ShapeSolverrejection message read the constant rather than a hard-coded "(15)", and update the test that pins that message.Detailed implementation plan
Affected Repositories
Branch Survey
Suggested branch:
feature/pointsolver-mcs-headroomStep 1 — measurement (autolens_profiling worktree)
scripts/point_source_image/likelihood_breakdown/solver_config_sweep.py: add_cfg("mcs18"|"mcs20"|"mcs24", "max_containing_size", mcs=…)beside the existingmcs8/mcs10rows (~l.300).max_containing_size()patch (~l.402) already rewritesArrayTriangles.__init__/for_limits_and_scale__defaults__and the module constant, and themax_containing_size_proofgate asserts the traced(mcs, 2)shape. Reuse both; add nothing new.--configsselection, or a--mcs-onlypreset, so the run covers control plus mcs15/18/20/24 and skips the full 26-config grid.uncapped_containing_countsto report, per MCS,draws_exceeding_cap(already present, ~l.1493) for every refinement step, not only step 0.euclid-ral-compute-10-4. Use the phase-4b submit pattern: branch-clone provenance asserts, and WALL-BASIScell: point_source_image/solver_config_sweep/simplewith ameasured-wallsource.check_submits.py --checklocally first.Step 2 — decision checkpoint (you)
Step 3 — PyAutoArray (
array/PyAutoArray)autoarray/structures/triangles/array.py:13MAX_CONTAINING_SIZE = N. The defaults at l.65 and l.110 bind the constant at import, so a source change propagates.CoordinateArrayTriangles.with_vertices→ArrayTriangles(...)inherits it; it uses the default.autoarray/structures/triangles/.test_autoarray/structures/triangles/test_coordinate_jax.py:(N,).test_np.py:25passesmax_containing_size=5explicitly and is unaffected._STEP0_CONTAINMENTroutes pad toself.max_containing_size, and the 4b fuzz/HLO tests stay green. The HLO guard checks the triangle-array size, not MCS.Step 4 — PyAutoLens (
lens/PyAutoLens)autolens/point/solver/shape_solver.py:585,600: turn the "(15)" docstring and message into text built fromaa...array.MAX_CONTAINING_SIZE. It stays a module-level plain string, so the "same message whether or not JAX is installed" property holds.test_autolens/point/triangles/test_shape_solver.py:578,601,608: update the pinned message to use the constant.point_solver.py:15,61,263andimplicit_diff.py:35,159name the constant without a number, so no change.Step 5 — pins and downstream
7.743201200876812: padded rows areinfsentinels. Check whether the log L is bit-identical at N. If not, trace the reason (a log-sum-exp over padded pairs) and re-pinFIDUCIAL_SOLVED_LOG_L_BY_BACKENDdeliberately, with an explanation.scripts/point_source/jax_likelihood/*(the exact −83.38049778 asserts) andjax_grad/gradient.py: run them against the branch. Any changed pin goes into a workspace_test PR with its reason. Positions shape(MCS, 2): grep consumers.Step 6 — A100 + ship
/ship_libraryfor PyAutoArray and PyAutoLens (Heart gate), then/ship_workspacefor the autolens_profiling data plus the ledger "Phase 4c" section inresults/notes/point_source_cpu_campaign.md. Add workspace_test only if a pin changed.Explicitly not in scope
nopadremoval (campaign remainder).Parallel claims (human-approved 2026-09-27)
interferometer-sparse-cache(PR perf(interferometer): cache curvature_matrix / data_vector on sparse and mapping inversions (#581) #582 awaiting merge). It touches onlyautoarray/inversion/inversion/interferometer*, so it is disjoint fromstructures/triangles/.workspace-config-cleanup(Lens#751 merged, awaiting release). This task touches onlyautolens/point/solver/shape_solver.pyand its test, so it is disjoint.point-source-source-plane-p2c. That task touchesscripts/point_source_source/…and the source-plane ledger; this one touchesscripts/point_source_image/…, the new hpc submits,results/breakdown/point_source_image/and the CPU ledger. Disjoint.Verification
all_gates_pass, the per-draw explanations of every image-set change, and the uncapped max per step.check_submits.py --checkandbuild_readme.py --checkclean.Original Prompt
Click to expand starting prompt
Point-source CPU speed-up phase 4c — raise
MAX_CONTAINING_SIZEheadroom (15 → ~20, measured)Type: feature
Target: autoarray
Repos:
Themes:
Difficulty: small
Autonomy: supervised
Priority: normal
Status: formalised
Consequence: judge
Review-minutes: 15
Unattended: ready
Epic: point-source-cpu-speed
Filed: 2026-09-26
Parent: complete/2026/09/point-source-cpu-p4.md (issue autolens_profiling#314)
Goal
Raise PyAutoArray's
MAX_CONTAINING_SIZEfrom 15 to about 20. Measure 18, 20 and 24 and pickthe smallest that clears the observed maximum with margin. The cap is the static per-step capacity
of containing triangles in the JAX
PointSolver, and it silently truncates when exceeded. Humandecision (2026-09-26): accepted as a phase-4a follow-up.
Evidence (phase 4a sweep, RAL job 356367, Xeon 8490H, fp64)
Source:
lens/autolens_profiling/results/breakdown/point_source_image/solver_config_sweep_hpc_ral_cpu_fp64.json,key
uncapped_containing_counts, counted in NumPy with no cap on 200 prior draws. Ledger: campaignnote, "Phase 4a".
12.
candidates. That is luck, not a guarantee.
workspace grid (see
draft/feature/autolens_workspace/pointsolver_grid_extent_per_package.md)pushes counts up.
1.11× and 1.06× speed.
Method
lens/autolens_profiling/scripts/point_source_image/likelihood_breakdown/solver_config_sweep.py.Add MCS 18 / 20 / 24 routes (the
max_containing_sizeblock already exists).cap sets the refinement grid size (720 rows per step at MCS 15: 240 kept triangles × 3) and the
f64[60]neighbourhoodsorts, so expect a modest cost, and measure it.
reference, except where the old cap was truncating (draw 12 and any others the uncapped count
flags). Those draws must be explained one by one.
7.743201200876812must stay bit-identical, or, if the padded shape changes it,explain why and re-pin deliberately. Check autolens_workspace_test
point_source/jax_likelihood/*and
jax_grad/gradient.pypins, and positions shape(MCS, 2).jax.debug-gated count of howoften the uncapped containing set exceeds the cap, so truncation is never silent again. It must not
cost anything on the default JAX path.
Traps
__defaults__binding trap.MAX_CONTAINING_SIZEis a module constant, butArrayTriangles.__init__andfor_limits_and_scalebind it as a default argument at import.Patching the module constant alone does nothing. The 4a cell had to patch those functions'
__defaults__too, and restore them by value. In the library change, make sure every defaultreads the new value (or switch to a
Nonesentinel resolved at call time), and grep PyAutoLens forany copy of the constant.
interferometer-transform-real-scatter. Check atstart_dev.Coordinate with phase 4b (shipped 2026-09-27, record
complete/2026/09/pointsolver-step0-gather.md, PyAutoArray#580), which also touches thetriangles code, and prefer landing 4b first.