Skip to content

feat: joint ell_comps disk constraint the MultiStart clipper projects onto (ClipperPriorBoxJoint) #1537

Description

@Jammy2211

Overview

ell_comps priors are an independent box per component, so 21.5 % of the prior area is non-physical; JAX tracer-mode validation is a no-op there, so gradient lanes settle at |e| ≥ 1 (20.1 % of MultiStart lane best points campaign-wide, 0 of 246 hit lanes; positions-on doubles it — autolens_profiling#182). PyAutoFit already has a class-declared, traceable constraint channel (autofit/mapper/prior_model/constraint.py) and PyAutoGalaxy's EllProfile already declares the disk constraint — but MultiStartGradient only counts violations. Decision (Option 1): give the declaration projectable structure and make the clipper project onto the disk. Reparameterisation (Option 2) rejected for migration cost.

Plan

  • New __model_ball_constraints__ class hook (sibling of __model_constraint__, duck-typed) → [(("ell_comps",), radius)]; EllProfile declares radius = convert.ELL_COMPS_MAGNITUDE_CLAMP (0.999) — NOT 1−margin, which sits in the dead-gradient annulus [0.999, 1).
  • AbstractModel.ball_constraint_index_pairs() → static (i0, i1, radius) via TuplePrior.prior_tuples + priors_ordered_by_id identity map.
  • New ClipperPriorBoxJoint(ClipperPriorBox): box clip first, then jittable radial shrink (xp.where, xp.maximum(r, tiny) — no NaN at r=0, no grad poisoning), clipped_mask True on both pair members so momentum resets. Opt-in subclass ⇒ existing ClipperPriorBox identifiers unchanged; pin __identifier_fields__=("margin","strict_epsilon") on the base (no-op today).
  • Guard/warn in AbstractBFGS._bounds_from (a ball is not a scipy Bounds).
  • PyAutoGalaxy: widen analysis/dataset.py save_results catch to SamplesException/FitException (mirror PyAutoLens#713) — separate commit.
  • Quantify on imaging/mge/hst CPU (two sweep.py arms, n_starts 16) — report fraction ≥1.0 and ≥0.999.
  • validate_ell_comps untouched.
Detailed implementation plan

Work Classification

Library (PyAutoFit primary, PyAutoGalaxy).

Affected Repositories

  • PyAutoFit (primary)
  • PyAutoGalaxy

Branch Survey

Repository Current Branch Dirty?
./PyAutoFit main clean
./PyAutoGalaxy main clean

Suggested branch: feature/ell-comps-disk-constraint
Worktree root: ~/Code/PyAutoLabs-wt/ell-comps-disk-constraint/

Implementation Steps

  1. autofit/mapper/prior_model/constraint.pyMODEL_BALL_CONSTRAINT = "__model_ball_constraints__", declares_ball_constraints(cls), ball_constraints_for(cls).
  2. autofit/mapper/prior_model/abstract.py (beside constrained_model_tuples ~:836) — ball_constraint_index_pairs(self), cached.
  3. autofit/non_linear/clipper.pyClipperPriorBoxJoint; docstring "Two consumers" extended; base __identifier_fields__ pinned.
  4. autofit/__init__.py (~:80-82) — export.
  5. autofit/non_linear/search/mle/bfgs/search.py:117-147 — guard/warn on a joint clipper.
  6. .../multi_start_gradient/search.py:1315-1319 — optional joint-clip count in the log line.
  7. autogalaxy/profiles/geometry_profiles.py (~:240) — __model_ball_constraints__ = ((("ell_comps",), convert.ELL_COMPS_MAGNITUDE_CLAMP),).
  8. autogalaxy/analysis/analysis/dataset.py (~:178) — exception widening + test.
  9. Fix stale comment autolens_profiling/scripts/misc/searches/clipper_campaign.py:23-24 (clipper DOES enter the identifier) — follow-up PR, not this one.

Tests

  • test_autofit/non_linear/test_clipper.py TestJointBall: (−1,−1)→radius r angle-preserved; interior bit-identical; (0,0) not NaN; mask on both members; batched input; xp=np only (+ optional importorskip jax jit/grad-finite).
  • multi_start_gradient: lane seeded at |e|=1.4 is ≤ radius after one step.
  • test_autogalaxy/profiles/test_model_constraint.py: every EllProfile subclass declares the ball; validate_ell_comps standalone unchanged.
  • test_autogalaxy/analysis/analysis/test_analysis_dataset.py: mirrored save_results test.

Key Files

  • PyAutoFit/autofit/non_linear/clipper.py, autofit/mapper/prior_model/{constraint,abstract}.py, autofit/non_linear/search/mle/bfgs/search.py
  • PyAutoGalaxy/autogalaxy/profiles/geometry_profiles.py, autogalaxy/convert.py (CLAMP), autogalaxy/profiles/validate.py (untouched), autogalaxy/analysis/analysis/dataset.py

Original Prompt

Click to expand starting prompt

Claude Development Prompt: Joint ell_comps Disk Constraint

Type: feature
Target: PyAutoGalaxy
Difficulty: medium
Autonomy: supervised
Priority: normal
Status: formalised
Issued: 2026-08-27

Goal

Give the elliptical-components parametrisation a constraint the searches can
actually honour, so a gradient MAP optimizer cannot spend its budget — or
finish — outside the physical region.

ell_comps = (e1, e2) is physical only inside the unit disk |e| < 1. Every
profile's default priors are two INDEPENDENT box priors on e1 and e2
(typically [-1, 1] each), which is a SQUARE. The disk covers pi/4 of that
square, so 21.5% of the prior box is non-physical by area, and nothing in
the search stops a lane from sitting there.

What was measured (autolens_profiling, 2026-08-27 harvest, issue #182)

  • The existing validation is a no-op where it matters. validate_ell_comps
    fires on standalone profile construction, but not on the tracer-mode path a
    MultiStart lane actually evaluates — so a lane at |e| >= 1 is never told.
  • 20.1% of MultiStart lane best points end at |e| >= 1 (1,252 of 6,240
    lane best points across the recorded campaign).
  • 0 of 246 lanes that HIT the target basin end there. The two populations
    are cleanly separated: ending outside the disk is a property of failed lanes,
    never of successful ones. That is what makes this a searchability defect
    rather than a cosmetic one — the wasted 20% is wasted budget, not a wasted
    answer.
  • Positions-on roughly DOUBLES the out-of-disk fraction (17% -> 29% of best
    points on the Phase-4 diagnostic arms). The positions penalty pushes lanes
    into the corners the box permits and the geometry does not.

Full write-ups: autolens_profiling/results/notes/inference/ phase_03_prodigy_reliability/RESULTS.md and phase_04_positions/RESULTS.md.

Options to weigh (this prompt does not pick one)

  1. A joint disk assertion the clipper honours. Express e1^2 + e2^2 < 1 as
    a constraint the search's prior-box clipper can project onto, rather than as
    a post-hoc validation that raises. The clipper already insets a box; a
    ball projection is the same operation in a different norm. Cheapest change,
    keeps the existing parameter names and every recorded identifier stable, and
    is the only option that helps a search already in flight.
  2. Reparameterise. Sample in a coordinate whose whole domain is physical —
    e.g. (magnitude, angle) with a bounded magnitude, or a squashing map onto
    the disk. Removes the problem by construction, but changes the model's
    parameter set, and therefore every target_id, prior config and recorded
    posterior that names ell_comps_0/ell_comps_1. It needs a migration story
    before it needs an implementation.

Hard constraint

Never make validate_ell_comps fire on tracers. Raising from the
tracer-mode path turns a 20%-of-lanes condition into a 20%-of-lanes crash in
the middle of multi-hour GPU fits, and would make a MultiStart search die on
exactly the lanes it is supposed to clip and move on from. The fix belongs in
the search's constraint handling (option 1) or in the parametrisation (option
2) — not in a validator that converts a survivable state into an exception.

Requirements

  • Decide between the two options above with the evidence in the write-ups
    named, and record the decision before implementing.
  • Whatever lands, add a test that a lane placed at |e| >= 1 is handled by the
    chosen mechanism (projected, or unrepresentable) rather than merely detected.
  • Quantify the effect on the measured 20.1% figure on at least one cell before
    claiming the defect is closed.
  • Do not change validate_ell_comps's standalone-construction behaviour.

Out of scope

  • The positions-penalty scaling question (Gate B pt 2, autolens_profiling
    issue feature/pickle #182) — the doubling is evidence FOR this task, not a task itself.
  • Re-running any autolens_profiling campaign.

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