Source code for caustics.lenses.multiplane

from typing import Annotated, Tuple
from operator import itemgetter
from warnings import warn

from caskade import forward

from ..backend_obj import backend, ArrayLike
from ..constants import arcsec_to_rad, rad_to_arcsec, c_Mpc_s, days_to_seconds
from .base import ThickLens, ThinLens, NameType, CosmologyType, ZType

__all__ = ("Multiplane",)


[docs] class Multiplane(ThickLens): """ Class for handling gravitational lensing with multiple lens planes. Attributes ---------- lenses list of ThinLens List of thin lenses. Parameters ---------- name: string Name of the lens. cosmology: Cosmology Cosmological parameters used for calculations. lenses: list[ThinLens] List of thin lenses. z_s: ZType Redshift of the source. """ def __init__( self, cosmology: CosmologyType, lenses: Tuple[ThinLens], name: NameType = None, z_s: ZType = None, ): super().__init__(cosmology, name=name, z_s=z_s) self.lenses = tuple(lenses) for lens in self.lenses: if lens.z_s.static: warn( f"Lens plane {lens.name} has a static source redshift. This is now overwritten by the Multiplane ({self.name}) source redshift. To prevent this warning, set the source redshift of the lens plane to be dynamic before adding to multiplane system." ) lens.z_s = self.z_s
[docs] @forward def get_z_ls(self) -> list[ArrayLike]: """ Get the redshifts of each lens in the multiplane. Returns -------- List[ArrayLike] Redshifts of the lenses. *Unit: unitless* """ return [lens.z_l.value for lens in self.lenses]
@forward def _raytrace_helper( self, x: ArrayLike, y: ArrayLike, z_s: Annotated[ArrayLike, "Param"], shapiro_time_delay: bool = True, geometric_time_delay: bool = True, ray_coords: bool = True, ): # Collect lens redshifts and ensure proper order z_ls = self.get_z_ls() lens_planes = [i for i, _ in sorted(enumerate(z_ls), key=itemgetter(1))] D_s = self.cosmology.transverse_comoving_distance(z_s) # Compute physical position on first lens plane D = self.cosmology.transverse_comoving_distance(z_ls[lens_planes[0]]) X, Y = x * arcsec_to_rad * D, y * arcsec_to_rad * D # fmt: skip # Initial angles are observation angles theta_x, theta_y = x, y # Store the time delays if shapiro_time_delay or geometric_time_delay: TD = backend.zeros_like(x) for i in lens_planes: z_next = z_ls[i + 1] if i != lens_planes[-1] else z_s # Compute deflection angle at current ray positions D_l = self.cosmology.transverse_comoving_distance(z_ls[i]) D = self.cosmology.transverse_comoving_distance_z1z2(z_ls[i], z_next) D_is = self.cosmology.transverse_comoving_distance_z1z2(z_ls[i], z_s) D_next = self.cosmology.transverse_comoving_distance(z_next) alpha_x, alpha_y = self.lenses[i].physical_deflection_angle( X * rad_to_arcsec / D_l, Y * rad_to_arcsec / D_l, ) # Update angle of rays after passing through lens (sum in eq 18) theta_x = theta_x - alpha_x theta_y = theta_y - alpha_y # Compute time delay tau_ij = (1 + z_ls[i]) * D_l * D_next / (D * c_Mpc_s) / days_to_seconds if shapiro_time_delay: beta_ij = D * D_s / (D_next * D_is) potential = self.lenses[i].potential( X * rad_to_arcsec / D_l, Y * rad_to_arcsec / D_l ) TD += (-tau_ij * beta_ij * arcsec_to_rad**2) * potential if geometric_time_delay: TD += (tau_ij * arcsec_to_rad**2 * 0.5) * (alpha_x**2 + alpha_y**2) # Propagate rays to next plane (basically eq 18) X = X + D * theta_x * arcsec_to_rad Y = Y + D * theta_y * arcsec_to_rad # Convert from physical position to angular position on the source plane D_end = self.cosmology.transverse_comoving_distance(z_s) if ray_coords and not (shapiro_time_delay or geometric_time_delay): return X * rad_to_arcsec / D_end, Y * rad_to_arcsec / D_end elif ray_coords and (shapiro_time_delay or geometric_time_delay): return X * rad_to_arcsec / D_end, Y * rad_to_arcsec / D_end, TD elif shapiro_time_delay or geometric_time_delay: return TD raise ValueError( "No return value specified. Must choose one or more of: ray_coords, shapiro_time_delay, or geometric_time_delay to be True." )
[docs] @forward def raytrace( self, x: ArrayLike, y: ArrayLike, ) -> tuple[ArrayLike, ArrayLike]: """Calculate the angular source positions corresponding to the observer positions x,y. See Margarita et al. 2013 for the formalism from the GLAMER -II code: https://ui.adsabs.harvard.edu/abs/2014MNRAS.445.1954P/abstract The primary equation used here is equation 18. With a slight correction it reads: .. math:: \\vec{x}^{i+1} = \\vec{x}^i + D_{i+1,i}\\left[\\vec{\\theta} - \\sum_{j=1}^{i}\\bf{\\alpha}^j(\\vec{x}^j)\\right] As an initialization we set the physical positions at the first lensing plane to be :math:`\\vec{\\theta}D_{1,0}` which is just propagation through regular space to the first plane. Note that :math:`\\vec{\\alpha}` is a physical deflection angle. The equation above converts straightforwardly into a recursion formula: .. math:: \\vec{x}^{i+1} = \\vec{x}^i + D_{i+1,i}\\vec{\theta}^{i} \\vec{\\theta}^{i+1} = \\vec{\\theta}^{i} - \\alpha^i(\\vec{x}^{i+1}) Here we set as initialization :math:`\\vec{\theta}^0 = theta` the observation angular coordinates and :math:`\\vec{x}^0 = 0` the initial physical coordinates (i.e. the observation rays come from a point at the observer). The indexing of :math:`\\vec{x}^i` and :math:`\\vec{\\theta}^i` indicates the properties at the plane :math:`i`, and 0 means the observer, 1 is the first lensing plane (infinitesimally after the plane since the deflection has been applied), and so on. Note that in the actual implementation we start at :math:`\\vec{x}^1` and :math:`\\vec{\\theta}^0` and begin at the second step in the recursion formula. Parameters ---------- x: ArrayLike angular x-coordinates in the image plane. *Unit: arcsec* y: ArrayLike angular y-coordinates in the image plane. *Unit: arcsec* Returns ------- x_component: ArrayLike Reduced deflection angle in the x-direction. *Unit: arcsec* y_component: ArrayLike Reduced deflection angle in the y-direction. *Unit: arcsec* References ---------- 1. Margarita Petkova, R. Benton Metcalf, and Carlo Giocoli. 2014. GLAMER II: multiple-plane lensing. MNRAS 445, 1954-1966. DOI:https://doi.org/10.1093/mnras/stu1860 """ # noqa: E501 return self._raytrace_helper( x, y, shapiro_time_delay=False, geometric_time_delay=False, ray_coords=True, )
[docs] @forward def effective_reduced_deflection_angle( self, x: ArrayLike, y: ArrayLike, ) -> tuple[ArrayLike, ArrayLike]: bx, by = self.raytrace(x, y) return x - bx, y - by
[docs] @forward def surface_density( self, x: ArrayLike, y: ArrayLike, *args, **kwargs, ) -> ArrayLike: """ Calculate the projected mass density. Parameters ---------- x: ArrayLike x-coordinates in the lens plane. *Unit: arcsec* y: ArrayLike y-coordinates in the lens plane. *Unit: arcsec* Returns ------- ArrayLike Projected mass density. *Unit: Msun/Mpc^2* Raises ------- NotImplementedError This method is not yet implemented. """ # TODO: rescale mass densities of each lens and sum raise NotImplementedError()
[docs] @forward def time_delay( self, x: ArrayLike, y: ArrayLike, shapiro_time_delay: bool = True, geometric_time_delay: bool = True, ) -> ArrayLike: """ Compute the time delay of light caused by the lensing. This is based on equation 6.22 in Petters et al. 2001. For the time delay of a light path from the observer to the source, the following equation is used:: \\Delta t = \\sum_{i=1}^{N-1} \\tau_{i,i+1} \\left[ \\frac{1}{2} \\left( \\vec{\\alpha}^i \\right)^2 - \\beta_{i,i+1} \\psi^i \\right] \\\\ \\tau_{i,j} = (1 + z_i) \\frac{D_i D_{j}}{D_{i,j} c} \\\\ \\beta_{i,j} = \\frac{D_{i,j} D_s}{D_{j} D_{i,s}} \\\\ where :math:`\\vec{\\alpha}^i` is the deflection angle at the i-th lens plane, :math:`\\psi^i` is the lensing potential at the i-th lens plane, :math:`D_i` is the comoving distance to the i-th lens plane, :math:`D_{i,j}` is the comoving distance between the i-th and j-th lens plane, :math:`D_s` is the comoving distance to the source, and :math:`D_{i,s}` is the comoving distance between the i-th lens plane and the source. This performs the same ray tracing as the :func:`raytrace` method, but computes the time delay along the way. Parameters ---------- x: ArrayLike x-coordinates in the image plane. *Unit: arcsec* y: ArrayLike y-coordinates in the image plane. *Unit: arcsec* shapiro_time_delay: bool Whether to include the Shapiro time delay component. geometric_time_delay: bool Whether to include the geometric time delay component. Returns ------- ArrayLike Time delay caused by the lensing. *Unit: days* References ---------- 1. Petters A. O., Levine H., Wambsganss J., 2001, Singularity Theory and Gravitational Lensing. Birkhauser, Boston 2. McCully et al. 2014, A new hybrid framework to efficiently model lines of sight to gravitational lenses """ return self._raytrace_helper( x, y, shapiro_time_delay=shapiro_time_delay, geometric_time_delay=geometric_time_delay, ray_coords=False, )