def __init__(self, lensModel, sourceModel): """ :param lensModel: lenstronomy LensModel() class instance :param sourceModel: LightModel () class instance The lightModel includes: - source_scale_factor_list: list of floats corresponding to the rescaled deflection angles to the specific source components. None indicates that the list will be set to 1, meaning a single source plane model (in single lens plane mode). - source_redshift_list: list of redshifts of the light components (in multi lens plane mode) """ self._lightModel = sourceModel self._lensModel = lensModel light_model_list = sourceModel.profile_type_list self._multi_lens_plane = lensModel.multi_plane self._source_redshift_list = sourceModel.redshift_list self._deflection_scaling_list = sourceModel.deflection_scaling_list self._multi_source_plane = True if self._multi_lens_plane is True: self._bkg_cosmo = Background(lensModel.cosmo) if self._source_redshift_list is None: self._multi_source_plane = False elif len(self._source_redshift_list) != len(light_model_list): raise ValueError("length of redshift_list must correspond to length of light_model_list") elif np.max(self._source_redshift_list) > self._lensModel.z_source: raise ValueError("redshift of source_redshift_list have to be smaler or equal to the one specified in the lens model.") else: self._sorted_source_redshift_index = self._index_ordering(self._source_redshift_list) else: if self._deflection_scaling_list is None: self._multi_source_plane = False elif len(self._deflection_scaling_list) != len(light_model_list): raise ValueError('length of scale_factor_list must correspond to length of light_model_list!')
def __init__(self, lens_model_list, lens_redshift_list, z_source_convention, cosmo=None, numerical_alpha_class=None): """ :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) """ self._cosmo_bkg = Background(cosmo) 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_model_list = lens_model_list self._lens_redshift_list = lens_redshift_list self._lens_model = SinglePlane( lens_model_list, numerical_alpha_class=numerical_alpha_class) 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 = [] self._reduced2physical_factor = [] 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) factor = self._cosmo_bkg.D_xy( 0, z_source_convention) / self._cosmo_bkg.D_xy( z_lens, z_source_convention) self._reduced2physical_factor.append(factor) z_before = z_lens
def __init__(self, lensModel, sourceModel): """ :param lensModel: LensModel() class instance :param sourceModel: LightModel() class instance. The lightModel includes: - source_scale_factor_list: list of floats corresponding to the rescaled deflection angles to the specific source components. None indicates that the list will be set to 1, meaning a single source plane model (in single lens plane mode). - source_redshift_list: list of redshifts of the light components (in multi lens plane mode) """ self._lightModel = sourceModel self._lensModel = lensModel light_model_list = sourceModel.profile_type_list self._multi_lens_plane = lensModel.multi_plane self._source_redshift_list = sourceModel.redshift_list self._deflection_scaling_list = sourceModel.deflection_scaling_list self._multi_source_plane = True if self._multi_lens_plane is True: if self._deflection_scaling_list is not None: raise ValueError( 'deflection scaling for different source planes not possible in combination of ' 'multi-lens plane modeling. You have to specify the redshifts of the sources instead.' ) self._bkg_cosmo = Background(lensModel.cosmo) if self._source_redshift_list is None: self._multi_source_plane = False elif len(self._source_redshift_list) != len(light_model_list): raise ValueError( "length of redshift_list must correspond to length of light_model_list" ) elif np.max(self._source_redshift_list) > self._lensModel.z_source: raise ValueError( "redshift of source_redshift_list have to be smaler or equal to the one specified in " "the lens model.") else: self._sorted_source_redshift_index = self._index_ordering( self._source_redshift_list) self._T0z_list = [] for z_stop in self._source_redshift_list: self._T0z_list.append(self._bkg_cosmo.T_xy(0, z_stop)) z_start = 0 self._T_ij_start_list = [] self._T_ij_end_list = [] for i, index_source in enumerate( self._sorted_source_redshift_index): z_stop = self._source_redshift_list[index_source] T_ij_start, T_ij_end = self._lensModel.lens_model.transverse_distance_start_stop( z_start, z_stop, include_z_start=False) self._T_ij_start_list.append(T_ij_start) self._T_ij_end_list.append(T_ij_end) z_start = z_stop else: if self._deflection_scaling_list is None: self._multi_source_plane = False elif len(self._deflection_scaling_list) != len(light_model_list): raise ValueError( 'length of scale_factor_list must correspond to length of light_model_list!' )
def __init__(self, z_lens, z_source, cosmo=None): """ :param z_lens: redshift of lens :param z_source: redshift of source :param cosmo: astropy.cosmology instance """ self.z_lens = z_lens self.z_source = z_source self.background = Background(cosmo=cosmo)
def __init__(self, z_source, lens_model_list, redshift_list, cosmo=None, **lensmodel_kwargs): """ :param cosmo: instance of astropy.cosmology :return: Background class with instance of astropy.cosmology """ if cosmo is None: from astropy.cosmology import default_cosmology cosmo = default_cosmology.get() self._cosmo_bkg = Background(cosmo) self._z_source = z_source if not len(lens_model_list) == len(redshift_list): raise ValueError( "The length of lens_model_list does not correspond to redshift_list" ) self._lens_model_list = lens_model_list self._redshift_list = redshift_list if len(lens_model_list) < 1: self._sorted_redshift_index = [] else: self._sorted_redshift_index = self._index_ordering(redshift_list) self._lens_model = SinglePlane(lens_model_list, **lensmodel_kwargs) z_before = 0 self._T_ij_list = [] self._T_z_list = [] self._reduced2physical_factor = [] for idex in self._sorted_redshift_index: z_lens = self._redshift_list[idex] if z_before == z_lens: delta_T = 0 else: delta_T = self._cosmo_bkg.T_xy(z_before, z_lens) self._T_ij_list.append(delta_T) T_z = self._cosmo_bkg.T_xy(0, z_lens) self._T_z_list.append(T_z) factor = self._cosmo_bkg.D_xy(0, z_source) / self._cosmo_bkg.D_xy( z_lens, z_source) self._reduced2physical_factor.append(factor) z_before = z_lens delta_T = self._cosmo_bkg.T_xy(z_before, z_source) self._T_ij_list.append(delta_T) self._T_z_source = self._cosmo_bkg.T_xy(0, z_source) sum_partial = np.sum(self._T_ij_list) if np.abs(sum_partial - self._T_z_source) > 0.1: print( "Numerics in multi-plane compromised by too narrow spacing of too many redshift bins" )
class TestCosmoProp(object): def setup(self): self.z_L = 0.8 self.z_S = 3.0 from astropy.cosmology import FlatLambdaCDM cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) self.bkg = Background(cosmo=cosmo) def test_scale_factor(self): z = 0.7 assert self.bkg.a_z(z) == 1. / (1 + z) def test_rho_crit(self): assert self.bkg.rho_crit == 135955133951.10692 def test_interpol(self): from astropy.cosmology import FlatLambdaCDM cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) bkg = Background(cosmo=cosmo) bkg_interp = Background(cosmo=cosmo, interp=True, num_interp=100, z_stop=10) d_xy = bkg.d_xy(z_observer=0.1, z_source=0.8) d_xy_interp = bkg_interp.d_xy(z_observer=0.1, z_source=0.8) npt.assert_almost_equal(d_xy_interp / d_xy, 1, decimal=5)
def __init__(self, z_source, lens_model_list, redshift_list, cosmo=None): """ :param cosmo: instance of astropy.cosmology :return: Background class with instance of astropy.cosmology """ from astropy.cosmology import default_cosmology if cosmo is None: cosmo = default_cosmology.get() self._cosmo_bkg = Background(cosmo) self._z_source = z_source if not len(lens_model_list) == len(redshift_list): raise ValueError("The length of lens_model_list does not correspond to redshift_list") self._lens_model_list = lens_model_list self._redshift_list = redshift_list self._sorted_redshift_index = self._index_ordering(redshift_list) self._lens_model = SinglePlane(lens_model_list)
class TestCosmoProp(object): def setup(self): self.z_L = 0.8 self.z_S = 3.0 from astropy.cosmology import FlatLambdaCDM cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) self.bkg = Background(cosmo=cosmo) def test_scale_factor(self): z = 0.7 assert self.bkg.a_z(z) == 1. / (1 + z) def test_rho_crit(self): assert self.bkg.rho_crit == 135955133951.10692
def test_interpol(self): from astropy.cosmology import FlatLambdaCDM cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) bkg = Background(cosmo=cosmo) bkg_interp = Background(cosmo=cosmo, interp=True, num_interp=100, z_stop=10) d_xy = bkg.d_xy(z_observer=0.1, z_source=0.8) d_xy_interp = bkg_interp.d_xy(z_observer=0.1, z_source=0.8) npt.assert_almost_equal(d_xy_interp / d_xy, 1, decimal=5)
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): """ 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. """ 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) 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
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 sourde redshift of the class instance. """ def __init__(self, lens_model_list, lens_redshift_list, z_source_convention, cosmo=None, numerical_alpha_class=None): """ :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) """ self._cosmo_bkg = Background(cosmo) 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) 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 = [] self._reduced2physical_factor = [] 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) factor = self._cosmo_bkg.d_xy( 0, z_source_convention) / self._cosmo_bkg.d_xy( z_lens, z_source_convention) self._reduced2physical_factor.append(factor) z_before = z_lens 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 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 :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 def ray_shooting_partial_steps(self, x, y, alpha_x, alpha_y, z_start, z_stop, kwargs_lens, include_z_start=False): """ ray-tracing through parts of the coin, starting with (x,y) and angles (alpha_x, alpha_y) at redshift z_start and then backwards to redshift z_stop. This function differs from 'ray_shooting_partial' in that it returns the angular position of the ray at each lens plane. :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 keep_range: bool, if True, only computes the angular diameter ratio between the first and last step once :param check_convention: flag to check the image position convention (leave this alone) :return: co-moving position and angles at redshift z_stop """ z_lens_last = z_start first_deflector = True pos_x, pos_y, redshifts, Tz_list = [], [], [], [] pos_x.append(x) pos_y.append(y) redshifts.append(z_start) Tz_list.append(self._cosmo_bkg.T_xy(0, z_start)) current_z = z_lens_last 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 z_lens != current_z: new_plane = True current_z = z_lens else: new_plane = False if first_deflector is True: delta_T = self._cosmo_bkg.T_xy(z_start, z_lens) 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 new_plane: pos_x.append(x) pos_y.append(y) redshifts.append(z_lens) Tz_list.append(self._T_z_list[i]) delta_T = self._cosmo_bkg.T_xy(z_lens_last, z_stop) x, y = self._ray_step_add(x, y, alpha_x, alpha_y, delta_T) pos_x.append(x) pos_y.append(y) redshifts.append(z_stop) T_z_source = self._cosmo_bkg.T_xy(0, z_stop) Tz_list.append(T_z_source) return pos_x, pos_y, redshifts, Tz_list 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 acending 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 z_lens: lens redshift :param z_source: source redshift :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 :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 :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 :param idex_lens: redshift of the deflector plane :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): if inclusive: return z_lens >= z_start else: return z_lens > z_start
class Image2SourceMapping(object): """ this class handles multiple source planes and performs the computation of predicted surface brightness at given image positions. The class is enable to deal with an arbitrary number of different source planes. There are two different settings: Single lens plane modelling: In case of a single deflector, lenstronomy models the reduced deflection angles (matched to the source plane in single source plane mode). Each source light model can be added a number (scale_factor) that rescales the reduced deflection angle to the specific source plane. Multiple lens plane modelling: The multi-plane lens modelling requires the assumption of a cosmology and the redshifts of the multiple lens and source planes. The backwards ray-tracing is performed and stopped at the different source plane redshift to compute the mapping between source to image plane. """ def __init__(self, lensModel, sourceModel): """ :param lensModel: lenstronomy LensModel() class instance :param sourceModel: LightModel () class instance The lightModel includes: - source_scale_factor_list: list of floats corresponding to the rescaled deflection angles to the specific source components. None indicates that the list will be set to 1, meaning a single source plane model (in single lens plane mode). - source_redshift_list: list of redshifts of the light components (in multi lens plane mode) """ self._lightModel = sourceModel self._lensModel = lensModel light_model_list = sourceModel.profile_type_list self._multi_lens_plane = lensModel.multi_plane self._source_redshift_list = sourceModel.redshift_list self._deflection_scaling_list = sourceModel.deflection_scaling_list self._multi_source_plane = True if self._multi_lens_plane is True: if self._deflection_scaling_list is not None: raise ValueError('deflection scaling for different source planes not possible in combination of ' 'multi-lens plane modeling. You have to specify the redshifts of the sources instead.') self._bkg_cosmo = Background(lensModel.cosmo) if self._source_redshift_list is None: self._multi_source_plane = False elif len(self._source_redshift_list) != len(light_model_list): raise ValueError("length of redshift_list must correspond to length of light_model_list") elif np.max(self._source_redshift_list) > self._lensModel.z_source: raise ValueError("redshift of source_redshift_list have to be smaler or equal to the one specified in " "the lens model.") else: self._sorted_source_redshift_index = self._index_ordering(self._source_redshift_list) self._T0z_list = [] for z_stop in self._source_redshift_list: self._T0z_list.append(self._bkg_cosmo.T_xy(0, z_stop)) z_start = 0 self._T_ij_start_list = [] self._T_ij_end_list = [] for i, index_source in enumerate(self._sorted_source_redshift_index): z_stop = self._source_redshift_list[index_source] T_ij_start, T_ij_end = self._lensModel.lens_model.transverse_distance_start_stop(z_start, z_stop, include_z_start=False) self._T_ij_start_list.append(T_ij_start) self._T_ij_end_list.append(T_ij_end) z_start = z_stop else: if self._deflection_scaling_list is None: self._multi_source_plane = False elif len(self._deflection_scaling_list) != len(light_model_list): raise ValueError('length of scale_factor_list must correspond to length of light_model_list!') def image2source(self, x, y, kwargs_lens, index_source): """ mapping of image plane to source plane coordinates WARNING: for multi lens plane computations and multi source planes, this computation can be slow and should be used as rarely as possible. :param x: image plane coordinate (angle) :param y: image plane coordinate (angle) :param kwargs_lens: lens model kwargs list :param index_source: int, index of source model :return: source plane coordinate corresponding to the source model of index idex_source """ if self._multi_source_plane is False: x_source, y_source = self._lensModel.ray_shooting(x, y, kwargs_lens) else: if self._multi_lens_plane is False: x_alpha, y_alpha = self._lensModel.alpha(x, y, kwargs_lens) scale_factor = self._deflection_scaling_list[index_source] x_source = x - x_alpha * scale_factor y_source = y - y_alpha * scale_factor else: z_stop = self._source_redshift_list[index_source] x_ = np.zeros_like(x) y_ = np.zeros_like(y) x_comov, y_comov, alpha_x, alpha_y = self._lensModel.lens_model.ray_shooting_partial(x_, y_, x, y, 0, z_stop, kwargs_lens, include_z_start=False) T_z = self._T0z_list[index_source] x_source = x_comov / T_z y_source = y_comov / T_z return x_source, y_source def image_flux_joint(self, x, y, kwargs_lens, kwargs_source, k=None): """ :param x: coordinate in image plane :param y: coordinate in image plane :param kwargs_lens: lens model kwargs list :param kwargs_source: source model kwargs list :return: surface brightness of all joint light components at image position (x, y) """ if self._multi_source_plane is False: x_source, y_source = self._lensModel.ray_shooting(x, y, kwargs_lens) return self._lightModel.surface_brightness(x_source, y_source, kwargs_source, k=k) else: flux = np.zeros_like(x) if self._multi_lens_plane is False: x_alpha, y_alpha = self._lensModel.alpha(x, y, kwargs_lens) for i in range(len(self._deflection_scaling_list)): scale_factor = self._deflection_scaling_list[i] x_source = x - x_alpha * scale_factor y_source = y - y_alpha * scale_factor if k is None or k ==i: flux += self._lightModel.surface_brightness(x_source, y_source, kwargs_source, k=i) else: x_comov = np.zeros_like(x) y_comov = np.zeros_like(y) alpha_x, alpha_y = x, y x_source, y_source = alpha_x, alpha_y z_start = 0 for i, index_source in enumerate(self._sorted_source_redshift_index): z_stop = self._source_redshift_list[index_source] if z_stop > z_start: T_ij_start = self._T_ij_start_list[i] T_ij_end = self._T_ij_end_list[i] x_comov, y_comov, alpha_x, alpha_y = self._lensModel.lens_model.ray_shooting_partial(x_comov, y_comov, alpha_x, alpha_y, z_start, z_stop, kwargs_lens, include_z_start=False, T_ij_start=T_ij_start, T_ij_end=T_ij_end) T_z = self._T0z_list[index_source] x_source = x_comov / T_z y_source = y_comov / T_z if k is None or k == i: flux += self._lightModel.surface_brightness(x_source, y_source, kwargs_source, k=index_source) z_start = z_stop return flux def image_flux_split(self, x, y, kwargs_lens, kwargs_source): """ :param x: coordinate in image plane :param y: coordinate in image plane :param kwargs_lens: lens model kwargs list :param kwargs_source: source model kwargs list :return: list of responses of every single basis component with default amplitude amp=1, in the same order as the light_model_list """ if self._multi_source_plane is False: x_source, y_source = self._lensModel.ray_shooting(x, y, kwargs_lens) return self._lightModel.functions_split(x_source, y_source, kwargs_source) else: response = [] n = 0 if self._multi_lens_plane is False: x_alpha, y_alpha = self._lensModel.alpha(x, y, kwargs_lens) for i in range(len(self._deflection_scaling_list)): scale_factor = self._deflection_scaling_list[i] x_source = x - x_alpha * scale_factor y_source = y - y_alpha * scale_factor response_i, n_i = self._lightModel.functions_split(x_source, y_source, kwargs_source, k=i) response += response_i n += n_i else: n_i_list = [] x_comov = np.zeros_like(x) y_comov = np.zeros_like(y) alpha_x, alpha_y = x, y x_source, y_source = alpha_x, alpha_y z_start = 0 for i, index_source in enumerate(self._sorted_source_redshift_index): z_stop = self._source_redshift_list[index_source] if z_stop > z_start: T_ij_start = self._T_ij_start_list[i] T_ij_end = self._T_ij_end_list[i] x_comov, y_comov, alpha_x, alpha_y = self._lensModel.lens_model.ray_shooting_partial(x_comov, y_comov, alpha_x, alpha_y, z_start, z_stop, kwargs_lens, include_z_start=False, T_ij_start=T_ij_start, T_ij_end=T_ij_end) T_z = self._T0z_list[index_source] x_source = x_comov / T_z y_source = y_comov / T_z response_i, n_i = self._lightModel.functions_split(x_source, y_source, kwargs_source, k=index_source) n_i_list.append(n_i) response += response_i n += n_i z_start = z_stop n_list = self._lightModel.num_param_linear_list(kwargs_source) response = self._re_order_split(response, n_list) return response, n @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) return sort_index def _re_order_split(self, response, n_list): """ :param response: splitted functions in order of redshifts :param n_list: list of number of response vectors per model in order of the model list (not redshift ordered) :return: reshuffled array in order of the function definition """ counter_regular = 0 n_sum_list_regular = [] for i in range(len(self._source_redshift_list)): n_sum_list_regular += [counter_regular] counter_regular += n_list[i] reshuffled = np.zeros_like(response) n_sum_sorted = 0 for i, index in enumerate(self._sorted_source_redshift_index): n_i = n_list[index] n_sum = n_sum_list_regular[index] reshuffled[n_sum:n_sum + n_i] = response[n_sum_sorted:n_sum_sorted + n_i] n_sum_sorted += n_i return reshuffled
class MultiLens(object): """ Multi-plane lensing class """ def __init__(self, z_source, lens_model_list, redshift_list, cosmo=None): """ :param cosmo: instance of astropy.cosmology :return: Background class with instance of astropy.cosmology """ from astropy.cosmology import default_cosmology if cosmo is None: cosmo = default_cosmology.get() self._cosmo_bkg = Background(cosmo) self._z_source = z_source if not len(lens_model_list) == len(redshift_list): raise ValueError("The length of lens_model_list does not correspond to redshift_list") self._lens_model_list = lens_model_list self._redshift_list = redshift_list self._sorted_redshift_index = self._index_ordering(redshift_list) self._lens_model = SinglePlane(lens_model_list) def ray_shooting(self, theta_x, theta_y, kwargs_lens, k=None): """ ray-tracing (backwards light cone) :param theta_x: angle in x-direction on the image :param theta_y: angle in y-direction on the image :param kwargs_lens: :return: angles in the source plane """ x = np.zeros_like(theta_x) y = np.zeros_like(theta_y) z_before = 0 alpha_x = theta_x alpha_y = theta_y for idex in self._sorted_redshift_index: z_lens = self._redshift_list[idex] delta_T = self._cosmo_bkg.T_xy(z_before, z_lens) x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) alpha_x, alpha_y = self._add_deflection(x, y, alpha_x, alpha_y, kwargs_lens, idex, z_lens) z_before = z_lens delta_T = self._cosmo_bkg.T_xy(z_before, self._z_source) x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) beta_x, beta_y = self._co_moving2angle(x, y, self._z_source) return beta_x, beta_y def arrival_time(self, theta_x, theta_y, kwargs_lens, k=None): """ 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: :return: travel time in unit of days """ dt_grav = np.zeros_like(theta_x) dt_geo = np.zeros_like(theta_x) x = np.zeros_like(theta_x) y = np.zeros_like(theta_y) z_before = 0 alpha_x = theta_x alpha_y = theta_y for idex in self._sorted_redshift_index: z_lens = self._redshift_list[idex] delta_T = self._cosmo_bkg.T_xy(z_before, z_lens) dt_geo_new = self._geometrical_delay(alpha_x, alpha_y, delta_T) x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) dt_grav_new = self._gravitational_delay(x, y, kwargs_lens, idex, z_lens) alpha_x, alpha_y = self._add_deflection(x, y, alpha_x, alpha_y, kwargs_lens, idex, z_lens) dt_geo = dt_geo + dt_geo_new dt_grav = dt_grav + dt_grav_new z_before = z_lens delta_T = self._cosmo_bkg.T_xy(z_before, self._z_source) dt_geo += self._geometrical_delay(alpha_x, alpha_y, delta_T) return dt_grav + dt_geo def alpha(self, theta_x, theta_y, kwargs_lens, k=None): """ reduced deflection angle :param theta_x: angle in x-direction :param theta_y: angle in y-direction :param kwargs_lens: lens model kwargs :return: """ beta_x, beta_y = self.ray_shooting(theta_x, theta_y, kwargs_lens) alpha_x = theta_x - beta_x alpha_y = theta_y - beta_y return alpha_x, alpha_y def hessian(self, theta_x, theta_y, kwargs_lens, k=None, diff=0.000001): """ computes the hessian components f_xx, f_yy, f_xy from f_x and f_y with numerical differentiation :param theta_x: x-position (preferentially arcsec) :type theta_x: numpy array :param theta_y: y-position (preferentially arcsec) :type theta_y: numpy array :param kwargs_lens: list of keyword arguments of lens model parameters matching the lens model classes :param diff: numerical differential step (float) :return: f_xx, f_xy, f_yx, f_yy """ alpha_ra, alpha_dec = self.alpha(theta_x, theta_y, kwargs_lens) alpha_ra_dx, alpha_dec_dx = self.alpha(theta_x + diff, theta_y, kwargs_lens) alpha_ra_dy, alpha_dec_dy = self.alpha(theta_x, theta_y + diff, kwargs_lens) dalpha_rara = (alpha_ra_dx - alpha_ra)/diff dalpha_radec = (alpha_ra_dy - alpha_ra)/diff dalpha_decra = (alpha_dec_dx - alpha_dec)/diff dalpha_decdec = (alpha_dec_dy - alpha_dec)/diff f_xx = dalpha_rara f_yy = dalpha_decdec f_xy = dalpha_radec f_yx = dalpha_decra return f_xx, f_xy, f_yy def _index_ordering(self, redshift_list): """ :param redshift_list: list of redshifts :return: indexes in acending 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 < self._z_source]) if len(sort_index) < 1: raise ValueError("There is no lens object between observer at z=0 and source at z=%s" % self._z_source) return sort_index def _reduced2physical_deflection(self, alpha_reduced, z_lens, z_source): """ alpha_reduced = D_ds/Ds alpha_physical :param alpha_reduced: reduced deflection angle :param z_lens: lens redshift :param z_source: source redshift :return: physical deflection angle """ factor = self._cosmo_bkg.D_xy(0, z_source) / self._cosmo_bkg.D_xy(z_lens, z_source) return alpha_reduced * factor def _gravitational_delay(self, x, y, kwargs_lens, idex, 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 idex: index of the lens model :return: gravitational delay in units of days as seen at z=0 """ theta_x, theta_y = self._co_moving2angle(x, y, z_lens) potential = self._lens_model.potential(theta_x, theta_y, kwargs_lens, k=idex) delay_days = self._lensing_potential2time_delay(potential, z_lens, z_source=self._z_source) return -delay_days def _geometrical_delay(self, alpha_x, alpha_y, delta_T): """ geometrical delay (evaluated at z=0) of a light ray with an angle relative to the shortest path :param alpha_x: angle relative to a straight path :param alpha_y: angle relative to a straight path :param delta_T: transversal diameter distance between the start and end of the ray :return: geometrical delay in units of days """ dt_days = (alpha_x**2 + alpha_y**2) / 2. * delta_T * const.Mpc / const.c / const.day_s * const.arcsec**2 return dt_days 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.D_dt(z_lens, z_source) delay_days = const.delay_arcsec2days(potential, D_dt) return delay_days def _co_moving2angle(self, x, y, z_lens): """ transforms co-moving distances Mpc into angles on the sky (radian) :param x: co-moving distance :param y: co-moving distance :param z_lens: redshift of plane :return: angles on the sky """ T_z = self._cosmo_bkg.T_xy(0, z_lens) theta_x = x / T_z theta_y = y / T_z return theta_x, theta_y def _ray_step(self, x, y, alpha_x, alpha_y, delta_T): """ ray propagation with small angle approximation :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: transversal angular diameter distance to the next step :return: """ x_ = x + alpha_x * delta_T y_ = y + alpha_y * delta_T return x_, y_ def _add_deflection(self, x, y, alpha_x, alpha_y, kwargs_lens, idex, z_lens): """ adds the pyhsical 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 idex: index of the lens model to be added :param z_lens: redshift of the deflector plane :return: updated physical deflection after deflector plane (in a backwards ray-tracing perspective) """ theta_x, theta_y = self._co_moving2angle(x, y, z_lens) alpha_x_red, alpha_y_red = self._lens_model.alpha(theta_x, theta_y, kwargs_lens, k=idex) alpha_x_phys = self._reduced2physical_deflection(alpha_x_red, z_lens, z_source=self._z_source) alpha_y_phys = self._reduced2physical_deflection(alpha_y_red, z_lens, z_source=self._z_source) alpha_x_new = alpha_x - alpha_x_phys alpha_y_new = alpha_y - alpha_y_phys return alpha_x_new, alpha_y_new
class LensCosmo(object): """ class to manage the physical units and distances present in a single plane lens with fixed input cosmology """ def __init__(self, z_lens, z_source, cosmo=None): """ :param z_lens: redshift of lens :param z_source: redshift of source :param cosmo: astropy.cosmology instance """ self.z_lens = z_lens self.z_source = z_source self.background = Background(cosmo=cosmo) @property def D_d(self): """ :return: angular diameter distance to the deflector [Mpc] """ return self.background.D_xy(0, self.z_lens) @property def D_s(self): """ :return: angular diameter distance to the source [Mpc] """ return self.background.D_xy(0, self.z_source) @property def D_ds(self): """ :return: angular diameter distance from deflector to source [Mpc] """ return self.background.D_xy(self.z_lens, self.z_source) @property def D_dt(self): """ :return: time delay distance [Mpc] """ return (1 + self.z_lens) * self.D_d * self.D_s / self.D_ds @property def epsilon_crit(self): """ returns the critical projected mass density in units of M_sun/Mpc^2 (physical units) :return: critical projected mass density """ if not hasattr(self, '_Epsilon_Crit'): const_SI = const.c ** 2 / (4 * np.pi * const.G) #c^2/(4*pi*G) in units of [kg/m] conversion = const.Mpc / const.M_sun # converts [kg/m] to [M_sun/Mpc] factor = const_SI*conversion #c^2/(4*pi*G) in units of [M_sun/Mpc] self._Epsilon_Crit = self.D_s/(self.D_d*self.D_ds) * factor #[M_sun/Mpc^2] return self._Epsilon_Crit def phys2arcsec_lens(self, phys): """ convert physical Mpc into arc seconds :param phys: physical distance [Mpc] :return: angular diameter [arcsec] """ return phys / self.D_d/const.arcsec def arcsec2phys_lens(self, arcsec): """ convert angular to physical quantities for lens plane :param arcsec: angular size at lens plane [arcsec] :return: physical size at lens plane [Mpc] """ return arcsec * const.arcsec * self.D_d def arcsec2phys_source(self, arcsec): """ convert angular to physical quantities for source plane :param arcsec: angular size at source plane [arcsec] :return: physical size at source plane [Mpc] """ return arcsec * const.arcsec * self.D_s def kappa2proj_mass(self, kappa): """ convert convergence to projected mass M_sun/Mpc^2 :param kappa: lensing convergence :return: projected mass [M_sun/Mpc^2] """ return kappa * self.epsilon_crit def mass_in_theta_E(self, theta_E): """ mass within Einstein radius (area * epsilon crit) [M_sun] :param theta_E: Einstein radius [arcsec] :return: mass within Einstein radius [M_sun] """ mass = self.arcsec2phys_lens(theta_E) ** 2 * np.pi * self.epsilon_crit return mass def mass_in_coin(self, theta_E): """ :param theta_E: Einstein radius [arcsec] :return: mass in coin calculated in mean density of the universe """ chi_L = self.background.T_xy(0, self.z_lens) chi_S = self.background.T_xy(0, self.z_source) return 1./3 * np.pi * (chi_L * theta_E * const.arcsec) ** 2 * chi_S * self.background.rho_crit #[M_sun/Mpc**3] def time_delay_units(self, fermat_pot, kappa_ext=0): """ :param delay_unitless: in units of arcsec^2 (e.g. Fermat potential) :param kappa_ext: unit-less :return: time delay in days """ D_dt = self.D_dt / (1. - kappa_ext) * const.Mpc # eqn 7 in Suyu et al. return D_dt / const.c * fermat_pot / const.day_s * const.arcsec ** 2 # * self.arcsec2phys_lens(1.)**2
class MultiPlane(object): """ Multi-plane lensing class """ def __init__(self, z_source, lens_model_list, redshift_list, cosmo=None, **lensmodel_kwargs): """ :param cosmo: instance of astropy.cosmology :return: Background class with instance of astropy.cosmology """ if cosmo is None: from astropy.cosmology import default_cosmology cosmo = default_cosmology.get() self._cosmo_bkg = Background(cosmo) self._z_source = z_source if not len(lens_model_list) == len(redshift_list): raise ValueError( "The length of lens_model_list does not correspond to redshift_list" ) self._lens_model_list = lens_model_list self._redshift_list = redshift_list if len(lens_model_list) < 1: self._sorted_redshift_index = [] else: self._sorted_redshift_index = self._index_ordering(redshift_list) self._lens_model = SinglePlane(lens_model_list, **lensmodel_kwargs) z_before = 0 self._T_ij_list = [] self._T_z_list = [] self._reduced2physical_factor = [] for idex in self._sorted_redshift_index: z_lens = self._redshift_list[idex] if z_before == z_lens: delta_T = 0 else: delta_T = self._cosmo_bkg.T_xy(z_before, z_lens) self._T_ij_list.append(delta_T) T_z = self._cosmo_bkg.T_xy(0, z_lens) self._T_z_list.append(T_z) factor = self._cosmo_bkg.D_xy(0, z_source) / self._cosmo_bkg.D_xy( z_lens, z_source) self._reduced2physical_factor.append(factor) z_before = z_lens delta_T = self._cosmo_bkg.T_xy(z_before, z_source) self._T_ij_list.append(delta_T) self._T_z_source = self._cosmo_bkg.T_xy(0, z_source) sum_partial = np.sum(self._T_ij_list) if np.abs(sum_partial - self._T_z_source) > 0.1: print( "Numerics in multi-plane compromised by too narrow spacing of too many redshift bins" ) def ray_shooting(self, theta_x, theta_y, kwargs_lens, k=None): """ ray-tracing (backwards light cone) :param theta_x: angle in x-direction on the image :param theta_y: angle in y-direction on the image :param kwargs_lens: :return: angles in the source plane """ x = np.zeros_like(theta_x) y = np.zeros_like(theta_y) alpha_x = theta_x alpha_y = theta_y i = -1 for i, idex in enumerate(self._sorted_redshift_index): delta_T = self._T_ij_list[i] x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) alpha_x, alpha_y = self._add_deflection(x, y, alpha_x, alpha_y, kwargs_lens, i) delta_T = self._T_ij_list[i + 1] x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) beta_x, beta_y = self._co_moving2angle_source(x, y) return beta_x, beta_y def ray_shooting_partial(self, x, y, alpha_x, alpha_y, z_start, z_stop, kwargs_lens, keep_range=False, include_z_start=False): """ ray-tracing through parts of the coin, starting with (x,y) and angles (alpha_x, alpha_y) at redshift z_start and then backwards to redshfit 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 keep_range: bool, if True, only computes the angular diameter ratio between the first and last step once :return: co-moving position and angles at redshift z_stop """ z_lens_last = z_start first_deflector = True for i, idex in enumerate(self._sorted_redshift_index): z_lens = self._redshift_list[idex] if self._start_condition(include_z_start, z_lens, z_start) and z_lens <= z_stop: #if z_lens > z_start and z_lens <= z_stop: if first_deflector is True: if keep_range is True: if not hasattr(self, '_cosmo_bkg_T_start'): self._cosmo_bkg_T_start = self._cosmo_bkg.T_xy( z_start, z_lens) delta_T = self._cosmo_bkg_T_start else: delta_T = self._cosmo_bkg.T_xy(z_start, z_lens) first_deflector = False else: delta_T = self._T_ij_list[i] x, y = self._ray_step(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 keep_range is True: if not hasattr(self, '_cosmo_bkg_T_stop'): self._cosmo_bkg_T_stop = self._cosmo_bkg.T_xy( z_lens_last, z_stop) delta_T = self._cosmo_bkg_T_stop else: delta_T = self._cosmo_bkg.T_xy(z_lens_last, z_stop) x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) return x, y, alpha_x, alpha_y def ray_shooting_partial_steps(self, x, y, alpha_x, alpha_y, z_start, z_stop, kwargs_lens, include_z_start=False): """ ray-tracing through parts of the coin, starting with (x,y) and angles (alpha_x, alpha_y) at redshift z_start and then backwards to redshfit z_stop. This function differs from 'ray_shooting_partial' in that it returns the angular position of the ray at each lens plane. :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 keep_range: bool, if True, only computes the angular diameter ratio between the first and last step once :return: co-moving position and angles at redshift z_stop """ z_lens_last = z_start first_deflector = True pos_x, pos_y, redshifts, Tz_list = [], [], [], [] pos_x.append(x) pos_y.append(y) redshifts.append(z_start) Tz_list.append(self._cosmo_bkg.T_xy(0, z_start)) current_z = z_lens_last for i, idex in enumerate(self._sorted_redshift_index): z_lens = self._redshift_list[idex] if self._start_condition(include_z_start, z_lens, z_start) and z_lens <= z_stop: if z_lens != current_z: new_plane = True current_z = z_lens else: new_plane = False if first_deflector is True: delta_T = self._cosmo_bkg.T_xy(z_start, z_lens) first_deflector = False else: delta_T = self._T_ij_list[i] x, y = self._ray_step(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 new_plane: pos_x.append(x) pos_y.append(y) redshifts.append(z_lens) Tz_list.append(self._T_z_list[i]) delta_T = self._cosmo_bkg.T_xy(z_lens_last, z_stop) x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) pos_x.append(x) pos_y.append(y) redshifts.append(self._z_source) Tz_list.append(self._T_z_source) return pos_x, pos_y, redshifts, Tz_list def arrival_time(self, theta_x, theta_y, kwargs_lens, k=None): """ 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: :return: travel time in unit of days """ dt_grav = np.zeros_like(theta_x) dt_geo = np.zeros_like(theta_x) x = np.zeros_like(theta_x) y = np.zeros_like(theta_y) alpha_x = theta_x alpha_y = theta_y i = 0 for i, idex in enumerate(self._sorted_redshift_index): z_lens = self._redshift_list[idex] delta_T = self._T_ij_list[i] dt_geo_new = self._geometrical_delay(alpha_x, alpha_y, delta_T) x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) 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_geo = dt_geo + dt_geo_new dt_grav = dt_grav + dt_grav_new delta_T = self._T_ij_list[i + 1] dt_geo += self._geometrical_delay(alpha_x, alpha_y, delta_T) x, y = self._ray_step(x, y, alpha_x, alpha_y, delta_T) beta_x, beta_y = self._co_moving2angle_source(x, y) dt_geo -= self._geometrical_delay(beta_x, beta_y, self._T_z_source) return dt_grav + dt_geo def alpha(self, theta_x, theta_y, kwargs_lens, k=None): """ reduced deflection angle :param theta_x: angle in x-direction :param theta_y: angle in y-direction :param kwargs_lens: lens model kwargs :return: """ beta_x, beta_y = self.ray_shooting(theta_x, theta_y, kwargs_lens) alpha_x = theta_x - beta_x alpha_y = theta_y - beta_y return alpha_x, alpha_y def hessian(self, theta_x, theta_y, kwargs_lens, k=None, diff=0.00000001): """ computes the hessian components f_xx, f_yy, f_xy from f_x and f_y with numerical differentiation :param theta_x: x-position (preferentially arcsec) :type theta_x: numpy array :param theta_y: y-position (preferentially arcsec) :type theta_y: numpy array :param kwargs_lens: list of keyword arguments of lens model parameters matching the lens model classes :param diff: numerical differential step (float) :return: f_xx, f_xy, f_yx, f_yy """ alpha_ra, alpha_dec = self.alpha(theta_x, theta_y, kwargs_lens) alpha_ra_dx, alpha_dec_dx = self.alpha(theta_x + diff, theta_y, kwargs_lens) alpha_ra_dy, alpha_dec_dy = self.alpha(theta_x, theta_y + diff, kwargs_lens) dalpha_rara = (alpha_ra_dx - alpha_ra) / diff dalpha_radec = (alpha_ra_dy - alpha_ra) / diff dalpha_decra = (alpha_dec_dx - alpha_dec) / diff dalpha_decdec = (alpha_dec_dy - alpha_dec) / diff f_xx = dalpha_rara f_yy = dalpha_decdec f_xy = dalpha_radec f_yx = dalpha_decra return f_xx, f_xy, f_yx, f_yy def _index_ordering(self, redshift_list): """ :param redshift_list: list of redshifts :return: indexes in acending 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 < self._z_source]) if len(sort_index) < 1: raise ValueError( "There is no lens object between observer at z=0 and source at z=%s" % self._z_source) return sort_index def _reduced2physical_deflection(self, alpha_reduced, idex_lens): """ alpha_reduced = D_ds/Ds alpha_physical :param alpha_reduced: reduced deflection angle :param z_lens: lens redshift :param z_source: source redshift :return: physical deflection angle """ factor = self._reduced2physical_factor[idex_lens] #factor = self._cosmo_bkg.D_xy(0, z_source) / self._cosmo_bkg.D_xy(z_lens, z_source) return alpha_reduced * factor def _gravitational_delay(self, x, y, kwargs_lens, idex, 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 idex: index of the lens model :return: gravitational delay in units of days as seen at z=0 """ theta_x, theta_y = self._co_moving2angle(x, y, idex) potential = self._lens_model.potential( theta_x, theta_y, kwargs_lens, k=self._sorted_redshift_index[idex]) delay_days = self._lensing_potential2time_delay( potential, z_lens, z_source=self._z_source) return -delay_days def _geometrical_delay(self, alpha_x, alpha_y, delta_T): """ geometrical delay (evaluated at z=0) of a light ray with an angle relative to the shortest path :param alpha_x: angle relative to a straight path :param alpha_y: angle relative to a straight path :param delta_T: transversal diameter distance between the start and end of the ray :return: geometrical delay in units of days """ dt_days = ( alpha_x**2 + alpha_y**2 ) / 2. * delta_T * const.Mpc / const.c / const.day_s * const.arcsec**2 return dt_days 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.D_dt(z_lens, z_source) delay_days = const.delay_arcsec2days(potential, D_dt) return delay_days def _co_moving2angle(self, x, y, idex): """ transforms co-moving distances Mpc into angles on the sky (radian) :param x: co-moving distance :param y: co-moving distance :param z_lens: redshift of plane :return: angles on the sky """ T_z = self._T_z_list[idex] #T_z = self._cosmo_bkg.T_xy(0, z_lens) theta_x = x / T_z theta_y = y / T_z return theta_x, theta_y def _co_moving2angle_source(self, x, y): """ special case of the co_moving2angle definition at the source redshift :param x: :param y: :return: """ T_z = self._T_z_source theta_x = x / T_z theta_y = y / T_z return theta_x, theta_y def _ray_step(self, x, y, alpha_x, alpha_y, delta_T): """ ray propagation with small angle approximation :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: transversal angular diameter distance to the next step :return: """ x_ = x + alpha_x * delta_T y_ = y + alpha_y * delta_T return x_, y_ def _add_deflection(self, x, y, alpha_x, alpha_y, kwargs_lens, idex): """ adds the pyhsical 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 idex: index of the lens model to be added :param idex_lens: redshift of the deflector plane :return: updated physical deflection after deflector plane (in a backwards ray-tracing perspective) """ theta_x, theta_y = self._co_moving2angle(x, y, idex) alpha_x_red, alpha_y_red = self._lens_model.alpha( theta_x, theta_y, kwargs_lens, k=self._sorted_redshift_index[idex]) alpha_x_phys = self._reduced2physical_deflection(alpha_x_red, idex) alpha_y_phys = self._reduced2physical_deflection(alpha_y_red, idex) alpha_x_new = alpha_x - alpha_x_phys alpha_y_new = alpha_y - alpha_y_phys return alpha_x_new, alpha_y_new def _start_condition(self, inclusive, z_lens, z_start): if inclusive: return z_lens >= z_start else: return z_lens > z_start
def test_foreground_shear(self): """ scenario: a shear field in the foreground of the main deflector is placed we compute the expected shear on the lens plain and effectively model the same system in a single plane configuration We check for consistency of the two approaches and whether the specific redshift of the foreground shear field has an impact on the arrival time surface :return: """ z_source = 1.5 z_lens = 0.5 z_shear = 0.2 x, y = np.array([1., 0.]), np.array([0., 2.]) from astropy.cosmology import default_cosmology from lenstronomy.Cosmo.background import Background cosmo = default_cosmology.get() cosmo_bkg = Background(cosmo) e1, e2 = 0.01, 0.01 # shear terms caused by z_shear on z_source lens_model_list = ['SIS', 'SHEAR'] redshift_list = [z_lens, z_shear] lensModelMutli = MultiPlane(z_source=z_source, lens_model_list=lens_model_list, lens_redshift_list=redshift_list) kwargs_lens_multi = [{ 'theta_E': 1, 'center_x': 0, 'center_y': 0 }, { 'e1': e1, 'e2': e2 }] alpha_x_multi, alpha_y_multi = lensModelMutli.alpha( x, y, kwargs_lens_multi) t_multi = lensModelMutli.arrival_time(x, y, kwargs_lens_multi) dt_multi = t_multi[0] - t_multi[1] physical_shear = cosmo_bkg.D_xy(0, z_source) / cosmo_bkg.D_xy( z_shear, z_source) foreground_factor = cosmo_bkg.D_xy(z_shear, z_lens) / cosmo_bkg.D_xy( 0, z_lens) * physical_shear print(foreground_factor) lens_model_simple_list = ['SIS', 'FOREGROUND_SHEAR', 'SHEAR'] kwargs_lens_single = [{ 'theta_E': 1, 'center_x': 0, 'center_y': 0 }, { 'e1': e1 * foreground_factor, 'e2': e2 * foreground_factor }, { 'e1': e1, 'e2': e2 }] lensModel = LensModel(lens_model_list=lens_model_simple_list) alpha_x_simple, alpha_y_simple = lensModel.alpha( x, y, kwargs_lens_single) npt.assert_almost_equal(alpha_x_simple, alpha_x_multi, decimal=8) npt.assert_almost_equal(alpha_y_simple, alpha_y_multi, decimal=8) ra_source, dec_source = lensModel.ray_shooting(x, y, kwargs_lens_single) ra_source_multi, dec_source_multi = lensModelMutli.ray_shooting( x, y, kwargs_lens_multi) npt.assert_almost_equal(ra_source, ra_source_multi, decimal=8) npt.assert_almost_equal(dec_source, dec_source_multi, decimal=8) fermat_pot = lensModel.fermat_potential(x, y, ra_source, dec_source, kwargs_lens_single) from lenstronomy.Cosmo.lens_cosmo import LensCosmo lensCosmo = LensCosmo(z_lens, z_source, cosmo=cosmo) Dt = lensCosmo.D_dt print(lensCosmo.D_dt) #t_simple = const.delay_arcsec2days(fermat_pot, Dt) t_simple = lensCosmo.time_delay_units(fermat_pot) dt_simple = t_simple[0] - t_simple[1] print(t_simple, t_multi) npt.assert_almost_equal(dt_simple / dt_multi, 1, decimal=2)
def setup(self): self.z_L = 0.8 self.z_S = 3.0 from astropy.cosmology import FlatLambdaCDM cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) self.bkg = Background(cosmo=cosmo)
class LensCosmo(object): """ class to manage the physical units and distances present in a single plane lens with fixed input cosmology """ def __init__(self, z_lens, z_source, cosmo=None): """ :param z_lens: redshift of lens :param z_source: redshift of source :param cosmo: astropy.cosmology instance """ self.z_lens = z_lens self.z_source = z_source self.background = Background(cosmo=cosmo) self.nfw_param = NFWParam() def a_z(self, z): """ convert redshift into scale factor :param z: redshift :return: scale factor """ return 1. / (1. + z) @property def h(self): return self.background.cosmo.H(0).value / 100. @property def D_d(self): """ :return: angular diameter distance to the deflector [Mpc] """ return self.background.D_xy(0, self.z_lens) @property def D_s(self): """ :return: angular diameter distance to the source [Mpc] """ return self.background.D_xy(0, self.z_source) @property def D_ds(self): """ :return: angular diameter distance from deflector to source [Mpc] """ return self.background.D_xy(self.z_lens, self.z_source) @property def D_dt(self): """ :return: time delay distance [Mpc] """ return (1 + self.z_lens) * self.D_d * self.D_s / self.D_ds @property def epsilon_crit(self): """ returns the critical projected mass density in units of M_sun/Mpc^2 (physical units) :return: critical projected mass density """ if not hasattr(self, '_Epsilon_Crit'): const_SI = const.c**2 / (4 * np.pi * const.G ) #c^2/(4*pi*G) in units of [kg/m] conversion = const.Mpc / const.M_sun # converts [kg/m] to [M_sun/Mpc] factor = const_SI * conversion #c^2/(4*pi*G) in units of [M_sun/Mpc] self._Epsilon_Crit = self.D_s / ( self.D_d * self.D_ds) * factor #[M_sun/Mpc^2] return self._Epsilon_Crit def phys2arcsec_lens(self, phys): """ convert physical Mpc into arc seconds :param phys: physical distance [Mpc] :return: angular diameter [arcsec] """ return phys / self.D_d / const.arcsec def arcsec2phys_lens(self, arcsec): """ convert angular to physical quantities for lens plane :param arcsec: angular size at lens plane [arcsec] :return: physical size at lens plane [Mpc] """ return arcsec * const.arcsec * self.D_d def arcsec2phys_source(self, arcsec): """ convert angular to physical quantities for source plane :param arcsec: angular size at source plane [arcsec] :return: physical size at source plane [Mpc] """ return arcsec * const.arcsec * self.D_s def kappa2proj_mass(self, kappa): """ convert convergence to projected mass M_sun/Mpc^2 :param kappa: lensing convergence :return: projected mass [M_sun/Mpc^2] """ return kappa * self.epsilon_crit def mass_in_theta_E(self, theta_E): """ mass within Einstein radius (area * epsilon crit) [M_sun] :param theta_E: Einstein radius [arcsec] :return: mass within Einstein radius [M_sun] """ mass = self.arcsec2phys_lens(theta_E)**2 * np.pi * self.epsilon_crit return mass def mass_in_coin(self, theta_E): """ :param theta_E: Einstein radius [arcsec] :return: mass in coin calculated in mean density of the universe """ chi_L = self.background.T_xy(0, self.z_lens) chi_S = self.background.T_xy(0, self.z_source) return 1. / 3 * np.pi * ( chi_L * theta_E * const.arcsec )**2 * chi_S * self.background.rho_crit #[M_sun/Mpc**3] def time_delay_units(self, fermat_pot, kappa_ext=0): """ :param fermat_pot: in units of arcsec^2 (e.g. Fermat potential) :param kappa_ext: unit-less :return: time delay in days """ D_dt = self.D_dt / (1. - kappa_ext) * const.Mpc # eqn 7 in Suyu et al. return D_dt / const.c * fermat_pot / const.day_s * const.arcsec**2 # * self.arcsec2phys_lens(1.)**2 def time_delay2fermat_pot(self, dt): """ :param dt: time delay in units of days :return: Fermat potential in units arcsec**2 for a given cosmology """ D_dt = self.D_dt * const.Mpc return dt * const.c * const.day_s / D_dt / const.arcsec**2 def nfw_angle2physical(self, Rs_angle, alpha_Rs): """ converts the angular parameters into the physical ones for an NFW profile :param alpha_Rs: observed bending angle at the scale radius in units of arcsec :param Rs: scale radius in units of arcsec :return: M200, r200, Rs_physical, c """ Rs = Rs_angle * const.arcsec * self.D_d theta_scaled = alpha_Rs * self.epsilon_crit * self.D_d * const.arcsec rho0 = theta_scaled / (4 * Rs**2 * (1 + np.log(1. / 2.))) rho0_com = rho0 / self.h**2 * self.a_z(self.z_lens)**3 c = self.nfw_param.c_rho0(rho0_com) r200 = c * Rs M200 = self.nfw_param.M_r200( r200 * self.h / self.a_z(self.z_lens)) / self.h return rho0, Rs, c, r200, M200 def nfw_physical2angle(self, M, c): """ converts the physical mass and concentration parameter of an NFW profile into the lensing quantities :param M: mass enclosed 200 rho_crit in units of M_sun :param c: NFW concentration parameter (r200/r_s) :return: alpha_Rs (observed bending angle at the scale radius, Rs_angle (angle at scale radius) (in units of arcsec) """ rho0, Rs, r200 = self.nfwParam_physical(M, c) Rs_angle = Rs / self.D_d / const.arcsec # Rs in arcsec alpha_Rs = rho0 * (4 * Rs**2 * (1 + np.log(1. / 2.))) return Rs_angle, alpha_Rs / self.epsilon_crit / self.D_d / const.arcsec def nfwParam_physical(self, M, c): """ returns the NFW parameters in physical units :param M: physical mass in M_sun :param c: concentration :return: """ r200 = self.nfw_param.r200_M(M * self.h) / self.h * self.a_z( self.z_lens) # physical radius r200 rho0 = self.nfw_param.rho0_c(c) * self.h**2 / self.a_z( self.z_lens)**3 # physical density in M_sun/Mpc**3 Rs = r200 / c return rho0, Rs, r200 def sis_theta_E2sigma_v(self, theta_E): """ converts the lensing Einstein radius into a physical velocity dispersion :param theta_E: Einstein radius (in arcsec) :return: velocity dispersion in units (km/s) """ v_sigma_c2 = theta_E * const.arcsec / (4 * np.pi) * self.D_s / self.D_ds return np.sqrt(v_sigma_c2) * const.c / 1000 def sis_sigma_v2theta_E(self, v_sigma): """ converts the velocity dispersion into an Einstein radius for a SIS profile :param v_sigma: velocity dispersion (km/s) :return: theta_E (arcsec) """ theta_E = 4 * np.pi * ( v_sigma * 1000. / const.c)**2 * self.D_ds / self.D_s / const.arcsec return theta_E
class LensCosmo(object): """ class to manage the physical units and distances present in a single plane lens with fixed input cosmology """ def __init__(self, z_lens, z_source, cosmo=None): """ :param z_lens: redshift of lens :param z_source: redshift of source :param cosmo: astropy.cosmology instance """ self.z_lens = z_lens self.z_source = z_source self.background = Background(cosmo=cosmo) self.nfw_param = NFWParam(cosmo=cosmo) def a_z(self, z): """ convert redshift into scale factor :param z: redshift :return: scale factor """ return 1. / (1. + z) @property def h(self): return self.background.cosmo.H(0).value / 100. @property def dd(self): """ :return: angular diameter distance to the deflector [Mpc] """ return self.background.d_xy(0, self.z_lens) @property def ds(self): """ :return: angular diameter distance to the source [Mpc] """ return self.background.d_xy(0, self.z_source) @property def dds(self): """ :return: angular diameter distance from deflector to source [Mpc] """ return self.background.d_xy(self.z_lens, self.z_source) @property def ddt(self): """ :return: time delay distance [Mpc] """ return (1 + self.z_lens) * self.dd * self.ds / self.dds @property def sigma_crit(self): """ returns the critical projected lensing mass density in units of M_sun/Mpc^2 :return: critical projected lensing mass density """ if not hasattr(self, '_sigma_crit_mpc'): const_SI = const.c**2 / (4 * np.pi * const.G ) # c^2/(4*pi*G) in units of [kg/m] conversion = const.Mpc / const.M_sun # converts [kg/m] to [M_sun/Mpc] factor = const_SI * conversion # c^2/(4*pi*G) in units of [M_sun/Mpc] self._sigma_crit_mpc = self.ds / ( self.dd * self.dds) * factor # [M_sun/Mpc^2] return self._sigma_crit_mpc @property def sigma_crit_angle(self): """ returns the critical surface density in units of M_sun/arcsec^2 (in physical solar mass units) when provided a physical mass per physical Mpc^2 :return: critical projected mass density """ if not hasattr(self, '_sigma_crit_arcsec'): const_SI = const.c**2 / (4 * np.pi * const.G ) # c^2/(4*pi*G) in units of [kg/m] conversion = const.Mpc / const.M_sun # converts [kg/m] to [M_sun/Mpc] factor = const_SI * conversion # c^2/(4*pi*G) in units of [M_sun/Mpc] self._sigma_crit_arcsec = self.ds / ( self.dd * self.dds) * factor * ( self.dd * const.arcsec)**2 # [M_sun/arcsec^2] return self._sigma_crit_arcsec def phys2arcsec_lens(self, phys): """ convert physical Mpc into arc seconds :param phys: physical distance [Mpc] :return: angular diameter [arcsec] """ return phys / self.dd / const.arcsec def arcsec2phys_lens(self, arcsec): """ convert angular to physical quantities for lens plane :param arcsec: angular size at lens plane [arcsec] :return: physical size at lens plane [Mpc] """ return arcsec * const.arcsec * self.dd def arcsec2phys_source(self, arcsec): """ convert angular to physical quantities for source plane :param arcsec: angular size at source plane [arcsec] :return: physical size at source plane [Mpc] """ return arcsec * const.arcsec * self.ds def kappa2proj_mass(self, kappa): """ convert convergence to projected mass M_sun/Mpc^2 :param kappa: lensing convergence :return: projected mass [M_sun/Mpc^2] """ return kappa * self.sigma_crit def mass_in_theta_E(self, theta_E): """ mass within Einstein radius (area * epsilon crit) [M_sun] :param theta_E: Einstein radius [arcsec] :return: mass within Einstein radius [M_sun] """ mass = self.arcsec2phys_lens(theta_E)**2 * np.pi * self.sigma_crit return mass def mass_in_coin(self, theta_E): """ :param theta_E: Einstein radius [arcsec] :return: mass in coin calculated in mean density of the universe """ chi_L = self.background.T_xy(0, self.z_lens) chi_S = self.background.T_xy(0, self.z_source) return 1. / 3 * np.pi * ( chi_L * theta_E * const.arcsec )**2 * chi_S * self.background.rho_crit #[M_sun/Mpc**3] def time_delay_units(self, fermat_pot, kappa_ext=0): """ :param fermat_pot: in units of arcsec^2 (e.g. Fermat potential) :param kappa_ext: unit-less external shear not accounted for in the Fermat potential :return: time delay in days """ D_dt = self.ddt * (1. - kappa_ext) * const.Mpc # eqn 7 in Suyu et al. return D_dt / const.c * fermat_pot / const.day_s * const.arcsec**2 # * self.arcsec2phys_lens(1.)**2 def time_delay2fermat_pot(self, dt): """ :param dt: time delay in units of days :return: Fermat potential in units arcsec**2 for a given cosmology """ D_dt = self.ddt * const.Mpc return dt * const.c * const.day_s / D_dt / const.arcsec**2 def nfw_angle2physical(self, Rs_angle, alpha_Rs): """ converts the angular parameters into the physical ones for an NFW profile :param alpha_Rs: observed bending angle at the scale radius in units of arcsec :param Rs: scale radius in units of arcsec :return: rho0, Rs, c, r200, M200 """ Rs = Rs_angle * const.arcsec * self.dd theta_scaled = alpha_Rs * self.sigma_crit * self.dd * const.arcsec rho0 = theta_scaled / (4 * Rs**2 * (1 + np.log(1. / 2.))) rho0_com = rho0 / self.h**2 c = self.nfw_param.c_rho0(rho0_com, self.z_lens) r200 = c * Rs M200 = self.nfw_param.M_r200(r200 * self.h, self.z_lens) / self.h return rho0, Rs, c, r200, M200 def nfw_physical2angle(self, M, c): """ converts the physical mass and concentration parameter of an NFW profile into the lensing quantities :param M: mass enclosed 200 rho_crit in units of M_sun (physical units, meaning no little h) :param c: NFW concentration parameter (r200/r_s) :return: Rs_angle (angle at scale radius) (in units of arcsec), alpha_Rs (observed bending angle at the scale radius """ rho0, Rs, r200 = self.nfwParam_physical(M, c) Rs_angle = Rs / self.dd / const.arcsec # Rs in arcsec alpha_Rs = rho0 * (4 * Rs**2 * (1 + np.log(1. / 2.))) return Rs_angle, alpha_Rs / self.sigma_crit / self.dd / const.arcsec def nfwParam_physical(self, M, c): """ returns the NFW parameters in physical units :param M: physical mass in M_sun :param c: concentration :return: rho0, Rs, r200 """ r200 = self.nfw_param.r200_M( M * self.h, self.z_lens) / self.h # physical radius r200 rho0 = self.nfw_param.rho0_c( c, self.z_lens) * self.h**2 # physical density in M_sun/Mpc**3 Rs = r200 / c return rho0, Rs, r200 def nfw_M_theta_vir(self, M): """ returns virial radius in angular units of arc seconds on the sky :param M: physical mass in M_sun :return: angle (in arc seconds) of the virial radius """ r200 = self.nfw_param.r200_M( M * self.h, self.z_lens) / self.h # physical radius r200 theta_r200 = r200 / self.dd / const.arcsec return theta_r200 def sis_theta_E2sigma_v(self, theta_E): """ converts the lensing Einstein radius into a physical velocity dispersion :param theta_E: Einstein radius (in arcsec) :return: velocity dispersion in units (km/s) """ v_sigma_c2 = theta_E * const.arcsec / (4 * np.pi) * self.ds / self.dds return np.sqrt(v_sigma_c2) * const.c / 1000 def sis_sigma_v2theta_E(self, v_sigma): """ converts the velocity dispersion into an Einstein radius for a SIS profile :param v_sigma: velocity dispersion (km/s) :return: theta_E (arcsec) """ theta_E = 4 * np.pi * (v_sigma * 1000. / const.c)**2 * self.dds / self.ds / const.arcsec return theta_E def uldm_angular2phys(self, kappa_0, theta_c): """ converts the anguar parameters entering the LensModel Uldm() (Ultra Light Dark Matter) class in physical masses, i.e. the total soliton mass and the mass of the particle :param kappa_0: central convergence of profile :param theta_c: core radius (in arcseconds) :return: m_eV_log10, M_sol_log10, the log10 of the masses, m in eV and M in M_sun """ D_Lens = self.dd * 10**6 # in parsec Sigma_c = self.sigma_crit * 10**(-12) # in M_sun / parsec^2 r_c = theta_c * const.arcsec * D_Lens rho0 = 2048 * np.sqrt(0.091) * kappa_0 * Sigma_c / (429 * np.pi * r_c) m_log10 = -22 + 0.5 * np.log10(190 / rho0 * (r_c / 100)**(-4)) M_log10 = 9 + np.log10(160 * 1.4 / r_c) - 2 * (m_log10 + 22) return m_log10, M_log10 def uldm_mphys2angular(self, m_log10, M_log10): """ converts physical ULDM mass in the ones, in angular units, that enter the LensModel Uldm() class :param m_log10: exponent of ULDM mass in eV :param M_log10: exponent of soliton mass in M_sun :return: kappa_0, theta_c, the central convergence and core radius (in arcseconds) """ D_Lens = self.dd * 10**6 # in parsec Sigma_c = self.sigma_crit * 10**(-12) # in M_sun/parsec^2 m22 = 10**(m_log10 + 22) M9 = 10**(M_log10 - 9) r_c = 160 * 1.4 * m22**(-2) * M9**(-1) # core radius in parsec rho0 = 190 * m22**(-2) * (r_c / 100)**( -4) # central density in M_sun/parsec^3 kappa_0 = 429 * np.pi * rho0 * r_c / (2048 * np.sqrt(0.091) * Sigma_c) theta_c = r_c / D_Lens / const.arcsec return kappa_0, theta_c