@@ -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 ):
0 commit comments