# mypy: disable-error-code="operator,union-attr,dict-item"
from typing import Optional, Union, Annotated, Callable
from caskade import forward, Param
from ..backend_obj import backend, ArrayLike
from .base import ThinLens, CosmologyType, NameType, ZType
from .func import (
physical_deflection_angle_enclosed_mass,
convergence_enclosed_mass,
reduced_from_physical_deflection_angle,
)
__all__ = ("EnclosedMass",)
[docs]
class EnclosedMass(ThinLens):
"""
A class for representing a lens with an enclosed mass profile. This generic
lens profile can represent any lens with a mass distribution that can be
described by a function that returns the enclosed mass as a function of
radius.
"""
_null_params = {
"x0": 0.0,
"y0": 0.0,
"q": 1.0,
"phi": 0.0,
"p": 1.0,
}
def __init__(
self,
cosmology: CosmologyType,
enclosed_mass: Callable,
z_l: ZType = None,
z_s: ZType = None,
x0: Annotated[
Optional[Union[ArrayLike, float]],
"The x-coordinate of the lens center",
True,
] = None,
y0: Annotated[
Optional[Union[ArrayLike, float]],
"The y-coordinate of the lens center",
True,
] = None,
q: Annotated[
Optional[Union[ArrayLike, float]], "The axis ratio of the lens", True
] = None,
phi: Annotated[
Optional[Union[ArrayLike, float]], "The position angle of the lens", True
] = None,
p: Annotated[
Optional[Union[ArrayLike, list[float]]],
"parameters for the enclosed mass function",
True,
] = None,
s: Annotated[
float, "Softening parameter to prevent numerical instabilities"
] = 0.0,
name: NameType = None,
**kwargs,
):
"""
Initialize an enclosed mass lens.
Parameters
----------
name : str
The name of the lens.
cosmology : Cosmology
The cosmology object that describes the Universe.
enclosed_mass : callable
A function that takes a radius and a set of parameters and returns the enclosed mass. Should be of the form ``enclosed_mass(r, p) -> M`` and returns units of Msun.
*Unit: Msun*
z_l : float
The redshift of the lens.
*Unit: unitless*
z_s : float
The redshift of the source.
*Unit: unitless*
x0 : float or ArrayLike, optional
The x-coordinate of the lens center.
*Unit: arcsec*
y0 : float or ArrayLike, optional
The y-coordinate of the lens center.
*Unit: arcsec*
q : float or ArrayLike, optional
The axis ratio of the lens. ratio of semi-minor to semi-major axis (b/a).
*Unit: unitless*
phi : float or ArrayLike, optional
The position angle of the lens.
*Unit: radians*
p : list[float] or ArrayLike, optional
The parameters for the enclosed mass function.
*Unit: user-defined*
s : float, optional
Softening parameter to prevent numerical instabilities.
*Unit: arcsec*
"""
super().__init__(cosmology, z_l, name=name, z_s=z_s, **kwargs)
self.enclosed_mass = enclosed_mass
self.x0 = Param("x0", x0, shape=(), units="arcsec")
self.y0 = Param("y0", y0, shape=(), units="arcsec")
self.q = Param("q", q, shape=(), units="unitless", valid=(0, 1))
self.phi = Param(
"phi", phi, shape=(), units="radians", valid=(0, backend.pi), cyclic=True
)
self.p = Param("p", p, units="user-defined")
self.s = s
[docs]
@forward
def physical_deflection_angle(
self,
x: ArrayLike,
y: ArrayLike,
x0: Annotated[ArrayLike, "Param"],
y0: Annotated[ArrayLike, "Param"],
q: Annotated[ArrayLike, "Param"],
phi: Annotated[ArrayLike, "Param"],
p: Annotated[ArrayLike, "Param"],
) -> tuple[ArrayLike, ArrayLike]:
"""
Calculate the physical deflection angle of the lens at a given position.
Parameters
----------
x: ArrayLike
The x-coordinate on the lens plane.
*Unit: arcsec*
y: ArrayLike
The y-coordinate on the lens plane.
*Unit: arcsec*
Returns
-------
The physical deflection angle at the given position. [ArrayLike, ArrayLike]
*Unit: arcsec*
"""
return physical_deflection_angle_enclosed_mass(
x0, y0, q, phi, lambda r: self.enclosed_mass(r, p), x, y, self.s
)
[docs]
@forward
def reduced_deflection_angle(self, x, y, z_s, z_l):
d_s = self.cosmology.angular_diameter_distance(z_s)
d_ls = self.cosmology.angular_diameter_distance_z1z2(z_l, z_s)
deflection_angle_x, deflection_angle_y = self.physical_deflection_angle(x, y)
return reduced_from_physical_deflection_angle(
deflection_angle_x, deflection_angle_y, d_s, d_ls
)
[docs]
@forward
def potential(
self,
x: ArrayLike,
y: ArrayLike,
*args,
**kwargs,
) -> ArrayLike:
raise NotImplementedError(
"Potential is not implemented for enclosed mass profiles."
)
[docs]
@forward
def convergence(
self,
x: ArrayLike,
y: ArrayLike,
z_s: Annotated[ArrayLike, "Param"],
z_l: Annotated[ArrayLike, "Param"],
x0: Annotated[ArrayLike, "Param"],
y0: Annotated[ArrayLike, "Param"],
q: Annotated[ArrayLike, "Param"],
phi: Annotated[ArrayLike, "Param"],
p: Annotated[ArrayLike, "Param"],
) -> ArrayLike:
"""
Calculate the dimensionless convergence of the lens at a given position.
Parameters
----------
x: ArrayLike
The x-coordinate on the lens plane.
*Unit: arcsec*
y: ArrayLike
The y-coordinate on the lens plane.
*Unit: arcsec*
Returns
-------
The dimensionless convergence at the given position. [ArrayLike]
*Unit: unitless*
"""
csd = self.cosmology.critical_surface_density(z_l, z_s)
return convergence_enclosed_mass(
x0,
y0,
q,
phi,
lambda r: self.enclosed_mass(r, p),
x,
y,
csd,
self.s,
)