Skip to content

fix: analytic Chameleon potential + jit-safe Chameleon profiles - #683

Merged
Jammy2211 merged 2 commits into
mainfrom
feature/chameleon-analytic-potential
Oct 30, 2026
Merged

Jammy2211 merged 2 commits into
mainfrom
feature/chameleon-analytic-potential

Conversation

@Jammy2211

@Jammy2211 Jammy2211 commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

mp.Chameleon.potential_2d_from did not match its own deflections. For Chameleon(ell_comps=(0.2,-0.1), intensity=1.0), ∇ψ came out ≈1.57 × α (≈ 1/q), an error of about 52% of max|α|. The deflections were already correct (div α = 2κ everywhere). The cause was the MGE potential: it passed ellipticity_convention="circularised" (copied from Sersic), but Chameleon's convergence uses the major-axis elliptical radius. Switching the convention alone still left ~0.35% of max|α| (8–11% at r≈3″), so this PR replaces the MGE route with the exact closed-form potential of the two cored isothermals (Keeton 2001). It reuses the same psi_from, normalisation and rescaled core radii as deflections_2d_via_analytic_from.

Closes #682.

API Changes

None to signatures. Behaviour change: Chameleon.potential_2d_from / ChameleonSph.potential_2d_from now return the exact analytic potential, normalised so ψ(centre) = 0, instead of the MGE approximation. Values change, most for elliptical profiles. It is also faster, since there is no MGE decomposition or quadrature.
See full details below.

Test Plan

  • New tests in test_chameleon.py: the central-finite-difference ∇ψ matches α to < 1e-4 of max|α| (elliptical, spherical, offset-centre large-core with M/L=2); the 5-point ∇²ψ matches 2κ to 1e-3; spot values (ψ = 0 at the centre, plus a regression value).
  • Control: on main, 7 of the 14 Chameleon tests fail; with the fix all 14 pass. The measured residual is ≈2e-8, which is finite-difference noise.
  • Full test_autogalaxy: 1461 passed. PyAutoLens test_autolens: 1015 passed, 1 xfailed (run on the first commit).
  • JAX eager (xp=jnp) matches numpy to 2e-16; new jit-over-ell_comps tests (mass deflections + potential, light image) pass, and fail on the pre-follow-up code.
  • CI.

Also fixed in this PR (follow-ups from review)

  • jax.jit: Chameleon.axis_ratio (mass and light) capped q with a Python if, which raised TracerBoolConversionError once parameters were traced. It now uses xp.where, as Gaussian does. lp.Chameleon.image_2d_from also dropped xp when calling image_2d_via_radii_from, which raised TracerArrayConversionError. Deflections, potential, convergence and the light image now all jit over ell_comps and match numpy to ~3e-16.
  • Docstring vs code (decision taken — flagged for review): the mass Chameleon / ChameleonSph docstrings claimed the second core is s0 + s1 ("increment"). The code uses core_radius_1 directly as the second core, in convergence, deflections and potential alike, with 2s/(1+q) rescaling and a 1/(1+q) prefactor. I changed the docstrings to state the implemented convergence and left the parametrisation alone. Changing the code would shift every existing Chameleon fit, and the light Chameleon docstring already describes core_radius_1 as the second core itself. The corrected formula reproduces the existing pinned convergence test value (0.018605).
Full API Changes (for automation & release notes)

Changed Behaviour

  • autogalaxy.profiles.mass.stellar.chameleon.Chameleon.potential_2d_from — now analytic (difference of two cored-isothermal potentials, Keeton 2001); gradient equals deflections_yx_2d_from; ψ(centre) = 0. Previously an MGE approximation with the wrong ellipticity convention.
  • ChameleonSph.potential_2d_from — inherits the above.

Changed Behaviour (follow-ups)

  • Chameleon.axis_ratio (mass mp.Chameleon, light lp.Chameleon) — now xp.where-capped; same values, now traceable under jax.jit.
  • lp.Chameleon.image_2d_from — passes xp through (JAX path no longer falls back to numpy).

Migration

  • None required; any stored Chameleon potential values (e.g. time delays / Fermat potentials) will change.

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

The MGE potential used the circularised ellipticity convention while the
convergence uses the major-axis elliptical radius, so grad(psi) was ~1/q times
the deflections. Replace it with the exact closed-form potential of the two
cored isothermals, built from the same psi_from / normalisation as the
analytic deflections, and pin grad(psi) = alpha and lap(psi) = 2 kappa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…matches code

- Chameleon.axis_ratio (mass + light) capped q with a Python if, which raised
  TracerBoolConversionError under jax.jit; use xp.where (as Gaussian does).
- lp.Chameleon.image_2d_from dropped xp when calling image_2d_via_radii_from.
- Mass Chameleon / ChameleonSph docstrings claimed the second core is s0 + s1;
  the code uses core_radius_1 directly with 2s/(1+q) rescaling and a
  1/(1+q) prefactor. Docstrings now state the implemented convergence.
- Tests: jit over ell_comps matches numpy for deflections, potential and image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jammy2211 Jammy2211 changed the title fix: analytic Chameleon potential consistent with its deflections fix: analytic Chameleon potential + jit-safe Chameleon profiles Oct 30, 2026
@Jammy2211
Jammy2211 merged commit 9b18db7 into main Oct 30, 2026
4 checks passed
@Jammy2211
Jammy2211 deleted the feature/chameleon-analytic-potential branch October 30, 2026 10:15
@pyautolabs-bot pyautolabs-bot Bot removed the pending-release PR queued for the next release build label Nov 1, 2026
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.

fix: Chameleon potential_2d_from inconsistent with its deflections

1 participant