import numpy as np
from lenstronomy.Cosmo.background import Background
from lenstronomy.LensModel.profile_list_base import ProfileListBase
import lenstronomy.Util.constants as const
__all__ = ["MultiPlaneBase"]
[docs]class MultiPlaneBase(ProfileListBase):
"""Multi-plane lensing class.
The lens model deflection angles are in units of reduced deflections from the
specified redshift of the lens to the source redshift of the class instance.
"""
[docs] def __init__(
self,
lens_model_list,
lens_redshift_list,
z_source_convention,
cosmo=None,
numerical_alpha_class=None,
cosmo_interp=False,
z_interp_stop=None,
num_z_interp=100,
kwargs_interp=None,
kwargs_synthesis=None,
):
"""
A description of the recursive multi-plane formalism can be found e.g. here: https://arxiv.org/abs/1312.1536
:param lens_model_list: list of lens model strings
:param lens_redshift_list: list of floats with redshifts of the lens models indicated in lens_model_list
:param z_source_convention: float, redshift of a source to define the reduced deflection angles of the lens
models. If None, 'z_source' is used.
:param cosmo: instance of astropy.cosmology
:param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model
(see documentation in Profiles/numerical_alpha)
:param kwargs_interp: interpolation keyword arguments specifying the numerics.
See description in the Interpolate() class. Only applicable for 'INTERPOL' and 'INTERPOL_SCALED' models.
:param kwargs_synthesis: keyword arguments for the 'SYNTHESIS' lens model, if applicable
"""
if z_interp_stop is None:
z_interp_stop = z_source_convention
self._cosmo_bkg = Background(
cosmo, interp=cosmo_interp, z_stop=z_interp_stop, num_interp=num_z_interp
)
self._z_source_convention = z_source_convention
if len(lens_redshift_list) > 0:
z_lens_max = np.max(lens_redshift_list)
if z_lens_max >= z_source_convention:
raise ValueError(
"deflector redshifts higher or equal the source redshift convention (%s >= %s for the "
"reduced lens model quantities not allowed (leads to negative reduced deflection "
"angles!" % (z_lens_max, z_source_convention)
)
if not len(lens_model_list) == len(lens_redshift_list):
raise ValueError(
"The length of lens_model_list does not correspond to redshift_list"
)
self._lens_redshift_list = lens_redshift_list
super(MultiPlaneBase, self).__init__(
lens_model_list,
numerical_alpha_class=numerical_alpha_class,
lens_redshift_list=lens_redshift_list,
z_source_convention=z_source_convention,
kwargs_interp=kwargs_interp,
kwargs_synthesis=kwargs_synthesis,
)
if len(lens_model_list) < 1:
self._sorted_redshift_index = []
else:
self._sorted_redshift_index = self._index_ordering(lens_redshift_list)
z_before = 0
T_z = 0
self._T_ij_list = []
self._T_z_list = []
# Sort redshift for vectorized reduced2physical factor calculation
if len(lens_model_list) < 1:
self._reduced2physical_factor = []
else:
z_sort = np.array(self._lens_redshift_list)[self._sorted_redshift_index]
z_source_array = np.ones(z_sort.shape) * z_source_convention
self._reduced2physical_factor = self._cosmo_bkg.d_xy(
0, z_source_convention
) / self._cosmo_bkg.d_xy(z_sort, z_source_array)
for idex in self._sorted_redshift_index:
z_lens = self._lens_redshift_list[idex]
if z_before == z_lens:
delta_T = 0
else:
T_z = self._cosmo_bkg.T_xy(0, z_lens)
delta_T = self._cosmo_bkg.T_xy(z_before, z_lens)
self._T_ij_list.append(delta_T)
self._T_z_list.append(T_z)
z_before = z_lens
[docs] def ray_shooting_partial(
self,
x,
y,
alpha_x,
alpha_y,
z_start,
z_stop,
kwargs_lens,
include_z_start=False,
T_ij_start=None,
T_ij_end=None,
):
"""Ray-tracing through parts of the coin, starting with (x,y) co-moving
distances and angles (alpha_x, alpha_y) at redshift z_start and then backwards
to redshift z_stop.
:param x: co-moving position [Mpc]
:param y: co-moving position [Mpc]
:param alpha_x: ray angle at z_start [arcsec]
:param alpha_y: ray angle at z_start [arcsec]
:param z_start: redshift of start of computation
:param z_stop: redshift where output is computed
:param kwargs_lens: lens model keyword argument list
:param include_z_start: bool, if True, includes the computation of the
deflection angle at the same redshift as the start of the ray-tracing.
ATTENTION: deflection angles at the same redshift as z_stop will be computed
always! This can lead to duplications in the computation of deflection
angles.
:param T_ij_start: transverse angular distance between the starting redshift to
the first lens plane to follow. If not set, will compute the distance each
time this function gets executed.
:param T_ij_end: transverse angular distance between the last lens plane being
computed and z_end. If not set, will compute the distance each time this
function gets executed.
:return: co-moving position and angles at redshift z_stop
"""
x = np.array(x, dtype=float)
y = np.array(y, dtype=float)
alpha_x = np.array(alpha_x)
alpha_y = np.array(alpha_y)
z_lens_last = z_start
first_deflector = True
for i, idex in enumerate(self._sorted_redshift_index):
z_lens = self._lens_redshift_list[idex]
if (
self._start_condition(include_z_start, z_lens, z_start)
and z_lens <= z_stop
):
if first_deflector is True:
if T_ij_start is None:
if z_start == 0:
delta_T = self._T_ij_list[0]
else:
delta_T = self._cosmo_bkg.T_xy(z_start, z_lens)
else:
delta_T = T_ij_start
first_deflector = False
else:
delta_T = self._T_ij_list[i]
x, y = self._ray_step_add(x, y, alpha_x, alpha_y, delta_T)
alpha_x, alpha_y = self._add_deflection(
x, y, alpha_x, alpha_y, kwargs_lens, i
)
z_lens_last = z_lens
if T_ij_end is None:
if z_lens_last == z_stop:
delta_T = 0
else:
delta_T = self._cosmo_bkg.T_xy(z_lens_last, z_stop)
else:
delta_T = T_ij_end
x, y = self._ray_step_add(x, y, alpha_x, alpha_y, delta_T)
return x, y, alpha_x, alpha_y
[docs] def transverse_distance_start_stop(self, z_start, z_stop, include_z_start=False):
"""Computes the transverse distance (T_ij) that is required by the ray-tracing
between the starting redshift and the first deflector afterwards and the last
deflector before the end of the ray-tracing.
:param z_start: redshift of the start of the ray-tracing
:param z_stop: stop of ray-tracing
:param include_z_start: boolean, if True includes the computation of the
starting position if the first deflector is at z_start
:return: T_ij_start, T_ij_end
"""
z_lens_last = z_start
first_deflector = True
T_ij_start = None
for i, idex in enumerate(self._sorted_redshift_index):
z_lens = self._lens_redshift_list[idex]
if (
self._start_condition(include_z_start, z_lens, z_start)
and z_lens <= z_stop
):
if first_deflector is True:
T_ij_start = self._cosmo_bkg.T_xy(z_start, z_lens)
first_deflector = False
z_lens_last = z_lens
T_ij_end = self._cosmo_bkg.T_xy(z_lens_last, z_stop)
return T_ij_start, T_ij_end
[docs] def geo_shapiro_delay(
self, theta_x, theta_y, kwargs_lens, z_stop, T_z_stop=None, T_ij_end=None
):
"""Geometric and Shapiro (gravitational) light travel time relative to a
straight path through the coordinate (0,0) Negative sign means earlier arrival
time.
:param theta_x: angle in x-direction on the image
:param theta_y: angle in y-direction on the image
:param kwargs_lens: lens model keyword argument list
:param z_stop: redshift of the source to stop the backwards ray-tracing
:param T_z_stop: optional, transversal angular distance from z=0 to z_stop
:param T_ij_end: optional, transversal angular distance between the last lensing
plane and the source plane
:return: dt_geo, dt_shapiro, [days]
"""
dt_grav = np.zeros_like(theta_x, dtype=float)
dt_geo = np.zeros_like(theta_x, dtype=float)
x = np.zeros_like(theta_x, dtype=float)
y = np.zeros_like(theta_y, dtype=float)
alpha_x = np.array(theta_x, dtype=float)
alpha_y = np.array(theta_y, dtype=float)
i = 0
z_lens_last = 0
for i, index in enumerate(self._sorted_redshift_index):
z_lens = self._lens_redshift_list[index]
if z_lens <= z_stop:
T_ij = self._T_ij_list[i]
x_new, y_new = self._ray_step(x, y, alpha_x, alpha_y, T_ij)
if i == 0:
pass
elif T_ij > 0:
T_j = self._T_z_list[i]
T_i = self._T_z_list[i - 1]
beta_i_x, beta_i_y = x / T_i, y / T_i
beta_j_x, beta_j_y = x_new / T_j, y_new / T_j
dt_geo_new = self._geometrical_delay(
beta_i_x, beta_i_y, beta_j_x, beta_j_y, T_i, T_j, T_ij
)
dt_geo += dt_geo_new
x, y = x_new, y_new
dt_grav_new = self._gravitational_delay(x, y, kwargs_lens, i, z_lens)
alpha_x, alpha_y = self._add_deflection(
x, y, alpha_x, alpha_y, kwargs_lens, i
)
dt_grav += dt_grav_new
z_lens_last = z_lens
if T_ij_end is None:
T_ij_end = self._cosmo_bkg.T_xy(z_lens_last, z_stop)
T_ij = T_ij_end
x_new, y_new = self._ray_step(x, y, alpha_x, alpha_y, T_ij)
if T_z_stop is None:
T_z_stop = self._cosmo_bkg.T_xy(0, z_stop)
T_j = T_z_stop
T_i = self._T_z_list[i]
beta_i_x, beta_i_y = x / T_i, y / T_i
beta_j_x, beta_j_y = x_new / T_j, y_new / T_j
dt_geo_new = self._geometrical_delay(
beta_i_x, beta_i_y, beta_j_x, beta_j_y, T_i, T_j, T_ij
)
dt_geo += dt_geo_new
return dt_geo, dt_grav
@staticmethod
def _index_ordering(redshift_list):
"""
:param redshift_list: list of redshifts
:return: indexes in ascending order to be evaluated (from z=0 to z=z_source)
"""
redshift_list = np.array(redshift_list)
# sort_index = np.argsort(redshift_list[redshift_list < z_source])
sort_index = np.argsort(redshift_list)
# if len(sort_index) < 1:
# Warning("There is no lens object between observer at z=0 and source at z=%s" % z_source)
return sort_index
def _reduced2physical_deflection(self, alpha_reduced, index_lens):
"""alpha_reduced = D_ds/Ds alpha_physical.
:param alpha_reduced: reduced deflection angle
:param index_lens: integer, index of the deflector plane
:return: physical deflection angle
"""
factor = self._reduced2physical_factor[index_lens]
return alpha_reduced * factor
def _gravitational_delay(self, x, y, kwargs_lens, index, z_lens):
"""
:param x: co-moving coordinate at the lens plane
:param y: co-moving coordinate at the lens plane
:param kwargs_lens: lens model keyword arguments
:param z_lens: redshift of the deflector
:param index: index of the lens model in sorted redshfit convention
:return: gravitational delay in units of days as seen at z=0
"""
theta_x, theta_y = self._co_moving2angle(x, y, index)
k = self._sorted_redshift_index[index]
potential = self.func_list[k].function(theta_x, theta_y, **kwargs_lens[k])
delay_days = self._lensing_potential2time_delay(
potential, z_lens, z_source=self._z_source_convention
)
return -delay_days
@staticmethod
def _geometrical_delay(beta_i_x, beta_i_y, beta_j_x, beta_j_y, T_i, T_j, T_ij):
"""
:param beta_i_x: angle on the sky at plane i
:param beta_i_y: angle on the sky at plane i
:param beta_j_x: angle on the sky at plane j
:param beta_j_y: angle on the sky at plane j
:param T_i: transverse diameter distance to z_i
:param T_j: transverse diameter distance to z_j
:param T_ij: transverse diameter distance from z_i to z_j
:return: excess delay relative to a straight line
"""
d_beta_x = beta_j_x - beta_i_x
d_beta_y = beta_j_y - beta_i_y
tau_ij = (
T_i * T_j / T_ij * const.Mpc / const.c / const.day_s * const.arcsec**2
)
return tau_ij * (d_beta_x**2 + d_beta_y**2) / 2
def _lensing_potential2time_delay(self, potential, z_lens, z_source):
"""Transforms the lensing potential (in units arcsec^2) to a gravitational time-
delay as measured at z=0.
:param potential: lensing potential
:param z_lens: redshift of the deflector
:param z_source: redshift of source for the definition of the lensing quantities
:return: gravitational time-delay in units of days
"""
D_dt = self._cosmo_bkg.ddt(z_lens, z_source)
delay_days = const.delay_arcsec2days(potential, D_dt)
return delay_days
def _co_moving2angle(self, x, y, index):
"""Transforms co-moving distances Mpc into angles on the sky (radian)
:param x: co-moving distance
:param y: co-moving distance
:param index: index of plane
:return: angles on the sky
"""
T_z = self._T_z_list[index]
theta_x = x / T_z
theta_y = y / T_z
return theta_x, theta_y
@staticmethod
def _ray_step(x, y, alpha_x, alpha_y, delta_T):
"""Ray propagation with small angle approximation The difference to
_ray_step_add() is that the previous input position (x, y) do NOT get
overwritten and are still accessible.
:param x: co-moving x-position
:param y: co-moving y-position
:param alpha_x: deflection angle in x-direction at (x, y)
:param alpha_y: deflection angle in y-direction at (x, y)
:param delta_T: transverse angular diameter distance to the next step
:return: co-moving position at the next step (backwards)
"""
x_ = x + alpha_x * delta_T
y_ = y + alpha_y * delta_T
return x_, y_
@staticmethod
def _ray_step_add(x, y, alpha_x, alpha_y, delta_T):
"""Ray propagation with small angle approximation The difference to _ray_step()
is that the previous input position (x, y) do get overwritten, which is faster.
:param x: co-moving x-position
:param y: co-moving y-position
:param alpha_x: deflection angle in x-direction at (x, y)
:param alpha_y: deflection angle in y-direction at (x, y)
:param delta_T: transverse angular diameter distance to the next step
:return: co-moving position at the next step (backwards)
"""
x += alpha_x * delta_T
y += alpha_y * delta_T
return x, y
def _add_deflection(self, x, y, alpha_x, alpha_y, kwargs_lens, index):
"""Adds the physical deflection angle of a single lens plane to the deflection
field.
:param x: co-moving distance at the deflector plane
:param y: co-moving distance at the deflector plane
:param alpha_x: physical angle (radian) before the deflector plane
:param alpha_y: physical angle (radian) before the deflector plane
:param kwargs_lens: lens model parameter kwargs
:param index: index of the lens model to be added in sorted redshift list
convention
:return: updated physical deflection after deflector plane (in a backwards ray-
tracing perspective)
"""
theta_x, theta_y = self._co_moving2angle(x, y, index)
k = self._sorted_redshift_index[index]
alpha_x_red, alpha_y_red = self.func_list[k].derivatives(
theta_x, theta_y, **kwargs_lens[k]
)
alpha_x_phys = self._reduced2physical_deflection(alpha_x_red, index)
alpha_y_phys = self._reduced2physical_deflection(alpha_y_red, index)
return alpha_x - alpha_x_phys, alpha_y - alpha_y_phys
@staticmethod
def _start_condition(inclusive, z_lens, z_start):
"""
:param inclusive: boolean, if True selects z_lens including z_start, else only selects z_lens > z_start
:param z_lens: deflector redshift
:param z_start: starting redshift (lowest redshift)
:return: boolean of condition
"""
if inclusive:
return z_lens >= z_start
else:
return z_lens > z_start