class TestLensModel(object): """ tests the source model routines """ def setup(self): self.lensModel = SinglePlane(['GAUSSIAN']) self.kwargs = [{ 'amp': 1., 'sigma_x': 2., 'sigma_y': 2., 'center_x': 0., 'center_y': 0. }] def test_potential(self): output = self.lensModel.potential(x=1., y=1., kwargs=self.kwargs) assert output == 0.77880078307140488 / (8 * np.pi) def test_alpha(self): output1, output2 = self.lensModel.alpha(x=1., y=1., kwargs=self.kwargs) assert output1 == -0.19470019576785122 / (8 * np.pi) assert output2 == -0.19470019576785122 / (8 * np.pi) def test_ray_shooting(self): delta_x, delta_y = self.lensModel.ray_shooting(x=1., y=1., kwargs=self.kwargs) assert delta_x == 1 + 0.19470019576785122 / (8 * np.pi) assert delta_y == 1 + 0.19470019576785122 / (8 * np.pi) def test_mass_2d(self): lensModel = SinglePlane(['GAUSSIAN_KAPPA']) output = lensModel.mass_2d(r=1, kwargs=self.kwargs) assert output == 0.11750309741540453
def __init__(self, kwargs_model, kwargs_cosmo, interpol_grid_num=100, log_integration=False, max_integrate=100, min_integrate=0.001): """ :param interpol_grid_num: :param log_integration: :param max_integrate: :param min_integrate: """ mass_profile_list = kwargs_model.get('mass_profile_list') light_profile_list = kwargs_model.get('light_profile_list') anisotropy_model = kwargs_model.get('anisotropy_model') self._interp_grid_num = interpol_grid_num self._log_int = log_integration self._max_integrate = max_integrate # maximal integration (and interpolation) in units of arcsecs self._min_integrate = min_integrate # min integration (and interpolation) in units of arcsecs self._max_interpolate = max_integrate # we chose to set the interpolation range to the integration range self._min_interpolate = min_integrate # we chose to set the interpolation range to the integration range self.lightProfile = LightProfile(light_profile_list, interpol_grid_num=interpol_grid_num, max_interpolate=max_integrate, min_interpolate=min_integrate) Anisotropy.__init__(self, anisotropy_type=anisotropy_model) self.cosmo = Cosmo(**kwargs_cosmo) self._mass_profile = SinglePlane(mass_profile_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 test_density(self): theta_E = 1 r = 1 lensModel = SinglePlane(lens_model_list=['SIS']) density = lensModel.density(r=r, kwargs=[{'theta_E': theta_E}]) sis = SIS() density_model = sis.density_lens(r=r, theta_E=theta_E) npt.assert_almost_equal(density, density_model, decimal=8)
class TestLensModel(object): """ tests the source model routines """ def setup(self): self.lensModel = SinglePlane(['GAUSSIAN']) self.kwargs = [{'amp': 1., 'sigma_x': 2., 'sigma_y': 2., 'center_x': 0., 'center_y': 0.}] def test_potential(self): output = self.lensModel.potential(x=1., y=1., kwargs=self.kwargs) assert output == 0.77880078307140488/(8*np.pi) def test_alpha(self): output1, output2 = self.lensModel.alpha(x=1., y=1., kwargs=self.kwargs) assert output1 == -0.19470019576785122/(8*np.pi) assert output2 == -0.19470019576785122/(8*np.pi) def test_ray_shooting(self): delta_x, delta_y = self.lensModel.ray_shooting(x=1., y=1., kwargs=self.kwargs) assert delta_x == 1 + 0.19470019576785122/(8*np.pi) assert delta_y == 1 + 0.19470019576785122/(8*np.pi) def test_mass_2d(self): lensModel = SinglePlane(['GAUSSIAN_KAPPA']) kwargs = [{'amp': 1., 'sigma': 2., 'center_x': 0., 'center_y': 0.}] output = lensModel.mass_2d(r=1, kwargs=kwargs) assert output == 0.11750309741540453 def test_density(self): theta_E = 1 r = 1 lensModel = SinglePlane(lens_model_list=['SIS']) density = lensModel.density(r=r, kwargs=[{'theta_E': theta_E}]) sis = SIS() density_model = sis.density_lens(r=r, theta_E=theta_E) npt.assert_almost_equal(density, density_model, decimal=8) def test_bool_list(self): lensModel = SinglePlane(['SPEP', 'SHEAR']) kwargs = [{'theta_E': 1, 'gamma': 2, 'e1': 0.1, 'e2': -0.1, 'center_x': 0, 'center_y': 0}, {'gamma1': 0.01, 'gamma2': -0.02}] alphax_1, alphay_1 = lensModel.alpha(1, 1, kwargs, k=0) alphax_1_list, alphay_1_list = lensModel.alpha(1, 1, kwargs, k=[0]) npt.assert_almost_equal(alphax_1, alphax_1_list, decimal=5) npt.assert_almost_equal(alphay_1, alphay_1_list, decimal=5) alphax_1_1, alphay_1_1 = lensModel.alpha(1, 1, kwargs, k=0) alphax_1_2, alphay_1_2 = lensModel.alpha(1, 1, kwargs, k=1) alphax_full, alphay_full = lensModel.alpha(1, 1, kwargs, k=None) npt.assert_almost_equal(alphax_1_1 + alphax_1_2, alphax_full, decimal=5) npt.assert_almost_equal(alphay_1_1 + alphay_1_2, alphay_full, decimal=5) def test_init(self): lens_model_list = ['TNFW', 'TRIPLE_CHAMELEON', 'SHEAR_GAMMA_PSI', 'CURVED_ARC', 'NFW_MC', 'ARC_PERT','MULTIPOLE'] lensModel = SinglePlane(lens_model_list=lens_model_list) assert lensModel.func_list[0].param_names[0] == 'Rs'
class MassProfile(object): """ mass profile class, only works if all the profiles are at one single lens plane """ def __init__(self, profile_list, kwargs_cosmo={ 'D_d': 1000, 'D_s': 2000, 'D_ds': 500 }, interpol_grid_num=1000, max_interpolate=100, min_interpolate=0.001): """ :param profile_list: """ self.model = SinglePlane(profile_list) self.cosmo = Cosmo(**kwargs_cosmo) self._interp_grid_num = interpol_grid_num self._max_interpolate = max_interpolate self._min_interpolate = min_interpolate def mass_3d_interp(self, r, kwargs, new_compute=False): """ :param r: in arc seconds :param kwargs: lens model parameters in arc seconds :return: mass enclosed physical radius in kg """ if not hasattr(self, '_log_mass_3d') or new_compute is True: r_array = np.logspace(np.log10(self._min_interpolate), np.log10(self._max_interpolate), self._interp_grid_num) mass_3d_array = self.model.mass_3d(r_array, kwargs) mass_3d_array[mass_3d_array < 10.**(-10)] = 10.**(-10) mass_dim_array = mass_3d_array * const.arcsec ** 2 * self.cosmo.D_d * self.cosmo.D_s \ / self.cosmo.D_ds * const.Mpc * const.c ** 2 / (4 * np.pi * const.G) f = interp1d(np.log(r_array), np.log(mass_dim_array / r_array), fill_value="extrapolate") self._log_mass_3d = f return np.exp(self._log_mass_3d(np.log(r))) * r def mass_3d(self, r, kwargs): """ mass enclosed a 3d radius :param r: in arc seconds :param kwargs: lens model parameters in arc seconds :return: mass enclosed physical radius in kg """ mass_dimless = self.model.mass_3d(r, kwargs) mass_dim = mass_dimless * const.arcsec ** 2 * self.cosmo.D_d * self.cosmo.D_s \ / self.cosmo.D_ds * const.Mpc * const.c ** 2 / (4 * np.pi * const.G) return mass_dim
def setup(self): self.lensModel = SinglePlane(['GAUSSIAN']) self.kwargs = [{ 'amp': 1., 'sigma_x': 2., 'sigma_y': 2., 'center_x': 0., 'center_y': 0. }]
class MassProfile(object): """ mass profile class """ def __init__(self, profile_list, kwargs_cosmo={ 'D_d': 1000, 'D_s': 2000, 'D_ds': 500 }, kwargs_numerics={}): """ :param profile_list: """ kwargs_options = {'lens_model_list': profile_list} self.model = SinglePlane(profile_list) self.cosmo = Cosmo(kwargs_cosmo) self._interp_grid_num = kwargs_numerics.get('interpol_grid_num', 1000) self._max_interpolate = kwargs_numerics.get('max_integrate', 100) self._min_interpolate = kwargs_numerics.get('min_integrate', 0.0001) def mass_3d_interp(self, r, kwargs, new_compute=False): """ :param r: in arc seconds :param kwargs: lens model parameters in arc seconds :return: mass enclosed physical radius in kg """ if not hasattr(self, '_log_mass_3d') or new_compute is True: r_array = np.logspace(np.log10(self._min_interpolate), np.log10(self._max_interpolate), self._interp_grid_num) mass_3d_array = self.model.mass_3d(r_array, kwargs) mass_3d_array[mass_3d_array < 10.**(-10)] = 10.**(-10) mass_dim_array = mass_3d_array * const.arcsec ** 3 * self.cosmo.D_d ** 2 * self.cosmo.D_s \ / self.cosmo.D_ds * const.Mpc * const.c ** 2 / (4 * np.pi * const.G) f = interp1d(np.log(r_array), np.log(mass_dim_array / r_array), fill_value="extrapolate") self._log_mass_3d = f return np.exp(self._log_mass_3d(np.log(r))) * r def mass_3d(self, r, kwargs): """ :param r: in arc seconds :param kwargs: lens model parameters in arc seconds :return: mass enclosed physical radius in kg """ mass_dimless = self.model.mass_3d(r, kwargs) mass_dim = mass_dimless * const.arcsec ** 3 * self.cosmo.D_d ** 2 * self.cosmo.D_s \ / self.cosmo.D_ds * const.Mpc * const.c ** 2 / (4 * np.pi * const.G) return mass_dim
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 test_raise(self): """ check whether raises occurs if fastell4py is not installed :return: """ if bool_test is False: with self.assertRaises(ImportError): SinglePlane(lens_model_list=['PEMD']) with self.assertRaises(ImportError): SinglePlane(lens_model_list=['SPEMD']) else: SinglePlane(lens_model_list=['PEMD', 'SPEMD'])
def __init__(self, lens_model_list, z_source=None, redshift_list=None, cosmo=None, multi_plane=False): """ :param lens_model_list: list of strings with lens model names :param foreground_shear: bool, when True, models a foreground non-linear shear distortion """ self.lens_model_list = lens_model_list self.z_source = z_source self.redshift_list = redshift_list self.cosmo = cosmo self.multi_plane = multi_plane if multi_plane is True: self.lens_model = MultiLens(z_source, lens_model_list, redshift_list, cosmo=cosmo) else: self.lens_model = SinglePlane(lens_model_list)
def test_init(self): lens_model_list = [ 'TNFW', 'TRIPLE_CHAMELEON', 'SHEAR_GAMMA_PSI', 'CURVED_ARC', 'NFW_MC', 'ARC_PERT' ] lensModel = SinglePlane(lens_model_list=lens_model_list) assert lensModel.func_list[0].param_names[0] == 'Rs'
def __init__(self, lens_model_list, kwargs_fixed, kwargs_lower=None, kwargs_upper=None, num_images=0, solver_type='NONE', num_shapelet_lens=0): """ :param kwargs_options: :param kwargs_fixed: """ self.model_list = lens_model_list self.kwargs_fixed = kwargs_fixed self._num_images = num_images self._solver_type = solver_type self._num_shapelet_lens = num_shapelet_lens lens_model = SinglePlane(lens_model_list=lens_model_list) name_list = [] for func in lens_model.func_list: name_list.append(func.param_names) self._param_name_list = name_list if kwargs_lower is None: kwargs_lower = [] for func in lens_model.func_list: kwargs_lower.append(func.lower_limit_default) if kwargs_upper is None: kwargs_upper = [] for func in lens_model.func_list: kwargs_upper.append(func.upper_limit_default) self.lower_limit = kwargs_lower self.upper_limit = kwargs_upper
def __init__(self, profile_list, kwargs_cosmo={ 'D_d': 1000, 'D_s': 2000, 'D_ds': 500 }, kwargs_numerics={}): """ :param profile_list: """ kwargs_options = {'lens_model_list': profile_list} self.model = SinglePlane(profile_list) self.cosmo = Cosmo(kwargs_cosmo) self._interp_grid_num = kwargs_numerics.get('interpol_grid_num', 1000) self._max_interpolate = kwargs_numerics.get('max_integrate', 100) self._min_interpolate = kwargs_numerics.get('min_integrate', 0.0001)
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 __init__(self, kwargs_model, kwargs_cosmo, interpol_grid_num=1000, log_integration=True, max_integrate=1000, min_integrate=0.0001, max_light_draw=None, lum_weight_int_method=True): """ What we need: - max projected R to have ACCURATE I_R_sigma values - make sure everything outside cancels out (or is not rendered) :param interpol_grid_num: number of interpolation bins for integrand and interpolated functions :param log_integration: bool, if True, performs the numerical integral in log space distance (adviced) (only applies for lum_weight_int_method=True) :param max_integrate: maximum radius (in arc seconds) of the Jeans equation integral (assumes zero tracer particles outside this radius) :param max_light_draw: float; (optional) if set, draws up to this radius, else uses max_interpolate value :param lum_weight_int_method: bool, luminosity weighted dispersion integral to calculate LOS projected Jean's solution. ATTENTION: currently less accurate than 3d solution :param min_integrate: """ mass_profile_list = kwargs_model.get('mass_profile_list') light_profile_list = kwargs_model.get('light_profile_list') anisotropy_model = kwargs_model.get('anisotropy_model') self._interp_grid_num = interpol_grid_num self._log_int = log_integration self._max_integrate = max_integrate # maximal integration (and interpolation) in units of arcsecs self._min_integrate = min_integrate # min integration (and interpolation) in units of arcsecs self._max_interpolate = max_integrate # we chose to set the interpolation range to the integration range self._min_interpolate = min_integrate # we chose to set the interpolation range to the integration range if max_light_draw is None: max_light_draw = max_integrate # make sure the actual solution for the kinematics is only computed way inside the integral self.lightProfile = LightProfile(light_profile_list, interpol_grid_num=interpol_grid_num, max_interpolate=max_integrate, min_interpolate=min_integrate, max_draw=max_light_draw) Anisotropy.__init__(self, anisotropy_type=anisotropy_model) self.cosmo = Cosmo(**kwargs_cosmo) self._mass_profile = SinglePlane(mass_profile_list) self._lum_weight_int_method = lum_weight_int_method
def __init__(self, lens_model_list, z_lens=None, z_source=None, lens_redshift_list=None, cosmo=None, multi_plane=False, numerical_alpha_class=None, observed_convention_index=None, z_source_convention=None): """ :param lens_model_list: list of strings with lens model names :param z_lens: redshift of the deflector (only considered when operating in single plane mode). Is only needed for specific functions that require a cosmology. :param z_source: redshift of the source: Needed in multi_plane option only, not required for the core functionalities in the single plane mode. :param lens_redshift_list: list of deflector redshift (corresponding to the lens model list), only applicable in multi_plane mode. :param cosmo: instance of the astropy cosmology class. If not specified, uses the default cosmology. :param multi_plane: bool, if True, uses multi-plane mode. Default is False. :param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model (see documentation in Profiles/numerical_alpha) :param observed_convention_index: a list of lens indexes that correspond to observed positions on the sky, not physical positions :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. """ self.lens_model_list = lens_model_list self.z_lens = z_lens if z_source_convention is None: z_source_convention = z_source self.z_source = z_source self._z_source_convention = z_source_convention self.redshift_list = lens_redshift_list if cosmo is None: cosmo = default_cosmology.get() self.cosmo = cosmo self.multi_plane = multi_plane if multi_plane is True: if z_source is None: raise ValueError('z_source needs to be set for multi-plane lens modelling.') self.lens_model = MultiPlane(z_source, lens_model_list, lens_redshift_list, cosmo=cosmo, numerical_alpha_class=numerical_alpha_class, observed_convention_index=observed_convention_index, z_source_convention=z_source_convention) else: self.lens_model = SinglePlane(lens_model_list, numerical_alpha_class=numerical_alpha_class) if z_lens is not None and z_source is not None: self._lensCosmo = LensCosmo(z_lens, z_source, cosmo=cosmo)
def __init__(self, profile_list, kwargs_cosmo={ 'D_d': 1000, 'D_s': 2000, 'D_ds': 500 }, interpol_grid_num=1000, max_interpolate=100, min_interpolate=0.001): """ :param profile_list: """ self.model = SinglePlane(profile_list) self.cosmo = Cosmo(**kwargs_cosmo) self._interp_grid_num = interpol_grid_num self._max_interpolate = max_interpolate self._min_interpolate = min_interpolate
def test_bool_list(self): lensModel = SinglePlane(['SPEMD', 'SHEAR']) kwargs = [{ 'theta_E': 1, 'gamma': 1, 'e1': 0.1, 'e2': -0.1, 'center_x': 0, 'center_y': 0 }, { 'e1': 0.01, 'e2': -0.02 }] alphax_1, alphay_1 = lensModel.alpha(1, 1, kwargs, k=0) alphax_1_list, alphay_1_list = lensModel.alpha(1, 1, kwargs, k=[0]) npt.assert_almost_equal(alphax_1, alphax_1_list, decimal=5) npt.assert_almost_equal(alphay_1, alphay_1_list, decimal=5) alphax_1_1, alphay_1_1 = lensModel.alpha(1, 1, kwargs, k=0) alphax_1_2, alphay_1_2 = lensModel.alpha(1, 1, kwargs, k=1) alphax_full, alphay_full = lensModel.alpha(1, 1, kwargs, k=None) npt.assert_almost_equal(alphax_1_1 + alphax_1_2, alphax_full, decimal=5) npt.assert_almost_equal(alphay_1_1 + alphay_1_2, alphay_full, decimal=5)
def __init__(self, lens_model_list, z_lens=None, z_source=None, lens_redshift_list=None, cosmo=None, multi_plane=False, numerical_alpha_class=None): """ :param lens_model_list: list of strings with lens model names :param z_lens: redshift of the deflector (only considered when operating in single plane mode). Is only needed for specific functions that require a cosmology. :param z_source: redshift of the source: Needed in multi_plane option only, not required for the core functionalities in the single plane mode. :param lens_redshift_list: list of deflector redshift (corresponding to the lens model list), only applicable in multi_plane mode. :param cosmo: instance of the astropy cosmology class. If not specified, uses the default cosmology. :param multi_plane: bool, if True, uses multi-plane mode. Default is False. :param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model (see documentation in Profiles/numerical_alpha) """ self.lens_model_list = lens_model_list self.z_lens = z_lens self.z_source = z_source self.redshift_list = lens_redshift_list self.cosmo = cosmo self.multi_plane = multi_plane if multi_plane is True: self.lens_model = MultiPlane( z_source, lens_model_list, lens_redshift_list, cosmo=cosmo, numerical_alpha_class=numerical_alpha_class) else: self.lens_model = SinglePlane( lens_model_list, numerical_alpha_class=numerical_alpha_class) if z_lens is not None and z_source is not None: self._lensCosmo = LensCosmo(z_lens, z_source, cosmo=self.cosmo)
def __init__(self, lens_model_list, kwargs_fixed, kwargs_lower=None, kwargs_upper=None, kwargs_logsampling=None, num_images=0, solver_type='NONE', num_shapelet_lens=0): """ :param lens_model_list: list of strings of lens model names :param kwargs_fixed: list of keyword arguments for model parameters to be held fixed :param kwargs_lower: list of keyword arguments of the lower bounds of the model parameters :param kwargs_upper: list of keyword arguments of the upper bounds of the model parameters :param kwargs_logsampling: list of keyword arguments of parameters to be sampled in log10 space :param num_images: number of images to be constrained by a non-linear solver (only relevant when shapelet potential functions are used) :param solver_type: string, type of non-linear solver (only relevant in this class when 'SHAPELETS' is the solver type) :param num_shapelet_lens: integer, number of shapelets in the lensing potential (only relevant when 'SHAPELET' lens model is used) """ self.model_list = lens_model_list self.kwargs_fixed = kwargs_fixed self._num_images = num_images self._solver_type = solver_type self._num_shapelet_lens = num_shapelet_lens lens_model = SinglePlane(lens_model_list=lens_model_list) name_list = [] for func in lens_model.func_list: name_list.append(func.param_names) self._param_name_list = name_list if kwargs_lower is None: kwargs_lower = [] for func in lens_model.func_list: kwargs_lower.append(func.lower_limit_default) if kwargs_upper is None: kwargs_upper = [] for func in lens_model.func_list: kwargs_upper.append(func.upper_limit_default) self.lower_limit = kwargs_lower self.upper_limit = kwargs_upper if kwargs_logsampling is None: kwargs_logsampling = [[] for i in range(len(self.model_list))] self.kwargs_logsampling = kwargs_logsampling
class NumericKinematics(Anisotropy): def __init__(self, kwargs_model, kwargs_cosmo, interpol_grid_num=1000, log_integration=True, max_integrate=1000, min_integrate=0.0001, lum_weight_int_method=False): """ What we need: - max projected R to have ACCURATE I_R_sigma values - make sure everything outside cancels out (or is not rendered) :param interpol_grid_num: number of interpolation bins for integrand and interpolated functions :param log_integration: bool, if True, performs the numerical integral in log space distance (adviced) :param max_integrate: maximum radius (in arc seconds) of the Jeans equation integral (assumes zero tracer particles outside this radius) :param lum_weight_int_method: bool, luminosity weighted dispersion integral to calculate LOS projected Jean's solution. ATTENTION: currently less accurate than 3d solution :param min_integrate: """ mass_profile_list = kwargs_model.get('mass_profile_list') light_profile_list = kwargs_model.get('light_profile_list') anisotropy_model = kwargs_model.get('anisotropy_model') self._interp_grid_num = interpol_grid_num self._log_int = log_integration self._max_integrate = max_integrate # maximal integration (and interpolation) in units of arcsecs self._min_integrate = min_integrate # min integration (and interpolation) in units of arcsecs self._max_interpolate = max_integrate # we chose to set the interpolation range to the integration range self._min_interpolate = min_integrate # we chose to set the interpolation range to the integration range self.lightProfile = LightProfile(light_profile_list, interpol_grid_num=interpol_grid_num, max_interpolate=max_integrate, min_interpolate=min_integrate) Anisotropy.__init__(self, anisotropy_type=anisotropy_model) self.cosmo = Cosmo(**kwargs_cosmo) self._mass_profile = SinglePlane(mass_profile_list) self._lum_weight_int_method = lum_weight_int_method def sigma_s2(self, r, R, kwargs_mass, kwargs_light, kwargs_anisotropy): """ returns unweighted los velocity dispersion for a specified projected radius :param r: 3d radius (not needed for this calculation) :param R: 2d projected radius (in angular units of arcsec) :param kwargs_mass: mass model parameters (following lenstronomy lens model conventions) :param kwargs_light: deflector light parameters (following lenstronomy light model conventions) :param kwargs_anisotropy: anisotropy parameters, may vary according to anisotropy type chosen. We refer to the Anisotropy() class for details on the parameters. :return: line-of-sight projected velocity dispersion at projected radius R """ if self._lum_weight_int_method is True: return self.sigma_s2_project_int(r, R, kwargs_mass, kwargs_light, kwargs_anisotropy) else: return self.sigma_s2_full(r, R, kwargs_mass, kwargs_light, kwargs_anisotropy) def sigma_s2_project_int(self, r, R, kwargs_mass, kwargs_light, kwargs_anisotropy): """ returns unweighted los velocity dispersion for a specified projected radius :param r: 3d radius (not needed for this calculation) :param R: 2d projected radius (in angular units of arcsec) :param kwargs_mass: mass model parameters (following lenstronomy lens model conventions) :param kwargs_light: deflector light parameters (following lenstronomy light model conventions) :param kwargs_anisotropy: anisotropy parameters, may vary according to anisotropy type chosen. We refer to the Anisotropy() class for details on the parameters. :return: line-of-sight projected velocity dispersion at projected radius R """ # TODO: this is potentially inaccurate as the light-only integral is analytically to infinity while the # nominator is numerically to a finite distance, so luminosity weighting might be off # this could lead to an under-prediction of the velocity dispersion I_R_sigma2 = self._I_R_sigma2_interp(R, kwargs_mass, kwargs_light, kwargs_anisotropy) I_R = self.lightProfile.light_2d(R, kwargs_light) return np.nan_to_num(I_R_sigma2 / I_R) def sigma_s2_full(self, r, R, kwargs_mass, kwargs_light, kwargs_anisotropy): """ returns unweighted los velocity dispersion for a specified projected radius :param r: 3d radius (not needed for this calculation) :param R: 2d projected radius (in angular units of arcsec) :param kwargs_mass: mass model parameters (following lenstronomy lens model conventions) :param kwargs_light: deflector light parameters (following lenstronomy light model conventions) :param kwargs_anisotropy: anisotropy parameters, may vary according to anisotropy type chosen. We refer to the Anisotropy() class for details on the parameters. :return: line-of-sight projected velocity dispersion at projected radius R from 3d radius r """ beta = self.beta_r(r, **kwargs_anisotropy) return (1 - beta * R**2 / r**2) * self.sigma_r2( r, kwargs_mass, kwargs_light, kwargs_anisotropy) def sigma_r2(self, r, kwargs_mass, kwargs_light, kwargs_anisotropy): """ computes numerically the solution of the Jeans equation for a specific 3d radius E.g. Equation (A1) of Mamon & Lokas https://arxiv.org/pdf/astro-ph/0405491.pdf l(r) \sigma_r(r) ^ 2 = 1/f(r) \int_r^{\infty} f(s) l(s) G M(s) / s^2 ds where l(r) is the 3d light profile M(s) is the enclosed 3d mass f is the solution to d ln(f)/ d ln(r) = 2 beta(r) :param r: 3d radius :param kwargs_mass: mass model parameters (following lenstronomy lens model conventions) :param kwargs_light: deflector light parameters (following lenstronomy light model conventions) :param kwargs_anisotropy: anisotropy parameters, may vary according to anisotropy type chosen. We refer to the Anisotropy() class for details on the parameters. :return: sigma_r**2 """ l_r = self.lightProfile.light_3d_interp(r, kwargs_light) f_r = self.anisotropy_solution(r, **kwargs_anisotropy) return 1 / f_r / l_r * self._jeans_solution_integral( r, kwargs_mass, kwargs_light, kwargs_anisotropy) * const.G / ( const.arcsec * self.cosmo.dd * const.Mpc) def mass_3d(self, r, kwargs): """ mass enclosed a 3d radius :param r: in arc seconds :param kwargs: lens model parameters in arc seconds :return: mass enclosed physical radius in kg """ mass_dimless = self._mass_profile.mass_3d(r, kwargs) mass_dim = mass_dimless * const.arcsec ** 2 * self.cosmo.dd * self.cosmo.ds / self.cosmo.dds * const.Mpc * \ const.c ** 2 / (4 * np.pi * const.G) return mass_dim def grav_potential(self, r, kwargs_mass): """ Gravitational potential in SI units :param r: radius (arc seconds) :param kwargs_mass: :return: gravitational potential """ mass_dim = self.mass_3d(r, kwargs_mass) grav_pot = -const.G * mass_dim / (r * const.arcsec * self.cosmo.dd * const.Mpc) return grav_pot def draw_light(self, kwargs_light): """ :param kwargs_light: keyword argument (list) of the light model :return: 3d radius (if possible), 2d projected radius, x-projected coordinate, y-projected coordinate """ r = self.lightProfile.draw_light_3d(kwargs_light, n=1)[0] R, x, y = util.project2d_random(r) # this code is a remnant of the 2d-only rendering # (can be used when accurate luminosity-weighted integrated velocity dispersion predictions are made) # R = self.lightProfile.draw_light_2d(kwargs_light, n=1)[0] # x, y = util.draw_xy(R) # r = None return r, R, x, y def delete_cache(self): """ delete interpolation function for a specific mass and light profile as well as for a specific anisotropy model :return: """ if hasattr(self, '_log_mass_3d'): del self._log_mass_3d if hasattr(self, '_interp_jeans_integral'): del self._interp_jeans_integral if hasattr(self, '_interp_I_R_sigma2'): del self._interp_I_R_sigma2 self.lightProfile.delete_cache() self.delete_anisotropy_cache() def _I_R_sigma2(self, R, kwargs_mass, kwargs_light, kwargs_anisotropy): """ equation A15 in Mamon&Lokas 2005 as a logarithmic numerical integral (if option is chosen) :param R: 2d projected radius (in angular units) :param kwargs_mass: mass model parameters (following lenstronomy lens model conventions) :param kwargs_light: deflector light parameters (following lenstronomy light model conventions) :param kwargs_anisotropy: anisotropy parameters, may vary according to anisotropy type chosen. We refer to the Anisotropy() class for details on the parameters. :return: integral of A15 in Mamon&Lokas 2005 """ R = max(R, self._min_integrate) max_integrate = self._max_integrate # make sure the integration of the Jeans equation is performed further out than the interpolation if self._log_int is True: min_log = np.log10(R + 0.001) max_log = np.log10(max_integrate) r_array = np.logspace(min_log, max_log, self._interp_grid_num) dlog_r = (np.log10(r_array[2]) - np.log10(r_array[1])) * np.log(10) IR_sigma2_dr = self._integrand_A15( r_array, R, kwargs_mass, kwargs_light, kwargs_anisotropy) * dlog_r * r_array else: r_array = np.linspace(R + 0.001, max_integrate, self._interp_grid_num) dr = r_array[2] - r_array[1] IR_sigma2_dr = self._integrand_A15( r_array, R, kwargs_mass, kwargs_light, kwargs_anisotropy) * dr IR_sigma2 = np.sum( IR_sigma2_dr) # integral from angle to physical scales return IR_sigma2 * 2 * const.G / (const.arcsec * self.cosmo.dd * const.Mpc) def _I_R_sigma2_interp(self, R, kwargs_mass, kwargs_light, kwargs_anisotropy): """ equation A15 in Mamon&Lokas 2005 as interpolation in log space :param R: projected radius :param kwargs_mass: mass profile keyword arguments :param kwargs_light: light model keyword arguments :param kwargs_anisotropy: stellar anisotropy keyword arguments :return: """ if not hasattr(self, '_interp_I_R_sigma2'): min_log = np.log10(self._min_integrate) max_log = np.log10(self._max_integrate) R_array = np.logspace(min_log, max_log, self._interp_grid_num) I_R_sigma2_array = [] for R_i in R_array: I_R_sigma2_array.append( self._I_R_sigma2(R_i, kwargs_mass, kwargs_light, kwargs_anisotropy)) self._interp_I_R_sigma2 = interp1d(np.log(R_array), np.array(I_R_sigma2_array), fill_value="extrapolate") return self._interp_I_R_sigma2(np.log(R)) def _integrand_A15(self, r, R, kwargs_mass, kwargs_light, kwargs_anisotropy): """ integrand of A15 (in log space) in Mamon&Lokas 2005 :param r: 3d radius in arc seconds :param R: 2d projected radius :param kwargs_mass: mass model parameters (following lenstronomy lens model conventions) :param kwargs_light: deflector light parameters (following lenstronomy light model conventions) :param kwargs_anisotropy: anisotropy parameters, may vary according to anisotropy type chosen. We refer to the Anisotropy() class for details on the parameters. :return: """ k_r = self.K(r, R, **kwargs_anisotropy) l_r = self.lightProfile.light_3d_interp(r, kwargs_light) m_r = self._mass_3d_interp(r, kwargs_mass) out = k_r * l_r * m_r / r return out def _jeans_solution_integral(self, r, kwargs_mass, kwargs_light, kwargs_anisotropy): """ interpolated solution of the integral \int_r^{\infty} f(s) l(s) G M(s) / s^2 ds :param r: 3d radius :param kwargs_mass: mass profile keyword arguments :param kwargs_light: light profile keyword arguments :param kwargs_anisotropy: anisotropy keyword arguments :return: interpolated solution of the Jeans integral (copped values at large radius as they become numerically inaccurate) """ if not hasattr(self, '_interp_jeans_integral'): min_log = np.log10(self._min_integrate) max_log = np.log10( self._max_integrate ) # we extend the integral but ignore these outer solutions in the interpolation r_array = np.logspace(min_log, max_log, self._interp_grid_num) dlog_r = (np.log10(r_array[2]) - np.log10(r_array[1])) * np.log(10) integrand_jeans = self._integrand_jeans_solution( r_array, kwargs_mass, kwargs_light, kwargs_anisotropy) * dlog_r * r_array #flip array from inf to finite integral_jeans_r = np.cumsum(np.flip(integrand_jeans)) #flip array back integral_jeans_r = np.flip(integral_jeans_r) #call 1d interpolation function self._interp_jeans_integral = interp1d( np.log(r_array[r_array <= self._max_integrate]), integral_jeans_r[r_array <= self._max_integrate], fill_value="extrapolate") return self._interp_jeans_integral(np.log(r)) def _integrand_jeans_solution(self, r, kwargs_mass, kwargs_light, kwargs_anisotropy): """ integrand of A1 (in log space) in Mamon&Lokas 2005 to calculate the Jeans equation numerically f(s) l(s) M(s) / s^2 :param r: :param kwargs_mass: :param kwargs_light: :param kwargs_anisotropy: :return: """ f_r = self.anisotropy_solution(r, **kwargs_anisotropy) l_r = self.lightProfile.light_3d_interp(r, kwargs_light) m_r = self._mass_3d_interp(r, kwargs_mass) out = f_r * l_r * m_r / r**2 return out def _mass_3d_interp(self, r, kwargs, new_compute=False): """ :param r: in arc seconds :param kwargs: lens model parameters in arc seconds :param new_compute: bool, if True, recomputes the interpolation :return: mass enclosed physical radius in kg """ if not hasattr(self, '_log_mass_3d') or new_compute is True: r_array = np.logspace(np.log10(self._min_interpolate), np.log10(self._max_interpolate), self._interp_grid_num) mass_3d_array = self.mass_3d(r_array, kwargs) mass_3d_array[mass_3d_array < 10.**(-10)] = 10.**(-10) #mass_dim_array = mass_3d_array * const.arcsec ** 2 * self.cosmo.dd * self.cosmo.ds \ # / self.cosmo.dds * const.Mpc * const.c ** 2 / (4 * np.pi * const.G) self._log_mass_3d = interp1d(np.log(r_array), np.log(mass_3d_array / r_array), fill_value="extrapolate") return np.exp(self._log_mass_3d(np.log(r))) * r
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 __init__(self, lens_model_list, z_lens=None, z_source=None, lens_redshift_list=None, cosmo=None, multi_plane=False, numerical_alpha_class=None, observed_convention_index=None, z_source_convention=None, cosmo_interp=False, z_interp_stop=None, num_z_interp=100): """ :param lens_model_list: list of strings with lens model names :param z_lens: redshift of the deflector (only considered when operating in single plane mode). Is only needed for specific functions that require a cosmology. :param z_source: redshift of the source: Needed in multi_plane option only, not required for the core functionalities in the single plane mode. :param lens_redshift_list: list of deflector redshift (corresponding to the lens model list), only applicable in multi_plane mode. :param cosmo: instance of the astropy cosmology class. If not specified, uses the default cosmology. :param multi_plane: bool, if True, uses multi-plane mode. Default is False. :param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model (see documentation in Profiles/numerical_alpha) :param observed_convention_index: a list of indices, corresponding to the lens_model_list element with same index, where the 'center_x' and 'center_y' kwargs correspond to observed (lensed) positions, not physical positions. The code will compute the physical locations when performing computations :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_interp: boolean (only employed in multi-plane mode), interpolates astropy.cosmology distances for faster calls when accessing several lensing planes :param z_interp_stop: (only in multi-plane with cosmo_interp=True); maximum redshift for distance interpolation This number should be higher or equal the maximum of the source redshift and/or the z_source_convention :param num_z_interp: (only in multi-plane with cosmo_interp=True); number of redshift bins for interpolating distances """ self.lens_model_list = lens_model_list self.z_lens = z_lens self.z_source = z_source self._z_source_convention = z_source_convention self.redshift_list = lens_redshift_list if cosmo is None: from astropy.cosmology import default_cosmology cosmo = default_cosmology.get() self.cosmo = cosmo self.multi_plane = multi_plane if multi_plane is True: if z_source is None: raise ValueError( 'z_source needs to be set for multi-plane lens modelling.') self.lens_model = MultiPlane( z_source, lens_model_list, lens_redshift_list, cosmo=cosmo, numerical_alpha_class=numerical_alpha_class, observed_convention_index=observed_convention_index, z_source_convention=z_source_convention, cosmo_interp=cosmo_interp, z_interp_stop=z_interp_stop, num_z_interp=num_z_interp) else: self.lens_model = SinglePlane( lens_model_list, numerical_alpha_class=numerical_alpha_class, lens_redshift_list=lens_redshift_list, z_source_convention=z_source_convention) if z_lens is not None and z_source is not None: self._lensCosmo = LensCosmo(z_lens, z_source, cosmo=cosmo)
class LensModel(object): """ class to handle an arbitrary list of lens models. This is the main lenstronomy LensModel API for all other modules. """ def __init__(self, lens_model_list, z_lens=None, z_source=None, lens_redshift_list=None, cosmo=None, multi_plane=False, numerical_alpha_class=None, observed_convention_index=None, z_source_convention=None, cosmo_interp=False, z_interp_stop=None, num_z_interp=100): """ :param lens_model_list: list of strings with lens model names :param z_lens: redshift of the deflector (only considered when operating in single plane mode). Is only needed for specific functions that require a cosmology. :param z_source: redshift of the source: Needed in multi_plane option only, not required for the core functionalities in the single plane mode. :param lens_redshift_list: list of deflector redshift (corresponding to the lens model list), only applicable in multi_plane mode. :param cosmo: instance of the astropy cosmology class. If not specified, uses the default cosmology. :param multi_plane: bool, if True, uses multi-plane mode. Default is False. :param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model (see documentation in Profiles/numerical_alpha) :param observed_convention_index: a list of indices, corresponding to the lens_model_list element with same index, where the 'center_x' and 'center_y' kwargs correspond to observed (lensed) positions, not physical positions. The code will compute the physical locations when performing computations :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_interp: boolean (only employed in multi-plane mode), interpolates astropy.cosmology distances for faster calls when accessing several lensing planes :param z_interp_stop: (only in multi-plane with cosmo_interp=True); maximum redshift for distance interpolation This number should be higher or equal the maximum of the source redshift and/or the z_source_convention :param num_z_interp: (only in multi-plane with cosmo_interp=True); number of redshift bins for interpolating distances """ self.lens_model_list = lens_model_list self.z_lens = z_lens self.z_source = z_source self._z_source_convention = z_source_convention self.redshift_list = lens_redshift_list if cosmo is None: from astropy.cosmology import default_cosmology cosmo = default_cosmology.get() self.cosmo = cosmo self.multi_plane = multi_plane if multi_plane is True: if z_source is None: raise ValueError( 'z_source needs to be set for multi-plane lens modelling.') self.lens_model = MultiPlane( z_source, lens_model_list, lens_redshift_list, cosmo=cosmo, numerical_alpha_class=numerical_alpha_class, observed_convention_index=observed_convention_index, z_source_convention=z_source_convention, cosmo_interp=cosmo_interp, z_interp_stop=z_interp_stop, num_z_interp=num_z_interp) else: self.lens_model = SinglePlane( lens_model_list, numerical_alpha_class=numerical_alpha_class, lens_redshift_list=lens_redshift_list, z_source_convention=z_source_convention) if z_lens is not None and z_source is not None: self._lensCosmo = LensCosmo(z_lens, z_source, cosmo=cosmo) def ray_shooting(self, x, y, kwargs, k=None): """ maps image to source position (inverse deflection) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: source plane positions corresponding to (x, y) in the image plane """ return self.lens_model.ray_shooting(x, y, kwargs, k=k) def fermat_potential(self, x_image, y_image, kwargs_lens, x_source=None, y_source=None): """ fermat potential (negative sign means earlier arrival time) for Multi-plane lensing, it computes the effective Fermat potential (derived from the arrival time and subtracted off the time-delay distance for the given cosmology). The units are given in arcsecond square. :param x_image: image position :param y_image: image position :param x_source: source position :param y_source: source position :param kwargs_lens: list of keyword arguments of lens model parameters matching the lens model classes :return: fermat potential in arcsec**2 without geometry term (second part of Eqn 1 in Suyu et al. 2013) as a list """ if hasattr(self.lens_model, 'fermat_potential'): return self.lens_model.fermat_potential(x_image, y_image, kwargs_lens, x_source, y_source) elif hasattr(self.lens_model, 'arrival_time') and hasattr( self, '_lensCosmo'): dt = self.lens_model.arrival_time(x_image, y_image, kwargs_lens) fermat_pot_eff = dt * const.c / self._lensCosmo.ddt / const.Mpc * const.day_s / const.arcsec**2 return fermat_pot_eff else: raise ValueError( 'In multi-plane lensing you need to provide a specific z_lens and z_source for which the ' 'effective Fermat potential is evaluated') def arrival_time(self, x_image, y_image, kwargs_lens, kappa_ext=0): """ :param x_image: image position :param y_image: image position :param kwargs_lens: lens model parameter keyword argument list :param kappa_ext: external convergence contribution not accounted in the lens model that leads to the same observables in position and relative fluxes but rescales the time delays :return: arrival time of image positions in units of days """ if hasattr(self.lens_model, 'arrival_time'): arrival_time = self.lens_model.arrival_time( x_image, y_image, kwargs_lens) else: fermat_pot = self.lens_model.fermat_potential( x_image, y_image, kwargs_lens) if not hasattr(self, '_lensCosmo'): raise ValueError( "LensModel class was not initialized with lens and source redshifts!" ) arrival_time = self._lensCosmo.time_delay_units(fermat_pot) arrival_time *= (1 - kappa_ext) return arrival_time def potential(self, x, y, kwargs, k=None): """ lensing potential :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: lensing potential in units of arcsec^2 """ return self.lens_model.potential(x, y, kwargs, k=k) def alpha(self, x, y, kwargs, k=None, diff=None): """ deflection angles :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :param diff: None or float. If set, computes the deflection as a finite numerical differential of the lensing potential. This differential is only applicable in the single lensing plane where the form of the lensing potential is analytically known :return: deflection angles in units of arcsec """ if diff is None: return self.lens_model.alpha(x, y, kwargs, k=k) elif self.multi_plane is False: return self._deflection_differential(x, y, kwargs, k=k, diff=diff) else: raise ValueError( 'numerical differentiation of lensing potential is not available in the multi-plane ' 'setting as analytical form of lensing potential is not available.' ) def hessian(self, x, y, kwargs, k=None, diff=None, diff_method='square'): """ hessian matrix :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :param diff: float, scale over which the finite numerical differential is computed. If None, then using the exact (if available) differentials. :param diff_method: string, 'square' or 'cross', indicating whether finite differentials are computed from a cross or a square of points around (x, y) :return: f_xx, f_xy, f_yx, f_yy components """ if diff is None: return self.lens_model.hessian(x, y, kwargs, k=k) elif diff_method == 'square': return self._hessian_differential_square(x, y, kwargs, k=k, diff=diff) elif diff_method == 'cross': return self._hessian_differential_cross(x, y, kwargs, k=k, diff=diff) else: raise ValueError( 'diff_method %s not supported. Chose among "square" or "cross".' % diff_method) def kappa(self, x, y, kwargs, k=None, diff=None, diff_method='square'): """ lensing convergence k = 1/2 laplacian(phi) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :param diff: float, scale over which the finite numerical differential is computed. If None, then using the exact (if available) differentials. :param diff_method: string, 'square' or 'cross', indicating whether finite differentials are computed from a cross or a square of points around (x, y) :return: lensing convergence """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs, k=k, diff=diff, diff_method=diff_method) kappa = 1. / 2 * (f_xx + f_yy) return kappa def curl(self, x, y, kwargs, k=None, diff=None, diff_method='square'): """ curl computation F_xy - F_yx :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :param diff: float, scale over which the finite numerical differential is computed. If None, then using the exact (if available) differentials. :param diff_method: string, 'square' or 'cross', indicating whether finite differentials are computed from a cross or a square of points around (x, y) :return: curl at position (x, y) """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs, k=k, diff=diff, diff_method=diff_method) return f_xy - f_yx def gamma(self, x, y, kwargs, k=None, diff=None, diff_method='square'): """ shear computation g1 = 1/2(d^2phi/dx^2 - d^2phi/dy^2) g2 = d^2phi/dxdy :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :param diff: float, scale over which the finite numerical differential is computed. If None, then using the exact (if available) differentials. :param diff_method: string, 'square' or 'cross', indicating whether finite differentials are computed from a cross or a square of points around (x, y) :return: gamma1, gamma2 """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs, k=k, diff=diff, diff_method=diff_method) gamma1 = 1. / 2 * (f_xx - f_yy) gamma2 = f_xy return gamma1, gamma2 def magnification(self, x, y, kwargs, k=None, diff=None, diff_method='square'): """ magnification mag = 1/det(A) A = 1 - d^2phi/d_ij :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :param diff: float, scale over which the finite numerical differential is computed. If None, then using the exact (if available) differentials. :param diff_method: string, 'square' or 'cross', indicating whether finite differentials are computed from a cross or a square of points around (x, y) :return: magnification """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs, k=k, diff=diff, diff_method=diff_method) det_A = (1 - f_xx) * (1 - f_yy) - f_xy * f_yx return 1. / det_A # attention, if dividing by zero def flexion(self, x, y, kwargs, k=None, diff=0.000001, hessian_diff=True): """ third derivatives (flexion) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: int or None, if set, only evaluates the differential from one model component :param diff: numerical differential length of Flexion :param hessian_diff: boolean, if true also computes the numerical differential length of Hessian (optional) :return: f_xxx, f_xxy, f_xyy, f_yyy """ if hessian_diff is not True: hessian_diff = None f_xx_dx, f_xy_dx, f_yx_dx, f_yy_dx = self.hessian(x + diff / 2, y, kwargs, k=k, diff=hessian_diff) f_xx_dy, f_xy_dy, f_yx_dy, f_yy_dy = self.hessian(x, y + diff / 2, kwargs, k=k, diff=hessian_diff) f_xx_dx_, f_xy_dx_, f_yx_dx_, f_yy_dx_ = self.hessian( x - diff / 2, y, kwargs, k=k, diff=hessian_diff) f_xx_dy_, f_xy_dy_, f_yx_dy_, f_yy_dy_ = self.hessian( x, y - diff / 2, kwargs, k=k, diff=hessian_diff) f_xxx = (f_xx_dx - f_xx_dx_) / diff f_xxy = (f_xx_dy - f_xx_dy_) / diff f_xyy = (f_xy_dy - f_xy_dy_) / diff f_yyy = (f_yy_dy - f_yy_dy_) / diff return f_xxx, f_xxy, f_xyy, f_yyy def set_static(self, kwargs): """ set this instance to a static lens model. This can improve the speed in evaluating lensing quantities at different positions but must not be used with different lens model parameters! :param kwargs: lens model keyword argument list :return: kwargs_updated (in case of image position convention in multiplane lensing this is changed) """ return self.lens_model.set_static(kwargs) def set_dynamic(self): """ deletes cache for static setting and makes sure the observed convention in the position of lensing profiles in the multi-plane setting is enabled. Dynamic is the default setting of this class enabling an accurate computation of lensing quantities with different parameters in the lensing profiles. :return: None """ self.lens_model.set_dynamic() def _deflection_differential(self, x, y, kwargs, k=None, diff=0.00001): """ :param x: x-coordinate :param y: y-coordinate :param kwargs: keyword argument list :param k: int or None, if set, only evaluates the differential from one model component :param diff: finite differential length :return: f_x, f_y """ phi_dx = self.lens_model.potential(x + diff / 2, y, kwargs=kwargs, k=k) phi_dy = self.lens_model.potential(x, y + diff / 2, kwargs=kwargs, k=k) phi_dx_ = self.lens_model.potential(x - diff / 2, y, kwargs=kwargs, k=k) phi_dy_ = self.lens_model.potential(x, y - diff / 2, kwargs=kwargs, k=k) f_x = (phi_dx - phi_dx_) / diff f_y = (phi_dy - phi_dy_) / diff return f_x, f_y def _hessian_differential_cross(self, x, y, kwargs, k=None, diff=0.00001): """ computes the numerical differentials over a finite range for f_xx, f_yy, f_xy from f_x and f_y The differentials are computed along the cross centered at (x, y). :param x: x-coordinate :param y: y-coordinate :param kwargs: lens model keyword argument list :param k: int, list of bools or None, indicating a subset of lens models to be evaluated :param diff: float, scale of the finite differential (diff/2 in each direction used to compute the differential :return: f_xx, f_xy, f_yx, f_yy """ alpha_ra_dx, alpha_dec_dx = self.alpha(x + diff / 2, y, kwargs, k=k) alpha_ra_dy, alpha_dec_dy = self.alpha(x, y + diff / 2, kwargs, k=k) alpha_ra_dx_, alpha_dec_dx_ = self.alpha(x - diff / 2, y, kwargs, k=k) alpha_ra_dy_, alpha_dec_dy_ = self.alpha(x, y - diff / 2, kwargs, k=k) dalpha_rara = (alpha_ra_dx - alpha_ra_dx_) / diff dalpha_radec = (alpha_ra_dy - alpha_ra_dy_) / diff dalpha_decra = (alpha_dec_dx - alpha_dec_dx_) / diff dalpha_decdec = (alpha_dec_dy - alpha_dec_dy_) / 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 _hessian_differential_square(self, x, y, kwargs, k=None, diff=0.00001): """ computes the numerical differentials over a finite range for f_xx, f_yy, f_xy from f_x and f_y The differentials are computed on the square around (x, y). This minimizes curl. :param x: x-coordinate :param y: y-coordinate :param kwargs: lens model keyword argument list :param k: int, list of booleans or None, indicating a subset of lens models to be evaluated :param diff: float, scale of the finite differential (diff/2 in each direction used to compute the differential :return: f_xx, f_xy, f_yx, f_yy """ alpha_ra_pp, alpha_dec_pp = self.alpha(x + diff / 2, y + diff / 2, kwargs, k=k) alpha_ra_pn, alpha_dec_pn = self.alpha(x + diff / 2, y - diff / 2, kwargs, k=k) alpha_ra_np, alpha_dec_np = self.alpha(x - diff / 2, y + diff / 2, kwargs, k=k) alpha_ra_nn, alpha_dec_nn = self.alpha(x - diff / 2, y - diff / 2, kwargs, k=k) f_xx = (alpha_ra_pp - alpha_ra_np + alpha_ra_pn - alpha_ra_nn) / diff / 2 f_xy = (alpha_ra_pp - alpha_ra_pn + alpha_ra_np - alpha_ra_nn) / diff / 2 f_yx = (alpha_dec_pp - alpha_dec_np + alpha_dec_pn - alpha_dec_nn) / diff / 2 f_yy = (alpha_dec_pp - alpha_dec_pn + alpha_dec_np - alpha_dec_nn) / diff / 2 return f_xx, f_xy, f_yx, f_yy
class LensModel(object): """ class to handle an arbitrary list of lens models """ def __init__(self, lens_model_list, z_source=None, redshift_list=None, cosmo=None, multi_plane=False): """ :param lens_model_list: list of strings with lens model names :param foreground_shear: bool, when True, models a foreground non-linear shear distortion """ self.lens_model_list = lens_model_list self.z_source = z_source self.redshift_list = redshift_list self.cosmo = cosmo self.multi_plane = multi_plane if multi_plane is True: self.lens_model = MultiLens(z_source, lens_model_list, redshift_list, cosmo=cosmo) else: self.lens_model = SinglePlane(lens_model_list) def ray_shooting(self, x, y, kwargs, k=None): """ maps image to source position (inverse deflection) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: source plane positions corresponding to (x, y) in the image plane """ return self.lens_model.ray_shooting(x, y, kwargs, k=k) def fermat_potential(self, x_image, y_image, x_source, y_source, kwargs_lens): """ fermat potential (negative sign means earlier arrival time) :param x_image: image position :param y_image: image position :param x_source: source position :param y_source: source position :param kwargs_lens: list of keyword arguments of lens model parameters matching the lens model classes :return: fermat potential in arcsec**2 without geometry term (second part of Eqn 1 in Suyu et al. 2013) as a list """ if self.multi_plane: raise ValueError("Fermat potential is not defined in multi-plane lensing. Please use single plane lens models.") else: return self.lens_model.fermat_potential(x_image, y_image, x_source, y_source, kwargs_lens) def arrival_time(self, x_image, y_image, kwargs_lens): """ :param x_image: :param y_image: :param kwargs_lens: :return: """ if self.multi_plane: return self.lens_model.arrival_time(x_image, y_image, kwargs_lens) else: raise ValueError( "arrival_time routine not defined for single plane lensing. Please use Fermat potential instead") def mass(self, x, y, epsilon_crit, kwargs): """ :param x: position :param y: position :param epsilon_crit: critical mass density of a lens :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :return: projected mass density in units of input epsilon_crit """ kappa = self.kappa(x, y, kwargs) mass = epsilon_crit * kappa return mass def potential(self, x, y, kwargs, k=None): """ lensing potential :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: lensing potential in units of arcsec^2 """ return self.lens_model.potential(x, y, kwargs, k=k) def alpha(self, x, y, kwargs, k=None): """ deflection angles :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: deflection angles in units of arcsec """ return self.lens_model.alpha(x, y, kwargs, k=k) def kappa(self, x, y, kwargs, k=None): """ lensing convergence k = 1/2 laplacian(phi) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: lensing convergence """ f_xx, f_xy, f_yy = self.hessian(x, y, kwargs, k=k) kappa = 1./2 * (f_xx + f_yy) # attention on units return kappa def gamma(self, x, y, kwargs, k=None): """ shear computation g1 = 1/2(d^2phi/dx^2 - d^2phi/dy^2) g2 = d^2phi/dxdy :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: gamma1, gamma2 """ f_xx, f_xy, f_yy = self.hessian(x, y, kwargs, k=k) gamma1 = 1./2 * (f_xx - f_yy) # attention on units gamma2 = f_xy # attention on units return gamma1, gamma2 def magnification(self, x, y, kwargs, k=None): """ magnification mag = 1/det(A) A = 1 - d^2phi/d_ij :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: magnification """ f_xx, f_xy, f_yy = self.hessian(x, y, kwargs, k=k) det_A = (1 - f_xx) * (1 - f_yy) - f_xy*f_xy return 1./det_A # attention, if dividing by zero def hessian(self, x, y, kwargs, k=None): """ hessian matrix :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: f_xx, f_xy, f_yy components """ return self.lens_model.hessian(x, y, kwargs, k=k) def mass_3d(self, r, kwargs, bool_list=None): """ computes the mass within a 3d sphere of radius r :param r: radius (in angular units) :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param bool_list: list of bools that are part of the output :return: mass (in angular units, modulo epsilon_crit) """ if self.multi_plane is True: raise ValueError("mass_3d is not supported for multi-lane lensing. Please use single plane instead.") else: return self.lens_model.mass_3d(r, kwargs, bool_list=bool_list) def mass_2d(self, r, kwargs, bool_list=None): """ computes the mass enclosed a projected (2d) radius r :param r: radius (in angular units) :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param bool_list: list of bools that are part of the output :return: projected mass (in angular units, modulo epsilon_crit) """ if self.multi_plane is True: raise ValueError("mass_2d is not supported for multi-lane lensing. Please use single plane instead.") else: return self.lens_model.mass_2d(r, kwargs, bool_list=bool_list)
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 TestLensModel(object): """ tests the source model routines """ def setup(self): self.lensModel = SinglePlane(['GAUSSIAN']) self.kwargs = [{ 'amp': 1., 'sigma_x': 2., 'sigma_y': 2., 'center_x': 0., 'center_y': 0. }] def test_potential(self): output = self.lensModel.potential(x=1., y=1., kwargs=self.kwargs) assert output == 0.77880078307140488 / (8 * np.pi) def test_alpha(self): output1, output2 = self.lensModel.alpha(x=1., y=1., kwargs=self.kwargs) assert output1 == -0.19470019576785122 / (8 * np.pi) assert output2 == -0.19470019576785122 / (8 * np.pi) def test_ray_shooting(self): delta_x, delta_y = self.lensModel.ray_shooting(x=1., y=1., kwargs=self.kwargs) assert delta_x == 1 + 0.19470019576785122 / (8 * np.pi) assert delta_y == 1 + 0.19470019576785122 / (8 * np.pi) def test_mass_2d(self): lensModel = SinglePlane(['GAUSSIAN_KAPPA']) kwargs = [{'amp': 1., 'sigma': 2., 'center_x': 0., 'center_y': 0.}] output = lensModel.mass_2d(r=1, kwargs=kwargs) assert output == 0.11750309741540453 def test_bool_list(self): lensModel = SinglePlane(['SPEMD', 'SHEAR']) kwargs = [{ 'theta_E': 1, 'gamma': 1, 'e1': 0.1, 'e2': -0.1, 'center_x': 0, 'center_y': 0 }, { 'e1': 0.01, 'e2': -0.02 }] alphax_1, alphay_1 = lensModel.alpha(1, 1, kwargs, k=0) alphax_1_list, alphay_1_list = lensModel.alpha(1, 1, kwargs, k=[0]) npt.assert_almost_equal(alphax_1, alphax_1_list, decimal=5) npt.assert_almost_equal(alphay_1, alphay_1_list, decimal=5) alphax_1_1, alphay_1_1 = lensModel.alpha(1, 1, kwargs, k=0) alphax_1_2, alphay_1_2 = lensModel.alpha(1, 1, kwargs, k=1) alphax_full, alphay_full = lensModel.alpha(1, 1, kwargs, k=None) npt.assert_almost_equal(alphax_1_1 + alphax_1_2, alphax_full, decimal=5) npt.assert_almost_equal(alphay_1_1 + alphay_1_2, alphay_full, decimal=5) def test_init(self): lens_model_list = ['TNFW', 'SPEMD_SMOOTH'] lensModel = SinglePlane(lens_model_list=lens_model_list) assert lensModel.func_list[0].param_names[0] == 'Rs'
class LensModel(object): """ class to handle an arbitrary list of lens models """ def __init__(self, lens_model_list, z_lens=None, z_source=None, lens_redshift_list=None, cosmo=None, multi_plane=False, numerical_alpha_class=None): """ :param lens_model_list: list of strings with lens model names :param z_lens: redshift of the deflector (only considered when operating in single plane mode). Is only needed for specific functions that require a cosmology. :param z_source: redshift of the source: Needed in multi_plane option only, not required for the core functionalities in the single plane mode. :param lens_redshift_list: list of deflector redshift (corresponding to the lens model list), only applicable in multi_plane mode. :param cosmo: instance of the astropy cosmology class. If not specified, uses the default cosmology. :param multi_plane: bool, if True, uses multi-plane mode. Default is False. :param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model (see documentation in Profiles/numerical_alpha) """ self.lens_model_list = lens_model_list self.z_lens = z_lens self.z_source = z_source self.redshift_list = lens_redshift_list self.cosmo = cosmo self.multi_plane = multi_plane if multi_plane is True: self.lens_model = MultiPlane( z_source, lens_model_list, lens_redshift_list, cosmo=cosmo, numerical_alpha_class=numerical_alpha_class) else: self.lens_model = SinglePlane( lens_model_list, numerical_alpha_class=numerical_alpha_class) if z_lens is not None and z_source is not None: self._lensCosmo = LensCosmo(z_lens, z_source, cosmo=self.cosmo) def ray_shooting(self, x, y, kwargs, k=None): """ maps image to source position (inverse deflection) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: source plane positions corresponding to (x, y) in the image plane """ return self.lens_model.ray_shooting(x, y, kwargs, k=k) def fermat_potential(self, x_image, y_image, x_source, y_source, kwargs_lens): """ fermat potential (negative sign means earlier arrival time) :param x_image: image position :param y_image: image position :param x_source: source position :param y_source: source position :param kwargs_lens: list of keyword arguments of lens model parameters matching the lens model classes :return: fermat potential in arcsec**2 without geometry term (second part of Eqn 1 in Suyu et al. 2013) as a list """ if hasattr(self.lens_model, 'fermat_potential'): return self.lens_model.fermat_potential(x_image, y_image, x_source, y_source, kwargs_lens) else: raise ValueError( "Fermat potential is not defined in multi-plane lensing. Please use single plane lens models." ) def arrival_time(self, x_image, y_image, kwargs_lens): """ :param x_image: image position :param y_image: image position :param kwargs_lens: lens model parameter keyword argument list :return: """ if hasattr(self.lens_model, 'arrival_time'): arrival_time = self.lens_model.arrival_time( x_image, y_image, kwargs_lens) else: x_source, y_source = self.lens_model.ray_shooting( x_image, y_image, kwargs_lens) fermat_pot = self.lens_model.fermat_potential( x_image, y_image, x_source, y_source, kwargs_lens) if not hasattr(self, '_lensCosmo'): raise ValueError( "LensModel class was not initalized with lens and source redshifts!" ) arrival_time = self._lensCosmo.time_delay_units(fermat_pot) return arrival_time def potential(self, x, y, kwargs, k=None): """ lensing potential :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: lensing potential in units of arcsec^2 """ return self.lens_model.potential(x, y, kwargs, k=k) def alpha(self, x, y, kwargs, k=None): """ deflection angles :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: deflection angles in units of arcsec """ return self.lens_model.alpha(x, y, kwargs, k=k) def hessian(self, x, y, kwargs, k=None): """ hessian matrix :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: f_xx, f_xy, f_yy components """ return self.lens_model.hessian(x, y, kwargs, k=k) def kappa(self, x, y, kwargs, k=None): """ lensing convergence k = 1/2 laplacian(phi) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: lensing convergence """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs, k=k) kappa = 1. / 2 * (f_xx + f_yy) return kappa def gamma(self, x, y, kwargs, k=None): """ shear computation g1 = 1/2(d^2phi/dx^2 - d^2phi/dy^2) g2 = d^2phi/dxdy :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: gamma1, gamma2 """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs, k=k) gamma1 = 1. / 2 * (f_xx - f_yy) gamma2 = f_xy return gamma1, gamma2 def magnification(self, x, y, kwargs, k=None): """ magnification mag = 1/det(A) A = 1 - d^2phi/d_ij :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param k: only evaluate the k-th lens model :return: magnification """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs, k=k) det_A = (1 - f_xx) * (1 - f_yy) - f_xy * f_yx return 1. / det_A # attention, if dividing by zero def flexion(self, x, y, kwargs, diff=0.000001): """ third derivatives (flexion) :param x: x-position (preferentially arcsec) :type x: numpy array :param y: y-position (preferentially arcsec) :type y: numpy array :param kwargs: list of keyword arguments of lens model parameters matching the lens model classes :param diff: numerical differential length of Hessian :return: f_xxx, f_xxy, f_xyy, f_yyy """ f_xx, f_xy, f_yx, f_yy = self.hessian(x, y, kwargs) f_xx_dx, f_xy_dx, f_yx_dx, f_yy_dx = self.hessian(x + diff, y, kwargs) f_xx_dy, f_xy_dy, f_yx_dy, f_yy_dy = self.hessian(x, y + diff, kwargs) f_xxx = (f_xx_dx - f_xx) / diff f_xxy = (f_xx_dy - f_xx) / diff f_xyy = (f_xy_dy - f_xy) / diff f_yyy = (f_yy_dy - f_yy) / diff return f_xxx, f_xxy, f_xyy, f_yyy
def test_mass_2d(self): lensModel = SinglePlane(['GAUSSIAN_KAPPA']) output = lensModel.mass_2d(r=1, kwargs=self.kwargs) assert output == 0.11750309741540453