from typing import Tuple
import numpy as np
import autoarray as aa
from autogalaxy.profiles.mass.abstract.abstract import MassProfile
# Within this profile family, PIEMass, dPIEMass, and dPIEMassSph are directly ported from Lenstool's C code, and have been thoroughly annotated and adapted for PyAutoLens.
# The dPIEPotential and dPIEPotentialSph profiles are modified from the original `dPIEPotential` and `dPIEPotentialSph`, which were implemented to PyAutoLens by Jackson O'Donnell.
def _ci05(x, y, eps, rcore, xp=np):
"""
Returns the first derivatives of the lens potential as complex number I'* = (∂ψ/∂x + i ∂ψ/∂y) / E0 for PIEMass at any positions (x,y),
see Kassiola & Kovner(1993) Eq. 4.1.2, which is the integral of Eq. 2.3.8.
Note here b0(or called E0) is out of the `_ci05`.
Parameters
----------
eps
The ellipticity of the corresponding profiles.
rcore
The inner core radius.
Returns
-------
complex
The value of the I'* term.
"""
sqe = xp.sqrt(eps)
axis_ratio = (1.0 - eps) / (1.0 + eps)
cxro = (1.0 + eps) * (1.0 + eps)
cyro = (1.0 - eps) * (1.0 - eps)
rem2 = x * x / cxro + y * y / cyro
##### I'* = zres = zci * ln(zis) = zci * ln(znum / zden), see Eq. 4.1.2 #####
# Define intermediate complex variables
zci = xp.array(0.0 + 1j * (-0.5 * (1.0 - eps * eps) / sqe), dtype=xp.complex128)
znum = xp.complex128(
axis_ratio * x
+ 1j * (2.0 * sqe * xp.sqrt(rcore * rcore + rem2) - y / axis_ratio)
)
zden = xp.complex128(x + 1j * (2.0 * rcore * sqe - y))
# zis = znum / zden = (a+bi)/(c+di) = [(ac+bd)+(bc-ad i)] / (c^2+d^2)
norm = zden.real * zden.real + zden.imag * zden.imag # |zden|^2
zis_re = (znum.real * zden.real + znum.imag * zden.imag) / norm
zis_im = (znum.imag * zden.real - znum.real * zden.imag) / norm
zis = xp.complex128(zis_re + 1j * zis_im)
# ln(zis) = ln(|zis|) + i*Arg(zis)
zis_mag = xp.abs(zis)
zis_re = xp.log(zis_mag)
zis_im = xp.angle(zis)
zis = xp.complex128(zis_re + 1j * zis_im)
# I'* = zres = zci * ln(zis)
zres = zci * zis
return zres
def _ci05f(x, y, eps, rcore, rcut, xp=np):
"""
Returns the first derivatives of the lens potential as complex number I'* = (∂ψ/∂x + i ∂ψ/∂y) / (b0 * ra / (rs - ra)) for dPIEMass at any positions (x,y),
which is the integral of Eq. 2.3.8 in Kassiola & Kovner(1993).
Note here (b0 * ra / (rs - ra)) is out of the `_ci05f`. The only difference of integral of Eq. 2.3.8 between dPIEMass and PIEMass is the \\kappa:
\\kappa(r_{em})_{dPIEMass} = rs / (rs - ra) * (\\kappa_{PIEMass,ra} - \\kappa_{PIEMass,rs}).
I*_{dPIEMass} = ra / (rs - ra) * (I*_{PIEMass}(ra) - I*_{PIEMass}(ra))
Parameters
----------
eps
The ellipticity of the corresponding profiles.
rcore
The inner core radius.
rcut
The outer cut radius.
Returns
-------
complex
The value of the I'* term.
"""
sqe = xp.sqrt(eps)
axis_ratio = (1.0 - eps) / (1.0 + eps)
cxro = (1.0 + eps) * (1.0 + eps)
cyro = (1.0 - eps) * (1.0 - eps)
rem2 = x * x / cxro + y * y / cyro
##### I'* = zres_rc - zres_rcut = zci * ln(zis_rc / zis_rcut) = zci * ln((znum_rc / zden_rc) / (znum_rcut / zden_rcut)) #####
# Define intermediate complex variables
zci = xp.array(0.0 + 1j * (-0.5 * (1.0 - eps * eps) / sqe), dtype=xp.complex128)
znum_rc = xp.complex128(
axis_ratio * x
+ 1j * (2.0 * sqe * xp.sqrt(rcore * rcore + rem2) - y / axis_ratio)
) # a + bi
zden_rc = xp.complex128(x + 1j * (2.0 * rcore * sqe - y)) # c + di
znum_rcut = xp.complex128(
axis_ratio * x + 1j * (2.0 * sqe * xp.sqrt(rcut * rcut + rem2) - y / axis_ratio)
) # a + ei
zden_rcut = xp.complex128(x + 1j * (2.0 * rcut * sqe - y)) # c + fi
# zis_rc = znum_rc / zden_rc = (a+bi)/(c+di)
# zis_rcut = znum_rcut / zden_rcut = (a+ei)/(c+fi)
# zis_tot = zis_rc / zis_rcut = (znum_rc / zden_rc) / (znum_rcut / zden_rcut)
# = [(ac - bf) + (af + bc)i] / [(ac - de) + (ad + ce)i]
# = (aa + bb*i) / (cc + dd*i)
# = (aa + bb*i) * (cc -dd*i) / (cc^2 + dd^2)
# = [(aa*cc + bb*dd) / (cc^ + dd^2)] + [(bb*cc - aa*dd) / (cc^2 + dd^2)]*i
# = aaa + bbb*i
aa = znum_rc.real * zden_rc.real - znum_rc.imag * zden_rcut.imag # ac - bf
bb = znum_rc.real * zden_rcut.imag + znum_rc.imag * zden_rc.real # af + bc
cc = znum_rc.real * zden_rc.real - zden_rc.imag * znum_rcut.imag # ac - de
dd = znum_rc.real * zden_rc.imag + zden_rc.real * znum_rcut.imag # ad + ce
norm = cc * cc + dd * dd
aaa = (aa * cc + bb * dd) / norm
bbb = (bb * cc - aa * dd) / norm
zis_tot = xp.complex128(aaa + 1j * bbb)
# ln(zis_tot) = ln(|zis_tot|) + i*Arg(zis_tot)
zis_tot_mag = xp.abs(zis_tot)
zr_re = xp.log(zis_tot_mag)
zr_im = xp.angle(zis_tot)
zr = xp.complex128(zr_re + 1j * zr_im)
# I'* = zci * ln(zis_tot)
zres = zci * zr
return zres
def _mdci05(x, y, eps, rcore, b0, xp=np):
"""
Returns the second derivatives (Hessian matrix) of the lens potential as complex number for PIEMass at any positions (x,y):
∂²ψ/∂x² = Re(∂I*/∂x), ∂²ψ/∂y² = Im(∂I*/∂y), ∂²ψ/∂x∂y = ∂²ψ/∂y∂x = Im(∂I*/∂x) = Re(∂I*/∂y)
see Kassiola & Kovner(1993) Eq. 4.1.4.
Parameters
----------
eps
The ellipticity of the corresponding profiles.
rcore
The inner core radius.
Returns
-------
complex
The value of the I'* term.
"""
# Calculate intermediate values
# I*(x,y) = b0 * ci * (-i) * (ln{ q * x + (2.0 * sqe * wrem - y * 1/q )*i} - ln{ x + (2.0 * rcore * sqe - y)*i})
# = b0 * ci * (-i) * (ln{ q * x + num1*i} - ln{ x + num2*i})
# = b0 * ci * (-i) * (ln{u(x,y)} - ln{v(x,y)})
sqe = xp.sqrt(eps)
axis_ratio = (1.0 - eps) / (1.0 + eps)
axis_ratio_inv = 1.0 / axis_ratio
cxro = (1.0 + eps) * (1.0 + eps)
cyro = (1.0 - eps) * (1.0 - eps)
ci = 0.5 * (1.0 - eps * eps) / sqe
wrem = xp.sqrt(rcore * rcore + x * x / cxro + y * y / cyro) # √(w(x,y))
num1 = 2.0 * sqe * wrem - y * axis_ratio_inv
den1 = axis_ratio * axis_ratio * x * x + num1 * num1 # |q * x + num1*i|^2
num2 = 2.0 * rcore * sqe - y
den2 = x * x + num2 * num2 # |x + num2*i|^2
# eg.
# ∂²ψ/∂x² = Re(∂I*/∂x) = b0 * didxre
# ∂I*/∂x = b0 * ci * (-i) * ∂(ln{u(x,y)} - ln{v(x,y)})∂x
# = b0 * ci * (-i) * (1/u * ∂u/∂x - 1/v * ∂v/∂x)
# ∂u/∂x = q + ∂(num1)/∂x * i
# = q + [2.0 * sqe * ∂(wrem)/∂x] * i
# = q + [2.0 * sqe * ∂(√(w(x,y)))/∂x] * i
# = q + [2.0 * sqe * x / cxro / wrem] * i
# 1/u * ∂u/∂x = {q + [2.0 * sqe * x / cxro / wrem] * i} / {q * x + num1*i}
# = {q + [2.0 * sqe * x / cxro / wrem] * i} * {q * x - num1*i} / |q * x + num1*i|^2
# = {q + [2.0 * sqe * x / cxro / wrem] * i} * {q * x - (2.0 * sqe * wrem - y / q)*i} / den1
# = {q^2 * x + 4.0 * sqe^2 * x - y / q * 2.0 * sqe * x / cxro / wrem} / den1 + q * { (2.0 * sqe * x^2 / cxro / wrem) - (2.0 * sqe * wrem - y / q)} / den1 * i
# = {x - 2.0 * sqe * x * y * q / cyro / wrem} / den1 + q * { (2.0 * sqe * x^2 / cxro / wrem) - (2.0 * sqe * wrem - y / q)} / den1 * i
# (-i) * (1/u * ∂u/∂x) = (2.0 * sqe * x * y * q / cyro / wrem - x) / den1 * i
# + q * { (2.0 * sqe * x^2 / cxro / wrem) - (2.0 * sqe * wrem - y / q)} / den1
# ∂v/∂x = 1 + ∂(num2)/∂x * i
# = 1
# 1/v * ∂v/∂x = 1 / (x + num2*i)
# = (x - num2*i) / |x + num2*i|^2
# = (x - num2*i) / den2
# -(-i) * (1/v * ∂v/∂x) = (x*i + num2) / den2
# ∂I*/∂x = b0 * ci * {(-i) * (1/u * ∂u/∂x) - (-i) * (1/v * ∂v/∂x)}
# Compute second derivatives
didxre = ci * (
axis_ratio
* (2.0 * sqe * x * x / cxro / wrem - 2.0 * sqe * wrem + y * axis_ratio_inv)
/ den1
+ num2 / den2
)
didyre = ci * ((2.0 * sqe * x * y * axis_ratio / cyro / wrem - x) / den1 + x / den2)
didyim = ci * (
(
2.0 * sqe * wrem * axis_ratio_inv
- y * axis_ratio_inv * axis_ratio_inv
- 4.0 * eps * y / cyro
+ 2.0 * sqe * y * y / cyro / wrem * axis_ratio_inv
)
/ den1
- num2 / den2
)
# Construct Hessian matrix components
a = b0 * didxre # ∂²ψ/∂x²
b = b0 * didyre # ∂²ψ/∂x∂y
c = b0 * didyre # ∂²ψ/∂y∂x
d = b0 * didyim # ∂²ψ/∂y²
return a, b, c, d
[docs]
class PIEMass(MassProfile):
def __init__(
self,
centre: Tuple[float, float] = (0.0, 0.0),
ell_comps: Tuple[float, float] = (0.0, 0.0),
ra: float = 0.1,
b0: float = 0.1,
):
"""
The Pseudo Isothermal Elliptical Mass Distribution(PIEMass) profiles, based on the formulaiton from
Kassiola & Kovner(1993) https://articles.adsabs.harvard.edu/pdf/1993ApJ...417..450K.
This profile is ported from Lenstool's C code, which has the same formulation.
This proflie describes an elliptic isothermal mass distribution with a finite core:
\\rho \\propto (ra^2 + R^2)^{-1}
The convergence is given by:
\\kappa(r_{em}) = \\kappa_0 * ra / \\sqrt{ ra^2 + r_{em}^2 }
(see Kassiola & Kovner(1993), Eq. 4.1.1)
where r_{em}^2 = x^2 / (1 + \\epsilon)^2 + y^2 / (1 - \\epsilon)^2, (see Kassiola & Kovner(1993), Eq. 2.3.6)
and \\kappa_0 = b_0 / 2 / r_a.
In this implementation:
- `ra` is the core radius in unit of arcseconds.
- `b0` is the lens strength in unit of arcseconds, when ra->0 & q->1, b0 is the Einstein radius.
`b0` is related to the central velocity dispersion \\sigma_0: b_0 = 4\\pi * \\sigma_0^2 / c^2 * (D_{LS} / D_{S}).
`b0` is not in the Intermediate-Axis-Convention for its r_{em}^2 = x^2 / (1 + \\epsilon)^2 + y^2 / (1 - \\epsilon)^2
Parameters
----------
centre
The (y,x) arc-second coordinates of the profile centre.
ra
The inner core radius in arcseconds.
b0
The lens strength in arcseconds.
"""
super().__init__(centre=centre, ell_comps=ell_comps)
self.ra = ra
self.b0 = b0
def _ellip(self, xp=np):
ellip = xp.sqrt(self.ell_comps[0] ** 2 + self.ell_comps[1] ** 2)
MAX_ELLIP = 0.99999
return xp.min(xp.array([ellip, MAX_ELLIP]))
[docs]
@aa.decorators.to_vector_yx
@aa.decorators.transform(rotate_back=True)
def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
"""
Calculate the deflection angles on a grid of (y,x) arc-second coordinates.
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
"""
ellip = self._ellip(xp)
factor = self.b0
zis = _ci05(
x=grid.array[:, 1], y=grid.array[:, 0], eps=ellip, rcore=self.ra, xp=xp
)
# This is in axes aligned to the major/minor axis
deflection_x = zis.real
deflection_y = zis.imag
return xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T)
def _convergence(self, radii, xp=np):
radsq = radii * radii
a = self.ra
return self.b0 / 2 * (1 / xp.sqrt(a**2 + radsq))
[docs]
@aa.decorators.to_array
@aa.decorators.transform
def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
"""
Returns the two-dimensional projected convergence on a grid of (y,x)
arc-second coordinates.
The `grid_2d_to_structure` decorator reshapes the ndarrays the convergence
is outputted on. See *aa.grid_2d_to_structure* for details.
Parameters
----------
grid
The grid of (y,x) arc-second coordinates on which the convergence is computed.
"""
ellip = self._ellip(xp)
grid_radii = xp.sqrt(
grid.array[:, 1] ** 2 / (1 + ellip) ** 2
+ grid.array[:, 0] ** 2 / (1 - ellip) ** 2
)
# Compute the convergence and deflection of a *circular* profile
kappa = self._convergence(grid_radii, xp)
return kappa
[docs]
@aa.decorators.transform
def analytical_hessian_2d_from(self, grid: "aa.type.Grid2DLike", xp=np, **kwargs):
"""
Calculate the hessian matrix on a grid of (y,x) arc-second coordinates.
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
"""
if grid.ndim != 2 or grid.shape[1] != 2:
raise ValueError("Grid must be a 2D array with shape (n, 2)")
ellip = self._ellip()
hessian_xx, hessian_xy, hessian_yx, hessian_yy = _mdci05(
x=grid.array[:, 1],
y=grid.array[:, 0],
eps=ellip,
rcore=self.ra,
b0=self.b0,
xp=xp,
)
return hessian_yy, hessian_xy, hessian_yx, hessian_xx
[docs]
def analytical_magnification_2d_from(
self, grid: "aa.type.Grid2DLike", xp=np, **kwargs
):
hessian_yy, hessian_xy, hessian_yx, hessian_xx = (
self.analytical_hessian_2d_from(grid=grid, xp=np)
)
det_A = (1 - hessian_xx) * (1 - hessian_yy) - hessian_xy * hessian_yx
return aa.Array2D(values=1.0 / det_A, mask=grid.mask)
[docs]
class dPIEMass(MassProfile):
r"""Dual pseudo-isothermal elliptical mass distribution (dPIE, mass parameterisation).
A two-component PIE profile with both a core radius :math:`r_a` and a
truncation radius :math:`r_s`. The three-dimensional density scales as
:math:`\rho \propto r^{-2}` in the transition region
:math:`r_a \leq R \leq r_s` and as :math:`\rho \propto r^{-4}` in the outer
parts, with the full form:
.. math::
\rho \propto \bigl[(r_a^2 + R^2)(r_s^2 + R^2)\bigr]^{-1}
The projected convergence is the difference of two PIE profiles:
.. math::
\kappa(r_{\rm em}) = \frac{b_0}{2} \frac{r_s}{r_s - r_a}
\left(
\frac{1}{\sqrt{r_a^2 + r_{\rm em}^2}}
- \frac{1}{\sqrt{r_s^2 + r_{\rm em}^2}}
\right)
where :math:`r_{\rm em}^2 = x^2/(1+\epsilon)^2 + y^2/(1-\epsilon)^2` is
the pseudo-elliptical radius and :math:`b_0` is the lens strength (equal to
the Einstein radius when :math:`r_a \to 0`, :math:`r_s \to \infty`, and
:math:`q \to 1`). This profile is ported directly from Lenstool's C code.
Parameters
----------
centre : (float, float)
(y, x) arc-second coordinates of the profile centre.
ell_comps : (float, float)
Ellipticity components (e1, e2) of the elliptical coordinate system.
ra : float
Inner core radius in arcseconds.
rs : float
Outer truncation radius in arcseconds.
b0 : float
Lens strength in arcseconds (Einstein radius in the limit
:math:`r_a \to 0`, :math:`r_s \to \infty`, :math:`q \to 1`).
References
----------
Kassiola & Kovner (1993), ApJ, 417, 450.
Eliasdottir et al. (2007), arXiv:0710.5636.
Limousin et al. (2005), A&A, 461, 881.
"""
def __init__(
self,
centre: Tuple[float, float] = (0.0, 0.0),
ell_comps: Tuple[float, float] = (0.0, 0.0),
ra: float = 0.0,
rs: float = 2.0,
b0: float = 0.1,
):
"""
The dual Pseudo Isothermal Elliptical Mass Distribution(dPIEMass) profiles, which is a *two component PIEMass* with both a core radius and a truncation radius,
see Eliasdottir (2007): https://arxiv.org/abs/0710.5636
This profile is ported from Lenstool's C code, which has the same formulation.
This proflie describes an elliptic isothermal mass distribution with a finite core, \\rho r^{-2} while in
the transition region (ra<=R<=rs),
and \\rho r^{-4} in the outer parts:
\\rho \\propto [(ra^2 + R^2) (rs^2 + R^2)]^{-1}
The convergence is given by two PIEMass with core radius ra and rs:
\\kappa(r_{em}) = rs / (rs - ra) * (\\kappa_{PIEMass,ra} - \\kappa_{PIEMass,rs})
= b_0 / 2 * rs / (rs - ra) * ( \\frac{1}{\\sqrt{ ra^2 + r_{em}^2}} - \\frac{1}{\\sqrt{ rs^2 + r_{em}^2}} )
where r_{em}^2 = x^2 / (1 + \\epsilon)^2 + y^2 / (1 - \\epsilon)^2.
Note in Eliasdottir (2007), E0 = 6\\pi * \\sigma_{dPIEPotential}^2 / c^2 * (D_{LS} / D_{S}). Eliasdottir's E0 is not the same as E0 in Kassiola & Kovner(1993) which is also b0.
There is \\frac{\\sigma_{dPIEPotential}^2}{\\sigma_0^2} = \\frac{2}{3} \frac{rs^2}{rs^2-ra^2},
thus E0(Kassiola & Kovner(1993)) = b0 = E0(Eliasdottir (2007)) * (rs^2 - ra^2) / rs^2. So when s->\\infty and a->0, they are equivalent.
In this implementation:
- `ra` is the core radius in unit of arcseconds.
- `rs` is the truncation radius in unit of arcseconds.
- `b0` is the lens strength in unit of arcseconds, when ra->0 & rs->\\infty & q->1, b0 is the Einstein radius.
`b0` is related to the central velocity dispersion \\sigma_0: b_0 = 4\\pi * \\sigma_0^2 / c^2 * (D_{LS} / D_{S})
`b0` is not in the Intermediate-Axis-Convention for its r_{em}^2 = x^2 / (1 + \\epsilon)^2 + y^2 / (1 - \\epsilon)^2
Parameters
----------
centre
The (y,x) arc-second coordinates of the profile centre.
ra
The inner core radius in arcseconds.
rs
The outer truncation radius in arcseconds.
b0
The lens strength in arcseconds.
"""
super().__init__(centre=centre, ell_comps=ell_comps)
self.ra = ra
self.rs = rs
self.b0 = b0
def _ellip(self, xp=np):
ellip = xp.sqrt(self.ell_comps[0] ** 2 + self.ell_comps[1] ** 2)
MAX_ELLIP = 0.99999
return xp.min(xp.array([ellip, MAX_ELLIP]))
[docs]
@aa.decorators.to_vector_yx
@aa.decorators.transform(rotate_back=True)
def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
"""
Calculate the deflection angles on a grid of (y,x) arc-second coordinates.
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
"""
ellip = self._ellip(xp)
factor = self.b0 * self.rs / (self.rs - self.ra)
zis = _ci05f(
x=grid.array[:, 1],
y=grid.array[:, 0],
eps=ellip,
rcore=self.ra,
rcut=self.rs,
xp=xp,
)
# This is in axes aligned to the major/minor axis
deflection_x = zis.real
deflection_y = zis.imag
return xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T)
def _convergence(self, radii, xp=np):
radsq = radii * radii
a, s = self.ra, self.rs
return (
self.b0
/ 2
* s
/ (s - a)
* (1 / xp.sqrt(a**2 + radsq) - 1 / xp.sqrt(s**2 + radsq))
)
[docs]
def convergence_func(self, grid_radius, xp=np):
return self._convergence(grid_radius, xp=xp)
[docs]
@aa.decorators.to_array
@aa.decorators.transform
def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
"""
Returns the two dimensional projected convergence on a grid of (y,x) arc-second coordinates.
The `grid_2d_to_structure` decorator reshapes the ndarrays the convergence is outputted on. See
*aa.grid_2d_to_structure* for a description of the output.
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the convergence is computed on.
"""
ellip = self._ellip(xp)
grid_radii = xp.sqrt(
grid.array[:, 1] ** 2 / (1 + ellip) ** 2
+ grid.array[:, 0] ** 2 / (1 - ellip) ** 2
)
kappa = self._convergence(grid_radii, xp)
return kappa
[docs]
@aa.decorators.transform
def analytical_hessian_2d_from(self, grid: "aa.type.Grid2DLike", xp=np, **kwargs):
"""
Calculate the hessian matrix on a grid of (y,x) arc-second coordinates.
Hessian_dPIEMass = rs * (rs - ra) * ( Hessian_PIEMass(ra) - Hessian_PIEMass(rs))
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
"""
if grid.ndim != 2 or grid.shape[1] != 2:
raise ValueError("Grid must be a 2D array with shape (n, 2)")
ellip = self._ellip()
t05 = self.rs / (self.rs - self.ra)
g05c_a, g05c_b, g05c_c, g05c_d = _mdci05(
x=grid.array[:, 1],
y=grid.array[:, 0],
eps=ellip,
rcore=self.ra,
b0=self.b0,
xp=xp,
)
g05cut_a, g05cut_b, g05cut_c, g05cut_d = _mdci05(
x=grid.array[:, 1],
y=grid.array[:, 0],
eps=ellip,
rcore=self.rs,
b0=self.b0,
xp=xp,
)
# Compute Hessian matrix components
hessian_xx = t05 * (g05c_a - g05cut_a)
hessian_xy = t05 * (g05c_b - g05cut_b)
hessian_yx = t05 * (g05c_c - g05cut_c)
hessian_yy = t05 * (g05c_d - g05cut_d)
return hessian_yy, hessian_xy, hessian_yx, hessian_xx
[docs]
def analytical_magnification_2d_from(
self, grid: "aa.type.Grid2DLike", xp=np, **kwargs
):
hessian_yy, hessian_xy, hessian_yx, hessian_xx = (
self.analytical_hessian_2d_from(grid=grid, xp=xp)
)
det_A = (1 - hessian_xx) * (1 - hessian_yy) - hessian_xy * hessian_yx
return aa.Array2D(values=1.0 / det_A, mask=grid.mask)
[docs]
@aa.over_sample
@aa.decorators.to_array
@aa.decorators.transform
def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
from autogalaxy.profiles.mass.abstract.mge import MGEDecomposer
radii_min = max(self.ra, 0.001) / 10.0
radii_max = self.rs * 20.0
sigmas = xp.exp(xp.linspace(xp.log(radii_min), xp.log(radii_max), 30))
mge_decomp = MGEDecomposer(mass_profile=self)
return mge_decomp.potential_2d_via_mge_from(
grid=grid, xp=xp, sigma_log_list=sigmas,
ellipticity_convention="major", three_D=False,
)
[docs]
class dPIEMassSph(dPIEMass):
r"""Spherical dual pseudo-isothermal mass distribution (dPIE, mass parameterisation).
The spherical limit of :class:`dPIEMass`. The projected convergence is:
.. math::
\kappa(r) = \frac{b_0}{2} \frac{r_s}{r_s - r_a}
\left(
\frac{1}{\sqrt{r_a^2 + r^2}}
- \frac{1}{\sqrt{r_s^2 + r^2}}
\right)
where :math:`r` is the circular projected radius, :math:`r_a` is the core
radius, :math:`r_s` is the truncation radius, and :math:`b_0` is the lens
strength (Einstein radius in the limits :math:`r_a \to 0`,
:math:`r_s \to \infty`).
Parameters
----------
centre : (float, float)
(y, x) arc-second coordinates of the profile centre.
ra : float
Inner core radius in arcseconds.
rs : float
Outer truncation radius in arcseconds.
b0 : float
Lens strength in arcseconds (Einstein radius in the limit
:math:`r_a \to 0`, :math:`r_s \to \infty`).
References
----------
Kassiola & Kovner (1993), ApJ, 417, 450.
Eliasdottir et al. (2007), arXiv:0710.5636.
Limousin et al. (2005), A&A, 461, 881.
"""
def __init__(
self,
centre: Tuple[float, float] = (0.0, 0.0),
ra: float = 0.1,
rs: float = 2.0,
b0: float = 1.0,
):
"""
The dual Pseudo Isothermal Elliptical Mass Distribution(dPIEMass) profiles without ellipticity, which is a *two component PIEMass* with both a core radius and a truncation radius,
see Eliasdottir (2007): https://arxiv.org/abs/0710.5636
This profile is ported from Lenstool's C code, which has the same formulation.
This proflie describes an spherical isothermal mass distribution with a finite core, \\rho r^{-2} while in the transition region (ra<=R<=rs),
and \\rho r^{-4} in the outer parts:
\\rho \\propto [(ra^2 + R^2) (rs^2 + R^2)]^{-1}
The convergence is given by two PIEMass with core radius ra and rs:
\\kappa(r_{em}) = rs / (rs - ra) * (\\kappa_{PIEMass,ra} - \\kappa_{PIEMass,rs})
= b_0 / 2 * rs / (rs - ra) * ( \\frac{1}{\\sqrt{ ra^2 + r_{em}^2}} - \\frac{1}{\\sqrt{ rs^2 + r_{em}^2}} )
where r_{em}^2 = x^2 / (1 + \\epsilon)^2 + y^2 / (1 - \\epsilon)^2.
Note in Eliasdottir (2007), E0 = 6\\pi * \\sigma_{dPIEPotential}^2 / c^2 * (D_{LS} / D_{S}). Eliasdottir's E0 is not the same as E0 in Kassiola & Kovner(1993) which is also b0.
There is \\frac{\\sigma_{dPIEPotential}^2}{\\sigma_0^2} = \\frac{2}{3} \frac{rs^2}{rs^2-ra^2},
thus E0(Kassiola & Kovner(1993)) = b0 = E0(Eliasdottir (2007)) * (rs^2 - ra^2) / rs^2. So when s->\\infty and a->0, they are equivalent.
In this implementation:
- `ra` is the core radius in unit of arcseconds.
- `rs` is the truncation radius in unit of arcseconds.
- `b0` is the lens strength in unit of arcseconds, when ra->0 & rs->\\infty & q->1, b0 is the Einstein radius.
`b0` is related to the central velocity dispersion \\sigma_0: b_0 = 4\\pi * \\sigma_0^2 / c^2 * (D_{LS} / D_{S})
`b0` is not in the Intermediate-Axis-Convention for its r_{em}^2 = x^2 / (1 + \\epsilon)^2 + y^2 / (1 - \\epsilon)^2
Parameters
----------
centre
The (y,x) arc-second coordinates of the profile centre.
ra
The inner core radius in arcseconds.
rs
The outer truncation radius in arcseconds.
b0
The lens strength in arcseconds.
"""
super().__init__(centre=centre, ell_comps=(0.0, 0.0))
self.ra = ra
self.rs = rs
self.b0 = b0
[docs]
@aa.decorators.to_vector_yx
@aa.decorators.transform(rotate_back=True)
def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
"""
Calculate the deflection angles on a grid of (y,x) arc-second coordinates.
Faster and equivalent to Eliasdottir (2007), see Eq. A19 and Eq. A20.
f(R,a,s) = {R/a} / {1 + \\sqrt{1 + (R/a)^2}} - {R/s} / {1 + \\sqrt{1 + (R/s)^2}}
= R / {\\sqrt{a^2 + R^2} + a} - R / {\\sqrt{s^2 + R^2} + s}
= R * (\\sqrt{a^2 + R^2} - a) / {a^2 + R^2 - a^2} - R * (\\sqrt{s^2 + R^2} - s) / {s^2 + R^2 - s^2}
= (\\sqrt{R^2 + a^2} - a - \\sqrt{R^2 + s^2} + s) / R
\\alpha = b0 * s / (s - a) * f(R,a,s)
deflection_x = \\alpha * grid[:, 1] / R
= grid[:, 1] * b0 * s / (s - a) * (\\sqrt{R^2 + a^2} - a - \\sqrt{R^2 + s^2} + s) / R^2
deflection_y = \\alpha * grid[:, 0] / R
= grid[:, 0] * b0 * s / (s - a) * (\\sqrt{R^2 + a^2} - a - \\sqrt{R^2 + s^2} + s) / R^2
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
"""
a = self.ra
s = self.rs
# radii = self.radial_grid_from(grid=grid, xp=xp, **kwargs)
# R2 = radii * radii
R2 = grid.array[:, 1] * grid.array[:, 1] + grid.array[:, 0] * grid.array[:, 0]
factor = xp.sqrt(R2 + a * a) - a - xp.sqrt(R2 + s * s) + s
factor *= self.b0 * s / (s - a) / R2
# This is in axes aligned to the major/minor axis
deflection_x = grid.array[:, 1] * factor
deflection_y = grid.array[:, 0] * factor
return xp.vstack((deflection_y, deflection_x)).T
[docs]
@aa.decorators.to_array
@aa.decorators.transform
def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
"""
Returns the two dimensional projected convergence on a grid of (y,x) arc-second coordinates.
The `grid_2d_to_structure` decorator reshapes the ndarrays the convergence is outputted on. See
*aa.grid_2d_to_structure* for a description of the output.
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the convergence is computed on.
"""
# already transformed to center on profile centre so this works
radsq = grid.array[:, 0] ** 2 + grid.array[:, 1] ** 2
return self._convergence(xp.sqrt(radsq), xp)
[docs]
@aa.decorators.transform
def analytical_hessian_2d_from(self, grid: "aa.type.Grid2DLike", xp=np, **kwargs):
"""
Calculate the hessian matrix on a grid of (y,x) arc-second coordinates.
Chain rule of second derivatives:
∂²ψ/∂x² = ∂²ψ/∂R² * (∂R/∂x)² + ∂²R/∂x² * ∂ψ/∂R
∂²ψ/∂y² = ∂²ψ/∂R² * (∂R/∂y)² + ∂²R/∂y² * ∂ψ/∂R
∂²ψ/∂x∂y = ∂²ψ/∂R² * ∂R/∂x * ∂R/∂y + ∂²R/∂x∂y * ∂ψ/∂R
Parameters
----------
grid
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
"""
if grid.ndim != 2 or grid.shape[1] != 2:
raise ValueError("Grid must be a 2D array with shape (n, 2)")
a = self.ra
s = self.rs
t05 = self.b0 * s / (s - a)
# We have known the first derivatives as `deflections_yx`:
# ∂ψ/∂R ∝ f(R,a,s) = (\\sqrt{R^2 + a^2} - a - \\sqrt{R^2 + s^2} + s) / R = z / R
# ∂ψ/∂x ∝ x * (\\sqrt{R^2 + a^2} - a - \\sqrt{R^2 + s^2} + s) / R^2 = x * z / R^2
# ∂ψ/∂y ∝ y * (\\sqrt{R^2 + a^2} - a - \\sqrt{R^2 + s^2} + s) / R^2 = y * z / R^2
# where z = (\\sqrt{R^2 + a^2} - a - \\sqrt{R^2 + s^2} + s) / R^2
# R = (x^2 + y^2)^(0.5)
# ∂R/∂x = x / R
# ∂R/∂y = y / R
# ∂²R/∂²x = y^2 / R^3
# ∂²R/∂²y = x^2 / R^3
# ∂²R/∂x∂y = - x*y / R^3
# ∂²ψ/∂²R = ∂(z/R)/∂R = (∂z/∂R * R - z * 1) / R^2
# = {( R^2 / √(R^2 + a^2)) - ( R^2 / √(R^2 + s^2)) - z} / R^2
# = p
R2 = grid.array[:, 1] * grid.array[:, 1] + grid.array[:, 0] * grid.array[:, 0]
z = xp.sqrt(R2 + a * a) - a - xp.sqrt(R2 + s * s) + s
p = (1.0 - a / xp.sqrt(a * a + R2)) * a / R2 - (
1.0 - s / xp.sqrt(s * s + R2)
) * s / R2
X = grid.array[:, 1] * grid.array[:, 1] / R2 # x^2 / R^2
Y = grid.array[:, 0] * grid.array[:, 0] / R2 # y^2 / R^2
XY = grid.array[:, 1] * grid.array[:, 0] / R2 # x*y / R^2
# ∂²ψ/∂x² = ∂²ψ/∂R² * (∂R/∂x)² + ∂²R/∂x² * ∂ψ/∂R
# = p * (x / R)^2 + y^2 / R^3 * z / R
# = p * x^2 / R^2 + z * y^2 / R^2 / R^2
# = p * X + z * Y / R2
# ∂²ψ/∂y² = ∂²ψ/∂R² * (∂R/∂y)² + ∂²R/∂y² * ∂ψ/∂R
# = p * (y / R)^2 + x^2 / R^3 * z / R
# = p * y^2 / R^2 + z * x^2 / R^2 / R^2
# = p * Y + z * X / R2
# ∂²ψ/∂x∂y = ∂²ψ/∂R² * ∂R/∂x * ∂R/∂y + ∂²R/∂x∂y * ∂ψ/∂R
# = p * (x / R) * (y / R) + (- x*y / R^3) * z / R
# = p * x*y / R^2 - z * x*y / R^2 / R^2
# = p * XY + z * XY / R2
# Compute Hessian matrix components
hessian_xx = t05 * (p * X + z * Y / R2)
hessian_xy = t05 * (p * XY - z * XY / R2)
hessian_yx = t05 * (p * XY - z * XY / R2)
hessian_yy = t05 * (p * Y + z * X / R2)
return hessian_yy, hessian_xy, hessian_yx, hessian_xx