Declare the ell_comps saturation constraint on EllProfile - #572
Merged
Conversation
`validate_ell_comps` already owns this geometry, but it signals by raising, which works only for concrete scalars — under a JAX trace the condition is a tracer and a `raise` is impossible, so it returns early (validate.py:153-154). Gradient searches therefore see nothing when a lane walks past the clamp into the region where the axis ratio saturates and the radial gradient dies. `EllProfile.__model_constraint__` states the same geometry as a traced, non-negative distance beyond the clamp, which PyAutoFit consumes on the traced path to count trapped multi-start lanes (PyAutoFit #1475). It is declared once on `EllProfile`, the single base every elliptical light and mass profile inherits and the one site where `ell_comps` is assigned, so no profile opts in individually. Spherical profiles subclass their elliptical counterpart and so inherit it too, with `ell_comps` pinned at (0, 0) it is always satisfied. The threshold is deliberately the clamp's 0.999, not the guard's 1.0. They answer different questions and the annulus between them is reachable: at magnitude 0.9995 the radial derivative is already exactly zero while validate_ell_comps still calls the point valid. Also introduces ELL_COMPS_MAGNITUDE_CLAMP so the clamp has one definition. It was a bare literal at three sites — convert.py's JAX and NumPy branches and the Sersic Cartesian eccentric-radius path — with the guard's separate 1.0 in a fourth file and nothing relating them. Value unchanged at every site. No behaviour change: the guard still rejects magnitude >= 1.0 exactly as before, and the constraint is read only by callers that ask for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GAFoogitLceTsgA7bfB4k
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
validate_ell_compsalready owns theell_compsgeometry, but it signals by raising, which works only for concrete scalars. Under a JAX trace the condition is a tracer and araiseis impossible, so it returns early (validate.py:153-154) — that escape hatch is load-bearing, or every jitted likelihood would crash instead of sampling.The consequence is that gradient searches see nothing when a lane walks past the clamp into the region where the axis ratio saturates and the radial gradient dies. Such a lane is finite, differentiable, and can never come back.
EllProfile.__model_constraint__states the same geometry as a traced, non-negative distance beyond the clamp. PyAutoFit reads it on the traced path to count trapped multi-start lanes (PyAutoLabs/PyAutoFit#1475).Placement
Declared once on
EllProfile— the single base every elliptical light and mass profile inherits, and the one site whereell_compsis assigned. No profile opts in individually.Spherical profiles subclass their elliptical counterpart (
IsothermalSph→Isothermal→ … →EllProfile), so they inherit it too. Withell_compspinned at(0, 0)it is always satisfied — correct, if a few wasted ops. Pinned by test rather than left implicit.The threshold is the clamp's, not the guard's
Deliberately
0.999, not1.0. They answer different questions — the clamp is where the gradient dies, the guard is where the geometry stops meaning anything — and the annulus between them is reachable:validate_ell_compsKeying the constraint to the guard's threshold would miss that band entirely.
One definition for the clamp
Adds
ELL_COMPS_MAGNITUDE_CLAMP. The clamp was a bare0.999literal at three sites —convert.py's JAX and NumPy branches, and the Sersic Cartesian eccentric-radius path from #571 — with the guard's separate1.0in a fourth file and nothing relating them. That is exactly the drift that produced the reachable annulus above; the constant states the relationship in one place.Value unchanged at every site. This is a literal-to-constant swap, not a numerical change.
No behaviour change
The guard still rejects magnitude >= 1.0 exactly as before. The constraint is read only by callers that ask for it, and nothing in this repo does.
Verification
ag.mp.Isothermal(no stand-in class), driven byaf.MultiStartProdigy:The constraint is discovered through inheritance alone — nothing in that model declares anything.
Merge order
PyAutoFit#1475 defines the protocol and should merge first. This declaration is inert without it —
__model_constraint__is a plain method that nothing calls until PyAutoFit looks for it — so there is no hard import dependency and this cannot break on an older PyAutoFit.Generated by Claude Code