Skip to content

feat: declare the ell_comps disk on EllProfile (PyAutoFit#1537) - #589

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/ell-comps-disk-constraint
Aug 28, 2026
Merged

feat: declare the ell_comps disk on EllProfile (PyAutoFit#1537)#589
Jammy2211 merged 1 commit into
mainfrom
feature/ell-comps-disk-constraint

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Depends on PyAutoLabs/PyAutoFit#1538 (feat: project ell_comps onto its disk with an opt-in joint clipper). This PR declares geometry that PyAutoFit#1538
introduces the machinery to read. Merge PyAutoFit#1538 first — without it
test_model_constraint.py's resolution tests reference
model.ball_constraint_index_pairs() and af.ClipperPriorBoxJoint, which do
not exist on autofit main. Task: PyAutoFit#1537.

EllProfile.__model_constraint__ already measures how far outside the
ellipticity clamp a profile's ell_comps sit, but a measure cannot say how to fix
one. This declares the structure — the disk itself — which is what a search
needs to put a lane back inside it:

__model_ball_constraints__ = ((("ell_comps",), convert.ELL_COMPS_MAGNITUDE_CLAMP),)

The radius is the clamp (0.999), deliberately not 1 - margin. Between 0.999
and 1.0 the conversion to an axis ratio saturates, so the likelihood is flat in the
radial direction: a lane projected into that annulus would be moved from a region
the model rejects into one the optimizer cannot leave. Projecting onto the clamp
puts it exactly at the edge of the region where the radial gradient is alive again.

Declared once at EllProfile, which is the single assignment site for ell_comps,
so it reaches every elliptical light and mass profile (28 classes across
ag.lp / ag.mp / ag.lmp, asserted by a namespace sweep so a profile added later
is covered without editing a list). The spherical subclasses inherit the
declaration but pin ell_comps to an instance, so PyAutoFit resolves no free pair
and projects nothing.

validate_ell_comps is untouched, on purpose. The prompt's hard constraint:
making the guard fire on the traced path would turn a 20%-of-lanes condition into a
20%-of-lanes crash in the middle of multi-hour GPU fits, and would kill a
MultiStart search on exactly the lanes it is supposed to clip and move on from. A
dedicated test class pins its standalone-construction behaviour — still rejects
|e| > 1 and the corner (0.8, 0.8), still accepts the saturating annulus, still
returns early for a non-concrete magnitude.

Also: widens AnalysisDataset.save_results' catch from bare AttributeError
to (AttributeError, af.exc.SamplesException, af.exc.FitException) with a logged
warning, mirroring PyAutoLens#713 line-for-line. Building the galaxies materializes
the maximum log likelihood sample as a model instance, which the model may reject;
writing an optional output file must never kill a completed fit before
paths.completed() is called (PyAutoFit#1535).

API Changes

Additive. EllProfile (and therefore every elliptical profile) gains the class
attribute __model_ball_constraints__. It is inert unless a search is configured
with af.ClipperPriorBoxJoint — no profile construction, evaluation, model
composition or identifier changes. AnalysisDataset.save_results now logs and
continues on two further exception types instead of propagating them.

See full details below.

Test Plan

  • test_autogalaxy/ full suite: 1144 passed (45s)
  • test_autogalaxy/profiles + test_autogalaxy/analysis: 768 passed
  • test_autolens/ against this branch: 553 passed (the declaration reaches
    lens models unchanged)
  • New coverage in test_autogalaxy/profiles/test_model_constraint.py: the
    namespace sweep (with a non-vacuity guard), the radius-is-the-clamp
    assertion, PyAutoFit resolving the declaration to ell_comps_0/ell_comps_1
    indices on a real lens model, the spherical case contributing no pair, an
    end-to-end projection of a lane at |e| = 1.4, and the four
    validate_ell_comps-is-unchanged assertions.
  • New coverage in test_analysis_dataset.py: save_results swallows and logs
    all three exception types and never writes galaxies.json (parametrised).
  • Unit tests are numpy-only — no import jax.

Measured, on one cell. With PyAutoFit#1538's clipper and an Isothermal lens
model, 200,000 uniform draws from the ell_comps prior box:

  • outside the disk before projection: 21.57% (analytic 1 - pi/4 = 21.46%)
  • outside the disk after projection: 0.00%

ell_comps = (0.9, 0.9) (|e| = 1.2728) projects to |e| = 0.999000 with the
angle preserved and both members masked; the default ClipperPriorBox leaves it
where it is.

Heart gate. pyauto-heart readiness is RED with the single red reason
release validation FAILED (stage integrate) — a known, human-authorised
override for this task, unrelated to this branch. Shipped to PR-open under that
override only; not merged.

Full API Changes (for automation & release notes)

Added

  • EllProfile.__model_ball_constraints__ = ((("ell_comps",), 0.999),) — inherited
    by every elliptical light/mass profile. Read by
    af.ClipperPriorBoxJoint (PyAutoFit#1538); inert otherwise.

Changed Behaviour

  • AnalysisDataset.save_results — the galaxies.json write now catches
    (AttributeError, af.exc.SamplesException, af.exc.FitException) and logs a
    warning, where it previously caught bare AttributeError and silently passed.
    A rejected maximum-log-likelihood instance no longer propagates out of
    save_results and costs the run its .completed marker (PyAutoFit#1535,
    mirrors PyAutoLens#713).

Unchanged (asserted)

  • validate_ell_comps — standalone-construction behaviour is deliberately
    identical, and pinned by test.

Migration

  • None. To use the projection:
    search = af.MultiStartAdam(clipper=af.ClipperPriorBoxJoint()) once
    PyAutoFit#1538 is merged.

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

https://claude.ai/code/session_011joies4k5TdRqezPUK8YET

…t onto it (PyAutoFit#1537)

`EllProfile.__model_constraint__` already MEASURES how far outside the ellipticity
clamp a profile sits, but a measure cannot say how to fix one. This declares the
structure PyAutoFit's new `ClipperPriorBoxJoint` projects onto:

    __model_ball_constraints__ = ((("ell_comps",), convert.ELL_COMPS_MAGNITUDE_CLAMP),)

The radius is the CLAMP (0.999), deliberately not `1 - margin`. Between 0.999 and
1.0 the conversion to an axis ratio saturates, so the likelihood is flat radially
and a lane projected into that annulus has nothing to climb back out on -- it
would be moved from a region the model rejects into one the optimizer cannot
leave. Projecting onto the clamp puts it exactly where the radial gradient is
alive again.

Declared once at `EllProfile`, so it reaches every elliptical light and mass
profile; the spherical subclasses inherit it but pin `ell_comps` to an instance,
so PyAutoFit resolves no pair and projects nothing.

`validate_ell_comps` is deliberately unchanged. Making the guard fire on the
traced path would turn a 20%-of-lanes condition into a 20%-of-lanes crash in the
middle of a multi-hour fit -- the fix belongs in the search's constraint
handling, not in a validator that converts a survivable state into an exception.

Also widens `AnalysisDataset.save_results`' catch to
`(AttributeError, af.exc.SamplesException, af.exc.FitException)`, mirroring
PyAutoLens#713: building the galaxies materializes the maximum log likelihood
sample as a model instance, which the model may reject, and writing an optional
output file must never kill a completed fit before `paths.completed()`
(PyAutoFit#1535).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011joies4k5TdRqezPUK8YET
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Aug 28, 2026
@Jammy2211
Jammy2211 merged commit 0fbe863 into main Aug 28, 2026
4 checks passed
@Jammy2211
Jammy2211 deleted the feature/ell-comps-disk-constraint branch August 28, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending-release PR queued for the next release build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant