Skip to content

Commit 50ee8db

Browse files
Jammy2211claude
authored andcommitted
fix: elliptical MGE potential via deflection line integral
Replace the circular E1 potential formula (which only worked for spherical profiles) with a Gauss-Legendre quadrature integration of the MGE deflection field along radial lines. This correctly handles elliptical profiles by construction — the potential is the integral of the (verified correct) MGE deflections. Sersic, SersicCore, Exponential, DevVaucouleurs, Chameleon, cNFW elliptical variants all now pass self-consistency checks (errors 1e-4 to 1e-3). Gaussian mass profile remains a known limitation (MGE decomposition of a single narrow Gaussian is inherently lossy). Fixes #459. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent bf6404e commit 50ee8db

2 files changed

Lines changed: 67 additions & 46 deletions

File tree

autogalaxy/profiles/mass/abstract/mge.py

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,22 @@ def potential_func_gaussian(self, grid_radii, sigma, intensity, xp=np):
463463
),
464464
)
465465

466+
_GL_NODES_20, _GL_WEIGHTS_20 = np.polynomial.legendre.leggauss(20)
467+
468+
@staticmethod
469+
def sigma_function(x, y, q, xp=np):
470+
"""Shajib (2019) sigma function for the elliptical Gaussian potential.
471+
472+
Returns (sigma_re, sigma_im) used in the line-integral computation
473+
of the lensing potential.
474+
"""
475+
w1 = MGEDecomposer.wofz(x + 1j * y, xp=xp)
476+
w2 = MGEDecomposer.wofz(q * x + 1j * y / q, xp=xp)
477+
exp_factor = xp.exp(-x * x * (1.0 - q * q) - y * y * (1.0 / (q * q) - 1.0))
478+
sigma_re = xp.imag(w1) - exp_factor * xp.imag(w2)
479+
sigma_im = -xp.real(w1) + exp_factor * xp.real(w2)
480+
return sigma_re, sigma_im
481+
466482
@aa.over_sample
467483
@aa.decorators.to_array
468484
@aa.decorators.transform
@@ -475,44 +491,19 @@ def potential_2d_via_mge_from(
475491
three_D: bool,
476492
ellipticity_convention: str,
477493
func_terms: int = 28,
494+
n_quad: int = 20,
478495
**kwargs,
479496
):
480-
"""Calculate the projected lensing potential at a given set of arc-second
481-
gridded coordinates, via MGE decomposition of the convergence profile.
497+
"""Calculate the lensing potential via MGE decomposition.
482498
483-
The potential for each Gaussian component is computed analytically using
484-
the E1 (exponential integral) formula from Shajib (2019).
485-
"""
486-
eccentric_radii = self.mass_profile.eccentric_radii_grid_from(
487-
grid=grid, xp=xp, **kwargs
488-
)
489-
490-
sigmas_factor = self.sigmas_factor_from(
491-
input_convention=ellipticity_convention,
492-
target_convention="circularised",
493-
xp=xp,
494-
)
499+
Integrates the MGE deflection field along radial lines from the
500+
origin to each grid point using Gauss-Legendre quadrature:
495501
496-
return self._potential_2d_via_mge_from(
497-
grid_radii=eccentric_radii,
498-
xp=xp,
499-
sigma_log_list=sigma_log_list,
500-
three_D=three_D,
501-
sigmas_factor=sigmas_factor,
502-
func_terms=func_terms,
503-
)
502+
psi(x, y) = integral_0^1 alpha(t*x, t*y) . (x, y) dt
504503
505-
def _potential_2d_via_mge_from(
506-
self,
507-
grid_radii,
508-
xp=np,
509-
*,
510-
sigma_log_list,
511-
three_D: bool,
512-
sigmas_factor: float = 1.0,
513-
func_terms: int = 28,
514-
**kwargs,
515-
):
504+
This correctly handles both spherical and elliptical profiles by
505+
reusing the existing (verified correct) MGE deflection machinery.
506+
"""
516507
sigma_log_array = xp.asarray(sigma_log_list, dtype=xp.float64)
517508

518509
amps, sigmas = self.decompose_convergence_via_mge(
@@ -522,20 +513,50 @@ def _potential_2d_via_mge_from(
522513
xp=xp,
523514
)
524515

525-
sigmas = sigmas_factor * xp.asarray(sigmas)[:, None]
526-
amps = xp.asarray(amps)[:, None]
527-
528-
grid_radii = grid_radii[None, ...]
516+
q = xp.asarray(self.axis_ratio(xp), dtype=xp.float64)
529517

530-
potential = xp.sum(
531-
self.potential_func_gaussian(
532-
grid_radii=aa.ArrayIrregular(grid_radii),
533-
sigma=sigmas,
534-
intensity=amps,
535-
xp=xp,
536-
),
537-
axis=0,
518+
sigmas_factor = self.sigmas_factor_from(
519+
input_convention=ellipticity_convention,
520+
target_convention="minor",
521+
xp=xp,
538522
)
523+
sigmas_scaled = sigmas_factor * sigma_log_array
524+
525+
y = xp.asarray(grid.array[:, 0], dtype=xp.float64)
526+
x = xp.asarray(grid.array[:, 1], dtype=xp.float64)
527+
n_pts = x.shape[0]
528+
529+
gl_nodes = xp.asarray(self._GL_NODES_20[:n_quad], dtype=xp.float64)
530+
gl_weights = xp.asarray(self._GL_WEIGHTS_20[:n_quad], dtype=xp.float64)
531+
532+
t_vals = 0.5 * (1.0 + gl_nodes)
533+
534+
potential = xp.zeros(n_pts, dtype=xp.float64)
535+
536+
for k in range(n_quad):
537+
t = t_vals[k]
538+
scaled_grid = aa.Grid2DIrregular(
539+
values=xp.stack([t * y, t * x], axis=-1)
540+
)
541+
542+
defl_at_t = xp.zeros((n_pts, 2), dtype=xp.float64)
543+
for j in range(len(amps)):
544+
sigma_j = sigmas_scaled[j]
545+
amp_j = amps[j]
546+
factor = amp_j * sigma_j * xp.sqrt(2.0 * xp.pi / (1.0 - q * q))
547+
548+
zeta_j = self.zeta_from(
549+
grid=scaled_grid,
550+
sigma_log_list=xp.array([sigma_j]),
551+
xp=xp,
552+
)[0]
553+
554+
defl_at_t[:, 0] += -factor * xp.imag(zeta_j)
555+
defl_at_t[:, 1] += factor * xp.real(zeta_j)
556+
557+
dot = defl_at_t[:, 0] * y + defl_at_t[:, 1] * x
558+
potential += 0.5 * gl_weights[k] * dot
559+
539560
return potential
540561

541562
def sigmas_factor_from(self, input_convention: str, target_convention: str, xp=np):

autogalaxy/profiles/mass/stellar/gaussian.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
127127

128128
radii_min = self.sigma / 100.0
129129
radii_max = self.sigma * 20.0
130-
sigmas = xp.exp(xp.linspace(xp.log(radii_min), xp.log(radii_max), 30))
130+
sigmas = xp.exp(xp.linspace(xp.log(radii_min), xp.log(radii_max), 100))
131131
mge_decomp = MGEDecomposer(mass_profile=self)
132132
return mge_decomp.potential_2d_via_mge_from(
133133
grid=grid, xp=xp, sigma_log_list=sigmas,

0 commit comments

Comments
 (0)