Example #1
0
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
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'
Example #3
0
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
Example #4
0
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
Example #5
0
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
Example #6
0
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)
Example #7
0
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 MultiPlaneBase(object):
    """
    Multi-plane lensing class

    The lens model deflection angles are in units of reduced deflections from the specified redshift of the lens to the
    sourde redshift of the class instance.
    """
    def __init__(self,
                 lens_model_list,
                 lens_redshift_list,
                 z_source_convention,
                 cosmo=None,
                 numerical_alpha_class=None):
        """

        :param lens_model_list: list of lens model strings
        :param lens_redshift_list: list of floats with redshifts of the lens models indicated in lens_model_list
        :param z_source_convention: float, redshift of a source to define the reduced deflection angles of the lens
        models. If None, 'z_source' is used.
        :param cosmo: instance of astropy.cosmology
        :param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model
        (see documentation in Profiles/numerical_alpha)

        """
        self._cosmo_bkg = Background(cosmo)
        self._z_source_convention = z_source_convention
        if len(lens_redshift_list) > 0:
            z_lens_max = np.max(lens_redshift_list)
            if z_lens_max >= z_source_convention:
                raise ValueError(
                    'deflector redshifts higher or equal the source redshift convention (%s >= %s for the reduced lens'
                    ' model quantities not allowed (leads to negative reduced deflection angles!'
                    % (z_lens_max, z_source_convention))
        if not len(lens_model_list) == len(lens_redshift_list):
            raise ValueError(
                "The length of lens_model_list does not correspond to redshift_list"
            )

        self._lens_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 ray_shooting_partial(self,
                             x,
                             y,
                             alpha_x,
                             alpha_y,
                             z_start,
                             z_stop,
                             kwargs_lens,
                             include_z_start=False,
                             T_ij_start=None,
                             T_ij_end=None):
        """
        ray-tracing through parts of the coin, starting with (x,y) co-moving distances and angles (alpha_x, alpha_y) at redshift z_start
        and then backwards to redshift z_stop

        :param x: co-moving position [Mpc]
        :param y: co-moving position [Mpc]
        :param alpha_x: ray angle at z_start [arcsec]
        :param alpha_y: ray angle at z_start [arcsec]
        :param z_start: redshift of start of computation
        :param z_stop: redshift where output is computed
        :param kwargs_lens: lens model keyword argument list
        :param include_z_start: bool, if True, includes the computation of the deflection angle at the same redshift as
        the start of the ray-tracing. ATTENTION: deflection angles at the same redshift as z_stop will be computed always!
        This can lead to duplications in the computation of deflection angles.
        :param T_ij_start: transverse angular distance between the starting redshift to the first lens plane to follow.
        If not set, will compute the distance each time this function gets executed.
        :param T_ij_end: transverse angular distance between the last lens plane being computed and z_end.
        If not set, will compute the distance each time this function gets executed.
        :return: co-moving position and angles at redshift z_stop
        """
        x = np.array(x, dtype=float)
        y = np.array(y, dtype=float)
        alpha_x = np.array(alpha_x)
        alpha_y = np.array(alpha_y)
        z_lens_last = z_start
        first_deflector = True

        for i, idex in enumerate(self._sorted_redshift_index):
            z_lens = self._lens_redshift_list[idex]

            if self._start_condition(include_z_start, z_lens,
                                     z_start) and z_lens <= z_stop:
                if first_deflector is True:
                    if T_ij_start is None:
                        if z_start == 0:
                            delta_T = self._T_ij_list[0]
                        else:
                            delta_T = self._cosmo_bkg.T_xy(z_start, z_lens)
                    else:
                        delta_T = T_ij_start
                    first_deflector = False
                else:
                    delta_T = self._T_ij_list[i]
                x, y = self._ray_step_add(x, y, alpha_x, alpha_y, delta_T)
                alpha_x, alpha_y = self._add_deflection(
                    x, y, alpha_x, alpha_y, kwargs_lens, i)

                z_lens_last = z_lens
        if T_ij_end is None:
            if z_lens_last == z_stop:
                delta_T = 0
            else:
                delta_T = self._cosmo_bkg.T_xy(z_lens_last, z_stop)
        else:
            delta_T = T_ij_end
        x, y = self._ray_step_add(x, y, alpha_x, alpha_y, delta_T)
        return x, y, alpha_x, alpha_y

    def transverse_distance_start_stop(self,
                                       z_start,
                                       z_stop,
                                       include_z_start=False):
        """
        computes the transverse distance (T_ij) that is required by the ray-tracing between the starting redshift and
        the first deflector afterwards and the last deflector before the end of the ray-tracing.

        :param z_start: redshift of the start of the ray-tracing
        :param z_stop: stop of ray-tracing
        :return: T_ij_start, T_ij_end
        """
        z_lens_last = z_start
        first_deflector = True
        T_ij_start = None
        for i, idex in enumerate(self._sorted_redshift_index):
            z_lens = self._lens_redshift_list[idex]
            if self._start_condition(include_z_start, z_lens,
                                     z_start) and z_lens <= z_stop:
                if first_deflector is True:
                    T_ij_start = self._cosmo_bkg.T_xy(z_start, z_lens)
                    first_deflector = False
                z_lens_last = z_lens
        T_ij_end = self._cosmo_bkg.T_xy(z_lens_last, z_stop)
        return T_ij_start, T_ij_end

    def ray_shooting_partial_steps(self,
                                   x,
                                   y,
                                   alpha_x,
                                   alpha_y,
                                   z_start,
                                   z_stop,
                                   kwargs_lens,
                                   include_z_start=False):
        """
        ray-tracing through parts of the coin, starting with (x,y) and angles (alpha_x, alpha_y) at redshift z_start
        and then backwards to redshift z_stop.

        This function differs from 'ray_shooting_partial' in that it returns the angular position of the ray
        at each lens plane.

        :param x: co-moving position [Mpc]
        :param y: co-moving position [Mpc]
        :param alpha_x: ray angle at z_start [arcsec]
        :param alpha_y: ray angle at z_start [arcsec]
        :param z_start: redshift of start of computation
        :param z_stop: redshift where output is computed
        :param kwargs_lens: lens model keyword argument list
        :param keep_range: bool, if True, only computes the angular diameter ratio between the first and last step once
        :param check_convention: flag to check the image position convention (leave this alone)
        :return: co-moving position and angles at redshift z_stop
        """
        z_lens_last = z_start
        first_deflector = True

        pos_x, pos_y, redshifts, Tz_list = [], [], [], []
        pos_x.append(x)
        pos_y.append(y)
        redshifts.append(z_start)
        Tz_list.append(self._cosmo_bkg.T_xy(0, z_start))

        current_z = z_lens_last

        for i, idex in enumerate(self._sorted_redshift_index):

            z_lens = self._lens_redshift_list[idex]

            if self._start_condition(include_z_start, z_lens,
                                     z_start) and z_lens <= z_stop:

                if z_lens != current_z:
                    new_plane = True
                    current_z = z_lens

                else:
                    new_plane = False

                if first_deflector is True:
                    delta_T = self._cosmo_bkg.T_xy(z_start, z_lens)

                    first_deflector = False
                else:
                    delta_T = self._T_ij_list[i]
                x, y = self._ray_step_add(x, y, alpha_x, alpha_y, delta_T)
                alpha_x, alpha_y = self._add_deflection(
                    x, y, alpha_x, alpha_y, kwargs_lens, i)
                z_lens_last = z_lens

                if new_plane:

                    pos_x.append(x)
                    pos_y.append(y)
                    redshifts.append(z_lens)
                    Tz_list.append(self._T_z_list[i])

        delta_T = self._cosmo_bkg.T_xy(z_lens_last, z_stop)

        x, y = self._ray_step_add(x, y, alpha_x, alpha_y, delta_T)

        pos_x.append(x)
        pos_y.append(y)
        redshifts.append(z_stop)
        T_z_source = self._cosmo_bkg.T_xy(0, z_stop)
        Tz_list.append(T_z_source)

        return pos_x, pos_y, redshifts, Tz_list

    def arrival_time(self,
                     theta_x,
                     theta_y,
                     kwargs_lens,
                     z_stop,
                     T_z_stop=None,
                     T_ij_end=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 = np.array(theta_x)
        alpha_y = np.array(theta_y)
        i = 0
        z_lens_last = 0
        for i, index in enumerate(self._sorted_redshift_index):
            z_lens = self._lens_redshift_list[index]
            if z_lens <= z_stop:
                T_ij = self._T_ij_list[i]
                x_new, y_new = self._ray_step(x, y, alpha_x, alpha_y, T_ij)
                if i == 0:
                    pass
                elif T_ij > 0:
                    T_j = self._T_z_list[i]
                    T_i = self._T_z_list[i - 1]
                    beta_i_x, beta_i_y = x / T_i, y / T_i
                    beta_j_x, beta_j_y = x_new / T_j, y_new / T_j
                    dt_geo_new = self._geometrical_delay(
                        beta_i_x, beta_i_y, beta_j_x, beta_j_y, T_i, T_j, T_ij)
                    dt_geo += dt_geo_new
                x, y = x_new, y_new
                dt_grav_new = self._gravitational_delay(
                    x, y, kwargs_lens, i, z_lens)
                alpha_x, alpha_y = self._add_deflection(
                    x, y, alpha_x, alpha_y, kwargs_lens, i)

                dt_grav += dt_grav_new
                z_lens_last = z_lens
        if T_ij_end is None:
            T_ij_end = self._cosmo_bkg.T_xy(z_lens_last, z_stop)
        T_ij = T_ij_end
        x_new, y_new = self._ray_step(x, y, alpha_x, alpha_y, T_ij)
        if T_z_stop is None:
            T_z_stop = self._cosmo_bkg.T_xy(0, z_stop)
        T_j = T_z_stop
        T_i = self._T_z_list[i]
        beta_i_x, beta_i_y = x / T_i, y / T_i
        beta_j_x, beta_j_y = x_new / T_j, y_new / T_j
        dt_geo_new = self._geometrical_delay(beta_i_x, beta_i_y, beta_j_x,
                                             beta_j_y, T_i, T_j, T_ij)
        dt_geo += dt_geo_new
        return dt_grav + dt_geo

    @staticmethod
    def _index_ordering(redshift_list):
        """

        :param redshift_list: list of redshifts
        :return: indexes in acending order to be evaluated (from z=0 to z=z_source)
        """
        redshift_list = np.array(redshift_list)
        #sort_index = np.argsort(redshift_list[redshift_list < z_source])
        sort_index = np.argsort(redshift_list)
        #if len(sort_index) < 1:
        #    Warning("There is no lens object between observer at z=0 and source at z=%s" % z_source)
        return sort_index

    def _reduced2physical_deflection(self, alpha_reduced, index_lens):
        """
        alpha_reduced = D_ds/Ds alpha_physical

        :param alpha_reduced: reduced deflection angle
        :param z_lens: lens redshift
        :param z_source: source redshift
        :return: physical deflection angle
        """
        factor = self._reduced2physical_factor[index_lens]
        return alpha_reduced * factor

    def _gravitational_delay(self, x, y, kwargs_lens, 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_convention)
        return -delay_days

    @staticmethod
    def _geometrical_delay(beta_i_x, beta_i_y, beta_j_x, beta_j_y, T_i, T_j,
                           T_ij):
        """

        :param beta_i_x: angle on the sky at plane i
        :param beta_i_y: angle on the sky at plane i
        :param beta_j_x: angle on the sky at plane j
        :param beta_j_y: angle on the sky at plane j
        :param T_i: transverse diameter distance to z_i
        :param T_j: transverse diameter distance to z_j
        :param T_ij: transverse diameter distance from z_i to z_j
        :return: excess delay relative to a straight line
        """
        d_beta_x = beta_j_x - beta_i_x
        d_beta_y = beta_j_y - beta_i_y
        tau_ij = T_i * T_j / T_ij * const.Mpc / const.c / const.day_s * const.arcsec**2
        return tau_ij * (d_beta_x**2 + d_beta_y**2) / 2

    def _lensing_potential2time_delay(self, potential, z_lens, z_source):
        """
        transforms the lensing potential (in units arcsec^2) to a gravitational time-delay as measured at z=0

        :param potential: lensing potential
        :param z_lens: redshift of the deflector
        :param z_source: redshift of source for the definition of the lensing quantities
        :return: gravitational time-delay in units of days
        """
        D_dt = self._cosmo_bkg.D_dt(z_lens, z_source)
        delay_days = const.delay_arcsec2days(potential, D_dt)
        return delay_days

    def _co_moving2angle(self, x, y, index):
        """
        transforms co-moving distances Mpc into angles on the sky (radian)

        :param x: co-moving distance
        :param y: co-moving distance
        :param index: index of plane
        :return: angles on the sky
        """
        T_z = self._T_z_list[index]
        theta_x = x / T_z
        theta_y = y / T_z
        return theta_x, theta_y

    @staticmethod
    def _ray_step(x, y, alpha_x, alpha_y, delta_T):
        """
        ray propagation with small angle approximation

        :param x: co-moving x-position
        :param y: co-moving y-position
        :param alpha_x: deflection angle in x-direction at (x, y)
        :param alpha_y: deflection angle in y-direction at (x, y)
        :param delta_T: transverse angular diameter distance to the next step
        :return: co-moving position at the next step (backwards)
        """
        x_ = x + alpha_x * delta_T
        y_ = y + alpha_y * delta_T
        return x_, y_

    @staticmethod
    def _ray_step_add(x, y, alpha_x, alpha_y, delta_T):
        """
        ray propagation with small angle approximation

        :param x: co-moving x-position
        :param y: co-moving y-position
        :param alpha_x: deflection angle in x-direction at (x, y)
        :param alpha_y: deflection angle in y-direction at (x, y)
        :param delta_T: transverse angular diameter distance to the next step
        :return: co-moving position at the next step (backwards)
        """
        x += alpha_x * delta_T
        y += alpha_y * delta_T
        return x, y

    def _add_deflection(self, x, y, alpha_x, alpha_y, kwargs_lens, index):
        """
        adds the physical deflection angle of a single lens plane to the deflection field

        :param x: co-moving distance at the deflector plane
        :param y: co-moving distance at the deflector plane
        :param alpha_x: physical angle (radian) before the deflector plane
        :param alpha_y: physical angle (radian) before the deflector plane
        :param kwargs_lens: lens model parameter kwargs
        :param index: index of the lens model to be added
        :param idex_lens: redshift of the deflector plane
        :return: updated physical deflection after deflector plane (in a backwards ray-tracing perspective)
        """
        theta_x, theta_y = self._co_moving2angle(x, y, index)
        alpha_x_red, alpha_y_red = self._lens_model.alpha(
            theta_x,
            theta_y,
            kwargs_lens,
            k=self._sorted_redshift_index[index])
        alpha_x_phys = self._reduced2physical_deflection(alpha_x_red, index)
        alpha_y_phys = self._reduced2physical_deflection(alpha_y_red, index)
        return alpha_x - alpha_x_phys, alpha_y - alpha_y_phys

    @staticmethod
    def _start_condition(inclusive, z_lens, z_start):

        if inclusive:
            return z_lens >= z_start
        else:
            return z_lens > z_start