From 3139a083f2c6ae7980d246b5d504d04b2d1273a5 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 10 Aug 2022 10:03:16 -0700 Subject: [PATCH 01/67] nfw_ellipse_cse profile ellipticity normalization changed --- .../Profiles/cored_steep_ellipsoid.py | 2 ++ .../LensModel/Profiles/nfw_ellipse_cse.py | 5 ++-- .../test_cored_steep_ellipsoid.py | 27 +++++++++++++++++ .../test_Profiles/test_nfw_ellipse_cse.py | 29 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py index 12a7fa6f7..65b59f970 100644 --- a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py +++ b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py @@ -19,6 +19,7 @@ class CSE(LensProfileBase): \\kappa(u;s) = \\frac{A}{2(s^2 + \\xi^2)^{3/2}} with + .. math:: \\xi(x, y) = \\sqrt{x^2 + \\frac{y^2}{q^2}} @@ -127,6 +128,7 @@ class CSEMajorAxis(LensProfileBase): \\kappa(u;s) = \\frac{A}{2(s^2 + \\xi^2)^{3/2}} with + .. math:: \\xi(x, y) = \\sqrt{x^2 + \\frac{y^2}{q^2}} diff --git a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py index 5928e8143..64e2e65a0 100644 --- a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py +++ b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py @@ -167,6 +167,7 @@ def _normalization(self, alpha_Rs, Rs, q): :return: normalization (m) """ rho0 = self.nfw.alpha2rho0(alpha_Rs, Rs) - rs_ = Rs / np.sqrt(q) - const = 4 * rho0 * rs_ ** 3 + c = 2 * q / (1 + q) # this is the same as 1 - e with e = (1. - q) / (1. + q) + rs_ = Rs # / np.sqrt(q) + const = 4 * rho0 * rs_ ** 3 / c return const diff --git a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py index 13d65d164..0b76f3233 100644 --- a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py +++ b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py @@ -43,6 +43,33 @@ def test_hessian(self): npt.assert_almost_equal(f_xy, [-0.13493, -0.], decimal=5) npt.assert_almost_equal(f_yy, [-0.03315, 0.27639], decimal=5) + def test_ellipticity(self): + """ + test the definition of the ellipticity normalization (along major axis or product averaged axes) + """ + x, y = np.linspace(start=0.001, stop=10, num=100), np.zeros(100) + kwargs_round = {'a': 2, 's': 1, 'e1': 0.3, 'e2': 0., 'center_x': 0, 'center_y': 0} + kwargs = {'a': 2, 's': 1, 'e1': 0.3, 'e2': 0., 'center_x': 0, 'center_y': 0} + + f_xx, f_xy, f_yx, f_yy = self.CSP.hessian(x, y, **kwargs_round) + kappa_round = 1. / 2 * (f_xx + f_yy) + + f_xx, f_xy, f_yx, f_yy = self.CSP.hessian(x, y, **kwargs) + kappa_major = 1. / 2 * (f_xx + f_yy) + + f_xx, f_xy, f_yx, f_yy = self.CSP.hessian(y, x, **kwargs) + kappa_minor = 1. / 2 * (f_xx + f_yy) + + npt.assert_almost_equal(kappa_major, kappa_round, decimal=4) + + # import matplotlib.pyplot as plt + # plt.plot(x, kappa_round, ':', label='round', alpha=0.5) + # plt.plot(x, kappa_major, ',-', label='major', alpha=0.5) + # plt.plot(x, kappa_minor, '--', label='minor', alpha=0.5) + # plt.legend() + # plt.show() + # assert 1 == 0 + if __name__ == '__main__': pytest.main() diff --git a/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py b/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py index fd107f6e5..24cbeae38 100644 --- a/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py +++ b/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py @@ -67,6 +67,35 @@ def test_mass_3d_lens(self): m_3d_cse = self.nfw_cse.mass_3d_lens(R, Rs, alpha_Rs) npt.assert_almost_equal(m_3d_nfw, m_3d_cse, decimal=8) + def test_ellipticity(self): + """ + test the definition of the ellipticity normalization (along major axis or product averaged axes) + """ + x, y = np.linspace(start=0.001, stop=10, num=100), np.zeros(100) + kwargs_round = {'alpha_Rs': 0.5, 'Rs': 2, 'center_x': 0, 'center_y': 0, 'e1': 0, 'e2': 0} + kwargs = {'alpha_Rs': 0.5, 'Rs': 2, 'center_x': 0, 'center_y': 0, 'e1': 0.5, 'e2': 0} + + f_xx, f_xy, f_yx, f_yy = self.nfw_cse.hessian(x, y, **kwargs_round) + kappa_round = 1. / 2 * (f_xx + f_yy) + + f_xx, f_xy, f_yx, f_yy = self.nfw_cse.hessian(x, y, **kwargs) + kappa_major = 1. / 2 * (f_xx + f_yy) + + f_xx, f_xy, f_yx, f_yy = self.nfw_cse.hessian(y, x, **kwargs) + kappa_minor = 1. / 2 * (f_xx + f_yy) + + # npt.assert_almost_equal(np.sqrt(kappa_minor**2 + kappa_major**2), kappa_round, decimal=4) + + # import matplotlib.pyplot as plt + # plt.plot(x, kappa_round/kappa_round, ':', label='round', alpha=0.5) + # plt.plot(x, kappa_major/kappa_round, ',-', label='major', alpha=0.5) + # plt.plot(x, kappa_minor/kappa_round, '--', label='minor', alpha=0.5) + # plt.plot(x, np.sqrt(kappa_minor * kappa_major)/kappa_round, '--', label='prod', alpha=0.5) + # plt.plot(x, np.sqrt(kappa_minor**2 + kappa_major**2) / kappa_round / 2, '--', label='square', alpha=0.5) + # plt.legend() + # plt.show() + # assert 1 == 0 + if __name__ == '__main__': pytest.main() From 07ba776023805cbc957554d738f0072f4579f8c8 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Mon, 15 Aug 2022 10:24:57 -0700 Subject: [PATCH 02/67] minor documentation update in LightCone --- lenstronomy/LensModel/LightConeSim/light_cone.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lenstronomy/LensModel/LightConeSim/light_cone.py b/lenstronomy/LensModel/LightConeSim/light_cone.py index 51a5e547d..97a2f6b31 100644 --- a/lenstronomy/LensModel/LightConeSim/light_cone.py +++ b/lenstronomy/LensModel/LightConeSim/light_cone.py @@ -25,8 +25,10 @@ class to perform multi-plane ray-tracing from convergence maps at different reds def __init__(self, mass_map_list, grid_spacing_list, redshift_list): """ - :param mass_map_list: 2d numpy array of mass map (in units Msol) + :param mass_map_list: 2d numpy array of mass map + (in units physical Solar masses enclosed in each pixel/gird point of the map) :param grid_spacing_list: list of grid spacing of the individual mass maps + in units of physical Mpc :param redshift_list: list of redshifts of the mass maps """ From 9c4a5b5055b1cbfe07dae2ecfabe85a8cd802cfa Mon Sep 17 00:00:00 2001 From: sibirrer Date: Thu, 18 Aug 2022 09:22:26 -0700 Subject: [PATCH 03/67] added features for Sersic mass profile to convert from stellar mass to k_eff and back, added documentation of this feature --- lenstronomy/Cosmo/lens_cosmo.py | 34 ++++++++++++++++++++++++ lenstronomy/LensModel/Profiles/nfw.py | 2 +- lenstronomy/LensModel/Profiles/sersic.py | 31 +++++++++++++++++++++ test/test_Cosmo/test_lens_cosmo.py | 10 +++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/lenstronomy/Cosmo/lens_cosmo.py b/lenstronomy/Cosmo/lens_cosmo.py index ee314ef9d..5659cf5d9 100644 --- a/lenstronomy/Cosmo/lens_cosmo.py +++ b/lenstronomy/Cosmo/lens_cosmo.py @@ -275,3 +275,37 @@ def uldm_mphys2angular(self, m_log10, M_log10): theta_c = r_c / D_Lens / const.arcsec return kappa_0, theta_c + def sersic_m_star2k_eff(self, m_star, R_sersic, n_sersic): + """ + translates a total stellar mass into 'k_eff', the convergence at + 'R_sersic' (effective radius or half-light radius) for a Sersic profile + + :param m_star: total stellar mass in physical Msun + :param R_sersic: half-light radius in arc seconds + :param n_sersic: Sersic index + :return: k_eff + """ + # compute mass integral + from lenstronomy.LensModel.Profiles.sersic_utils import SersicUtil + sersic_util = SersicUtil() + norm_integral = sersic_util.total_flux(amp=1, R_sersic=R_sersic, n_sersic=n_sersic) + # compute total kappa normalization and re + k_eff = m_star / self.sigma_crit_angle + # renormalize + k_eff /= norm_integral + return k_eff + + def sersic_k_eff2m_star(self, k_eff, R_sersic, n_sersic): + """ + translates convergence at half-light radius to total integrated physical stellar mass for a Sersic profile + + :param k_eff: lensing convergence at half-light radius + :param R_sersic: half-light radius in arc seconds + :param n_sersic: Sersic index + :return: stellar mass in physical Msun + """ + from lenstronomy.LensModel.Profiles.sersic_utils import SersicUtil + sersic_util = SersicUtil() + norm_integral = sersic_util.total_flux(amp=1, R_sersic=R_sersic, n_sersic=n_sersic) + m_star = k_eff *self.sigma_crit_angle * norm_integral + return m_star diff --git a/lenstronomy/LensModel/Profiles/nfw.py b/lenstronomy/LensModel/Profiles/nfw.py index 8237ad422..a622245a5 100644 --- a/lenstronomy/LensModel/Profiles/nfw.py +++ b/lenstronomy/LensModel/Profiles/nfw.py @@ -34,7 +34,7 @@ class NFW(LensProfileBase): The lens model calculation uses angular units as arguments! So to execute a deflection angle calculation one uses - >>> from lenstronomy.LensModel.Profiles.hernquist import NFW + >>> from lenstronomy.LensModel.Profiles.nfw import NFW >>> nfw = NFW() >>> alpha_x, alpha_y = nfw.derivatives(x=1, y=1, Rs=Rs_angle, alpha_Rs=alpha_Rs, center_x=0, center_y=0) diff --git a/lenstronomy/LensModel/Profiles/sersic.py b/lenstronomy/LensModel/Profiles/sersic.py index 064e4cd22..077fe3720 100644 --- a/lenstronomy/LensModel/Profiles/sersic.py +++ b/lenstronomy/LensModel/Profiles/sersic.py @@ -12,6 +12,37 @@ class Sersic(SersicUtil, LensProfileBase): """ this class contains functions to evaluate a Sersic mass profile: https://arxiv.org/pdf/astro-ph/0311559.pdf + + .. math:: + \\kappa(R) = \\kappa_{\\rm eff} \\exp \\left[ -b_n (R/R_{\\rm Sersic})^{\\frac{1}{n}}\\right] + + with :math:`b_{n}\\approx 1.999n-0.327` + + Examples for converting physical mass units into convergence units used in the definition of this profile + --------------------------------------------------------------------------------------------------------- + >>> from lenstronomy.Cosmo.lens_cosmo import LensCosmo + >>> from astropy.cosmology import FlatLambdaCDM + >>> cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) + >>> lens_cosmo = LensCosmo(z_lens=0.5, z_source=1.5, cosmo=cosmo) + + We define the half-light radius R_sersic (arc seconds on the sky) and Sersic index n_sersic + >>> R_sersic = 2 + >>> n_sersic = 4 + + Here we compute k_eff, the convergence at the half-light radius R_sersic for a stellar mass in Msun + + >>> k_eff = lens_cosmo.sersic_m_star2k_eff(m_star=10**11.5, R_sersic=R_sersic, n_sersic=n_sersic) + + And here we perform the inverse calculation given k_eff to return the physical stellar mass. + + >>> m_star = lens_cosmo.sersic_k_eff2m_star(k_eff=k_eff, R_sersic=R_sersic, n_sersic=n_sersic) + + The lens model calculation uses angular units as arguments! So to execute a deflection angle calculation one uses + + >>> from lenstronomy.LensModel.Profiles.sersic import Sersic + >>> sersic = Sersic() + >>> alpha_x, alpha_y = sersic.derivatives(x=1, y=1, k_eff=k_eff, R_sersic=R_sersic, center_x=0, center_y=0) + """ param_names = ['k_eff', 'R_sersic', 'n_sersic', 'center_x', 'center_y'] lower_limit_default = {'k_eff': 0, 'R_sersic': 0, 'n_sersic': 0.5, 'center_x': -100, 'center_y': -100} diff --git a/test/test_Cosmo/test_lens_cosmo.py b/test/test_Cosmo/test_lens_cosmo.py index 83b5d1143..db4fc6634 100644 --- a/test/test_Cosmo/test_lens_cosmo.py +++ b/test/test_Cosmo/test_lens_cosmo.py @@ -108,6 +108,16 @@ def test_a_z(self): a = self.lensCosmo.a_z(z=1) npt.assert_almost_equal(a, 0.5) + def test_sersic_m_star2k_eff(self): + m_star = 10**11.5 + R_sersic = 1 + n_sersic = 4 + k_eff = self.lensCosmo.sersic_m_star2k_eff(m_star, R_sersic, n_sersic) + npt.assert_almost_equal(k_eff, 0.1294327891669961, decimal=5) + + m_star_out = self.lensCosmo.sersic_k_eff2m_star(k_eff, R_sersic, n_sersic) + npt.assert_almost_equal(m_star_out, m_star, decimal=6) + if __name__ == '__main__': pytest.main() From 2c155cfc8fcf0312b1aac713cc74471a7f1fa035 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Thu, 18 Aug 2022 09:38:47 -0700 Subject: [PATCH 04/67] minor documentation update --- lenstronomy/LensModel/Profiles/sersic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lenstronomy/LensModel/Profiles/sersic.py b/lenstronomy/LensModel/Profiles/sersic.py index 077fe3720..7dc776fa3 100644 --- a/lenstronomy/LensModel/Profiles/sersic.py +++ b/lenstronomy/LensModel/Profiles/sersic.py @@ -20,6 +20,9 @@ class Sersic(SersicUtil, LensProfileBase): Examples for converting physical mass units into convergence units used in the definition of this profile --------------------------------------------------------------------------------------------------------- + + We first define an AstroPy cosmology instance and a LensCosmo class instance with a lens and source redshift. + >>> from lenstronomy.Cosmo.lens_cosmo import LensCosmo >>> from astropy.cosmology import FlatLambdaCDM >>> cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) From 17a9484b04ea63c501f12e2eecaf924168d33d4d Mon Sep 17 00:00:00 2001 From: sibirrer Date: Thu, 18 Aug 2022 10:33:37 -0700 Subject: [PATCH 05/67] minor change in documentation --- docs/index.rst | 2 +- lenstronomy/LensModel/Profiles/sersic.py | 6 +----- lenstronomy/Sampling/likelihood.py | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 593c8e48c..f9b057ea5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,7 +12,7 @@ Contents: :maxdepth: 2 installation - modules + lenstronomy usage contributing mailinglist diff --git a/lenstronomy/LensModel/Profiles/sersic.py b/lenstronomy/LensModel/Profiles/sersic.py index 7dc776fa3..8633f1402 100644 --- a/lenstronomy/LensModel/Profiles/sersic.py +++ b/lenstronomy/LensModel/Profiles/sersic.py @@ -1,5 +1,4 @@ __author__ = 'sibirrer' -#this file contains a class to make a gaussian import numpy as np import lenstronomy.Util.util as util @@ -29,6 +28,7 @@ class Sersic(SersicUtil, LensProfileBase): >>> lens_cosmo = LensCosmo(z_lens=0.5, z_source=1.5, cosmo=cosmo) We define the half-light radius R_sersic (arc seconds on the sky) and Sersic index n_sersic + >>> R_sersic = 2 >>> n_sersic = 4 @@ -103,10 +103,6 @@ def hessian(self, x, y, n_sersic, R_sersic, k_eff, center_x=0, center_y=0): d_alpha_dr = self.d_alpha_dr(x, y, n_sersic, R_sersic, k_eff, center_x, center_y) alpha = -self.alpha_abs(x, y, n_sersic, R_sersic, k_eff, center_x, center_y) - #f_xx_ = d_alpha_dr * calc_util.d_r_dx(x_, y_) * x_/r + alpha * calc_util.d_x_diffr_dx(x_, y_) - #f_yy_ = d_alpha_dr * calc_util.d_r_dy(x_, y_) * y_/r + alpha * calc_util.d_y_diffr_dy(x_, y_) - #f_xy_ = d_alpha_dr * calc_util.d_r_dy(x_, y_) * x_/r + alpha * calc_util.d_x_diffr_dy(x_, y_) - f_xx = -(d_alpha_dr/r + alpha/r**2) * x_**2/r + alpha/r f_yy = -(d_alpha_dr/r + alpha/r**2) * y_**2/r + alpha/r f_xy = -(d_alpha_dr/r + alpha/r**2) * x_*y_/r diff --git a/lenstronomy/Sampling/likelihood.py b/lenstronomy/Sampling/likelihood.py index 0b6b29fa4..3f512face 100644 --- a/lenstronomy/Sampling/likelihood.py +++ b/lenstronomy/Sampling/likelihood.py @@ -156,6 +156,13 @@ def __call__(self, a): def logL(self, args, verbose=False): """ routine to compute X2 given variable parameters for a MCMC/PSO chain + + Parameters + ---------- + args : tuple or list of floats + ordered parameter values that are being sampled + verbose : boolean + if True, makes print statements about individual likelihood components """ # extract parameters kwargs_return = self.param.args2kwargs(args) @@ -166,6 +173,21 @@ def logL(self, args, verbose=False): return self.log_likelihood(kwargs_return, verbose=verbose) def log_likelihood(self, kwargs_return, verbose=False): + """ + + Parameters + ---------- + kwargs_return : keyword arguments + need to contain 'kwargs_lens', 'kwargs_source', 'kwargs_lens_light', 'kwargs_ps', 'kwargs_special' + These entries themselfs are lists of keyword argument of the parameters entering the model to be evaluated + verbose : boolean + if True, makes print statements about individual likelihood components + + Returns + ------- + logL : float + log likelihood of the data given the model (natural logarithm) + """ kwargs_lens, kwargs_source, kwargs_lens_light, kwargs_ps, kwargs_special = kwargs_return['kwargs_lens'], \ kwargs_return['kwargs_source'], \ kwargs_return['kwargs_lens_light'], \ From a07589f0cd315a4bfdedbd99c60f608499a1b210 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Thu, 18 Aug 2022 10:36:32 -0700 Subject: [PATCH 06/67] minor change in documentation --- docs/index.rst | 2 +- lenstronomy/LensModel/Profiles/sersic.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f9b057ea5..0fd0fada6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,11 +12,11 @@ Contents: :maxdepth: 2 installation - lenstronomy usage contributing mailinglist authors published affiliatedpackages + lenstronomy history diff --git a/lenstronomy/LensModel/Profiles/sersic.py b/lenstronomy/LensModel/Profiles/sersic.py index 8633f1402..ae8ed4a56 100644 --- a/lenstronomy/LensModel/Profiles/sersic.py +++ b/lenstronomy/LensModel/Profiles/sersic.py @@ -17,8 +17,10 @@ class Sersic(SersicUtil, LensProfileBase): with :math:`b_{n}\\approx 1.999n-0.327` - Examples for converting physical mass units into convergence units used in the definition of this profile - --------------------------------------------------------------------------------------------------------- + Examples + -------- + + Example for converting physical mass units into convergence units used in the definition of this profile. We first define an AstroPy cosmology instance and a LensCosmo class instance with a lens and source redshift. From 5a842876c72dd75298e6a147d0ccace63dd60edf Mon Sep 17 00:00:00 2001 From: sibirrer Date: Thu, 18 Aug 2022 10:42:57 -0700 Subject: [PATCH 07/67] minor changes in AUTHORS.rst --- AUTHORS.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 341beb3c4..bbcbed768 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -9,7 +9,7 @@ Current maintainers * Anowar Shajib `ajshajib `_ * Daniel Gilman `dangilman `_ -lenstronomy developer `mailing list `_ +Contact the lenstronomy developers via `email `_ if you have questions. @@ -51,5 +51,7 @@ Contributors (alphabetic) Past development lead --------------------- -The initial source code of lenstronomy was developed by Simon Birrer `sibirrer `_ -in 2014-2018 and made public in 2018. The lenstronomy developement moved to the project repository in 2022. \ No newline at end of file +The initial source code of lenstronomy was developed by Simon Birrer (`sibirrer `_) +in 2014-2018 and made public in 2018. From 2018-2022 the development of lenstronomy was hosted on Simon Birrer's +repository with increased contributions from many people. +The lenstronomy development moved to the `project repository `_ in 2022. \ No newline at end of file From 28a9322818e086f1313e5270c8589053543b9344 Mon Sep 17 00:00:00 2001 From: Sydney Erickson Date: Thu, 18 Aug 2022 11:46:11 -0700 Subject: [PATCH 08/67] Add option for custom pixel grid kwargs in DataAPI class --- lenstronomy/SimulationAPI/data_api.py | 24 +++++++++++++++++++----- test/test_SimulationAPI/test_data_api.py | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lenstronomy/SimulationAPI/data_api.py b/lenstronomy/SimulationAPI/data_api.py index 0e9a9d4a6..b6f14e151 100644 --- a/lenstronomy/SimulationAPI/data_api.py +++ b/lenstronomy/SimulationAPI/data_api.py @@ -14,13 +14,17 @@ class DataAPI(SingleBand): options are available. Have a look in the specific modules if you are interested in. """ - def __init__(self, numpix, **kwargs_single_band): + def __init__(self, numpix, kwargs_pixel_grid=None, **kwargs_single_band): """ :param numpix: number of pixels per axis in the simulation to be modelled + :param kwargs_pixel_grid: if None, uses default pixel grid option + if defined, must contain keyword arguments for custom pixel grid + (ra_at_xy_0,dec_at_xy_0,transform_pix2angle) :param kwargs_single_band: keyword arguments used to create instance of SingleBand class """ self.numpix = numpix + self.kwargs_pixel_grid = kwargs_pixel_grid SingleBand.__init__(self, **kwargs_single_band) @property @@ -39,13 +43,23 @@ def kwargs_data(self): :return: keyword arguments for ImageData class instance """ - x_grid, y_grid, ra_at_xy_0, dec_at_xy_0, x_at_radec_0, y_at_radec_0, transform_pix2angle, transform_angle2pix = util.make_grid_with_coordtransform( - numPix=self.numpix, deltapix=self.pixel_scale, subgrid_res=1, left_lower=False, inverse=False) + # default pixel grid + if self.kwargs_pixel_grid is None: + _, _, ra_at_xy_0, dec_at_xy_0, _, _, transform_pix2angle, _ = ( + util.make_grid_with_coordtransform(numPix=self.numpix, + deltapix=self.pixel_scale, subgrid_res=1, + left_lower=False, inverse=False) ) + # user defined pixel grid + else: + ra_at_xy_0 = self.kwargs_pixel_grid['ra_at_xy_0'] + dec_at_xy_0 = self.kwargs_pixel_grid['dec_at_xy_0'] + transform_pix2angle = self.kwargs_pixel_grid['transform_pix2angle'] # CCD gain corrected exposure time to allow a direct Poisson estimates based on IID counts scaled_exposure_time = self.flux_iid(1) - kwargs_data = {'image_data': np.zeros((self.numpix, self.numpix)), 'ra_at_xy_0': ra_at_xy_0, + kwargs_data = {'image_data': np.zeros((self.numpix, self.numpix)), + 'ra_at_xy_0': ra_at_xy_0, 'dec_at_xy_0': dec_at_xy_0, 'transform_pix2angle': transform_pix2angle, 'background_rms': self.background_noise, 'exposure_time': scaled_exposure_time} - return kwargs_data + return kwargs_data \ No newline at end of file diff --git a/test/test_SimulationAPI/test_data_api.py b/test/test_SimulationAPI/test_data_api.py index 9575acb9f..98193502d 100644 --- a/test/test_SimulationAPI/test_data_api.py +++ b/test/test_SimulationAPI/test_data_api.py @@ -30,6 +30,15 @@ def setup(self): kwargs_data = util.merge_dicts(kwargs_instrument, kwargs_observations) self.api_pixel = DataAPI(numpix=numpix, data_count_unit='ADU', **kwargs_data) + self.ra_at_xy_0 = 0.02 + self.dec_at_xy_0 = 0.02 + self.transform_pix2angle = [[-self.pixel_scale,0],[0,self.pixel_scale]] + kwargs_pixel_grid = {'ra_at_xy_0':self.ra_at_xy_0,'dec_at_xy_0':self.dec_at_xy_0, + 'transform_pix2angle':self.transform_pix2angle} + self.api_pixel_grid = DataAPI(numpix=numpix, + kwargs_pixel_grid=kwargs_pixel_grid, + data_count_unit='ADU',**self.kwargs_data) + def test_data_class(self): data_class = self.api.data_class assert data_class.pixel_width == self.pixel_scale @@ -40,6 +49,12 @@ def test_psf_class(self): psf_class = self.api_pixel.psf_class assert psf_class.psf_type == 'PIXEL' + def test_kwargs_data(self): + kwargs_data = self.api.kwargs_data + assert kwargs_data['ra_at_xy_0'] != self.ra_at_xy_0 + kwargs_data = self.api_pixel_grid.kwargs_data + assert kwargs_data['ra_at_xy_0'] == self.ra_at_xy_0 + class TestRaise(unittest.TestCase): From ce7c1479b5a4255dd8581e98440ed97952ed00c4 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Thu, 18 Aug 2022 11:53:16 -0700 Subject: [PATCH 09/67] addresses issue #344 --- lenstronomy/LensModel/Profiles/curved_arc_tan_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lenstronomy/LensModel/Profiles/curved_arc_tan_diff.py b/lenstronomy/LensModel/Profiles/curved_arc_tan_diff.py index be06a413f..626abf2ac 100644 --- a/lenstronomy/LensModel/Profiles/curved_arc_tan_diff.py +++ b/lenstronomy/LensModel/Profiles/curved_arc_tan_diff.py @@ -26,7 +26,7 @@ class CurvedArcTanDiff(LensProfileBase): """ param_names = ['tangential_stretch', 'radial_stretch', 'curvature', 'dtan_dtan', 'direction', 'center_x', 'center_y'] lower_limit_default = {'tangential_stretch': -100, 'radial_stretch': -5, 'curvature': 0.000001, 'dtan_dtan': -10, 'direction': -np.pi, 'center_x': -100, 'center_y': -100} - upper_limit_default = {'tangential_stretch': 100, 'radial_stretch': 5, 'curvature': 100, 'dtan_dtab': 10, 'direction': np.pi, 'center_x': 100, 'center_y': 100} + upper_limit_default = {'tangential_stretch': 100, 'radial_stretch': 5, 'curvature': 100, 'dtan_dtan': 10, 'direction': np.pi, 'center_x': 100, 'center_y': 100} def __init__(self): self._sie = SIE(NIE=True) From 5f5db317436eb5f98aa4b3c636da6180627bd8aa Mon Sep 17 00:00:00 2001 From: Sydney Erickson Date: Thu, 18 Aug 2022 13:12:08 -0700 Subject: [PATCH 10/67] Check for pixel grid kwargs upon DataAPI initialization --- lenstronomy/SimulationAPI/data_api.py | 25 +++++++++++++----------- test/test_SimulationAPI/test_data_api.py | 6 ++++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lenstronomy/SimulationAPI/data_api.py b/lenstronomy/SimulationAPI/data_api.py index b6f14e151..2fa0e148b 100644 --- a/lenstronomy/SimulationAPI/data_api.py +++ b/lenstronomy/SimulationAPI/data_api.py @@ -19,12 +19,16 @@ def __init__(self, numpix, kwargs_pixel_grid=None, **kwargs_single_band): :param numpix: number of pixels per axis in the simulation to be modelled :param kwargs_pixel_grid: if None, uses default pixel grid option - if defined, must contain keyword arguments for custom pixel grid - (ra_at_xy_0,dec_at_xy_0,transform_pix2angle) + if defined, must contain keyword arguments PixelGrid() class :param kwargs_single_band: keyword arguments used to create instance of SingleBand class """ self.numpix = numpix - self.kwargs_pixel_grid = kwargs_pixel_grid + if kwargs_pixel_grid is not None: + required_keys = ['ra_at_xy_0','dec_at_xy_0','transform_pix2angle'] + if not all(k in kwargs_pixel_grid for k in required_keys): + raise ValueError('Missing 1 or more required'+ + 'kwargs_pixel_grid parameters') + self._kwargs_pixel_grid = kwargs_pixel_grid SingleBand.__init__(self, **kwargs_single_band) @property @@ -44,16 +48,15 @@ def kwargs_data(self): :return: keyword arguments for ImageData class instance """ # default pixel grid - if self.kwargs_pixel_grid is None: - _, _, ra_at_xy_0, dec_at_xy_0, _, _, transform_pix2angle, _ = ( - util.make_grid_with_coordtransform(numPix=self.numpix, - deltapix=self.pixel_scale, subgrid_res=1, - left_lower=False, inverse=False) ) + if self._kwargs_pixel_grid is None: + _, _, ra_at_xy_0, dec_at_xy_0, _, _, transform_pix2angle, _ = util.make_grid_with_coordtransform( + numPix=self.numpix, deltapix=self.pixel_scale, subgrid_res=1, + left_lower=False, inverse=False) # user defined pixel grid else: - ra_at_xy_0 = self.kwargs_pixel_grid['ra_at_xy_0'] - dec_at_xy_0 = self.kwargs_pixel_grid['dec_at_xy_0'] - transform_pix2angle = self.kwargs_pixel_grid['transform_pix2angle'] + ra_at_xy_0 = self._kwargs_pixel_grid['ra_at_xy_0'] + dec_at_xy_0 = self._kwargs_pixel_grid['dec_at_xy_0'] + transform_pix2angle = self._kwargs_pixel_grid['transform_pix2angle'] # CCD gain corrected exposure time to allow a direct Poisson estimates based on IID counts scaled_exposure_time = self.flux_iid(1) kwargs_data = {'image_data': np.zeros((self.numpix, self.numpix)), diff --git a/test/test_SimulationAPI/test_data_api.py b/test/test_SimulationAPI/test_data_api.py index 98193502d..235fed430 100644 --- a/test/test_SimulationAPI/test_data_api.py +++ b/test/test_SimulationAPI/test_data_api.py @@ -87,3 +87,9 @@ def test_raise(self): with self.assertRaises(ValueError): data_api = DataAPI(numpix=numpix, data_count_unit='ADU', **kwargs_data) psf_class = data_api.psf_class + + kwargs_data['kernel_point_source'] = np.ones((3, 3)) + kwargs_pixel_grid = {'ra_at_xy_0':0.02,'dec_at_xy_0':0.02} + with self.assertRaises(ValueError): + data_api = DataAPI(numpix=numpix,kwargs_pixel_grid=kwargs_pixel_grid, + **kwargs_data) From ad9e8fb3cf73309a8a7659a47b87a01b9eeafa5b Mon Sep 17 00:00:00 2001 From: sibirrer Date: Thu, 18 Aug 2022 22:27:27 -0700 Subject: [PATCH 11/67] minor documentation formatting improvements --- docs/conf.py | 3 +- docs/lenstronomy.Data.rst | 57 ++++++++++++------- lenstronomy/Cosmo/background.py | 5 +- lenstronomy/Cosmo/cosmo_solver.py | 8 ++- lenstronomy/Cosmo/lcdm.py | 6 +- lenstronomy/Cosmo/lens_cosmo.py | 12 ++++ lenstronomy/Cosmo/nfw_param.py | 1 + lenstronomy/Data/coord_transforms.py | 5 ++ lenstronomy/GalKin/analytic_kinematics.py | 6 +- lenstronomy/GalKin/anisotropy.py | 4 +- lenstronomy/GalKin/aperture_types.py | 3 + lenstronomy/GalKin/cosmo.py | 3 +- lenstronomy/GalKin/numeric_kinematics.py | 2 +- lenstronomy/ImSim/de_lens.py | 2 + lenstronomy/ImSim/differential_extinction.py | 2 +- lenstronomy/LensModel/Profiles/general_nfw.py | 4 +- lenstronomy/LensModel/Profiles/nfw.py | 1 + lenstronomy/LensModel/Profiles/shear.py | 6 +- lenstronomy/LensModel/Profiles/sie.py | 6 ++ lenstronomy/LensModel/Profiles/spemd.py | 1 + lenstronomy/LensModel/Profiles/spep.py | 12 ++-- lenstronomy/LensModel/Profiles/splcore.py | 18 +++++- lenstronomy/LensModel/Profiles/spp.py | 28 +++++---- lenstronomy/LensModel/Profiles/tnfw.py | 2 + lenstronomy/LensModel/Profiles/uldm.py | 53 ++++++++++------- lenstronomy/LightModel/Profiles/sersic.py | 4 +- lenstronomy/LightModel/Profiles/shapelets.py | 1 + lenstronomy/Plots/plot_util.py | 16 +++--- lenstronomy/Sampling/Pool/multiprocessing.py | 43 ++++++-------- lenstronomy/Sampling/Pool/pool.py | 16 +++--- lenstronomy/Util/image_util.py | 18 +++--- lenstronomy/Util/kernel_util.py | 8 ++- lenstronomy/Util/prob_density.py | 5 ++ lenstronomy/Util/util.py | 9 ++- lenstronomy/Workflow/fitting_sequence.py | 4 +- lenstronomy/Workflow/psf_fitting.py | 1 + 36 files changed, 235 insertions(+), 140 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 402c636d0..cb6fd97ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,8 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']\ +# , 'sphinx.ext.autosectionlabel'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/lenstronomy.Data.rst b/docs/lenstronomy.Data.rst index aadffa78e..b2e47e67c 100644 --- a/docs/lenstronomy.Data.rst +++ b/docs/lenstronomy.Data.rst @@ -1,38 +1,53 @@ -lenstronomy\.Data package -========================= +lenstronomy.Data package +======================== Submodules ---------- -lenstronomy\.Data\.coord\_transforms module -------------------------------------------- +lenstronomy.Data.coord\_transforms module +----------------------------------------- .. automodule:: lenstronomy.Data.coord_transforms - :members: - :undoc-members: - :show-inheritance: + :members: + :undoc-members: + :show-inheritance: -lenstronomy\.Data\.imaging\_data module ---------------------------------------- +lenstronomy.Data.image\_noise module +------------------------------------ + +.. automodule:: lenstronomy.Data.image_noise + :members: + :undoc-members: + :show-inheritance: + +lenstronomy.Data.imaging\_data module +------------------------------------- .. automodule:: lenstronomy.Data.imaging_data - :members: - :undoc-members: - :show-inheritance: + :members: + :undoc-members: + :show-inheritance: -lenstronomy\.Data\.psf module ------------------------------ +lenstronomy.Data.pixel\_grid module +----------------------------------- -.. automodule:: lenstronomy.Data.psf - :members: - :undoc-members: - :show-inheritance: +.. automodule:: lenstronomy.Data.pixel_grid + :members: + :undoc-members: + :show-inheritance: +lenstronomy.Data.psf module +--------------------------- + +.. automodule:: lenstronomy.Data.psf + :members: + :undoc-members: + :show-inheritance: Module contents --------------- .. automodule:: lenstronomy.Data - :members: - :undoc-members: - :show-inheritance: + :members: + :undoc-members: + :show-inheritance: diff --git a/lenstronomy/Cosmo/background.py b/lenstronomy/Cosmo/background.py index b05411483..9e43a4c8b 100644 --- a/lenstronomy/Cosmo/background.py +++ b/lenstronomy/Cosmo/background.py @@ -29,9 +29,11 @@ def __init__(self, cosmo=None, interp=False, **kwargs_interp): else: self.cosmo = cosmo - def a_z(self, z): + @staticmethod + def a_z(z): """ returns scale factor (a_0 = 1) for given redshift + :param z: redshift :return: scale factor """ @@ -72,6 +74,7 @@ def T_xy(self, z_observer, z_source): def rho_crit(self): """ critical density + :return: value in M_sol/Mpc^3 """ h = self.cosmo.H(0).value / 100. diff --git a/lenstronomy/Cosmo/cosmo_solver.py b/lenstronomy/Cosmo/cosmo_solver.py index 339e7db18..e7e6b8cdb 100644 --- a/lenstronomy/Cosmo/cosmo_solver.py +++ b/lenstronomy/Cosmo/cosmo_solver.py @@ -85,15 +85,20 @@ class InvertCosmo(object): """ class to do an interpolation and call the inverse of this interpolation to get H_0 and omega_m """ - def __init__(self, z_d, z_s, H0_range=np.linspace(10, 100, 90), omega_m_range=np.linspace(0.05, 1, 95)): + def __init__(self, z_d, z_s, H0_range=None, omega_m_range=None): self.z_d = z_d self.z_s = z_s + if H0_range is None: + H0_range = np.linspace(10, 100, 90) + if omega_m_range is None: + omega_m_range = np.linspace(0.05, 1, 95) self._H0_range = H0_range self._omega_m_range = omega_m_range def _make_interpolation(self): """ creates an interpolation grid in H_0, omega_m and computes quantities in Dd and Ds_Dds + :return: """ grid2d = np.dstack(np.meshgrid(self._H0_range, self._omega_m_range)).reshape(-1, 2) @@ -115,6 +120,7 @@ def _make_interpolation(self): def get_cosmo(self, Dd, Ds_Dds): """ return the values of H0 and omega_m computed with an interpolation + :param Dd: flat :param Ds_Dds: float :return: diff --git a/lenstronomy/Cosmo/lcdm.py b/lenstronomy/Cosmo/lcdm.py index 491313652..618f5d21e 100644 --- a/lenstronomy/Cosmo/lcdm.py +++ b/lenstronomy/Cosmo/lcdm.py @@ -40,6 +40,7 @@ def _get_cosom(self, H_0, Om0, Ode0=None): def D_d(self, H_0, Om0, Ode0=None): """ angular diameter to deflector + :param H_0: Hubble parameter [km/s/Mpc] :param Om0: normalized matter density at present time :return: float [Mpc] @@ -50,6 +51,7 @@ def D_d(self, H_0, Om0, Ode0=None): def D_s(self, H_0, Om0, Ode0=None): """ angular diameter to source + :param H_0: Hubble parameter [km/s/Mpc] :param Om0: normalized matter density at present time :return: float [Mpc] @@ -60,6 +62,7 @@ def D_s(self, H_0, Om0, Ode0=None): def D_ds(self, H_0, Om0, Ode0=None): """ angular diameter from deflector to source + :param H_0: Hubble parameter [km/s/Mpc] :param Om0: normalized matter density at present time :return: float [Mpc] @@ -69,7 +72,8 @@ def D_ds(self, H_0, Om0, Ode0=None): def D_dt(self, H_0, Om0, Ode0=None): """ - time delay distance + time-delay distance + :param H_0: Hubble parameter [km/s/Mpc] :param Om0: normalized matter density at present time :return: float [Mpc] diff --git a/lenstronomy/Cosmo/lens_cosmo.py b/lenstronomy/Cosmo/lens_cosmo.py index 5659cf5d9..93af64b4c 100644 --- a/lenstronomy/Cosmo/lens_cosmo.py +++ b/lenstronomy/Cosmo/lens_cosmo.py @@ -30,6 +30,7 @@ def __init__(self, z_lens, z_source, cosmo=None): def a_z(self, z): """ convert redshift into scale factor + :param z: redshift :return: scale factor """ @@ -75,6 +76,7 @@ def ddt(self): def sigma_crit(self): """ returns the critical projected lensing mass density in units of M_sun/Mpc^2 + :return: critical projected lensing mass density """ if not hasattr(self, '_sigma_crit_mpc'): @@ -89,6 +91,7 @@ def sigma_crit_angle(self): """ returns the critical surface density in units of M_sun/arcsec^2 (in physical solar mass units) when provided a physical mass per physical Mpc^2 + :return: critical projected mass density """ if not hasattr(self, '_sigma_crit_arcsec'): @@ -101,6 +104,7 @@ def sigma_crit_angle(self): def phys2arcsec_lens(self, phys): """ convert physical Mpc into arc seconds + :param phys: physical distance [Mpc] :return: angular diameter [arcsec] """ @@ -109,6 +113,7 @@ def phys2arcsec_lens(self, phys): def arcsec2phys_lens(self, arcsec): """ convert angular to physical quantities for lens plane + :param arcsec: angular size at lens plane [arcsec] :return: physical size at lens plane [Mpc] """ @@ -117,6 +122,7 @@ def arcsec2phys_lens(self, arcsec): def arcsec2phys_source(self, arcsec): """ convert angular to physical quantities for source plane + :param arcsec: angular size at source plane [arcsec] :return: physical size at source plane [Mpc] """ @@ -125,6 +131,7 @@ def arcsec2phys_source(self, arcsec): def kappa2proj_mass(self, kappa): """ convert convergence to projected mass M_sun/Mpc^2 + :param kappa: lensing convergence :return: projected mass [M_sun/Mpc^2] """ @@ -133,6 +140,7 @@ def kappa2proj_mass(self, kappa): def mass_in_theta_E(self, theta_E): """ mass within Einstein radius (area * epsilon crit) [M_sun] + :param theta_E: Einstein radius [arcsec] :return: mass within Einstein radius [M_sun] """ @@ -225,6 +233,7 @@ def nfw_M_theta_r200(self, M): def sis_theta_E2sigma_v(self, theta_E): """ converts the lensing Einstein radius into a physical velocity dispersion + :param theta_E: Einstein radius (in arcsec) :return: velocity dispersion in units (km/s) """ @@ -234,6 +243,7 @@ def sis_theta_E2sigma_v(self, theta_E): def sis_sigma_v2theta_E(self, v_sigma): """ converts the velocity dispersion into an Einstein radius for a SIS profile + :param v_sigma: velocity dispersion (km/s) :return: theta_E (arcsec) """ @@ -245,6 +255,7 @@ def uldm_angular2phys(self, kappa_0, theta_c): converts the anguar parameters entering the LensModel Uldm() (Ultra Light Dark Matter) class in physical masses, i.e. the total soliton mass and the mass of the particle + :param kappa_0: central convergence of profile :param theta_c: core radius (in arcseconds) :return: m_eV_log10, M_sol_log10, the log10 of the masses, m in eV and M in M_sun @@ -261,6 +272,7 @@ def uldm_mphys2angular(self, m_log10, M_log10): """ converts physical ULDM mass in the ones, in angular units, that enter the LensModel Uldm() class + :param m_log10: exponent of ULDM mass in eV :param M_log10: exponent of soliton mass in M_sun :return: kappa_0, theta_c, the central convergence and core radius (in arcseconds) diff --git a/lenstronomy/Cosmo/nfw_param.py b/lenstronomy/Cosmo/nfw_param.py index 592c6a952..687b11e75 100644 --- a/lenstronomy/Cosmo/nfw_param.py +++ b/lenstronomy/Cosmo/nfw_param.py @@ -81,6 +81,7 @@ def rho0_c(self, c, z): def c_rho0(self, rho0, z): """ computes the concentration given density normalization rho_0 in h^2/Mpc^3 (physical) (inverse of function rho0_c) + :param rho0: density normalization in h^2/Mpc^3 (physical) :param z: redshift :return: concentration parameter c diff --git a/lenstronomy/Data/coord_transforms.py b/lenstronomy/Data/coord_transforms.py index fc73e5a5d..a6f2a14d1 100644 --- a/lenstronomy/Data/coord_transforms.py +++ b/lenstronomy/Data/coord_transforms.py @@ -14,6 +14,7 @@ class to handle linear coordinate transformations of a square pixel image def __init__(self, transform_pix2angle, ra_at_xy_0, dec_at_xy_0): """ initialize the coordinate-to-pixel transform and their inverse + :param transform_pix2angle: 2x2 matrix, mapping of pixel to coordinate :param ra_at_xy_0: ra coordinate at pixel (0,0) :param dec_at_xy_0: dec coordinate at pixel (0,0) @@ -82,6 +83,7 @@ def map_pix2coord(self, x, y): def pixel_area(self): """ angular area of a pixel in the image + :return: area [arcsec^2] """ return np.abs(linalg.det(self._Mpix2a)) @@ -90,6 +92,7 @@ def pixel_area(self): def pixel_width(self): """ size of pixel + :return: sqrt(pixel_area) """ return np.sqrt(self.pixel_area) @@ -110,6 +113,7 @@ def coordinate_grid(self, nx, ny): def shift_coordinate_system(self, x_shift, y_shift, pixel_unit=False): """ shifts the coordinate system + :param x_shift: shift in x (or RA) :param y_shift: shift in y (or DEC) :param pixel_unit: bool, if True, units of pixels in input, otherwise RA/DEC @@ -121,6 +125,7 @@ def _shift_coordinates(self, x_shift, y_shift, pixel_unit=False): """ shifts the coordinate system + :param x_shift: shift in x (or RA) :param y_shift: shift in y (or DEC) :param pixel_unit: bool, if True, units of pixels in input, otherwise RA/DEC diff --git a/lenstronomy/GalKin/analytic_kinematics.py b/lenstronomy/GalKin/analytic_kinematics.py index 4b8133e9f..798ba2be8 100644 --- a/lenstronomy/GalKin/analytic_kinematics.py +++ b/lenstronomy/GalKin/analytic_kinematics.py @@ -15,7 +15,7 @@ class AnalyticKinematics(Anisotropy): """ class to compute eqn 20 in Suyu+2010 with a Monte-Carlo from rendering from the - light profile distribution and displacing them with a Gaussian seeing convolution + light profile distribution and displacing them with a Gaussian seeing convolution. This class assumes spherical symmetry in light and mass distribution and - a Hernquist light profile (parameterised by the half-light radius) @@ -25,9 +25,7 @@ class to compute eqn 20 in Suyu+2010 with a Monte-Carlo from rendering from the the spectral rendering approach to compute the seeing convolved slit measurement is presented in Birrer et al. 2016. The stellar anisotropy is parameterised based on Osipkov 1979; Merritt 1985. - Units - ----- - all units are meant to be in angular arc seconds. The physical units are fold in through the angular diameter + All units are meant to be in angular arc seconds. The physical units are fold in through the angular diameter distances """ diff --git a/lenstronomy/GalKin/anisotropy.py b/lenstronomy/GalKin/anisotropy.py index 4d4e772dd..a2476e8c3 100644 --- a/lenstronomy/GalKin/anisotropy.py +++ b/lenstronomy/GalKin/anisotropy.py @@ -42,6 +42,7 @@ def __init__(self, anisotropy_type): def beta_r(self, r, **kwargs): """ returns the anisotropy parameter at a given radius + :param r: 3d radius :param kwargs: parameters of the specified anisotropy model :return: beta(r) @@ -73,7 +74,8 @@ def anisotropy_solution(self, r, **kwargs): def delete_anisotropy_cache(self): """ deletes cached interpolations for a fixed anisotropy model - :return: + + :return: None """ if hasattr(self._model, 'delete_cache'): self._model.delete_cache() diff --git a/lenstronomy/GalKin/aperture_types.py b/lenstronomy/GalKin/aperture_types.py index 12ee0a341..0c9b5bc40 100644 --- a/lenstronomy/GalKin/aperture_types.py +++ b/lenstronomy/GalKin/aperture_types.py @@ -39,6 +39,7 @@ def aperture_select(self, ra, dec): def num_segments(self): """ number of segments with separate measurements of the velocity dispersion + :return: int """ return 1 @@ -101,6 +102,7 @@ def aperture_select(self, ra, dec): def num_segments(self): """ number of segments with separate measurements of the velocity dispersion + :return: int """ return 1 @@ -161,6 +163,7 @@ def aperture_select(self, ra, dec): def num_segments(self): """ number of segments with separate measurements of the velocity dispersion + :return: int """ return 1 diff --git a/lenstronomy/GalKin/cosmo.py b/lenstronomy/GalKin/cosmo.py index a2a36f5bb..ca6ccbf93 100644 --- a/lenstronomy/GalKin/cosmo.py +++ b/lenstronomy/GalKin/cosmo.py @@ -25,8 +25,9 @@ def __init__(self, d_d, d_s, d_ds): def arcsec2phys_lens(self, theta): """ converts are seconds to physical units on the deflector + :param theta: angle observed on the sky in units of arc seconds - :return: pyhsical distance of the angle in units of Mpc + :return: physical distance of the angle in units of Mpc """ return theta * const.arcsec * self.dd diff --git a/lenstronomy/GalKin/numeric_kinematics.py b/lenstronomy/GalKin/numeric_kinematics.py index 9174376b6..153eef943 100644 --- a/lenstronomy/GalKin/numeric_kinematics.py +++ b/lenstronomy/GalKin/numeric_kinematics.py @@ -179,7 +179,7 @@ def delete_cache(self): def _I_R_sigma2(self, R, kwargs_mass, kwargs_light, kwargs_anisotropy): """ - equation A15 in Mamon&Lokas 2005 as a logarithmic numerical integral (if option is chosen) + equation A15 in Mamon & Lokas 2005 as a logarithmic numerical integral (if option is chosen) :param R: 2d projected radius (in angular units) :param kwargs_mass: mass model parameters (following lenstronomy lens model conventions) diff --git a/lenstronomy/ImSim/de_lens.py b/lenstronomy/ImSim/de_lens.py index 33f4fcbe1..4844d9a99 100644 --- a/lenstronomy/ImSim/de_lens.py +++ b/lenstronomy/ImSim/de_lens.py @@ -11,6 +11,7 @@ def get_param_WLS(A, C_D_inv, d, inv_bool=True): """ returns the parameter values given + :param A: response matrix Nd x Ns (Nd = # data points, Ns = # parameters) :param C_D_inv: inverse covariance matrix of the data, Nd x Nd, diagonal form :param d: data array, 1-d Nd @@ -44,6 +45,7 @@ def get_param_WLS(A, C_D_inv, d, inv_bool=True): def marginalisation_const(M_inv): """ get marginalisation constant 1/2 log(M_beta) for flat priors + :param M_inv: 2D covariance matrix :return: float """ diff --git a/lenstronomy/ImSim/differential_extinction.py b/lenstronomy/ImSim/differential_extinction.py index f76faf880..4111f2a98 100644 --- a/lenstronomy/ImSim/differential_extinction.py +++ b/lenstronomy/ImSim/differential_extinction.py @@ -14,7 +14,7 @@ def __init__(self, optical_depth_model=None, tau0_index=0): """ :param optical_depth_model: list of strings naming the profiles (same convention as LightModel module) - describing the optical depth of the extinction + describing the optical depth of the extinction """ if optical_depth_model is None: optical_depth_model = [] diff --git a/lenstronomy/LensModel/Profiles/general_nfw.py b/lenstronomy/LensModel/Profiles/general_nfw.py index ffad78dab..8e87a4e87 100644 --- a/lenstronomy/LensModel/Profiles/general_nfw.py +++ b/lenstronomy/LensModel/Profiles/general_nfw.py @@ -17,10 +17,8 @@ class GNFW(LensProfileBase): \\rho = \\rho_0 (x^g (1+x^2)^((n-g)/2))^{-1} For g = 1.0 and n=3, it is approximately the same as an NFW profile - The original reference is [1]_ + The original reference is [1]_. - References - ---------- .. [1] Munoz, Kochanek and Keeton, (2001), astro-ph/0103009, doi:10.1086/322314 TODO: implement the gravitational potential for this profile diff --git a/lenstronomy/LensModel/Profiles/nfw.py b/lenstronomy/LensModel/Profiles/nfw.py index a622245a5..7d57ba70d 100644 --- a/lenstronomy/LensModel/Profiles/nfw.py +++ b/lenstronomy/LensModel/Profiles/nfw.py @@ -19,6 +19,7 @@ class NFW(LensProfileBase): Examples for converting angular to physical mass units ------------------------------------------------------ + >>> from lenstronomy.Cosmo.lens_cosmo import LensCosmo >>> from astropy.cosmology import FlatLambdaCDM >>> cosmo = FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.05) diff --git a/lenstronomy/LensModel/Profiles/shear.py b/lenstronomy/LensModel/Profiles/shear.py index 031fb1ba9..75a607fc4 100644 --- a/lenstronomy/LensModel/Profiles/shear.py +++ b/lenstronomy/LensModel/Profiles/shear.py @@ -75,8 +75,8 @@ class to model a shear field with shear strength and direction. The translation is as follow: .. math:: - \\gamma_1 = \\gamma_{ext} \\cos(2 \\phi_{ext} - \\gamma_2 = \\gamma_{ext} \\sin(2 \\phi_{ext} + \\gamma_1 = \\gamma_{ext} \\cos(2 \\phi_{ext}) + \\gamma_2 = \\gamma_{ext} \\sin(2 \\phi_{ext}) """ param_names = ['gamma_ext', 'psi_ext', 'ra_0', 'dec_0'] @@ -121,7 +121,7 @@ class ShearReduced(LensProfileBase): To keep the magnification at unity, it requires .. math:: - (1-\\kappa)^2 - \\gamma_1^2 - \\gamma_2^ = 1 + (1-\\kappa)^2) - \\gamma_1^2 - \\gamma_2^ = 1 Thus, for given pair of reduced shear :math:`(\\gamma'_1, \\gamma'_2)`, an additional convergence term is calculated and added to the lensing distortions. diff --git a/lenstronomy/LensModel/Profiles/sie.py b/lenstronomy/LensModel/Profiles/sie.py index d2dcf66d3..ef2cc3932 100644 --- a/lenstronomy/LensModel/Profiles/sie.py +++ b/lenstronomy/LensModel/Profiles/sie.py @@ -106,6 +106,7 @@ def hessian(self, x, y, theta_E, e1, e2, center_x=0, center_y=0): def theta2rho(theta_E): """ converts projected density parameter (in units of deflection) into 3d density parameter + :param theta_E: :return: """ @@ -117,6 +118,7 @@ def theta2rho(theta_E): def mass_3d(r, rho0, e1=0, e2=0): """ mass enclosed a 3d sphere or radius r + :param r: radius in angular units :param rho0: density at angle=1 :return: mass in angular units @@ -138,6 +140,7 @@ def mass_3d_lens(self, r, theta_E, e1=0, e2=0): def mass_2d(self, r, rho0, e1=0, e2=0): """ mass enclosed projected 2d sphere of radius r + :param r: :param rho0: :param e1: @@ -163,6 +166,7 @@ def mass_2d_lens(self, r, theta_E, e1=0, e2=0): def grav_pot(self, x, y, rho0, e1=0, e2=0, center_x=0, center_y=0): """ gravitational potential (modulo 4 pi G and rho0 in appropriate units) + :param x: :param y: :param rho0: @@ -197,6 +201,7 @@ def density_lens(self, r, theta_E, e1=0, e2=0): def density(r, rho0, e1=0, e2=0): """ computes the density + :param r: radius in angles :param rho0: density at angle=1 :return: density at r @@ -208,6 +213,7 @@ def density(r, rho0, e1=0, e2=0): def density_2d(x, y, rho0, e1=0, e2=0, center_x=0, center_y=0): """ projected density + :param x: :param y: :param rho0: diff --git a/lenstronomy/LensModel/Profiles/spemd.py b/lenstronomy/LensModel/Profiles/spemd.py index 088920be6..1dfe8deb3 100644 --- a/lenstronomy/LensModel/Profiles/spemd.py +++ b/lenstronomy/LensModel/Profiles/spemd.py @@ -216,6 +216,7 @@ def convert_params(theta_E, gamma, q, s_scale): def is_not_empty(x1, x2): """ Check if float or not an empty array + :return: True if x1 and x2 are either floats/ints or an non-empty array, False if e.g. objects are [] :rtype: bool """ diff --git a/lenstronomy/LensModel/Profiles/spep.py b/lenstronomy/LensModel/Profiles/spep.py index b4847a208..4d579cd7a 100644 --- a/lenstronomy/LensModel/Profiles/spep.py +++ b/lenstronomy/LensModel/Profiles/spep.py @@ -30,10 +30,10 @@ def function(self, x, y, theta_E, gamma, e1, e2, center_x=0, center_y=0): :type theta_E: float. :param gamma: power law slope of mass profifle :type gamma: <2 float - :param q: Axis ratio - :type q: 0 infinity :return: mass inside radius r @@ -178,6 +184,7 @@ def mass_3d(self, r, rho0, r_core, gamma): def mass_3d_lens(self, r, sigma0, r_core, gamma): """ mass enclosed a 3d sphere or radius r + :param r: radius [arcsec] :param sigma0: convergence at r = 0 :param r_core: core radius [arcsec] @@ -190,9 +197,10 @@ def mass_3d_lens(self, r, sigma0, r_core, gamma): def mass_2d(self, r, rho0, r_core, gamma): """ mass enclosed projected 2d disk of radius r + :param r: radius [arcsec] :param rho0: density at r = 0 in units [rho_0_physical / sigma_crit] (which should be equal to [1/arcsec]) - where rho_0_physical is a physical density normalization and sigma_crit is the critical density for lensing + where rho_0_physical is a physical density normalization and sigma_crit is the critical density for lensing :param r_core: core radius [arcsec] :param gamma: logarithmic slope at r -> infinity :return: projected mass inside disk of radius r @@ -202,9 +210,10 @@ def mass_2d(self, r, rho0, r_core, gamma): def mass_2d_lens(self, r, sigma0, r_core, gamma): """ mass enclosed projected 2d disk of radius r + :param r: radius [arcsec] :param sigma0: convergence at r = 0 - where rho_0_physical is a physical density normalization and sigma_crit is the critical density for lensing + where rho_0_physical is a physical density normalization and sigma_crit is the critical density for lensing :param r_core: core radius [arcsec] :param gamma: logarithmic slope at r -> infinity :return: projected mass inside disk of radius r @@ -216,6 +225,7 @@ def mass_2d_lens(self, r, sigma0, r_core, gamma): def _safe_r_division(r, r_core, x_min=1e-6): """ Avoids accidental division by 0 + :param r: radius in arcsec :param r_core: core radius in arcsec :return: a minimum value of r @@ -231,6 +241,7 @@ def _safe_r_division(r, r_core, x_min=1e-6): def _sigma2rho0(sigma0, r_core): """ Converts the convergence normalization to the 3d normalization + :param sigma0: convergence at r=0 :param r_core: core radius [arcsec] :return: density normalization in units 1/arcsec, or rho_0_physical / sigma_crit @@ -241,6 +252,7 @@ def _sigma2rho0(sigma0, r_core): def _rho02sigma(rho0, r_core): """ Converts the convergence normalization to the 3d normalization + :param rho0: convergence at r=0 :param r_core: core radius [arcsec] :return: density normalization in units 1/arcsec, or rho_0_physical / sigma_crit diff --git a/lenstronomy/LensModel/Profiles/spp.py b/lenstronomy/LensModel/Profiles/spp.py index 0834e604e..9927ea929 100644 --- a/lenstronomy/LensModel/Profiles/spp.py +++ b/lenstronomy/LensModel/Profiles/spp.py @@ -20,6 +20,8 @@ def function(self, x, y, theta_E, gamma, center_x=0, center_y=0): """ :param x: set of x-coordinates :type x: array of size (n) + :param y: set of y-coordinates + :type y: array of size (n) :param theta_E: Einstein radius of lens :type theta_E: float. :param gamma: power law slope of mass profile @@ -85,6 +87,7 @@ def hessian(self, x, y, theta_E, gamma, center_x=0., center_y=0.): def rho2theta(rho0, gamma): """ converts 3d density into 2d projected density parameter + :param rho0: :param gamma: :return: @@ -99,6 +102,7 @@ def rho2theta(rho0, gamma): def theta2rho(theta_E, gamma): """ converts projected density parameter (in units of deflection) into 3d density parameter + :param theta_E: :param gamma: :return: @@ -112,9 +116,10 @@ def theta2rho(theta_E, gamma): def mass_3d(r, rho0, gamma): """ mass enclosed a 3d sphere or radius r + :param r: - :param a: - :param s: + :param rho0: + :param gamma: :return: """ mass_3d = 4 * np.pi * rho0 /(-gamma + 3) * r ** (-gamma + 3) @@ -134,10 +139,10 @@ def mass_3d_lens(self, r, theta_E, gamma): def mass_2d(self, r, rho0, gamma): """ mass enclosed projected 2d sphere of radius r + :param r: :param rho0: - :param a: - :param s: + :param gamma: :return: """ alpha = np.sqrt(np.pi) * special.gamma(1. / 2 * (-1 + gamma)) / special.gamma(gamma / 2.) * r ** (2 - gamma)/(3 - gamma) * 2 * rho0 @@ -158,11 +163,11 @@ def mass_2d_lens(self, r, theta_E, gamma): def grav_pot(self, x, y, rho0, gamma, center_x=0, center_y=0): """ gravitational potential (modulo 4 pi G and rho0 in appropriate units) + :param x: :param y: :param rho0: - :param a: - :param s: + :param gamma: :param center_x: :param center_y: :return: @@ -178,11 +183,10 @@ def grav_pot(self, x, y, rho0, gamma, center_x=0, center_y=0): def density(r, rho0, gamma): """ computes the density - :param x: - :param y: + + :param r: :param rho0: - :param a: - :param s: + :param gamma: :return: """ rho = rho0 / r**gamma @@ -201,11 +205,11 @@ def density_lens(self, r, theta_E, gamma): def density_2d(x, y, rho0, gamma, center_x=0, center_y=0): """ projected density + :param x: :param y: :param rho0: - :param a: - :param s: + :param gamma: :param center_x: :param center_y: :return: diff --git a/lenstronomy/LensModel/Profiles/tnfw.py b/lenstronomy/LensModel/Profiles/tnfw.py index f252c2388..3aea301ca 100644 --- a/lenstronomy/LensModel/Profiles/tnfw.py +++ b/lenstronomy/LensModel/Profiles/tnfw.py @@ -58,6 +58,7 @@ def function(self, x, y, Rs, alpha_Rs, r_trunc, center_x=0, center_y=0): def _L(self, x, tau): """ Logarithm that appears frequently + :param x: r/Rs :param tau: t/Rs :return: @@ -68,6 +69,7 @@ def _L(self, x, tau): def F(self, x): """ Classic NFW function in terms of arctanh and arctan + :param x: r/Rs :return: """ diff --git a/lenstronomy/LensModel/Profiles/uldm.py b/lenstronomy/LensModel/Profiles/uldm.py index 44ff9ded4..f47d8d1eb 100644 --- a/lenstronomy/LensModel/Profiles/uldm.py +++ b/lenstronomy/LensModel/Profiles/uldm.py @@ -2,10 +2,9 @@ # this file contains a class to compute the Ultra Light Dark Matter soliton profile import numpy as np -import scipy.interpolate as interp from scipy.special import gamma, hyp2f1 from lenstronomy.LensModel.Profiles.base_profile import LensProfileBase -import lenstronomy.Util.constants as const + __all__ = ['Uldm'] @@ -29,24 +28,27 @@ class Uldm(LensProfileBase): different from 8 to model solitons which feel the influence of background potential (see 2105.10873) The profile has, as parameters: - :param kappa_0: central convergence - :param theta_c: core radius (in arcseconds) - :param slope: exponent entering the profile, default value is 8 + + - kappa_0: central convergence + - theta_c: core radius (in arcseconds) + - slope: exponent entering the profile, default value is 8 """ _s = 0.000001 # numerical limit for minimal radius param_names = ['kappa_0', 'theta_c', 'slope', 'center_x', 'center_y'] lower_limit_default = {'kappa_0': 0, 'theta_c': 0, 'slope': 3.5, 'center_x': -100, 'center_y': -100} upper_limit_default = {'kappa_0': 1., 'theta_c': 100, 'slope': 10, 'center_x': 100, 'center_y': 100} - def rhotilde(self, kappa_0, theta_c, slope=8): + @staticmethod + def rhotilde(kappa_0, theta_c, slope=8): """ Computes the central density in angular units + :param kappa_0: central convergence of profile :param theta_c: core radius (in arcsec) :param slope: exponent entering the profile :return: central density in 1/arcsec """ - a_factor_sqrt = np.sqrt( (0.5)**(-1/slope) -1) + a_factor_sqrt = np.sqrt(0.5**(-1/slope) - 1) num_factor = gamma(slope) / gamma(slope - 1/2) * a_factor_sqrt / np.sqrt(np.pi) return kappa_0 * num_factor / theta_c @@ -76,7 +78,8 @@ def function(self, x, y, kappa_0, theta_c, center_x=0, center_y=0, slope=8): hyp3f2(1, 1, slope - 0.5, 2, 2, -(a_factor_sqrt * r_i / theta_c)**2.) for r_i in r], dtype=float) return hypgeom - def alpha_radial(self, r, kappa_0, theta_c, slope=8): + @staticmethod + def alpha_radial(r, kappa_0, theta_c, slope=8): """ returns the radial part of the deflection angle @@ -86,8 +89,8 @@ def alpha_radial(self, r, kappa_0, theta_c, slope=8): :param r: radius where the deflection angle is computed :return: radial deflection angle """ - a_factor = (0.5)**(-1./slope) -1 - prefactor = 2./(2*slope -3) * kappa_0 * theta_c**2 / a_factor + a_factor = 0.5**(-1./slope) - 1 + prefactor = 2./(2*slope - 3) * kappa_0 * theta_c**2 / a_factor denominator_factor = (1 + a_factor * r**2/theta_c**2)**(slope - 3./2) return prefactor/r * (1 - 1/denominator_factor) @@ -107,7 +110,7 @@ def derivatives(self, x, y, kappa_0, theta_c, center_x=0, center_y=0, slope=8): x_ = x - center_x y_ = y - center_y R = np.sqrt(x_**2 + y_**2) - R = np.maximum(R,0.00000001) + R = np.maximum(R, 0.00000001) f_x = self.alpha_radial(R, kappa_0, theta_c, slope) * x_ / R f_y = self.alpha_radial(R, kappa_0, theta_c, slope) * y_ / R return f_x, f_y @@ -126,8 +129,8 @@ def hessian(self, x, y, kappa_0, theta_c, center_x=0, center_y=0, slope=8): x_ = x - center_x y_ = y - center_y R = np.sqrt(x_**2 + y_**2) - R = np.maximum(R,0.00000001) - a_factor = (0.5)**(-1./slope) -1 + R = np.maximum(R, 0.00000001) + a_factor = 0.5**(-1./slope) - 1 prefactor = 2./(2*slope -3) * kappa_0 * theta_c**2 / a_factor # denominator factor denominator = 1 + a_factor * R**2/theta_c**2 @@ -142,6 +145,7 @@ def density(self, R, kappa_0, theta_c, slope=8): """ three dimensional ULDM profile in angular units (rho0_physical = rho0_angular Sigma_crit / D_lens) + :param R: radius of interest :param kappa_0: central convergence of profile :param theta_c: core radius (in arcsec) @@ -149,8 +153,8 @@ def density(self, R, kappa_0, theta_c, slope=8): :return: rho(R) density in angular units """ rhotilde = self.rhotilde(kappa_0, theta_c, slope) - a_factor = (0.5)**(-1./slope) -1 - return rhotilde/(1 + a_factor* (R/theta_c)**2)**slope + a_factor = 0.5**(-1./slope) - 1 + return rhotilde/(1 + a_factor * (R/theta_c)**2)**slope def density_lens(self, r, kappa_0, theta_c, slope=8): """ @@ -166,7 +170,8 @@ def density_lens(self, r, kappa_0, theta_c, slope=8): """ return self.density(r, kappa_0, theta_c, slope) - def kappa_r(self, R, kappa_0, theta_c, slope=8): + @staticmethod + def kappa_r(R, kappa_0, theta_c, slope=8): """ convergence of the cored density profile. This routine is also for testing @@ -176,16 +181,16 @@ def kappa_r(self, R, kappa_0, theta_c, slope=8): :param slope: exponent entering the profile :return: convergence at r """ - a_factor = (0.5)**(-1./slope) -1 - return kappa_0 * (1 + a_factor * (R/theta_c)**2)**(1./2 - slope) - + a_factor = (0.5)**(-1./slope) -1 + return kappa_0 * (1 + a_factor * (R/theta_c)**2)**(1./2 - slope) def density_2d(self, x, y, kappa_0, theta_c, center_x=0, center_y=0, slope=8): """ projected two dimensional ULDM profile (convergence * Sigma_crit), but given our units convention for rho0, it is basically the convergence - :param R: radius of interest + :param x: x-coordinate + :param y: y-coordinate :param kappa_0: central convergence of profile :param theta_c: core radius (in arcsec) :param slope: exponent entering the profile @@ -198,7 +203,8 @@ def density_2d(self, x, y, kappa_0, theta_c, center_x=0, center_y=0, slope=8): def _mass_integral(self, x, slope=8): """ - Returns the analitic result of the integral appearing in mass expression + Returns the analytic result of the integral appearing in mass expression + :param slope: exponent entering the profile :return: integral result """ @@ -208,6 +214,7 @@ def _mass_integral(self, x, slope=8): def mass_3d(self, R, kappa_0, theta_c, slope=8): """ mass enclosed a 3d sphere or radius r + :param R: radius in arcseconds :param kappa_0: central convergence of profile :param theta_c: core radius (in arcsec) @@ -215,7 +222,7 @@ def mass_3d(self, R, kappa_0, theta_c, slope=8): :return: mass of soliton in angular units """ rhotilde = self.rhotilde(kappa_0, theta_c, slope) - a_factor = (0.5)**(-1./slope) -1 + a_factor = 0.5**(-1./slope) - 1 prefactor = 4. * np.pi * rhotilde * theta_c**3 / (a_factor)**(1.5) m_3d = prefactor * (self._mass_integral(R/theta_c * np.sqrt(a_factor), slope) - self._mass_integral(0, slope) ) @@ -224,6 +231,7 @@ def mass_3d(self, R, kappa_0, theta_c, slope=8): def mass_3d_lens(self, r, kappa_0, theta_c, slope=8): """ mass enclosed a 3d sphere or radius r + :param r: radius over which the mass is computed :param kappa_0: central convergence of profile :param theta_c: core radius (in arcsec) @@ -236,6 +244,7 @@ def mass_3d_lens(self, r, kappa_0, theta_c, slope=8): def mass_2d(self, R, kappa_0, theta_c, slope=8): """ mass enclosed a 2d sphere or radius r + :param R: radius over which the mass is computed :param kappa_0: central convergence of profile :param theta_c: core radius (in arcsec) diff --git a/lenstronomy/LightModel/Profiles/sersic.py b/lenstronomy/LightModel/Profiles/sersic.py index cf6f06e96..187a3c3ee 100644 --- a/lenstronomy/LightModel/Profiles/sersic.py +++ b/lenstronomy/LightModel/Profiles/sersic.py @@ -51,6 +51,7 @@ class SersicElliptic(SersicUtil): this class contains functions to evaluate an elliptical Sersic function .. math:: + I(R) = I_0 \\exp \\left[ -b_n (R/R_{\\rm Sersic})^{\\frac{1}{n}}\\right] with :math:`I_0 = amp`, @@ -93,8 +94,9 @@ class CoreSersic(SersicUtil): this class contains the Core-Sersic function introduced by e.g Trujillo et al. 2004 .. math:: + I(R) = I' \\left[1 + (R_b/R)^{\\alpha} \\right]^{\\gamma / \\alpha} - \\exp \\left{ -b_n \\left[(R^{\\alpha} + R_b^{\alpha})/R_e^{\\alpha} \\right]^{1 / (n\\alpha)} \\right} + \\exp \\left{ -b_n \\left[(R^{\\alpha} + R_b^{\\alpha})/R_e^{\\alpha} \\right]^{1 / (n\\alpha)} \\right} with diff --git a/lenstronomy/LightModel/Profiles/shapelets.py b/lenstronomy/LightModel/Profiles/shapelets.py index b96688cd1..96d76341e 100644 --- a/lenstronomy/LightModel/Profiles/shapelets.py +++ b/lenstronomy/LightModel/Profiles/shapelets.py @@ -284,6 +284,7 @@ def shapelet_basis_2d(self, num_order, beta, numPix, deltaPix=1, center_x=0, cen def decomposition(self, image, x, y, n_max, beta, deltaPix, center_x=0, center_y=0): """ decomposes an image into the shapelet coefficients in same order as for the function call + :param image: :param x: :param y: diff --git a/lenstronomy/Plots/plot_util.py b/lenstronomy/Plots/plot_util.py index 9e796ce2d..f830ff890 100644 --- a/lenstronomy/Plots/plot_util.py +++ b/lenstronomy/Plots/plot_util.py @@ -10,14 +10,14 @@ def sqrt(inputArray, scale_min=None, scale_max=None): """Performs sqrt scaling of the input numpy array. - @type inputArray: numpy array - @param inputArray: image data array - @type scale_min: float - @param scale_min: minimum data value - @type scale_max: float - @param scale_max: maximum data value - @rtype: numpy array - @return: image data array + :type inputArray: numpy array + :param inputArray: image data array + :type scale_min: float + :param scale_min: minimum data value + :type scale_max: float + :param scale_max: maximum data value + :rtype: numpy array + :return: image data array """ diff --git a/lenstronomy/Sampling/Pool/multiprocessing.py b/lenstronomy/Sampling/Pool/multiprocessing.py index 66de7dff9..4599c3b45 100644 --- a/lenstronomy/Sampling/Pool/multiprocessing.py +++ b/lenstronomy/Sampling/Pool/multiprocessing.py @@ -43,25 +43,20 @@ class MultiPool(Pool): A modified version of :class:`multiprocessing.pool.Pool` that has better behavior with regard to ``KeyboardInterrupts`` in the :func:`map` method. (Original author: `Peter K. G. Williams `_) - - Parameters - ---------- - processes : int, optional - The number of worker processes to use; defaults to the number of CPUs. - - initializer : callable, optional - If specified, a callable that will be invoked by each worker process when it starts. - - initargs : iterable, optional - Arguments for ``initializer``; it will be called as ``initializer(*initargs)``. - - kwargs: - Extra arguments passed to the :class:`multiprocessing.pool.Pool` superclass. - """ wait_timeout = 3600 def __init__(self, processes=None, initializer=None, initargs=(), **kwargs): + """ + + :param processes: The number of worker processes to use; defaults to the number of CPUs. + :type processes: int, optional + :param initializer: If specified, a callable that will be invoked by each worker process when it starts. + :type initializer: callable, optional + :param initargs: Arguments for ``initializer``; it will be called as ``initializer(*initargs)``. + :type initargs: iterable, optional + :param kwargs: Extra arguments passed to the :class:`multiprocessing.pool.Pool` superclass. + """ new_initializer = functools.partial(_initializer_wrapper, initializer) super(MultiPool, self).__init__(processes, new_initializer, initargs, **kwargs) @@ -86,26 +81,22 @@ def map(self, func, iterable, chunksize=None, callback=None): Parameters ---------- - func : callable - A function or callable object that is executed on each element of + :param func: A function or callable object that is executed on each element of the specified ``tasks`` iterable. This object must be picklable (i.e. it can't be a function scoped within a function or a ``lambda`` function). This should accept a single positional argument and return a single object. - iterable : iterable - A list or iterable of tasks. Each task can be itself an iterable + :type func: callable + :param iterable: A list or iterable of tasks. Each task can be itself an iterable (e.g., tuple) of values or data to pass in to the worker function. - callback : callable, optional - An optional callback function (or callable) that is called with the + :type iterable: iterable + :param callback: An optional callback function (or callable) that is called with the result from each worker run and is executed on the master process. This is useful for, e.g., saving results to a file, since the callback is only called on the master thread. + :type callback: callable, optional - Returns - ------- - results : list - A list of results from the output of each ``worker()`` call. - + :return: A list of results from the output of each ``worker()`` call. """ if callback is None: diff --git a/lenstronomy/Sampling/Pool/pool.py b/lenstronomy/Sampling/Pool/pool.py index 837b9ea15..59256ed7b 100644 --- a/lenstronomy/Sampling/Pool/pool.py +++ b/lenstronomy/Sampling/Pool/pool.py @@ -50,18 +50,16 @@ def choose_pool(mpi=False, processes=1, **kwargs): Choose between the different pools given options from, e.g., argparse. - Parameters - ---------- - mpi : bool, optional - Use the MPI processing pool, :class:`~schwimmbad.mpi.MPIPool`. By + + :param mpi: Use the MPI processing pool, :class:`~schwimmbad.mpi.MPIPool`. By default, ``False``, will use the :class:`~schwimmbad.serial.SerialPool`. - processes : int, optional - Use the multiprocessing pool, + :type mpi: bool, optional + :param processes: Use the multiprocessing pool, :class:`~schwimmbad.multiprocessing.MultiPool`, with this number of processes. By default, ``processes=1``, will use them:class:`~schwimmbad.serial.SerialPool`. - - Any additional kwargs are passed in to the pool class initializer selected by the arguments. - + :type processes: int, optional + :param kwargs: Any additional kwargs are passed in to the pool class initializer selected by the arguments. + :type kwargs: keyword arguments """ # Imports moved here to avoid crashing at import time if dependencies # are missing diff --git a/lenstronomy/Util/image_util.py b/lenstronomy/Util/image_util.py index 65c160277..b990fdddd 100644 --- a/lenstronomy/Util/image_util.py +++ b/lenstronomy/Util/image_util.py @@ -15,6 +15,7 @@ def add_layer2image(grid2d, x_pos, y_pos, kernel, order=1): """ adds a kernel on the grid2d image at position x_pos, y_pos with an interpolated subgrid pixel shift of order=order + :param grid2d: 2d pixel grid (i.e. image) :param x_pos: x-position center (pixel coordinate) of the layer to be added :param y_pos: y-position center (pixel coordinate) of the layer to be added @@ -35,6 +36,7 @@ def add_layer2image(grid2d, x_pos, y_pos, kernel, order=1): def add_layer2image_int(grid2d, x_pos, y_pos, kernel): """ adds a kernel on the grid2d image at position x_pos, y_pos at integer positions of pixel + :param grid2d: 2d pixel grid (i.e. image) :param x_pos: x-position center (pixel coordinate) of the layer to be added :param y_pos: y-position center (pixel coordinate) of the layer to be added @@ -121,6 +123,7 @@ def rotateImage(img, angle): def re_size_array(x_in, y_in, input_values, x_out, y_out): """ resizes 2d array (i.e. image) to new coordinates. So far only works with square output aligned with coordinate axis. + :param x_in: :param y_in: :param input_values: @@ -138,6 +141,7 @@ def re_size_array(x_in, y_in, input_values, x_out, y_out): def symmetry_average(image, symmetry): """ symmetry averaged image + :param image: :param symmetry: :return: @@ -176,8 +180,6 @@ def coordInImage(x_coord, y_coord, num_pix, deltapix): checks whether image positions are within the pixel image in units of arcsec if not: remove it - :param imcoord: image coordinate (in units of angels) [[x,y,delta,magnification][...]] - :type imcoord: (n,4) numpy array :returns: image positions within the pixel image """ idex = [] @@ -243,14 +245,7 @@ def rebin_image(bin_size, image, wht_map, sigma_bkg, ra_coords, dec_coords, idex def rebin_coord_transform(factor, x_at_radec_0, y_at_radec_0, Mpix2coord, Mcoord2pix): """ adopt coordinate system and transformation between angular and pixel coordinates of a re-binned image - :param bin_size: - :param ra_0: - :param dec_0: - :param x_0: - :param y_0: - :param Matrix: - :param Matrix_inv: - :return: + """ factor = int(factor) Mcoord2pix_resized = Mcoord2pix / factor @@ -265,7 +260,7 @@ def rebin_coord_transform(factor, x_at_radec_0, y_at_radec_0, Mpix2coord, Mcoord def stack_images(image_list, wht_list, sigma_list): """ stacks images and saves new image as a fits file - :param image_name_list: list of image_names to be stacked + :return: """ image_stacked = np.zeros_like(image_list[0]) @@ -286,6 +281,7 @@ def cut_edges(image, num_pix): """ cuts out the edges of a 2d image and returns re-sized image to numPix center is well defined for odd pixel sizes. + :param image: 2d numpy array :param num_pix: square size of cut out image :return: cutout image with size numPix diff --git a/lenstronomy/Util/kernel_util.py b/lenstronomy/Util/kernel_util.py index 90f4f8ed2..2595f2355 100644 --- a/lenstronomy/Util/kernel_util.py +++ b/lenstronomy/Util/kernel_util.py @@ -19,8 +19,8 @@ def de_shift_kernel(kernel, shift_x, shift_y, iterations=20, fractional_step_siz de-shifts a shifted kernel to the center of a pixel. This is performed iteratively. The input kernel is the solution of a linear interpolated shift of a sharper kernel centered in the middle of the - pixel. To find the de-shifted kernel, we perform an iterative correction of proposed de-shifted kernels and compare - its shifted version with the input kernel. + pixel. To find the de-shifted kernel, we perform an iterative correction of proposed de-shifted kernels and compare + its shifted version with the input kernel. :param kernel: (shifted) kernel, e.g. a star in an image that is not centered in the pixel grid :param shift_x: x-offset relative to the center of the pixel (sub-pixel shift) @@ -149,6 +149,7 @@ def subgrid_kernel(kernel, subgrid_res, odd=False, num_iter=100): def kernel_pixelsize_change(kernel, deltaPix_in, deltaPix_out): """ change the pixel size of a given kernel + :param kernel: :param deltaPix_in: :param deltaPix_out: @@ -169,6 +170,7 @@ def kernel_pixelsize_change(kernel, deltaPix_in, deltaPix_out): def cut_psf(psf_data, psf_size): """ cut the psf properly + :param psf_data: image of PSF :param psf_size: size of psf :return: re-sized and re-normalized PSF @@ -371,6 +373,7 @@ def averaging_even_kernel(kernel_high_res, subgrid_res): def cutout_source(x_pos, y_pos, image, kernelsize, shift=True): """ cuts out point source (e.g. PSF estimate) out of image and shift it to the center of a pixel + :param x_pos: :param y_pos: :param image: @@ -434,6 +437,7 @@ def fwhm_kernel(kernel): def estimate_amp(data, x_pos, y_pos, psf_kernel): """ estimates the amplitude of a point source located at x_pos, y_pos + :param data: :param x_pos: :param y_pos: diff --git a/lenstronomy/Util/prob_density.py b/lenstronomy/Util/prob_density.py index 518a43ec2..ad662c46f 100644 --- a/lenstronomy/Util/prob_density.py +++ b/lenstronomy/Util/prob_density.py @@ -16,6 +16,7 @@ def pdf(self, x, e=0., w=1., a=0.): """ probability density function see: https://en.wikipedia.org/wiki/Skew_normal_distribution + :param x: input value :param e: :param w: @@ -28,6 +29,7 @@ def pdf(self, x, e=0., w=1., a=0.): def pdf_skew(self, x, mu, sigma, skw): """ function with different parameterisation + :param x: :param mu: mean :param sigma: sigma @@ -61,6 +63,7 @@ def _alpha_delta(self, delta): def _w_sigma_delta(self, sigma, delta): """ invert variance + :param sigma: :param delta: :return: w parameter @@ -84,6 +87,7 @@ def _e_mu_w_delta(self, mu, w, delta): def map_mu_sigma_skw(self, mu, sigma, skw): """ map to parameters e, w, a + :param mu: mean :param sigma: standard deviation :param skw: skewness @@ -125,6 +129,7 @@ def compute_lower_upper_errors(sample, num_sigma=1): """ computes the upper and lower sigma from the median value. This functions gives good error estimates for skewed pdf's + :param sample: 1-D sample :param num_sigma: integer, number of sigmas to be returned :return: median, lower_sigma, upper_sigma diff --git a/lenstronomy/Util/util.py b/lenstronomy/Util/util.py index 009010e18..1572d707e 100644 --- a/lenstronomy/Util/util.py +++ b/lenstronomy/Util/util.py @@ -95,6 +95,7 @@ def map_coord2pix(ra, dec, x_0, y_0, M): """ this routines performs a linear transformation between two coordinate systems. Mainly used to transform angular into pixel coordinates in an image + :param ra: ra coordinates :param dec: dec coordinates :param x_0: pixel value in x-axis of ra,dec = 0,0 @@ -226,6 +227,7 @@ def make_grid(numPix, deltapix, subgrid_res=1, left_lower=False): def make_grid_transformed(numPix, Mpix2Angle): """ returns grid with linear transformation (deltaPix and rotation) + :param numPix: number of Pixels :param Mpix2Angle: 2-by-2 matrix to mat a pixel to a coordinate :return: coordinate grid @@ -281,7 +283,6 @@ def grid_from_coordinate_transform(nx, ny, Mpix2coord, ra_at_xy_0, dec_at_xy_0): """ return a grid in x and y coordinates that satisfy the coordinate system - :param nx: number of pixels in x-axis :param ny: number of pixels in y-axis :param Mpix2coord: transformation matrix (2x2) of pixels into coordinate displacements @@ -303,6 +304,7 @@ def grid_from_coordinate_transform(nx, ny, Mpix2coord, ra_at_xy_0, dec_at_xy_0): def get_axes(x, y): """ computes the axis x and y of a given 2d grid + :param x: :param y: :return: @@ -321,6 +323,7 @@ def get_axes(x, y): def averaging(grid, numGrid, numPix): """ resize 2d pixel grid with numGrid to numPix and averages over the pixels + :param grid: higher resolution pixel grid :param numGrid: number of pixels per axis in the high resolution input image :param numPix: lower number of pixels per axis in the output image (numGrid/numPix is integer number) @@ -403,6 +406,7 @@ def compare_distance(x_mapped, y_mapped): def min_square_dist(x_1, y_1, x_2, y_2): """ return minimum of quadratic distance of pairs (x1, y1) to pairs (x2, y2) + :param x_1: :param y_1: :param x_2: @@ -467,6 +471,7 @@ def select_best(array, criteria, num_select, highest=True): def points_on_circle(radius, num_points, connect_ends=True): """ returns a set of uniform points around a circle + :param radius: radius of the circle :param num_points: number of points on the circle :param connect_ends: boolean, if True, start and end point are the same @@ -585,6 +590,7 @@ def hyper2F2_array(a, b, c, d, x): def make_subgrid(ra_coord, dec_coord, subgrid_res=2): """ return a grid with subgrid resolution + :param ra_coord: :param dec_coord: :param subgrid_res: @@ -620,6 +626,7 @@ def make_subgrid(ra_coord, dec_coord, subgrid_res=2): def convert_bool_list(n, k=None): """ returns a bool list of the length of the lens models + if k = None: returns bool list with True's if k is int, returns bool list with False's but k'th is True if k is a list of int, e.g. [0, 3, 5], returns a bool list with True's in the integers listed and False elsewhere diff --git a/lenstronomy/Workflow/fitting_sequence.py b/lenstronomy/Workflow/fitting_sequence.py index 0908232e4..c70f2eb7c 100644 --- a/lenstronomy/Workflow/fitting_sequence.py +++ b/lenstronomy/Workflow/fitting_sequence.py @@ -164,7 +164,8 @@ def best_fit_likelihood(self): @property def bic(self): """ - returns the bayesian information criterion of the model. + Bayesian information criterion (BIC) of the model. + :return: bic value, float """ num_data = self.likelihoodModule.num_data @@ -493,6 +494,7 @@ def update_settings(self, kwargs_model=None, kwargs_constraints=None, kwargs_lik def set_param_value(self, **kwargs): """ Set a parameter to a specific value. `kwargs` are below. + :param lens: [[i_model, ['param1', 'param2',...], [...]] :type lens: :param source: [[i_model, ['param1', 'param2',...], [...]] diff --git a/lenstronomy/Workflow/psf_fitting.py b/lenstronomy/Workflow/psf_fitting.py index 343a3f6f0..dbaad9572 100644 --- a/lenstronomy/Workflow/psf_fitting.py +++ b/lenstronomy/Workflow/psf_fitting.py @@ -401,6 +401,7 @@ def cutout_psf_single(x, y, image, mask, kernel_size, kernel_init): def combine_psf(kernel_list_new, kernel_old, factor=1., stacking_option='median', symmetry=1): """ updates psf estimate based on old kernel and several new estimates + :param kernel_list_new: list of new PSF kernels estimated from the point sources in the image (un-normalized) :param kernel_old: old PSF kernel :param factor: weight of updated estimate based on new and old estimate, factor=1 means new estimate, From f80206746dcbc8b324a58e01f339b1a8baf35ccb Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 19 Aug 2022 13:01:58 -0700 Subject: [PATCH 12/67] minor style and documentation change in gnfw.py --- lenstronomy/LensModel/Profiles/general_nfw.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lenstronomy/LensModel/Profiles/general_nfw.py b/lenstronomy/LensModel/Profiles/general_nfw.py index 8e87a4e87..5006d58d8 100644 --- a/lenstronomy/LensModel/Profiles/general_nfw.py +++ b/lenstronomy/LensModel/Profiles/general_nfw.py @@ -14,7 +14,7 @@ class GNFW(LensProfileBase): This class contains a double power law profile with flexible inner and outer logarithmic slopes g and n .. math:: - \\rho = \\rho_0 (x^g (1+x^2)^((n-g)/2))^{-1} + \\rho(r) = \\frac{\\rho_0}{r^{\\gamma}} \\frac{Rs^{n}}{\\left(r^2 + Rs^2 \\right)^{(n - \\gamma)/2}} For g = 1.0 and n=3, it is approximately the same as an NFW profile The original reference is [1]_. @@ -68,14 +68,15 @@ def hessian(self, x, y, Rs, alpha_Rs, gamma_inner, gamma_outer, center_x=0, cent y_ = y - center_y R = np.sqrt(x_ ** 2 + y_ ** 2) R = np.maximum(R, 0.00000001) - kappa = self.density_2d(R, 0, Rs, rho0_input,gamma_inner, gamma_outer) - gamma1, gamma2 = self.nfwGamma(R, Rs, rho0_input,gamma_inner, gamma_outer, x_, y_) + kappa = self.density_2d(R, 0, Rs, rho0_input, gamma_inner, gamma_outer) + gamma1, gamma2 = self.nfwGamma(R, Rs, rho0_input, gamma_inner, gamma_outer, x_, y_) f_xx = kappa + gamma1 f_yy = kappa - gamma1 f_xy = gamma2 return f_xx, f_xy, f_xy, f_yy - def density(self, R, Rs, rho0, gamma_inner, gamma_outer): + @staticmethod + def density(R, Rs, rho0, gamma_inner, gamma_outer): """ three dimensional NFW profile @@ -88,7 +89,7 @@ def density(self, R, Rs, rho0, gamma_inner, gamma_outer): """ x = R/Rs outer_slope = (gamma_outer-gamma_inner)/2 - return rho0 / (x**gamma_inner * (1 +x ** 2) ** outer_slope) + return rho0 / (x**gamma_inner * (1 + x ** 2) ** outer_slope) def density_lens(self, r, Rs, alpha_Rs, gamma_inner, gamma_outer): """ @@ -112,7 +113,7 @@ def density_2d(self, x, y, Rs, rho0, gamma_inner, gamma_outer, center_x=0, cente :param x: angular position (normally in units of arc seconds) :param y: angular position (normally in units of arc seconds) :param Rs: turn over point in the slope of the NFW profile in angular unit - :param alpha_Rs: deflection (angular units) at projected Rs + :param rho0: density normalization at Rs :param gamma_inner: logarithmic profile slope interior to Rs :param gamma_outer: logarithmic profile slope outside Rs :param center_x: profile center (same units as x) @@ -126,7 +127,8 @@ def density_2d(self, x, y, Rs, rho0, gamma_inner, gamma_outer, center_x=0, cente Fx = self._f(x, gamma_inner, gamma_outer) return 2 * rho0 * Rs * Fx - def mass_3d(self, r, Rs, rho0, gamma_inner, gamma_outer): + @staticmethod + def mass_3d(r, Rs, rho0, gamma_inner, gamma_outer): """ mass enclosed a 3d sphere or radius r @@ -164,7 +166,7 @@ def mass_2d(self, R, Rs, rho0, gamma_inner, gamma_outer): """ mass enclosed a 2d cylinder or projected radius R - :param r: 3d radius + :param R: 3d radius :param Rs: scale radius :param rho0: central density normalization :param gamma_inner: logarithmic profile slope interior to Rs @@ -182,7 +184,7 @@ def nfwAlpha(self, R, Rs, rho0, gamma_inner, gamma_outer, ax_x, ax_y): deflection angel of NFW profile (times Sigma_crit D_OL) along the projection to coordinate 'axis' - :param r: 3d radius + :param R: 3d radius :param Rs: scale radius :param rho0: central density normalization :param gamma_inner: logarithmic profile slope interior to Rs @@ -202,7 +204,7 @@ def nfwGamma(self, R, Rs, rho0, gamma_inner, gamma_outer, ax_x, ax_y): shear gamma of NFW profile (times Sigma_crit) along the projection to coordinate 'axis' - :param r: 3d radius + :param R: 3d radius :param Rs: scale radius :param rho0: central density normalization :param gamma_inner: logarithmic profile slope interior to Rs @@ -229,8 +231,8 @@ def _f(X, g, n): :param n: logarithmic profile slope exterior to Rs :return: solution to the projection integral """ - if n==3: - n=3.001 # for numerical stability + if n == 3: + n = 3.001 # for numerical stability hyp2f1_term = hyp2f1((n-1)/2, g/2, n/2, 1/(1+X**2)) beta_term = beta((n-1)/2, 0.5) return 0.5 * beta_term * hyp2f1_term * (1+X**2) ** ((1-n)/2) @@ -246,8 +248,8 @@ def _g(X, g, n): :param n: logarithmic profile slope exterior to Rs :return: solution of the integral over projected mass """ - if n==3: - n=3.001 # for numerical stability + if n == 3: + n = 3.001 # for numerical stability xi = 1 + X**2 hyp2f1_term = hyp2f1((n - 3) / 2, g / 2, n / 2, 1 / xi) beta_term_1 = beta((n - 3) / 2, (3-g)/2) @@ -267,7 +269,7 @@ def alpha2rho0(self, alpha_Rs, Rs, gamma_inner, gamma_outer): """ gx = self._g(1.0, gamma_inner, gamma_outer) - rho0 = alpha_Rs / (4. * Rs **2 * gx / 1.0 ** 2) + rho0 = alpha_Rs / (4. * Rs ** 2 * gx / 1.0 ** 2) return rho0 def rho02alpha(self, rho0, Rs, gamma_inner, gamma_outer): @@ -282,5 +284,5 @@ def rho02alpha(self, rho0, Rs, gamma_inner, gamma_outer): :return: deflection angle at RS """ gx = self._g(1.0, gamma_inner, gamma_outer) - alpha_Rs = rho0 * (4. * Rs **2 * gx / 1.0 ** 2) + alpha_Rs = rho0 * (4. * Rs ** 2 * gx / 1.0 ** 2) return alpha_Rs From 203442d809bcb594c42a8c5bc5c09874edbb7832 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 19 Aug 2022 13:21:14 -0700 Subject: [PATCH 13/67] testing improved --- test/test_Cosmo/test_cosmo_solver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_Cosmo/test_cosmo_solver.py b/test/test_Cosmo/test_cosmo_solver.py index e9d54a37d..1d3c313e1 100644 --- a/test/test_Cosmo/test_cosmo_solver.py +++ b/test/test_Cosmo/test_cosmo_solver.py @@ -74,6 +74,7 @@ def setup(self): self.z_d, self.z_s = 0.295, 0.658 self.invertCosmo = InvertCosmo(z_d=self.z_d, z_s=self.z_s, H0_range=np.linspace(10, 100, 50), omega_m_range=np.linspace(0.05, 1, 50)) + self.invertCosmo_default = InvertCosmo(z_d=self.z_d, z_s=self.z_s) def test_get_cosmo(self): H0 = 80 @@ -83,6 +84,10 @@ def test_get_cosmo(self): npt.assert_almost_equal(H0_new, H0, decimal=1) npt.assert_almost_equal(omega_m_new, omega_m, decimal=3) + H0_new, omega_m_new = self.invertCosmo_default.get_cosmo(Dd, Ds_Dds) + npt.assert_almost_equal(H0_new, H0, decimal=1) + npt.assert_almost_equal(omega_m_new, omega_m, decimal=3) + H0_new, omega_m_new = self.invertCosmo.get_cosmo(Dd=1, Ds_Dds=1) assert H0_new == -1 From 51b161c2b409b46e402a0f67b24412bf20720e0c Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sat, 20 Aug 2022 13:36:54 -0700 Subject: [PATCH 14/67] documentation including the __init__() arguments --- docs/conf.py | 5 +++ lenstronomy/Analysis/kinematics_api.py | 2 +- lenstronomy/Cosmo/background.py | 2 +- lenstronomy/Cosmo/kde_likelihood.py | 10 ++--- lenstronomy/Data/image_noise.py | 4 +- lenstronomy/Data/imaging_data.py | 5 +-- lenstronomy/Data/psf.py | 6 +-- lenstronomy/GalKin/aperture.py | 2 +- lenstronomy/GalKin/aperture_types.py | 4 +- .../ImSim/Numerics/adaptive_numerics.py | 2 +- lenstronomy/ImSim/Numerics/convolution.py | 4 +- lenstronomy/ImSim/Numerics/grid.py | 4 +- lenstronomy/ImSim/Numerics/numerics.py | 18 ++++---- lenstronomy/ImSim/image2source_mapping.py | 13 +++--- .../LensModel/MultiPlane/multi_plane.py | 10 ++--- .../QuadOptimizer/multi_plane_fast.py | 2 +- .../LensModel/QuadOptimizer/optimizer.py | 2 +- lenstronomy/LensModel/lens_model.py | 20 ++++----- .../LensModel/lens_model_extensions.py | 12 +++--- lenstronomy/LensModel/profile_list_base.py | 2 +- lenstronomy/LightModel/light_model.py | 2 +- lenstronomy/PointSource/point_source.py | 8 ++-- lenstronomy/PointSource/point_source_param.py | 4 +- .../Sampling/Likelihoods/image_likelihood.py | 4 +- .../Likelihoods/position_likelihood.py | 8 ++-- .../Sampling/Likelihoods/prior_likelihood.py | 3 +- .../Likelihoods/time_delay_likelihood.py | 4 +- lenstronomy/Sampling/Pool/multiprocessing.py | 2 - lenstronomy/Sampling/likelihood.py | 42 +++++++++---------- lenstronomy/Sampling/parameters.py | 2 +- lenstronomy/SimulationAPI/model_api.py | 12 +++--- lenstronomy/SimulationAPI/observation_api.py | 6 +-- lenstronomy/Workflow/fitting_sequence.py | 2 +- lenstronomy/Workflow/psf_fitting.py | 2 +- 34 files changed, 114 insertions(+), 116 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cb6fd97ee..4b38aae57 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,11 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']\ # , 'sphinx.ext.autosectionlabel'] +autodoc_default_options = { + 'member-order': 'bysource', + 'special-members': '__init__', +} + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/lenstronomy/Analysis/kinematics_api.py b/lenstronomy/Analysis/kinematics_api.py index 52b1fdd71..8e03c1ef0 100644 --- a/lenstronomy/Analysis/kinematics_api.py +++ b/lenstronomy/Analysis/kinematics_api.py @@ -51,7 +51,7 @@ def __init__(self, z_lens, z_source, kwargs_model, kwargs_aperture, kwargs_seein :param kwargs_mge_mass: keyword arguments that go into the MGE decomposition routine :param kwargs_mge_light: keyword arguments that go into the MGE decomposition routine :param sampling_number: int, number of spectral rendering to compute the light weighted integrated LOS - dispersion within the aperture. This keyword should be chosen high enough to result in converged results within the tolerance. + dispersion within the aperture. This keyword should be chosen high enough to result in converged results within the tolerance. :param num_kin_sampling: number of kinematic renderings on a total IFU :param num_psf_sampling: number of PSF displacements for each kinematic rendering on the IFU """ diff --git a/lenstronomy/Cosmo/background.py b/lenstronomy/Cosmo/background.py index 9e43a4c8b..30e5df32a 100644 --- a/lenstronomy/Cosmo/background.py +++ b/lenstronomy/Cosmo/background.py @@ -17,7 +17,7 @@ def __init__(self, cosmo=None, interp=False, **kwargs_interp): :param cosmo: instance of astropy.cosmology :param interp: boolean, if True, uses interpolated cosmology to evaluate specific redshifts :param kwargs_interp: keyword arguments of CosmoInterp specifying the interpolation interval and maximum - redshift + redshift :return: Background class with instance of astropy.cosmology """ diff --git a/lenstronomy/Cosmo/kde_likelihood.py b/lenstronomy/Cosmo/kde_likelihood.py index 1d4dea836..dbed4d4d0 100644 --- a/lenstronomy/Cosmo/kde_likelihood.py +++ b/lenstronomy/Cosmo/kde_likelihood.py @@ -14,11 +14,11 @@ def __init__(self, D_d_sample, D_delta_t_sample, kde_type='scipy_gaussian', band :param D_d_sample: 1-d numpy array of angular diameter distances to the lens plane :param D_delta_t_sample: 1-d numpy array of time-delay distances - kde_type : string - The kernel to use. Valid kernels are - 'scipy_gaussian' or - ['gaussian'|'tophat'|'epanechnikov'|'exponential'|'linear'|'cosine'] - Default is 'gaussian'. + :param kde_type: The kernel to use. Valid kernels are + 'scipy_gaussian' or + ['gaussian'|'tophat'|'epanechnikov'|'exponential'|'linear'|'cosine'] + Default is 'gaussian'. + :type kde_type: string :param bandwidth: width of kernel (in same units as the angular diameter quantities) """ values = np.vstack([D_d_sample, D_delta_t_sample]) diff --git a/lenstronomy/Data/image_noise.py b/lenstronomy/Data/image_noise.py index d49535d36..138099fe7 100644 --- a/lenstronomy/Data/image_noise.py +++ b/lenstronomy/Data/image_noise.py @@ -18,10 +18,10 @@ def __init__(self, image_data, exposure_time=None, background_rms=None, noise_ma :param image_data: numpy array, pixel data values :param exposure_time: int or array of size the data; exposure time - (common for all pixels or individually for each individual pixel) + (common for all pixels or individually for each individual pixel) :param background_rms: root-mean-square value of Gaussian background noise :param noise_map: int or array of size the data; joint noise sqrt(variance) of each individual pixel. - Overwrites meaning of background_rms and exposure_time. + Overwrites meaning of background_rms and exposure_time. :param gradient_boost_factor: None or float, variance terms added in quadrature scaling with gradient^2 * gradient_boost_factor """ diff --git a/lenstronomy/Data/imaging_data.py b/lenstronomy/Data/imaging_data.py index 778047782..d86ddb750 100644 --- a/lenstronomy/Data/imaging_data.py +++ b/lenstronomy/Data/imaging_data.py @@ -29,8 +29,7 @@ class to handle the data, coordinate system and masking, including convolution w If this keyword is set, the other noise properties will be ignored. - Notes: - ------ + ** notes ** the likelihood for the data given model P(data|model) is defined in the function below. Please make sure that your definitions and units of 'exposure_map', 'background_rms' and 'image_data' are in accordance with the likelihood function. In particular, make sure that the Poisson noise contribution is defined in the count rate. @@ -43,7 +42,7 @@ def __init__(self, image_data, exposure_time=None, background_rms=None, noise_ma :param image_data: 2d numpy array of the image data :param exposure_time: int or array of size the data; exposure time - (common for all pixels or individually for each individual pixel) + (common for all pixels or individually for each individual pixel) :param background_rms: root-mean-square value of Gaussian background noise in units counts per second :param noise_map: int or array of size the data; joint noise sqrt(variance) of each individual pixel. :param gradient_boost_factor: None or float, variance terms added in quadrature scaling with diff --git a/lenstronomy/Data/psf.py b/lenstronomy/Data/psf.py index 8b7be2eb7..d6ff24a54 100644 --- a/lenstronomy/Data/psf.py +++ b/lenstronomy/Data/psf.py @@ -24,9 +24,9 @@ def __init__(self, psf_type='NONE', fwhm=None, truncation=5, pixel_size=None, ke :param kernel_point_source: 2d numpy array, odd length, centered PSF of a point source (if not normalized, will be normalized) :param psf_error_map: uncertainty in the PSF model per pixel (size of data, not super-sampled). 2d numpy array. - Size can be larger or smaller than the pixel-sized PSF model and if so, will be matched. - This error will be added to the pixel error around the position of point sources as follows: - sigma^2_i += 'psf_error_map'_j * (point_source_flux_i)**2 + Size can be larger or smaller than the pixel-sized PSF model and if so, will be matched. + This error will be added to the pixel error around the position of point sources as follows: + sigma^2_i += 'psf_error_map'_j * (point_source_flux_i)**2 :param point_source_supersampling_factor: int, supersampling factor of kernel_point_source. This is the input PSF to this class and does not need to be the choice in the modeling (thought preferred if modeling choses supersampling) diff --git a/lenstronomy/GalKin/aperture.py b/lenstronomy/GalKin/aperture.py index e3669d476..bc9287e44 100644 --- a/lenstronomy/GalKin/aperture.py +++ b/lenstronomy/GalKin/aperture.py @@ -26,7 +26,7 @@ def __init__(self, aperture_type, **kwargs_aperture): :param aperture_type: string :param kwargs_aperture: keyword arguments reflecting the aperture type chosen. - We refer to the specific class instances for documentation. + We refer to the specific class instances for documentation. """ if aperture_type == 'slit': self._aperture = Slit(**kwargs_aperture) diff --git a/lenstronomy/GalKin/aperture_types.py b/lenstronomy/GalKin/aperture_types.py index 0c9b5bc40..ee75e6412 100644 --- a/lenstronomy/GalKin/aperture_types.py +++ b/lenstronomy/GalKin/aperture_types.py @@ -199,7 +199,7 @@ def __init__(self, r_bins, center_ra=0, center_dec=0): """ :param r_bins: array of radial bins to average the dispersion spectra in ascending order. - It starts with the inner-most edge to the outermost edge. + It starts with the inner-most edge to the outermost edge. :param center_ra: center of the sphere :param center_dec: center of the sphere """ @@ -231,7 +231,7 @@ def shell_ifu_select(ra, dec, r_bin, center_ra=0, center_dec=0): :param ra: angular coordinate of photon/ray :param dec: angular coordinate of photon/ray :param r_bin: array of radial bins to average the dispersion spectra in ascending order. - It starts with the inner-most edge to the outermost edge. + It starts with the inner-most edge to the outermost edge. :param center_ra: center of the sphere :param center_dec: center of the sphere :return: boolean, True if within the radial range, False otherwise diff --git a/lenstronomy/ImSim/Numerics/adaptive_numerics.py b/lenstronomy/ImSim/Numerics/adaptive_numerics.py index 35ade94dc..ce568ca21 100644 --- a/lenstronomy/ImSim/Numerics/adaptive_numerics.py +++ b/lenstronomy/ImSim/Numerics/adaptive_numerics.py @@ -27,7 +27,7 @@ def __init__(self, kernel_super, supersampling_factor, conv_supersample_pixels, :param supersampling_factor: factor of supersampling relative to pixel grid :param conv_supersample_pixels: bool array same size as data, pixels to be convolved and their light to be blurred :param supersampling_kernel_size: number of pixels (in units of the image pixels) that are convolved with the - supersampled kernel + supersampled kernel :param compute_pixels: bool array of size of image, these pixels (if True) will get blurred light from other pixels :param nopython: bool, numba jit setting to use python or compiled. :param cache: bool, numba jit setting to use cache diff --git a/lenstronomy/ImSim/Numerics/convolution.py b/lenstronomy/ImSim/Numerics/convolution.py index 766c83958..90c057145 100644 --- a/lenstronomy/ImSim/Numerics/convolution.py +++ b/lenstronomy/ImSim/Numerics/convolution.py @@ -191,7 +191,7 @@ def __init__(self, kernel_supersampled, supersampling_factor, supersampling_kern :param kernel_supersampled: kernel in supersampled pixels :param supersampling_factor: supersampling factor relative to the image pixel grid :param supersampling_kernel_size: number of pixels (in units of the image pixels) that are convolved with the - supersampled kernel + supersampled kernel """ # n_high = len(kernel_supersampled) self._supersampling_factor = supersampling_factor @@ -253,7 +253,7 @@ def __init__(self, sigma_list, fraction_list, pixel_scale, supersampling_factor= :param fraction_list: fraction of flux to be convoled with each Gaussian kernel :param pixel_scale: scale of pixel width (to convert sigmas into units of pixels) :param truncation: float. Truncate the filter at this many standard deviations. - Default is 4.0. + Default is 4.0. """ self._num_gaussians = len(sigma_list) self._sigmas_scaled = np.array(sigma_list) / pixel_scale diff --git a/lenstronomy/ImSim/Numerics/grid.py b/lenstronomy/ImSim/Numerics/grid.py index 2192ef825..8f0ffa04a 100644 --- a/lenstronomy/ImSim/Numerics/grid.py +++ b/lenstronomy/ImSim/Numerics/grid.py @@ -24,7 +24,7 @@ def __init__(self, nx, ny, transform_pix2angle, ra_at_xy_0, dec_at_xy_0, supersa :param supersampling_indexes: bool array of shape nx x ny, corresponding to pixels being super_sampled :param supersampling_factor: int, factor (per axis) of super-sampling :param flux_evaluate_indexes: bool array of shape nx x ny, corresponding to pixels being evaluated - (for both low and high res). Default is None, replaced by setting all pixels to being evaluated. + (for both low and high res). Default is None, replaced by setting all pixels to being evaluated. """ super(AdaptiveGrid, self).__init__(transform_pix2angle, ra_at_xy_0, dec_at_xy_0) self._nx = nx @@ -171,7 +171,7 @@ def __init__(self, nx, ny, transform_pix2angle, ra_at_xy_0, dec_at_xy_0, supersa :param dec_at_xy_0: dec coordinate at pixel (0,0) :param supersampling_factor: int, factor (per axis) of super-sampling :param flux_evaluate_indexes: bool array of shape nx x ny, corresponding to pixels being evaluated - (for both low and high res). Default is None, replaced by setting all pixels to being evaluated. + (for both low and high res). Default is None, replaced by setting all pixels to being evaluated. """ super(RegularGrid, self).__init__(transform_pix2angle, ra_at_xy_0, dec_at_xy_0) self._supersampling_factor = supersampling_factor diff --git a/lenstronomy/ImSim/Numerics/numerics.py b/lenstronomy/ImSim/Numerics/numerics.py index ceb89d759..09a12fdbe 100644 --- a/lenstronomy/ImSim/Numerics/numerics.py +++ b/lenstronomy/ImSim/Numerics/numerics.py @@ -24,19 +24,19 @@ def __init__(self, pixel_grid, psf, supersampling_factor=1, compute_mode='regula :param compute_mode: options are: 'regular', 'adaptive' :param supersampling_factor: int, factor of higher resolution sub-pixel sampling of surface brightness :param supersampling_convolution: bool, if True, performs (part of) the convolution on the super-sampled - grid/pixels + grid/pixels :param supersampling_kernel_size: int (odd number), size (in regular pixel units) of the super-sampled - convolution + convolution :param flux_evaluate_indexes: boolean 2d array of size of image (or None, then initiated as gird of True's). - Pixels indicated with True will be used to perform the surface brightness computation (and possible lensing - ray-shooting). Pixels marked as False will be assigned a flux value of zero (or ignored in the adaptive - convolution) + Pixels indicated with True will be used to perform the surface brightness computation (and possible lensing + ray-shooting). Pixels marked as False will be assigned a flux value of zero (or ignored in the adaptive + convolution) :param supersampled_indexes: 2d boolean array (only used in mode='adaptive') of pixels to be supersampled (in - surface brightness and if supersampling_convolution=True also in convolution). All other pixels not set to =True - will not be super-sampled. + surface brightness and if supersampling_convolution=True also in convolution). All other pixels not set to =True + will not be super-sampled. :param compute_indexes: 2d boolean array (only used in compute_mode='adaptive'), marks pixel that the response after - convolution is computed (all others =0). This can be set to likelihood_mask in the Likelihood module for - consistency. + convolution is computed (all others =0). This can be set to likelihood_mask in the Likelihood module for + consistency. :param point_source_supersampling_factor: super-sampling resolution of the point source placing :param convolution_kernel_size: int, odd number, size of convolution kernel. If None, takes size of point_source_kernel :param convolution_type: string, 'fft', 'grid', 'fft_static' mode of 2d convolution diff --git a/lenstronomy/ImSim/image2source_mapping.py b/lenstronomy/ImSim/image2source_mapping.py index bef75906c..5cfc137ba 100644 --- a/lenstronomy/ImSim/image2source_mapping.py +++ b/lenstronomy/ImSim/image2source_mapping.py @@ -24,12 +24,13 @@ class Image2SourceMapping(object): def __init__(self, lensModel, sourceModel): """ - :param lensModel: lenstronomy LensModel() class instance - :param sourceModel: LightModel () class instance - The lightModel includes: - - source_scale_factor_list: list of floats corresponding to the rescaled deflection angles to the specific source - components. None indicates that the list will be set to 1, meaning a single source plane model (in single lens plane mode). - - source_redshift_list: list of redshifts of the light components (in multi lens plane mode) + :param lensModel: LensModel() class instance + :param sourceModel: LightModel() class instance. + + The lightModel includes: + + - source_scale_factor_list: list of floats corresponding to the rescaled deflection angles to the specific source components. None indicates that the list will be set to 1, meaning a single source plane model (in single lens plane mode). + - source_redshift_list: list of redshifts of the light components (in multi lens plane mode) """ self._lightModel = sourceModel self._lensModel = lensModel diff --git a/lenstronomy/LensModel/MultiPlane/multi_plane.py b/lenstronomy/LensModel/MultiPlane/multi_plane.py index 92f92c117..c2212c513 100644 --- a/lenstronomy/LensModel/MultiPlane/multi_plane.py +++ b/lenstronomy/LensModel/MultiPlane/multi_plane.py @@ -25,16 +25,16 @@ def __init__(self, z_source, lens_model_list, lens_redshift_list, cosmo=None, nu :param lens_redshift_list: list of floats with redshifts of the lens models indicated in lens_model_list :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) + (see documentation in Profiles/numerical_alpha) :param kwargs_interp: interpolation keyword arguments specifying the numerics. See description in the Interpolate() class. Only applicable for 'INTERPOL' and 'INTERPOL_SCALED' models. :param 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 + 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 ignore_observed_positions: bool, if True, will ignore the conversion between observed to physical - position of deflectors + position of deflectors :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. + models. If None, 'z_source' is used. """ if z_source_convention is None: diff --git a/lenstronomy/LensModel/QuadOptimizer/multi_plane_fast.py b/lenstronomy/LensModel/QuadOptimizer/multi_plane_fast.py index 53b6df42f..738f41bab 100644 --- a/lenstronomy/LensModel/QuadOptimizer/multi_plane_fast.py +++ b/lenstronomy/LensModel/QuadOptimizer/multi_plane_fast.py @@ -29,7 +29,7 @@ def __init__(self, x_image, y_image, z_lens, z_source, lens_model_list, redshift :param astropy_instance: instance of astropy to pass to lens model :param param_class: an instance of ParamClass (see documentation in QuadOptimmizer.param_manager) :param foreground_rays: (optional) pre-computed foreground rays from a previous iteration, if they are not specified - they will be re-computed + they will be re-computed :param tol_source: source plane chi^2 sigma :param numerical_alpha_class: class for computing numerically tabulated deflection angles """ diff --git a/lenstronomy/LensModel/QuadOptimizer/optimizer.py b/lenstronomy/LensModel/QuadOptimizer/optimizer.py index 2b20fdc49..088f6c624 100644 --- a/lenstronomy/LensModel/QuadOptimizer/optimizer.py +++ b/lenstronomy/LensModel/QuadOptimizer/optimizer.py @@ -39,7 +39,7 @@ def __init__(self, x_image, y_image, lens_model_list, redshift_list, z_lens, z_s :param re_optimize_scale: float, controls how tight the initial spread of particles is :param pso_convergence_mean: when to terminate the PSO fit :param foreground_rays: (optional) can pass in pre-computed foreground light rays from a previous fit - so as to not waste time recomputing them + so as to not waste time recomputing them :param tol_source: sigma in the source plane chi^2 :param tol_simplex_func: tolerance for the downhill simplex optimization :param simplex_n_iterations: number of iterations per dimension for the downhill simplex optimization diff --git a/lenstronomy/LensModel/lens_model.py b/lenstronomy/LensModel/lens_model.py index 0ec82c58a..36d7a1b41 100644 --- a/lenstronomy/LensModel/lens_model.py +++ b/lenstronomy/LensModel/lens_model.py @@ -20,28 +20,28 @@ def __init__(self, lens_model_list, z_lens=None, z_source=None, lens_redshift_li :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. + 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. + 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. + 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) + (see documentation in Profiles/numerical_alpha) :param kwargs_interp: interpolation keyword arguments specifying the numerics. See description in the Interpolate() class. Only applicable for 'INTERPOL' and 'INTERPOL_SCALED' models. :param 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 + 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. + 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 + 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 + 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 + distances """ self.lens_model_list = lens_model_list self.z_lens = z_lens diff --git a/lenstronomy/LensModel/lens_model_extensions.py b/lenstronomy/LensModel/lens_model_extensions.py index f829d7ed4..d201d2192 100644 --- a/lenstronomy/LensModel/lens_model_extensions.py +++ b/lenstronomy/LensModel/lens_model_extensions.py @@ -13,12 +13,12 @@ def __init__(self, lensModel): """ :param lensModel: instance of the LensModel() class, or with same functionalities. - In particular, the following definitions are required to execute all functionalities presented in this class: - def ray_shooting() - def magnification() - def kappa() - def alpha() - def hessian() + In particular, the following definitions are required to execute all functionalities presented in this class: + def ray_shooting() + def magnification() + def kappa() + def alpha() + def hessian() """ self._lensModel = lensModel diff --git a/lenstronomy/LensModel/profile_list_base.py b/lenstronomy/LensModel/profile_list_base.py index 4b468c18d..c22777fa7 100644 --- a/lenstronomy/LensModel/profile_list_base.py +++ b/lenstronomy/LensModel/profile_list_base.py @@ -30,7 +30,7 @@ def __init__(self, lens_model_list, numerical_alpha_class=None, lens_redshift_li :param lens_model_list: list of strings with lens model names :param numerical_alpha_class: an instance of a custom class for use in NumericalAlpha() lens model - deflection angles as a lens model. See the documentation in Profiles.numerical_deflections + deflection angles as a lens model. See the documentation in Profiles.numerical_deflections :param kwargs_interp: interpolation keyword arguments specifying the numerics. See description in the Interpolate() class. Only applicable for 'INTERPOL' and 'INTERPOL_SCALED' models. """ diff --git a/lenstronomy/LightModel/light_model.py b/lenstronomy/LightModel/light_model.py index 2babf2538..07ad0cd2c 100644 --- a/lenstronomy/LightModel/light_model.py +++ b/lenstronomy/LightModel/light_model.py @@ -28,7 +28,7 @@ def __init__(self, light_model_list, deflection_scaling_list=None, source_redshi :param light_model_list: list of light models :param deflection_scaling_list: list of floats indicating a relative scaling of the deflection angle from the reduced angles in the lens model definition (optional, only possible in single lens plane with multiple source - planes) + planes) :param source_redshift_list: list of redshifts for the different light models (optional and only used in multi-plane lensing in conjunction with a cosmology model) :param smoothing: smoothing factor for certain models (deprecated) diff --git a/lenstronomy/PointSource/point_source.py b/lenstronomy/PointSource/point_source.py index 7e3169495..e59308574 100644 --- a/lenstronomy/PointSource/point_source.py +++ b/lenstronomy/PointSource/point_source.py @@ -21,7 +21,7 @@ def __init__(self, point_source_type_list, lensModel=None, fixed_magnification_l This option then requires to provide a 'source_amp' amplitude of the source brightness instead of 'point_amp' the list of image brightnesses. :param additional_images_list: list of booleans (same length as point_source_type_list). If True, search for - additional images of the same source is conducted. + additional images of the same source is conducted. :param flux_from_point_source_list: list of booleans (optional), if set, will only return image positions (for imaging modeling) for the subset of the point source lists that =True. This option enables to model imaging data with transient point sources, when the point source positions are measured and present at a @@ -32,10 +32,8 @@ def __init__(self, point_source_type_list, lensModel=None, fixed_magnification_l equation is conducted with the lens model parameters provided. This can increase the speed as multiple times the image positions are requested for the same lens model. Attention in usage! :param kwargs_lens_eqn_solver: keyword arguments specifying the numerical settings for the lens equation solver - see LensEquationSolver() class for details - - for the kwargs_lens_eqn_solver parameters: have a look at the lensEquationSolver class, such as: - min_distance=0.01, search_window=5, precision_limit=10**(-10), num_iter_max=100 + see LensEquationSolver() class for details ,such as: + min_distance=0.01, search_window=5, precision_limit=10**(-10), num_iter_max=100 """ self._lensModel = lensModel diff --git a/lenstronomy/PointSource/point_source_param.py b/lenstronomy/PointSource/point_source_param.py index 890deb944..d5ffd4b1d 100644 --- a/lenstronomy/PointSource/point_source_param.py +++ b/lenstronomy/PointSource/point_source_param.py @@ -16,9 +16,9 @@ def __init__(self, model_list, kwargs_fixed, num_point_source_list=None, linear_ :param kwargs_fixed: list of keyword arguments with parameters to be held fixed :param num_point_source_list: list of number of point sources per point source model class :param linear_solver: bool, if True, does not return linear parameters for the sampler - (will be solved linearly instead) + (will be solved linearly instead) :param fixed_magnification_list: list of booleans, if entry is True, keeps one overall scaling among the - point sources in this class + point sources in this class """ self.model_list = model_list if num_point_source_list is None: diff --git a/lenstronomy/Sampling/Likelihoods/image_likelihood.py b/lenstronomy/Sampling/Likelihoods/image_likelihood.py index 6dc48e758..fb7734a77 100644 --- a/lenstronomy/Sampling/Likelihoods/image_likelihood.py +++ b/lenstronomy/Sampling/Likelihoods/image_likelihood.py @@ -19,9 +19,9 @@ def __init__(self, multi_band_list, multi_band_type, kwargs_model, bands_compute :param image_likelihood_mask_list: list of boolean 2d arrays of size of images marking the pixels to be evaluated in the likelihood :param source_marg: marginalization addition on the imaging likelihood based on the covariance of the inferred - linear coefficients + linear coefficients :param linear_prior: float or list of floats (when multi-linear setting is chosen) indicating the range of - linear amplitude priors when computing the marginalization term. + linear amplitude priors when computing the marginalization term. :param check_positive_flux: bool, option to punish models that do not have all positive linear amplitude parameters :param kwargs_pixelbased: keyword arguments with various settings related to the pixel-based solver diff --git a/lenstronomy/Sampling/Likelihoods/position_likelihood.py b/lenstronomy/Sampling/Likelihoods/position_likelihood.py index cedbf8798..f5baefc86 100644 --- a/lenstronomy/Sampling/Likelihoods/position_likelihood.py +++ b/lenstronomy/Sampling/Likelihoods/position_likelihood.py @@ -19,7 +19,7 @@ def __init__(self, point_source_class, image_position_uncertainty=0.005, astrome :param image_position_uncertainty: uncertainty in image position uncertainty (1-sigma Gaussian radially), this is applicable for astrometric uncertainties as well as if image positions are provided as data :param astrometric_likelihood: bool, if True, evaluates the astrometric uncertainty of the predicted and modeled - image positions with an offset 'delta_x_image' and 'delta_y_image' + image positions with an offset 'delta_x_image' and 'delta_y_image' :param image_position_likelihood: bool, if True, evaluates the likelihood of the model predicted image position given the data/measured image positions :param ra_image_list: list or RA image positions per model component @@ -32,11 +32,11 @@ def __init__(self, point_source_class, image_position_uncertainty=0.005, astrome :param source_position_sigma: r.m.s. value corresponding to a 1-sigma Gaussian likelihood accepted by the model precision in matching the source position :param force_no_add_image: bool, if True, will punish additional images appearing in the frame of the modelled - image(first calculate them) + image(first calculate them) :param restrict_image_number: bool, if True, searches for all appearing images in the frame of the data and - compares with max_num_images + compares with max_num_images :param max_num_images: integer, maximum number of appearing images. Default is the number of images given in - the Param() class + the Param() class """ self._pointSource = point_source_class # TODO replace with public function of ray_shooting diff --git a/lenstronomy/Sampling/Likelihoods/prior_likelihood.py b/lenstronomy/Sampling/Likelihoods/prior_likelihood.py index 3e456ded7..b1383f90f 100644 --- a/lenstronomy/Sampling/Likelihoods/prior_likelihood.py +++ b/lenstronomy/Sampling/Likelihoods/prior_likelihood.py @@ -34,8 +34,7 @@ def __init__(self, prior_lens=None, prior_source=None, prior_lens_light=None, pr :param prior_special_kde: list of [param_name, samples] :param prior_extinction_kde: list of [index_model, param_name, samples] - :param prior_lens_lognormal: list of [index_model, param_name, mean, 1-sigma - priors] + :param prior_lens_lognormal: list of [index_model, param_name, mean, 1-sigma priors] :param prior_source_lognormal: list of [index_model, param_name, mean, 1-sigma priors] :param prior_lens_light_lognormal: list of [index_model, param_name, mean, 1-sigma priors] :param prior_ps_lognormal: list of [index_model, param_name, mean, 1-sigma priors] diff --git a/lenstronomy/Sampling/Likelihoods/time_delay_likelihood.py b/lenstronomy/Sampling/Likelihoods/time_delay_likelihood.py index a302fdd41..598e7475a 100644 --- a/lenstronomy/Sampling/Likelihoods/time_delay_likelihood.py +++ b/lenstronomy/Sampling/Likelihoods/time_delay_likelihood.py @@ -13,10 +13,10 @@ def __init__(self, time_delays_measured, time_delays_uncertainties, lens_model_c :param time_delays_measured: relative time delays (in days) in respect to the first image of the point source :param time_delays_uncertainties: time-delay uncertainties in same order as time_delay_measured. Alternatively - a full covariance matrix that describes the likelihood. + a full covariance matrix that describes the likelihood. :param lens_model_class: instance of the LensModel() class :param point_source_class: instance of the PointSource() class, note: the first point source type is the one the - time delays are imposed on + time delays are imposed on """ if time_delays_measured is None: diff --git a/lenstronomy/Sampling/Pool/multiprocessing.py b/lenstronomy/Sampling/Pool/multiprocessing.py index 4599c3b45..e28d0f8d4 100644 --- a/lenstronomy/Sampling/Pool/multiprocessing.py +++ b/lenstronomy/Sampling/Pool/multiprocessing.py @@ -79,8 +79,6 @@ def map(self, func, iterable, chunksize=None, callback=None): :meth:`multiprocessing.pool.Pool.map()`, without catching ``KeyboardInterrupt``. - Parameters - ---------- :param func: A function or callable object that is executed on each element of the specified ``tasks`` iterable. This object must be picklable (i.e. it can't be a function scoped within a function or a diff --git a/lenstronomy/Sampling/likelihood.py b/lenstronomy/Sampling/likelihood.py index 3f512face..f807c3cf0 100644 --- a/lenstronomy/Sampling/likelihood.py +++ b/lenstronomy/Sampling/likelihood.py @@ -43,7 +43,7 @@ def __init__(self, kwargs_data_joint, kwargs_model, param_class, image_likelihoo into the conventions of the imSim_class :param image_likelihood: bool, option to compute the imaging likelihood :param source_position_likelihood: bool, if True, ray-traces image positions back to source plane and evaluates - relative errors in respect ot the position_uncertainties in the image plane + relative errors in respect ot the position_uncertainties in the image plane :param check_bounds: bool, option to punish the hard bounds in parameter space :param check_matched_source_position: bool, option to check whether point source position of solver finds a solution to match all the image positions in the same source plane coordinate @@ -58,13 +58,13 @@ def __init__(self, kwargs_data_joint, kwargs_model, param_class, image_likelihoo :param image_likelihood_mask_list: list of boolean 2d arrays of size of images marking the pixels to be evaluated in the likelihood :param force_no_add_image: bool, if True: computes ALL image positions of the point source. If there are more - images predicted than modelled, a punishment occures + images predicted than modelled, a punishment occurs :param source_marg: marginalization addition on the imaging likelihood based on the covariance of the inferred linear coefficients :param linear_prior: float or list of floats (when multi-linear setting is chosen) indicating the range of - linear amplitude priors when computing the marginalization term. + linear amplitude priors when computing the marginalization term. :param restrict_image_number: bool, if True: computes ALL image positions of the point source. If there are more - images predicted than indicated in max_num_images, a punishment occurs + images predicted than indicated in max_num_images, a punishment occurs :param max_num_images: int, see restrict_image_number :param bands_compute: list of bools with same length as data objects, indicates which "band" to include in the fitting @@ -157,12 +157,12 @@ def logL(self, args, verbose=False): """ routine to compute X2 given variable parameters for a MCMC/PSO chain - Parameters - ---------- - args : tuple or list of floats - ordered parameter values that are being sampled - verbose : boolean - if True, makes print statements about individual likelihood components + + :param args: ordered parameter values that are being sampled + :type args: tuple or list of floats + :param verbose: if True, makes print statements about individual likelihood components + :type verbose: boolean + :returns: log likelihood of the data given the model (natural logarithm) """ # extract parameters kwargs_return = self.param.args2kwargs(args) @@ -175,18 +175,16 @@ def logL(self, args, verbose=False): def log_likelihood(self, kwargs_return, verbose=False): """ - Parameters - ---------- - kwargs_return : keyword arguments - need to contain 'kwargs_lens', 'kwargs_source', 'kwargs_lens_light', 'kwargs_ps', 'kwargs_special' - These entries themselfs are lists of keyword argument of the parameters entering the model to be evaluated - verbose : boolean - if True, makes print statements about individual likelihood components - - Returns - ------- - logL : float - log likelihood of the data given the model (natural logarithm) + + :param kwargs_return: need to contain 'kwargs_lens', 'kwargs_source', 'kwargs_lens_light', 'kwargs_ps', + 'kwargs_special'. These entries themselves are lists of keyword argument of the parameters entering the model + to be evaluated + :type kwargs_return: keyword arguments + :param verbose: if True, makes print statements about individual likelihood components + :type verbose: boolean + + :returns: + - logL (float) log likelihood of the data given the model (natural logarithm) """ kwargs_lens, kwargs_source, kwargs_lens_light, kwargs_ps, kwargs_special = kwargs_return['kwargs_lens'], \ kwargs_return['kwargs_source'], \ diff --git a/lenstronomy/Sampling/parameters.py b/lenstronomy/Sampling/parameters.py index 45640dc39..cb860cc3e 100644 --- a/lenstronomy/Sampling/parameters.py +++ b/lenstronomy/Sampling/parameters.py @@ -144,7 +144,7 @@ def __init__(self, kwargs_model, in ascending numbering e.g. [-1, 0, 0, 1, 0, 2], -1 indicating not sampled fixed indexes. These indexes are the sample as for the lens :param source_grid_offset: optional, if True when using a pixel-based modelling (e.g. with STARLETS-like profiles), - adds two additional sampled parameters describing RA/Dec offsets between data coordinate grid and pixelated source plane coordinate grid. + adds two additional sampled parameters describing RA/Dec offsets between data coordinate grid and pixelated source plane coordinate grid. :param num_shapelet_lens: number of shapelet coefficients in the 'SHAPELETS_CART' or 'SHAPELETS_POLAR' mass profile. :param log_sampling_lens: Sample the log10 of the lens model parameters. Format : [[i_lens, ['param_name1', 'param_name2', ...]], [...], ...], """ diff --git a/lenstronomy/SimulationAPI/model_api.py b/lenstronomy/SimulationAPI/model_api.py index a74f4153f..785d2b571 100644 --- a/lenstronomy/SimulationAPI/model_api.py +++ b/lenstronomy/SimulationAPI/model_api.py @@ -23,20 +23,20 @@ def __init__(self, lens_model_list=None, z_lens=None, z_source=None, lens_redshi :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. + 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. This will be the redshift of the source - plane (if not further specified the 'source_redshift_list') and the point source redshift - (regardless of 'source_redshift_list') + not required for the core functionalities in the single plane mode. This will be the redshift of the source + plane (if not further specified the 'source_redshift_list') and the point source redshift + (regardless of 'source_redshift_list') :param lens_redshift_list: list of deflector redshift (corresponding to the lens model list), - only applicable in multi_plane mode. + only applicable in multi_plane mode. :param source_light_model_list: list of strings with source light model names (lensed light profiles) :param lens_light_model_list: list of strings with lens light model names (not lensed light profiles) :param point_source_model_list: list of strings with point source model names :param source_redshift_list: list of redshifts of the source profiles (optional) :param cosmo: instance of the astropy cosmology class. If not specified, uses the default cosmology. :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. + models. If None, 'z_source' is used. """ if lens_model_list is None: lens_model_list = [] diff --git a/lenstronomy/SimulationAPI/observation_api.py b/lenstronomy/SimulationAPI/observation_api.py index bb2630baf..7f89c527f 100644 --- a/lenstronomy/SimulationAPI/observation_api.py +++ b/lenstronomy/SimulationAPI/observation_api.py @@ -43,9 +43,9 @@ def __init__(self, exposure_time, sky_brightness=None, seeing=None, num_exposure :param num_exposures: number of exposures that are combined :param psf_type: string, type of PSF ('GAUSSIAN' and 'PIXEL' supported) :param kernel_point_source: 2d numpy array, model of PSF centered with odd number of pixels per axis - (optional when psf_type='PIXEL' is chosen) + (optional when psf_type='PIXEL' is chosen) :param point_source_supersampling_factor: int, supersampling factor of kernel_point_source - (optional when psf_type='PIXEL' is chosen) + (optional when psf_type='PIXEL' is chosen) """ self._exposure_time = exposure_time self._sky_brightness_ = sky_brightness @@ -155,7 +155,7 @@ def __init__(self, pixel_scale, exposure_time, magnitude_zero_point, read_noise= :param magnitude_zero_point: magnitude in which 1 count (e-) per second per arcsecond square is registered :param num_exposures: number of exposures that are combined :param point_source_supersampling_factor: int, supersampling factor of kernel_point_source - (optional when psf_type='PIXEL' is chosen) + (optional when psf_type='PIXEL' is chosen) :param data_count_unit: string, unit of the data (not noise properties - see other definitions), 'e-': (electrons assumed to be IID), 'ADU': (analog-to-digital unit) diff --git a/lenstronomy/Workflow/fitting_sequence.py b/lenstronomy/Workflow/fitting_sequence.py index c70f2eb7c..a51ef6652 100644 --- a/lenstronomy/Workflow/fitting_sequence.py +++ b/lenstronomy/Workflow/fitting_sequence.py @@ -40,7 +40,7 @@ def __init__(self, kwargs_data_joint, kwargs_model, kwargs_constraints, kwargs_l 'extinction_model': [kwargs_init, kwargs_sigma, kwargs_fixed, kwargs_lower, kwargs_upper] 'special': [kwargs_init, kwargs_sigma, kwargs_fixed, kwargs_lower, kwargs_upper] :param mpi: MPI option (bool), if True, will launch an MPI Pool job for the steps in the fitting sequence where - possible + possible :param verbose: bool, if True prints temporary results and indicators of the fitting process """ self.kwargs_data_joint = kwargs_data_joint diff --git a/lenstronomy/Workflow/psf_fitting.py b/lenstronomy/Workflow/psf_fitting.py index dbaad9572..f89e39bf0 100644 --- a/lenstronomy/Workflow/psf_fitting.py +++ b/lenstronomy/Workflow/psf_fitting.py @@ -405,7 +405,7 @@ def combine_psf(kernel_list_new, kernel_old, factor=1., stacking_option='median' :param kernel_list_new: list of new PSF kernels estimated from the point sources in the image (un-normalized) :param kernel_old: old PSF kernel :param factor: weight of updated estimate based on new and old estimate, factor=1 means new estimate, - factor=0 means old estimate + factor=0 means old estimate :param stacking_option: option of stacking, mean or median :param symmetry: imposed symmetry of PSF estimate :return: updated PSF estimate and error_map associated with it From 217a9fc2fd4a3bc807e9fdc650c72b5b5ca4c785 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sun, 21 Aug 2022 14:43:24 -0700 Subject: [PATCH 15/67] nautilus sampler debugged and upgraded to latest version --- lenstronomy/Sampling/Samplers/nautilus.py | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lenstronomy/Sampling/Samplers/nautilus.py b/lenstronomy/Sampling/Samplers/nautilus.py index 70c9564a8..5bedff066 100644 --- a/lenstronomy/Sampling/Samplers/nautilus.py +++ b/lenstronomy/Sampling/Samplers/nautilus.py @@ -49,7 +49,7 @@ def nautilus_sampling(self, prior_type='uniform', mpi=False, thread_count=1, ver raise ValueError('prior_type %s is not supported for Nautilus wrapper.' % prior_type) # loop through prior pool = choose_pool(mpi=mpi, processes=thread_count, use_dill=True) - sampler = Sampler(prior, likelihood=self.likelihood, pool=pool, **kwargs_nautilus) + sampler = Sampler(prior, likelihood=self.likelihood, pool=pool, pass_struct=False, **kwargs_nautilus) time_start = time.time() if one_step is True: sampler.add_bound() diff --git a/requirements.txt b/requirements.txt index 883bb3ed3..4efbf8b51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ pyyaml pyxdg h5py zeus-mcmc -nautilus-sampler==0.1.0 +nautilus-sampler>=0.2.0 schwimmbad diff --git a/setup.py b/setup.py index e547a199c..07d354fde 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def run_tests(self): ] tests_require = ['pytest>=2.3', "mock", 'colossus==1.3.0', 'slitronomy==0.3.2', 'emcee>=3.0.0', 'dynesty', 'nestcheck', 'pymultinest', 'zeus-mcmc>=2.4.0', - 'nautilus-sampler==0.1.0', + 'nautilus-sampler>=0.2.0', ] PACKAGE_PATH = os.path.abspath(os.path.join(__file__, os.pardir)) From 997382ee4b32f4e64fe138fe7fb3e8690cfa52d8 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sun, 21 Aug 2022 19:21:53 -0700 Subject: [PATCH 16/67] changed ndimage.interpolation.shift with ndimage.shift --- lenstronomy/Util/image_util.py | 3 +-- lenstronomy/Util/kernel_util.py | 7 ++++--- lenstronomy/Workflow/psf_fitting.py | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lenstronomy/Util/image_util.py b/lenstronomy/Util/image_util.py index b990fdddd..981e0cd3e 100644 --- a/lenstronomy/Util/image_util.py +++ b/lenstronomy/Util/image_util.py @@ -3,7 +3,6 @@ import numpy as np from scipy import ndimage from scipy import interpolate -from scipy.ndimage import interpolation as interp import copy import lenstronomy.Util.util as util @@ -28,7 +27,7 @@ def add_layer2image(grid2d, x_pos, y_pos, kernel, order=1): y_int = int(round(y_pos)) shift_x = x_int - x_pos shift_y = y_int - y_pos - kernel_shifted = interp.shift(kernel, shift=[-shift_y, -shift_x], order=order) + kernel_shifted = ndimage.shift(kernel, shift=[-shift_y, -shift_x], order=order) return add_layer2image_int(grid2d, x_int, y_int, kernel_shifted) diff --git a/lenstronomy/Util/kernel_util.py b/lenstronomy/Util/kernel_util.py index 2595f2355..fb8f91422 100644 --- a/lenstronomy/Util/kernel_util.py +++ b/lenstronomy/Util/kernel_util.py @@ -4,6 +4,7 @@ import numpy as np import copy import scipy.ndimage.interpolation as interp +from scipy import ndimage import lenstronomy.Util.util as util import lenstronomy.Util.image_util as image_util from lenstronomy.LightModel.Profiles.gaussian import Gaussian @@ -37,11 +38,11 @@ def de_shift_kernel(kernel, shift_x, shift_y, iterations=20, fractional_step_siz int_shift_y = int(round(shift_y)) frac_y_shift = shift_y - int_shift_y kernel_init = copy.deepcopy(kernel_new) - kernel_init_shifted = copy.deepcopy(interp.shift(kernel_init, shift=[int_shift_y, int_shift_x], order=1)) - kernel_new = interp.shift(kernel_new, shift=[int_shift_y, int_shift_x], order=1) + kernel_init_shifted = copy.deepcopy(ndimage.shift(kernel_init, shift=[int_shift_y, int_shift_x], order=1)) + kernel_new = ndimage.shift(kernel_new, shift=[int_shift_y, int_shift_x], order=1) norm = np.sum(kernel_init_shifted) for i in range(iterations): - kernel_shifted_inv = interp.shift(kernel_new, shift=[-frac_y_shift, -frac_x_shift], order=1) + kernel_shifted_inv = ndimage.shift(kernel_new, shift=[-frac_y_shift, -frac_x_shift], order=1) delta = kernel_init_shifted - kernel_norm(kernel_shifted_inv) * norm kernel_new += delta * fractional_step_size kernel_new = kernel_norm(kernel_new) * norm diff --git a/lenstronomy/Workflow/psf_fitting.py b/lenstronomy/Workflow/psf_fitting.py index f89e39bf0..af55b8b85 100644 --- a/lenstronomy/Workflow/psf_fitting.py +++ b/lenstronomy/Workflow/psf_fitting.py @@ -7,6 +7,7 @@ import numpy as np import copy import scipy.ndimage.interpolation as interp +from scipy import ndimage __all__ = ['PsfFitting'] @@ -315,11 +316,11 @@ def psf_estimate_individual(self, ra_image, dec_image, point_amp, residuals, cut shift_y = (y_int - y_[l]) * supersampling_factor # for odd number super-sampling if supersampling_factor % 2 == 1: - residuals_shifted = interp.shift(residual_cutout_mask, shift=[shift_y, shift_x], order=1) + residuals_shifted = ndimage.shift(residual_cutout_mask, shift=[shift_y, shift_x], order=1) else: # for even number super-sampling half a super-sampled pixel offset needs to be performed - residuals_shifted = interp.shift(residual_cutout_mask, shift=[shift_y - 0.5, shift_x - 0.5], order=1) + residuals_shifted = ndimage.shift(residual_cutout_mask, shift=[shift_y - 0.5, shift_x - 0.5], order=1) # and the last column and row need to be removed residuals_shifted = residuals_shifted[:-1, :-1] @@ -379,7 +380,7 @@ def cutout_psf_single(x, y, image, mask, kernel_size, kernel_init): # shift the initial kernel to the shift of the star shift_x = x_int - x shift_y = y_int - y - kernel_shifted = interp.shift(kernel_enlarged, shift=[-shift_y, -shift_x], order=1) + kernel_shifted = ndimage.shift(kernel_enlarged, shift=[-shift_y, -shift_x], order=1) # compute normalization of masked and unmasked region of the shifted kernel # norm_masked = np.sum(kernel_shifted[mask_i == 0]) norm_unmasked = np.sum(kernel_shifted[mask_cutout == 1]) @@ -515,7 +516,7 @@ def error_map_estimate(self, kernel, star_cutout_list, amp, x_pos, y_pos, error_ y_int = int(round(y)) shift_x = x_int - x shift_y = y_int - y - kernel_shifted = interp.shift(kernel, shift=[-shift_y, -shift_x], order=1) + kernel_shifted = ndimage.shift(kernel, shift=[-shift_y, -shift_x], order=1) # multiply kernel with amplitude model = kernel_shifted * amp_i # compute residuals From 3386d5e8169bc76ddc8db44af6a7c9c777108478 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sun, 21 Aug 2022 22:31:23 -0700 Subject: [PATCH 17/67] remove deprecation warnings --- lenstronomy/ImSim/Numerics/convolution.py | 6 +++--- lenstronomy/LightModel/Profiles/starlets_util.py | 16 ++++++++-------- lenstronomy/Sampling/Samplers/nautilus.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lenstronomy/ImSim/Numerics/convolution.py b/lenstronomy/ImSim/Numerics/convolution.py index 90c057145..29a9c9987 100644 --- a/lenstronomy/ImSim/Numerics/convolution.py +++ b/lenstronomy/ImSim/Numerics/convolution.py @@ -152,7 +152,7 @@ def _static_pre_compute(self, image): # in1, s1, in2, s2 = in2, s2, in1, s1 # Speed up FFT by padding to optimal size for FFTPACK - fshape = [fftpack.helper.next_fast_len(int(d)) for d in shape] + fshape = [fftpack.next_fast_len(int(d)) for d in shape] fslice = tuple([slice(0, int(sz)) for sz in shape]) # Pre-1.9 NumPy FFT routines are not threadsafe. For older NumPys, make # sure we only call rfftn/irfftn from one thread at a time. @@ -276,10 +276,10 @@ def convolution2d(self, image): image_conv = None for i in range(self._num_gaussians): if image_conv is None: - image_conv = ndimage.filters.gaussian_filter(image, self._sigmas_scaled[i], mode='nearest', + image_conv = ndimage.gaussian_filter(image, self._sigmas_scaled[i], mode='nearest', truncate=self._truncation) * self._fraction_list[i] else: - image_conv += ndimage.filters.gaussian_filter(image, self._sigmas_scaled[i], mode='nearest', + image_conv += ndimage.gaussian_filter(image, self._sigmas_scaled[i], mode='nearest', truncate=self._truncation) * self._fraction_list[i] return image_conv diff --git a/lenstronomy/LightModel/Profiles/starlets_util.py b/lenstronomy/LightModel/Profiles/starlets_util.py index 0731886e8..89c647cce 100644 --- a/lenstronomy/LightModel/Profiles/starlets_util.py +++ b/lenstronomy/LightModel/Profiles/starlets_util.py @@ -1,7 +1,7 @@ -__author__ = 'herjy', 'aymgal' +__author__ = 'herjy', 'aymgal', 'sibirrer' import numpy as np -import scipy.ndimage.filters as scf +from scipy import ndimage from lenstronomy.Util.package_util import exporter export, __all__ = exporter() @@ -47,17 +47,17 @@ def transform(img, n_scales, second_gen=False): ######Calculates c(j+1) ###### Line convolution - cnew = scf.convolve1d(c, newh[0, :], axis=0, mode=mode) + cnew = ndimage.convolve1d(c, newh[0, :], axis=0, mode=mode) ###### Column convolution - cnew = scf.convolve1d(cnew, newh[0, :], axis=1, mode=mode) + cnew = ndimage.convolve1d(cnew, newh[0, :], axis=1, mode=mode) if second_gen: ###### hoh for g; Column convolution - hc = scf.convolve1d(cnew, newh[0, :], axis=0, mode=mode) + hc = ndimage.convolve1d(cnew, newh[0, :], axis=0, mode=mode) ###### hoh for g; Line convolution - hc = scf.convolve1d(hc, newh[0, :], axis=1, mode=mode) + hc = ndimage.convolve1d(hc, newh[0, :], axis=1, mode=mode) ###### wj+1 = cj - hcj+1 wave[i, :, :] = c - hc @@ -101,9 +101,9 @@ def inverse_transform(wave, fast=True, second_gen=False): H = np.dot(newh.T, newh) ###### Line convolution - cnew = scf.convolve1d(cJ, newh[0, :], axis=0, mode=mode) + cnew = ndimage.convolve1d(cJ, newh[0, :], axis=0, mode=mode) ###### Column convolution - cnew = scf.convolve1d(cnew, newh[0, :], axis=1, mode=mode) + cnew = ndimage.convolve1d(cnew, newh[0, :], axis=1, mode=mode) cJ = cnew + wave[lvl-1-i, :, :] diff --git a/lenstronomy/Sampling/Samplers/nautilus.py b/lenstronomy/Sampling/Samplers/nautilus.py index 5bedff066..5dd15e3dc 100644 --- a/lenstronomy/Sampling/Samplers/nautilus.py +++ b/lenstronomy/Sampling/Samplers/nautilus.py @@ -44,7 +44,7 @@ def nautilus_sampling(self, prior_type='uniform', mpi=False, thread_count=1, ver if prior_type == 'uniform': for i in range(self._num_param): prior.add_parameter(dist=(self._lower_limit[i], self._upper_limit[i])) - print(self._num_param, prior, 'test') + print(self._num_param, prior.dimensionality(), 'number of param, dimensionality') else: raise ValueError('prior_type %s is not supported for Nautilus wrapper.' % prior_type) # loop through prior From e5fb66b1ed79e5dbf9e432753f5d35936c23fea0 Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Mon, 27 Jun 2022 18:25:13 -0700 Subject: [PATCH 18/67] Refactor how parameters are handled in PointSourceParam and SpecialParam --- lenstronomy/PointSource/point_source_param.py | 194 +++++------- lenstronomy/Sampling/param_group.py | 151 ++++++++++ lenstronomy/Sampling/special_param.py | 284 +++++++----------- test/test_Sampling/test_parameters.py | 2 +- 4 files changed, 338 insertions(+), 293 deletions(-) create mode 100644 lenstronomy/Sampling/param_group.py diff --git a/lenstronomy/PointSource/point_source_param.py b/lenstronomy/PointSource/point_source_param.py index d5ffd4b1d..1af9119e6 100644 --- a/lenstronomy/PointSource/point_source_param.py +++ b/lenstronomy/PointSource/point_source_param.py @@ -2,6 +2,36 @@ __all__ = ['PointSourceParam'] +from ..Sampling.param_group import ModelParamGroup, SingleParam, ArrayParam + + +class SourcePositionParam(SingleParam): + param_names = ['ra_source', 'dec_source'] + _kwargs_lower = {'ra_source': -100, 'dec_source': -100} + _kwargs_upper = {'ra_source': 100, 'dec_source': 100} + + +class LensedPosition(ArrayParam): + _kwargs_lower = {'ra_image': -100, 'dec_image': -100, } + _kwargs_upper = {'ra_image': 100, 'dec_image': 100, } + def __init__(self, num_images): + self.on = int(num_images) > 0 + self.param_names = {'ra_image': int(num_images), 'dec_image': int(num_images)} + + +class SourceAmp(SingleParam): + param_names = ['source_amp'] + _kwargs_lower = {'source_amp': 0} + _kwargs_upper = {'source_amp': 100} + + +class PointAmp(ArrayParam): + _kwargs_lower = {'point_amp': 0} + _kwargs_upper = {'point_amp': 100} + def __init__(self, fixed_magnification): + self.on = int(fixed_magnification) > 0 + self.param_names = {'point_amp': int(fixed_magnification)} + class PointSourceParam(object): """ @@ -32,36 +62,40 @@ def __init__(self, model_list, kwargs_fixed, num_point_source_list=None, linear_ self.kwargs_fixed = self.add_fix_linear(kwargs_fixed) self._linear_solver = linear_solver + self.param_groups = [] + for i, model in enumerate(self.model_list): + params = [] + num = num_point_source_list[i] + if model in ['LENSED_POSITION', 'UNLENSED']: + params.append(LensedPosition(num)) + elif model == 'SOURCE_POSITION': + params.append(SourcePositionParam(True)) + else: + raise ValueError("%s not a valid point source model" % model) + + if fixed_magnification_list[i] and model in ['LENSED_POSITION', 'SOURCE_POSITION']: + params.append(SourceAmp(True)) + else: + params.append(PointAmp(num)) + + self.param_groups.append(params) + if kwargs_lower is None: kwargs_lower = [] - for k, model in enumerate(self.model_list): - num = self._num_point_sources_list[k] - if model in ['LENSED_POSITION', 'UNLENSED']: - fixed_low = {'ra_image': [-100] * num, 'dec_image': [-100] * num} - elif model in ['SOURCE_POSITION']: - fixed_low = {'ra_source': -100, 'dec_source': -100} - else: - raise ValueError("%s not a valid point source model" % model) - if self._fixed_magnification_list[k] is True and model in ['LENSED_POSITION', 'SOURCE_POSITION']: - fixed_low['source_amp'] = 0 - else: - fixed_low['point_amp'] = np.zeros(num) - kwargs_lower.append(fixed_low) + for model_params in self.param_groups: + fixed_lower = {} + for grp in model_params: + fixed_lower = dict(fixed_lower, **grp.kwargs_lower) + kwargs_lower.append(fixed_lower) + if kwargs_upper is None: kwargs_upper = [] - for k, model in enumerate(self.model_list): - num = self._num_point_sources_list[k] - if model in ['LENSED_POSITION', 'UNLENSED']: - fixed_high = {'ra_image': [100] * num, 'dec_image': [100] * num} - elif model in ['SOURCE_POSITION']: - fixed_high = {'ra_source': 100, 'dec_source': 100} - else: - raise ValueError("%s not a valid point source model" % model) - if self._fixed_magnification_list[k] is True and model in ['LENSED_POSITION', 'SOURCE_POSITION']: - fixed_high['source_amp'] = 100 - else: - fixed_high['point_amp'] = np.ones(num)*100 - kwargs_upper.append(fixed_high) + for model_params in self.param_groups: + fixed_upper = {} + for grp in model_params: + fixed_upper = dict(fixed_upper, **grp.kwargs_upper) + kwargs_upper.append(fixed_upper) + self.lower_limit = kwargs_lower self.upper_limit = kwargs_upper @@ -73,45 +107,10 @@ def get_params(self, args, i): :return: keyword argument list of point sources, index relevant for the next class """ kwargs_list = [] - for k, model in enumerate(self.model_list): - kwargs = {} - kwargs_fixed = self.kwargs_fixed[k] - if model in ['LENSED_POSITION', 'UNLENSED']: - if 'ra_image' not in kwargs_fixed: - kwargs['ra_image'] = np.array(args[i:i + self._num_point_sources_list[k]]) - i += self._num_point_sources_list[k] - else: - kwargs['ra_image'] = kwargs_fixed['ra_image'] - if 'dec_image' not in kwargs_fixed: - kwargs['dec_image'] = np.array(args[i:i + self._num_point_sources_list[k]]) - i += self._num_point_sources_list[k] - else: - kwargs['dec_image'] = kwargs_fixed['dec_image'] - if model in ['SOURCE_POSITION']: - if 'ra_source' not in kwargs_fixed: - kwargs['ra_source'] = args[i] - i += 1 - else: - kwargs['ra_source'] = kwargs_fixed['ra_source'] - if 'dec_source' not in kwargs_fixed: - kwargs['dec_source'] = args[i] - i += 1 - else: - kwargs['dec_source'] = kwargs_fixed['dec_source'] - # amplitude parameter handling - if self._fixed_magnification_list[k] is True and model in ['LENSED_POSITION', 'SOURCE_POSITION']: - if 'source_amp' not in kwargs_fixed: - kwargs['source_amp'] = args[i] - i += 1 - else: - kwargs['source_amp'] = kwargs_fixed['source_amp'] - else: - if 'point_amp' not in kwargs_fixed: - kwargs['point_amp'] = np.array(args[i:i + self._num_point_sources_list[k]]) - i += self._num_point_sources_list[k] - else: - kwargs['point_amp'] = kwargs_fixed['point_amp'] - + for k, param_group in enumerate(self.param_groups): + kwargs, i = ModelParamGroup.compose_get_params( + param_group, args, i, kwargs_fixed=self.kwargs_fixed[k] + ) kwargs_list.append(kwargs) return kwargs_list, i @@ -122,32 +121,12 @@ def set_params(self, kwargs_list): :return: sorted list of parameters being sampled extracted from kwargs_list """ args = [] - for k, model in enumerate(self.model_list): + for k, param_group in enumerate(self.param_groups): kwargs = kwargs_list[k] kwargs_fixed = self.kwargs_fixed[k] - if model in ['LENSED_POSITION', 'UNLENSED']: - if 'ra_image' not in kwargs_fixed: - x_pos = kwargs['ra_image'][0:self._num_point_sources_list[k]] - for x in x_pos: - args.append(x) - if 'dec_image' not in kwargs_fixed: - y_pos = kwargs['dec_image'][0:self._num_point_sources_list[k]] - for y in y_pos: - args.append(y) - if model in ['SOURCE_POSITION']: - if 'ra_source' not in kwargs_fixed: - args.append(kwargs['ra_source']) - if 'dec_source' not in kwargs_fixed: - args.append(kwargs['dec_source']) - # amplitude parameter handling - if self._fixed_magnification_list[k] is True and model in ['LENSED_POSITION', 'SOURCE_POSITION']: - if 'source_amp' not in kwargs_fixed: - args.append(kwargs['source_amp']) - else: - if 'point_amp' not in kwargs_fixed: - amp = kwargs['point_amp'][0:self._num_point_sources_list[k]] - for a in amp: - args.append(a) + args.extend(ModelParamGroup.compose_set_params( + param_group, kwargs, kwargs_fixed=kwargs_fixed + )) return args def num_param(self): @@ -156,36 +135,13 @@ def num_param(self): :return: int, list of parameter names """ - num = 0 - name_list = [] - for k, model in enumerate(self.model_list): - kwargs_fixed = self.kwargs_fixed[k] - if model in ['LENSED_POSITION', 'UNLENSED']: - if 'ra_image' not in kwargs_fixed: - num += self._num_point_sources_list[k] - for i in range(self._num_point_sources_list[k]): - name_list.append('ra_image') - if 'dec_image' not in kwargs_fixed: - num += self._num_point_sources_list[k] - for i in range(self._num_point_sources_list[k]): - name_list.append('dec_image') - if model in ['SOURCE_POSITION']: - if 'ra_source' not in kwargs_fixed: - num += 1 - name_list.append('ra_source') - if 'dec_source' not in kwargs_fixed: - num += 1 - name_list.append('dec_source') - # amplitude handling - if self._fixed_magnification_list[k] is True and model in ['LENSED_POSITION', 'SOURCE_POSITION']: - if 'source_amp' not in kwargs_fixed: - num += 1 - name_list.append('source_amp') - else: - if 'point_amp' not in kwargs_fixed: - num += self._num_point_sources_list[k] - for i in range(self._num_point_sources_list[k]): - name_list.append('point_amp') + num, name_list = 0, [] + for k, param_group in enumerate(self.param_groups): + n, names = ModelParamGroup.compose_num_params( + param_group, kwargs_fixed=self.kwargs_fixed[k] + ) + num += n + name_list += names return num, name_list def add_fix_linear(self, kwargs_fixed): diff --git a/lenstronomy/Sampling/param_group.py b/lenstronomy/Sampling/param_group.py new file mode 100644 index 000000000..3142341c9 --- /dev/null +++ b/lenstronomy/Sampling/param_group.py @@ -0,0 +1,151 @@ + + + +class ModelParamGroup: + def num_params(self): + raise NotImplementedError + + def set_params(self, kwargs): + raise NotImplementedError + + def get_params(self, args, i): + raise NotImplementedError + + @staticmethod + def compose_num_params(each_group, *args, **kwargs): + tot_param = 0 + param_names = [] + for group in each_group: + npar, names = group.num_params(*args, **kwargs) + tot_param += npar + param_names += names + return tot_param, param_names + + @staticmethod + def compose_set_params(each_group, param_kwargs, *args, **kwargs): + output_args = [] + for group in each_group: + output_args += group.set_params(param_kwargs, *args, **kwargs) + return output_args + + @staticmethod + def compose_get_params(each_group, flat_args, i, *args, **kwargs): + output_kwargs = {} + for group in each_group: + kwargs_grp, i = group.get_params(flat_args, i, *args, **kwargs) + output_kwargs = dict(output_kwargs, **kwargs_grp) + return output_kwargs, i + +class SingleParam(ModelParamGroup): + ''' + Helper for handling parameters in the SpecialGroup. + + Internal use, please ignore. Check below for the actual definitions of special parameters. + ''' + def __init__(self, on): + self.on = bool(on) + + def num_params(self, kwargs_fixed): + if self.on: + npar, names = 0, [] + for name in self.param_names: + if name not in kwargs_fixed: + npar += 1 + names.append(name) + return npar, names + return 0, [] + + def set_params(self, kwargs, kwargs_fixed): + if self.on: + output = [] + for name in self.param_names: + if name not in kwargs_fixed: + output.append(kwargs[name]) + return output + return [] + + def get_params(self, args, i, kwargs_fixed): + out = {} + if self.on: + for name in self.param_names: + if name in kwargs_fixed: + out[name] = kwargs_fixed[name] + else: + out[name] = args[i] + i += 1 + return out, i + + @property + def kwargs_lower(self): + if not self.on: + return {} + return self._kwargs_lower + + @property + def kwargs_upper(self): + if not self.on: + return {} + return self._kwargs_upper + +class ArrayParam(ModelParamGroup): + ''' + Helper for handling parameters in the SpecialGroup. + + Internal use, please ignore. Check below for the actual definitions of special parameters. + ''' + def num_params(self, kwargs_fixed): + if not self.on: + return 0, [] + + npar = 0 + names = [] + for name, count in self.param_names.items(): + if name not in kwargs_fixed: + npar += count + names += [name] * count + + return npar, names + + def set_params(self, kwargs, kwargs_fixed): + if not self.on: + return [] + + args = [] + for name, count in self.param_names.items(): + if name not in kwargs_fixed: + args.extend(kwargs[name]) + return args + + def get_params(self, args, i, kwargs_fixed): + if not self.on: + return {}, i + + params = {} + for name, count in self.param_names.items(): + if name not in kwargs_fixed: + params[name] = args[i:i + count] + i += count + else: + params[name] = kwargs_fixed[name] + + return params, i + + @property + def kwargs_lower(self): + if not self.on: + return {} + + out = {} + for name, count in self.param_names.items(): + out[name] = [self._kwargs_lower[name]] * count + return out + + @property + def kwargs_upper(self): + if not self.on: + return {} + + out = {} + for name, count in self.param_names.items(): + out[name] = [self._kwargs_upper[name]] * count + return out diff --git a/lenstronomy/Sampling/special_param.py b/lenstronomy/Sampling/special_param.py index 95d5d355e..4020bd3a3 100644 --- a/lenstronomy/Sampling/special_param.py +++ b/lenstronomy/Sampling/special_param.py @@ -2,6 +2,80 @@ __all__ = ['SpecialParam'] +from .param_group import ModelParamGroup, SingleParam, ArrayParam + + +# ==================================== # +# == Defining individual parameters == # +# ==================================== # + + +class DdtSamplingParam(SingleParam): + param_names = ['D_dt'] + _kwargs_lower = {'D_dt': 0} + _kwargs_upper = {'D_dt': 100000} + + +class SourceSizeParam(SingleParam): + param_names = ['source_size'] + _kwargs_lower = {'source_size': 0} + _kwargs_upper = {'source_size': 1} + + +class SourceGridOffsetParam(SingleParam): + param_names = ['delta_x_source_grid', 'delta_y_source_grid'] + _kwargs_lower = { + 'delta_x_source_grid': -100, + 'delta_y_source_grid': -100 + } + _kwargs_upper = { + 'delta_x_source_grid': 100, + 'delta_y_source_grid': 100 + } + + +class MassScalingParam(ArrayParam): + _kwargs_lower = {'scale_factor': 0} + _kwargs_upper = {'scale_factor': 1000} + def __init__(self, num_scale_factor): + self.on = int(num_scale_factor) > 0 + self.param_names = {'scale_factor': int(num_scale_factor)} + + +class PointSourceOffsetParam(ArrayParam): + _kwargs_lower = {'delta_x_image': -1, 'delta_y_image': -1} + _kwargs_upper = {'delta_x_image': 1, 'delta_y_image': 1} + def __init__(self, offset, num_images): + self.on = offset and (int(num_images) > 0) + self.param_names = { + 'delta_x_image': int(num_images), + 'delta_y_image': int(num_images), + } + + +class Tau0ListParam(ArrayParam): + _kwargs_lower = {'tau0_list': 0} + _kwargs_upper = {'tau0_list': 1000} + def __init__(self, num_tau0): + self.on = int(num_tau0) > 0 + self.param_names = {'tau0_list': int(num_tau0)} + + +class ZSamplingParam(ArrayParam): + _kwargs_lower = {'z_sampling': 0} + _kwargs_upper = {'z_sampling': 1000} + def __init__(self, num_z_sampling): + self.on = int(num_z_sampling) > 0 + self.param_names = {'z_sampling': int(num_z_sampling)} + + + + +# ======================================== # +# == All together: Composing into class == # +# ======================================== # + + class SpecialParam(object): """ @@ -32,59 +106,31 @@ def __init__(self, Ddt_sampling=False, mass_scaling=False, num_scale_factor=1, k Warning: this is only defined for pixel-based source modelling (e.g. 'SLIT_STARLETS' light profile) """ - self._D_dt_sampling = Ddt_sampling - self._mass_scaling = mass_scaling - self._num_scale_factor = num_scale_factor - self._point_source_offset = point_source_offset - self._num_images = num_images - self._num_tau0 = num_tau0 - self._num_z_sampling = num_z_sampling - if num_z_sampling > 0: - self._z_sampling = True - else: - self._z_sampling = False + self._D_dt_sampling = DdtSamplingParam(Ddt_sampling) + # FIXME mass_scaling argument now unused + if not mass_scaling: + num_scale_factor = 0 + self._mass_scaling = MassScalingParam(num_scale_factor) + # FIXME point_source_offset argument now unused + self._point_source_offset = PointSourceOffsetParam(point_source_offset, num_images) + self._source_size = SourceSizeParam(source_size) + self._tau0 = Tau0ListParam(num_tau0) + self._z_sampling = ZSamplingParam(num_z_sampling) + self._source_grid_offset = SourceGridOffsetParam(source_grid_offset) if kwargs_fixed is None: kwargs_fixed = {} self._kwargs_fixed = kwargs_fixed - self._source_size = source_size - self._source_grid_offset = source_grid_offset + if kwargs_lower is None: kwargs_lower = {} - if self._D_dt_sampling is True: - kwargs_lower['D_dt'] = 0 - if self._mass_scaling is True: - kwargs_lower['scale_factor'] = [0] * self._num_scale_factor - if self._point_source_offset is True: - kwargs_lower['delta_x_image'] = [-1] * self._num_images - kwargs_lower['delta_y_image'] = [-1] * self._num_images - if self._source_size is True: - kwargs_lower['source_size'] = 0 - if self._num_tau0 > 0: - kwargs_lower['tau0_list'] = [0] * self._num_tau0 - if self._z_sampling is True: - kwargs_lower['z_sampling'] = [0] * self._num_z_sampling - if self._source_grid_offset: - kwargs_lower['delta_x_source_grid'] = -100 - kwargs_lower['delta_y_source_grid'] = -100 + for group in self._param_groups: + kwargs_lower = dict(kwargs_lower, **group.kwargs_lower) if kwargs_upper is None: kwargs_upper = {} - if self._D_dt_sampling is True: - kwargs_upper['D_dt'] = 100000 - if self._mass_scaling is True: - kwargs_upper['scale_factor'] = [1000] * self._num_scale_factor - if self._point_source_offset is True: - kwargs_upper['delta_x_image'] = [1] * self._num_images - kwargs_upper['delta_y_image'] = [1] * self._num_images - if self._source_size is True: - kwargs_upper[source_size] = 1 - if self._num_tau0 > 0: - kwargs_upper['tau0_list'] = [1000] * self._num_tau0 - if self._z_sampling is True: - kwargs_upper['z_sampling'] = [20] * self._num_z_sampling - if self._source_grid_offset: - kwargs_upper['delta_x_source_grid'] = 100 - kwargs_upper['delta_y_source_grid'] = 100 + for group in self._param_groups: + kwargs_upper = dict(kwargs_upper, **group.kwargs_upper) + self.lower_limit = kwargs_lower self.upper_limit = kwargs_upper @@ -95,60 +141,10 @@ def get_params(self, args, i): :param i: integer, list index to start the read out for this class :return: keyword arguments related to args, index after reading out arguments of this class """ - kwargs_special = {} - if self._D_dt_sampling is True: - if 'D_dt' not in self._kwargs_fixed: - kwargs_special['D_dt'] = args[i] - i += 1 - else: - kwargs_special['D_dt'] = self._kwargs_fixed['D_dt'] - if self._mass_scaling is True: - if 'scale_factor' not in self._kwargs_fixed: - kwargs_special['scale_factor'] = args[i: i + self._num_scale_factor] - i += self._num_scale_factor - else: - kwargs_special['scale_factor'] = self._kwargs_fixed['scale_factor'] - if self._point_source_offset is True: - if 'delta_x_image' not in self._kwargs_fixed: - kwargs_special['delta_x_image'] = args[i: i + self._num_images] - i += self._num_images - else: - kwargs_special['delta_x_image'] = self._kwargs_fixed['delta_x_image'] - if 'delta_y_image' not in self._kwargs_fixed: - kwargs_special['delta_y_image'] = args[i: i + self._num_images] - i += self._num_images - else: - kwargs_special['delta_y_image'] = self._kwargs_fixed['delta_y_image'] - if self._source_size is True: - if 'source_size' not in self._kwargs_fixed: - kwargs_special['source_size'] = args[i] - i += 1 - else: - kwargs_special['source_size'] = self._kwargs_fixed['source_size'] - if self._num_tau0 > 0: - if 'tau0_list' not in self._kwargs_fixed: - kwargs_special['tau0_list'] = args[i:i + self._num_tau0] - i += self._num_tau0 - else: - kwargs_special['tau0_list'] = self._kwargs_fixed['tau0_list'] - if self._z_sampling is True: - if 'z_sampling' not in self._kwargs_fixed: - kwargs_special['z_sampling'] = args[i:i + self._num_z_sampling] - i += self._num_z_sampling - else: - kwargs_special['z_sampling'] = self._kwargs_fixed['z_sampling'] - if self._source_grid_offset: - if 'delta_x_source_grid' not in self._kwargs_fixed: - kwargs_special['delta_x_source_grid'] = args[i] - i += 1 - else: - kwargs_special['delta_x_source_grid'] = self._kwargs_fixed['delta_x_source_grid'] - if 'delta_y_source_grid' not in self._kwargs_fixed: - kwargs_special['delta_y_source_grid'] = args[i] - i += 1 - else: - kwargs_special['delta_y_source_grid'] = self._kwargs_fixed['delta_y_source_grid'] - return kwargs_special, i + result = ModelParamGroup.compose_get_params( + self._param_groups, args, i, kwargs_fixed=self._kwargs_fixed + ) + return result def set_params(self, kwargs_special): """ @@ -156,83 +152,25 @@ def set_params(self, kwargs_special): :param kwargs_special: keyword arguments with parameter settings :return: argument list of the sampled parameters extracted from kwargs_special """ - args = [] - if self._D_dt_sampling is True: - if 'D_dt' not in self._kwargs_fixed: - args.append(kwargs_special['D_dt']) - if self._mass_scaling is True: - if 'scale_factor' not in self._kwargs_fixed: - for i in range(self._num_scale_factor): - args.append(kwargs_special['scale_factor'][i]) - if self._point_source_offset is True: - if 'delta_x_image' not in self._kwargs_fixed: - for i in range(self._num_images): - args.append(kwargs_special['delta_x_image'][i]) - if 'delta_y_image' not in self._kwargs_fixed: - for i in range(self._num_images): - args.append(kwargs_special['delta_y_image'][i]) - if self._source_size is True: - if 'source_size' not in self._kwargs_fixed: - args.append(kwargs_special['source_size']) - if self._num_tau0 > 0: - if 'tau0_list' not in self._kwargs_fixed: - for i in range(self._num_tau0): - args.append(kwargs_special['tau0_list'][i]) - if self._z_sampling is True: - if 'z_sampling' not in self._kwargs_fixed: - for i in range(self._num_z_sampling): - args.append(kwargs_special['z_sampling'][i]) - if self._source_grid_offset is True: - if 'delta_x_source_grid' not in self._kwargs_fixed: - args.append(kwargs_special['delta_x_source_grid']) - if 'delta_y_source_grid' not in self._kwargs_fixed: - args.append(kwargs_special['delta_y_source_grid']) - return args + return ModelParamGroup.compose_set_params( + self._param_groups, kwargs_special, kwargs_fixed=self._kwargs_fixed + ) def num_param(self): """ :return: integer, number of free parameters sampled (and managed) by this class, parameter names (list of strings) """ - num = 0 - string_list = [] - if self._D_dt_sampling is True: - if 'D_dt' not in self._kwargs_fixed: - num += 1 - string_list.append('D_dt') - if self._mass_scaling is True: - if 'scale_factor' not in self._kwargs_fixed: - num += self._num_scale_factor - for i in range(self._num_scale_factor): - string_list.append('scale_factor') - if self._point_source_offset is True: - if 'delta_x_image' not in self._kwargs_fixed: - num += self._num_images - for i in range(self._num_images): - string_list.append('delta_x_image') - if 'delta_y_image' not in self._kwargs_fixed: - num += self._num_images - for i in range(self._num_images): - string_list.append('delta_y_image') - if self._source_size is True: - if 'source_size' not in self._kwargs_fixed: - num += 1 - string_list.append('source_size') - if self._num_tau0 > 0: - if 'tau0_list' not in self._kwargs_fixed: - num += self._num_tau0 - for i in range(self._num_tau0): - string_list.append('tau0') - if self._z_sampling is True: - if 'z_sampling' not in self._kwargs_fixed: - num += self._num_z_sampling - for i in range(self._num_z_sampling): - string_list.append('z') - if self._source_grid_offset is True: - if 'delta_x_source_grid' not in self._kwargs_fixed: - num += 1 - string_list.append('delta_x_source_grid') - if 'delta_y_source_grid' not in self._kwargs_fixed: - num += 1 - string_list.append('delta_y_source_grid') - return num, string_list + return ModelParamGroup.compose_num_params( + self._param_groups, kwargs_fixed=self._kwargs_fixed + ) + + @property + def _param_groups(self): + return [self._D_dt_sampling, + self._mass_scaling, + self._point_source_offset, + self._source_size, + self._tau0, + self._z_sampling, + self._source_grid_offset] diff --git a/test/test_Sampling/test_parameters.py b/test/test_Sampling/test_parameters.py index 107136856..c18b4d9a4 100644 --- a/test/test_Sampling/test_parameters.py +++ b/test/test_Sampling/test_parameters.py @@ -103,7 +103,7 @@ def test_get_cosmo(self): args = param_class.kwargs2args(kwargs_true_lens, kwargs_true_source, kwargs_lens_light=kwargs_true_lens_light, kwargs_ps=kwargs_true_ps, kwargs_special={'D_dt': 1000}) - assert param_class.specialParams._D_dt_sampling is True + assert param_class.specialParams._D_dt_sampling.on def test_mass_scaling(self): kwargs_model = {'lens_model_list': ['SIS', 'NFW', 'NFW', 'SIS', 'SERSIC', 'HERNQUIST']} From f5ce2a6be010baa1b707f693528d83f1d82d9c7f Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Thu, 18 Aug 2022 14:32:53 -0700 Subject: [PATCH 19/67] make old point source offset param used --- lenstronomy/Sampling/special_param.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lenstronomy/Sampling/special_param.py b/lenstronomy/Sampling/special_param.py index 4020bd3a3..935be4cc7 100644 --- a/lenstronomy/Sampling/special_param.py +++ b/lenstronomy/Sampling/special_param.py @@ -107,12 +107,13 @@ def __init__(self, Ddt_sampling=False, mass_scaling=False, num_scale_factor=1, k """ self._D_dt_sampling = DdtSamplingParam(Ddt_sampling) - # FIXME mass_scaling argument now unused if not mass_scaling: num_scale_factor = 0 self._mass_scaling = MassScalingParam(num_scale_factor) - # FIXME point_source_offset argument now unused - self._point_source_offset = PointSourceOffsetParam(point_source_offset, num_images) + if point_source_offset: + self._point_source_offset = PointSourceOffsetParam(True, num_images) + else: + self._point_source_offset = PointSourceOffsetParam(False, 0) self._source_size = SourceSizeParam(source_size) self._tau0 = Tau0ListParam(num_tau0) self._z_sampling = ZSamplingParam(num_z_sampling) From de4896944028e9a75d8417ae673c417553c51f7c Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Mon, 22 Aug 2022 12:28:17 -0700 Subject: [PATCH 20/67] general parameter scaling: implementation with added test passing --- lenstronomy/Sampling/parameters.py | 62 ++++++++++++++++++------- lenstronomy/Sampling/special_param.py | 37 ++++++++++++++- test/test_Sampling/test_parameters.py | 66 +++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 18 deletions(-) diff --git a/lenstronomy/Sampling/parameters.py b/lenstronomy/Sampling/parameters.py index cb860cc3e..c047b3571 100644 --- a/lenstronomy/Sampling/parameters.py +++ b/lenstronomy/Sampling/parameters.py @@ -77,6 +77,8 @@ def __init__(self, kwargs_model, joint_source_with_source=[], joint_lens_with_light=[], joint_source_with_point_source=[], joint_lens_light_with_point_source=[], joint_extinction_with_lens_light=[], joint_lens_with_source_light=[], mass_scaling_list=None, point_source_offset=False, + # General scaling: need names of params and number of individual params + general_scaling=None, num_point_source_list=None, image_plane_source_list=None, solver_type='NONE', Ddt_sampling=None, source_size=False, num_tau0=0, lens_redshift_sampling_indexes=None, source_redshift_sampling_indexes=None, source_grid_offset=False, num_shapelet_lens=0, @@ -211,6 +213,14 @@ def __init__(self, kwargs_model, else: self._num_scale_factor = 0 self._mass_scaling = False + + if general_scaling is not None: + self._general_scaling = True + self._general_scaling_masks = dict(general_scaling) + else: + self._general_scaling = False + self._general_scaling_masks = dict() + self._point_source_offset = point_source_offset if num_point_source_list is None: num_point_source_list = [1] * len(self._point_source_model_list) @@ -270,6 +280,7 @@ def __init__(self, kwargs_model, kwargs_lower=kwargs_lower_extinction, kwargs_upper=kwargs_upper_extinction, linear_solver=False) self.specialParams = SpecialParam(Ddt_sampling=Ddt_sampling, mass_scaling=self._mass_scaling, + general_scaling_params=self._general_scaling_masks, kwargs_fixed=kwargs_fixed_special, num_scale_factor=self._num_scale_factor, kwargs_lower=kwargs_lower_special, kwargs_upper=kwargs_upper_special, point_source_offset=self._point_source_offset, num_images=self._num_images, @@ -544,24 +555,41 @@ def update_lens_scaling(self, kwargs_special, kwargs_lens, inverse=False): :return: updated lens model keyword argument list """ kwargs_lens_updated = copy.deepcopy(kwargs_lens) - if self._mass_scaling is False: + # If we do not scaling, there's nothing to be done + if not (self._mass_scaling or self._general_scaling): return kwargs_lens_updated - scale_factor_list = np.array(kwargs_special['scale_factor']) - if inverse is True: - scale_factor_list = 1. / np.array(kwargs_special['scale_factor']) - for i, kwargs in enumerate(kwargs_lens_updated): - if self._mass_scaling_list[i] is not False: - scale_factor = scale_factor_list[self._mass_scaling_list[i] - 1] - if 'theta_E' in kwargs: - kwargs['theta_E'] *= scale_factor - elif 'alpha_Rs' in kwargs: - kwargs['alpha_Rs'] *= scale_factor - elif 'alpha_1' in kwargs: - kwargs['alpha_1'] *= scale_factor - elif 'sigma0' in kwargs: - kwargs['sigma0'] *= scale_factor - elif 'k_eff' in kwargs: - kwargs['k_eff'] *= scale_factor + + if self._mass_scaling: + scale_factor_list = np.array(kwargs_special['scale_factor']) + if inverse is True: + scale_factor_list = 1. / np.array(kwargs_special['scale_factor']) + for i, kwargs in enumerate(kwargs_lens_updated): + if self._mass_scaling_list[i] is not False: + scale_factor = scale_factor_list[self._mass_scaling_list[i] - 1] + if 'theta_E' in kwargs: + kwargs['theta_E'] *= scale_factor + elif 'alpha_Rs' in kwargs: + kwargs['alpha_Rs'] *= scale_factor + elif 'alpha_1' in kwargs: + kwargs['alpha_1'] *= scale_factor + elif 'sigma0' in kwargs: + kwargs['sigma0'] *= scale_factor + elif 'k_eff' in kwargs: + kwargs['k_eff'] *= scale_factor + + if self._general_scaling: + for param_name in self._general_scaling_masks.keys(): + factors = kwargs_special[f'{param_name}_scale_factor'] + _pows = kwargs_special[f'{param_name}_scale_pow'] + + for i, kwargs in enumerate(kwargs_lens_updated): + scale_idx = self._general_scaling_masks[param_name][i] + if scale_idx is not False: + if inverse: + kwargs[param_name] = (kwargs[param_name] / factors[scale_idx - 1]) ** (1 / _pows[scale_idx - 1]) + else: + kwargs[param_name] = factors[scale_idx - 1] * kwargs[param_name]**_pows[scale_idx - 1] + return kwargs_lens_updated def _add_fixed_lens(self, kwargs_fixed, kwargs_init): diff --git a/lenstronomy/Sampling/special_param.py b/lenstronomy/Sampling/special_param.py index 935be4cc7..84dc050ea 100644 --- a/lenstronomy/Sampling/special_param.py +++ b/lenstronomy/Sampling/special_param.py @@ -2,6 +2,7 @@ __all__ = ['SpecialParam'] +import numpy as np from .param_group import ModelParamGroup, SingleParam, ArrayParam @@ -69,6 +70,35 @@ def __init__(self, num_z_sampling): self.param_names = {'z_sampling': int(num_z_sampling)} +class GeneralScalingParam(ArrayParam): + # Mass scaling needs: + # - name of scaled param + # - number of scaled params + def __init__(self, params: dict): + # params is a dictionary + self.param_names = {} + self._kwargs_lower = {} + self._kwargs_upper = {} + + if params: + self.on = True + else: + self.on = False + return + + for name, array in params.items(): + num_param = np.max(array) + + if num_param > 0: + fac_name = f'{name}_scale_factor' + self.param_names[fac_name] = num_param + self._kwargs_lower[fac_name] = 0 + self._kwargs_upper[fac_name] = 1000 + + pow_name = f'{name}_scale_pow' + self.param_names[pow_name] = num_param + self._kwargs_lower[pow_name] = -10 + self._kwargs_upper[pow_name] = 10 # ======================================== # @@ -83,7 +113,8 @@ class that handles special parameters that are not directly part of a specific m These includes cosmology relevant parameters, astrometric errors and overall scaling parameters. """ - def __init__(self, Ddt_sampling=False, mass_scaling=False, num_scale_factor=1, kwargs_fixed=None, kwargs_lower=None, + def __init__(self, Ddt_sampling=False, mass_scaling=False, num_scale_factor=1, + general_scaling_params=None, kwargs_fixed=None, kwargs_lower=None, kwargs_upper=None, point_source_offset=False, source_size=False, num_images=0, num_tau0=0, num_z_sampling=0, source_grid_offset=False): """ @@ -110,6 +141,9 @@ def __init__(self, Ddt_sampling=False, mass_scaling=False, num_scale_factor=1, k if not mass_scaling: num_scale_factor = 0 self._mass_scaling = MassScalingParam(num_scale_factor) + + self._general_scaling = GeneralScalingParam(general_scaling_params or dict()) + if point_source_offset: self._point_source_offset = PointSourceOffsetParam(True, num_images) else: @@ -170,6 +204,7 @@ def num_param(self): def _param_groups(self): return [self._D_dt_sampling, self._mass_scaling, + self._general_scaling, self._point_source_offset, self._source_size, self._tau0, diff --git a/test/test_Sampling/test_parameters.py b/test/test_Sampling/test_parameters.py index c18b4d9a4..0eef85324 100644 --- a/test/test_Sampling/test_parameters.py +++ b/test/test_Sampling/test_parameters.py @@ -144,6 +144,72 @@ def test_mass_scaling(self): assert kwargs_lens[1]['alpha_Rs'] == 0.1 assert kwargs_lens[2]['alpha_Rs'] == 0.3 + def test_general_scaling(self): + kwargs_model = {'lens_model_list': ['PJAFFE', 'PJAFFE', 'NFW', 'PJAFFE', 'NFW']} + # Scale Rs for two of the PJAFFEs, and sigma0 for a different set of PJAFFEs + # Scale alpha_Rs for the NFWs + kwargs_constraints = { + 'general_scaling': { + 'Rs': [1, False, False, 1, False], + 'sigma0': [False, 1, False, 1, False], + 'alpha_Rs': [False, False, 1, False, 1], + } + } + # PJAFFE: sigma0, Ra, Rs, center_x, center_y + # NFW: Rs, alpha_Rs, center_x, center_y + kwargs_fixed_lens = [ + {'Rs': 2.0, 'center_x': 1.0}, + {'sigma0': 2.0, 'Ra': 2.0, 'Rs': 3.0, 'center_y': 1.5}, + {'alpha_Rs': 0.1}, + {'Ra': 0.1, 'center_x': 0, 'center_y': 0}, + {'Rs': 3, 'center_x': -1, 'center_y': 3}, + ] + kwargs_fixed_cosmo = {} + param_class = Param(kwargs_model, kwargs_fixed_lens=kwargs_fixed_lens, kwargs_fixed_special=kwargs_fixed_cosmo + , **kwargs_constraints) + kwargs_lens = [{'sigma0': 3, 'Ra': 2, 'center_y': 5}, + {'center_x': 1.}, + {'Rs': 3, 'center_x': 0.0, 'center_y': -1.0}, + {'sigma0': 3, 'Rs': 1.5}, + {'alpha_Rs': 4}] + kwargs_source = [] + kwargs_lens_light = [] + kwargs_ps = [] + # Define the scaling and power for each parameter + kwargs_cosmo = { + 'Rs_scale_factor': [2.0], + 'Rs_scale_pow': [1.1], + 'sigma0_scale_factor': [3], + 'sigma0_scale_pow': [2.0], + 'alpha_Rs_scale_factor': [0.3], + 'alpha_Rs_scale_pow': [0.5], + } + args = param_class.kwargs2args(kwargs_lens, kwargs_source, kwargs_lens_light, kwargs_ps, kwargs_special=kwargs_cosmo) + num, names = param_class.num_param() + print(names) + print(args) + + kwargs_return = param_class.args2kwargs(args) + kwargs_lens = kwargs_return['kwargs_lens'] + print('kwargs_lens:', kwargs_lens) + np.testing.assert_almost_equal(kwargs_lens[0]['Rs'], 2.0 * 2.0**1.1) + np.testing.assert_almost_equal(kwargs_lens[0]['sigma0'], 3) + np.testing.assert_almost_equal(kwargs_lens[1]['Rs'], 3.0) + np.testing.assert_almost_equal(kwargs_lens[1]['sigma0'], 3.0 * 2.0**2.0) + np.testing.assert_almost_equal(kwargs_lens[2]['alpha_Rs'], 0.3 * 0.1**0.5) + np.testing.assert_almost_equal(kwargs_lens[3]['Rs'], 2.0 * 1.5**1.1) + np.testing.assert_almost_equal(kwargs_lens[3]['sigma0'], 3.0 * 3**2.0) + np.testing.assert_almost_equal(kwargs_lens[4]['alpha_Rs'], 0.3 * 4**0.5) + + kwargs_return = param_class.args2kwargs(args, bijective=True) + kwargs_lens = kwargs_return['kwargs_lens'] + np.testing.assert_almost_equal(kwargs_lens[0]['Rs'], 2.0) + np.testing.assert_almost_equal(kwargs_lens[1]['sigma0'], 2.0) + np.testing.assert_almost_equal(kwargs_lens[2]['alpha_Rs'], 0.1) + np.testing.assert_almost_equal(kwargs_lens[3]['Rs'], 1.5) + np.testing.assert_almost_equal(kwargs_lens[3]['sigma0'], 3) + np.testing.assert_almost_equal(kwargs_lens[4]['alpha_Rs'], 4) + def test_joint_lens_with_light(self): kwargs_model = {'lens_model_list': ['CHAMELEON'], 'lens_light_model_list': ['CHAMELEON']} i_light, k_lens = 0, 0 From 455683b006538a7ace1de41fcdc0444f781f088e Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Mon, 22 Aug 2022 13:30:48 -0700 Subject: [PATCH 21/67] add additional test --- test/test_Sampling/test_special_param.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_Sampling/test_special_param.py b/test/test_Sampling/test_special_param.py index 88edbfbee..68b8ded50 100644 --- a/test/test_Sampling/test_special_param.py +++ b/test/test_Sampling/test_special_param.py @@ -79,6 +79,18 @@ def test_source_grid_offsets(self): assert kwargs_new['delta_x_source_grid'] == kwargs_fixed['delta_x_source_grid'] assert kwargs_new['delta_y_source_grid'] == kwargs_fixed['delta_y_source_grid'] + def test_general_scaling(self): + kwargs_fixed = {} + param = SpecialParam(kwargs_fixed=kwargs_fixed, + general_scaling_params={'param': [False, 1, 1, False, 2]}) + args = param.set_params({'param_scale_factor': [1, 2], 'param_scale_pow': [3, 4]}) + assert len(args) == 4 + num_param, param_list = param.num_param() + assert num_param == 4 + kwargs_new, _ = param.get_params(args, i=0) + assert kwargs_new['param_scale_factor'] == [1, 2] + assert kwargs_new['param_scale_pow'] == [3, 4] + if __name__ == '__main__': pytest.main() From 20dc123f262223e48866574e34988ec512b3cf34 Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Mon, 22 Aug 2022 13:54:07 -0700 Subject: [PATCH 22/67] trivial changes --- lenstronomy/Sampling/parameters.py | 1 + lenstronomy/Workflow/update_manager.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lenstronomy/Sampling/parameters.py b/lenstronomy/Sampling/parameters.py index c047b3571..d39492115 100644 --- a/lenstronomy/Sampling/parameters.py +++ b/lenstronomy/Sampling/parameters.py @@ -670,6 +670,7 @@ def print_setting(self): num, param_list = self.num_param() num_linear = self.num_param_linear() + # TODO print settings of specailParams? print("The following model options are chosen:") print("Lens models:", self._lens_model_list) print("Source models:", self._source_light_model_list) diff --git a/lenstronomy/Workflow/update_manager.py b/lenstronomy/Workflow/update_manager.py index 2234bf0e8..1dd3a8771 100644 --- a/lenstronomy/Workflow/update_manager.py +++ b/lenstronomy/Workflow/update_manager.py @@ -300,9 +300,7 @@ def update_fixed(self, lens_add_fixed=None, source_add_fixed=None, lens_light_ad if special_add_fixed is None: special_add_fixed = [] for param_name in special_add_fixed: - if param_name in special_fixed: - pass - else: + if param_name not in special_fixed: special_fixed[param_name] = special_temp[param_name] if special_remove_fixed is None: special_remove_fixed = [] From 77a48b012a69ddf0b33f6f4d6bdc8269f56c725e Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Mon, 22 Aug 2022 16:42:10 -0700 Subject: [PATCH 23/67] fixed relative import error; improve documentation --- lenstronomy/PointSource/point_source_param.py | 16 +++- lenstronomy/Sampling/param_group.py | 55 ++++++++++- lenstronomy/Sampling/parameters.py | 36 ++++++++ lenstronomy/Sampling/special_param.py | 31 ++++++- test/test_Sampling/test_param_groups.py | 91 +++++++++++++++++++ test/test_Sampling/test_parameters.py | 28 +++--- 6 files changed, 233 insertions(+), 24 deletions(-) create mode 100644 test/test_Sampling/test_param_groups.py diff --git a/lenstronomy/PointSource/point_source_param.py b/lenstronomy/PointSource/point_source_param.py index 1af9119e6..143047d24 100644 --- a/lenstronomy/PointSource/point_source_param.py +++ b/lenstronomy/PointSource/point_source_param.py @@ -2,16 +2,22 @@ __all__ = ['PointSourceParam'] -from ..Sampling.param_group import ModelParamGroup, SingleParam, ArrayParam +from lenstronomy.Sampling.param_group import ModelParamGroup, SingleParam, ArrayParam class SourcePositionParam(SingleParam): + ''' + Source position parameter, ra_source and dec_source + ''' param_names = ['ra_source', 'dec_source'] _kwargs_lower = {'ra_source': -100, 'dec_source': -100} _kwargs_upper = {'ra_source': 100, 'dec_source': 100} class LensedPosition(ArrayParam): + ''' + Represents lensed positions, possibly many. ra_image and dec_image + ''' _kwargs_lower = {'ra_image': -100, 'dec_image': -100, } _kwargs_upper = {'ra_image': 100, 'dec_image': 100, } def __init__(self, num_images): @@ -20,12 +26,18 @@ def __init__(self, num_images): class SourceAmp(SingleParam): + ''' + Source amplification + ''' param_names = ['source_amp'] _kwargs_lower = {'source_amp': 0} _kwargs_upper = {'source_amp': 100} class PointAmp(ArrayParam): + ''' + Point amplification, possibly many + ''' _kwargs_lower = {'point_amp': 0} _kwargs_upper = {'point_amp': 100} def __init__(self, fixed_magnification): @@ -35,7 +47,7 @@ def __init__(self, fixed_magnification): class PointSourceParam(object): """ - + Point source parameters """ def __init__(self, model_list, kwargs_fixed, num_point_source_list=None, linear_solver=True, diff --git a/lenstronomy/Sampling/param_group.py b/lenstronomy/Sampling/param_group.py index 3142341c9..d5cb64432 100644 --- a/lenstronomy/Sampling/param_group.py +++ b/lenstronomy/Sampling/param_group.py @@ -1,14 +1,46 @@ - +__author__ = 'jhodonnell' +__all__ = ['ModelParamGroup', 'SingleParam', 'ArrayParam'] class ModelParamGroup: + ''' + This abstract class represents any lenstronomy fitting parameters used + in the Param class. + + Subclasses should implement num_params(), set_params(), and get_params() + to convert parameters from lenstronomy's semantic dictionary format to a + flattened array format and back. + + This class also contains three static methods to easily aggregate groups + of parameter classes, called `compose_num_params()`, `compose_set_params()`, + and `compose_get_params()`. + ''' def num_params(self): + ''' + Tells the number of parameters that this group samples and theri names. + + returns: 2-tuple of (num param, list of names) + ''' raise NotImplementedError def set_params(self, kwargs): + ''' + Converts lenstronomy semantic parameters in dictionary format into a + flattened array of parameters. + + The flattened array is for use in optimization algorithms, e.g. MCMC, + Particle swarm, etc. + ''' raise NotImplementedError def get_params(self, args, i): + ''' + Converts a flattened array of parameters back into a lenstronomy dictionary, + starting at index i. + + args: list of floats + returns: dictionary of parameters + ''' raise NotImplementedError @staticmethod @@ -36,11 +68,17 @@ def compose_get_params(each_group, flat_args, i, *args, **kwargs): output_kwargs = dict(output_kwargs, **kwargs_grp) return output_kwargs, i + class SingleParam(ModelParamGroup): ''' - Helper for handling parameters in the SpecialGroup. + Helper for handling parameters which are a single float. - Internal use, please ignore. Check below for the actual definitions of special parameters. + Subclasses should define: + + on: (bool) Whether this parameter is sampled + param_names: List of strings, the name of each parameter + _kwargs_lower: Dictionary. Lower bounds of each parameter + _kwargs_upper: Dictionary. Upper bounds of each parameter ''' def __init__(self, on): self.on = bool(on) @@ -89,9 +127,16 @@ def kwargs_upper(self): class ArrayParam(ModelParamGroup): ''' - Helper for handling parameters in the SpecialGroup. + Helper for handling parameters which are an array of values. Examples + include mass_scaling, which is an array of scaling parameters, and wavelet + or gaussian decompositions which have different coefficients for each mode. + + Subclasses should define: - Internal use, please ignore. Check below for the actual definitions of special parameters. + on: (bool) Whether this parameter is sampled + param_names: Dictionary mapping the name of each parameter to the number of values needed. + _kwargs_lower: Dictionary. Lower bounds of each parameter + _kwargs_upper: Dictionary. Upper bounds of each parameter ''' def num_params(self, kwargs_fixed): if not self.on: diff --git a/lenstronomy/Sampling/parameters.py b/lenstronomy/Sampling/parameters.py index d39492115..27997d218 100644 --- a/lenstronomy/Sampling/parameters.py +++ b/lenstronomy/Sampling/parameters.py @@ -47,6 +47,42 @@ class that handles the parameter constraints. In particular when different model 'joint_lens_with_source_light': [[i_source, k_lens, ['param_name1', 'param_name2', ...]], [...], ...], joint parameter between lens model and source light model. Samples light model parameter only. + 'mass_scaling_list': e.g. [False, 1, 1, False, 2, False, 1, ...] + Links lens models to have their masses scaled together. In this example, + masses with False are not scaled, masses labeled 1 are scaled together, + and those labeled 2 are scaled together independent of 1, etc. + + 'general_scaling': { 'param1': [False, 1, 1, False, 1, ...], 'param2': [1, 1, 1, False, 2, 2, ...] } + Generalized parameter scaling. Input should be a dictionary mapping + parameter names to the masks defining which lens models are scaled together, + in the same format as for 'mass_scaling_list'. For each scaled parameter, + two special params will be added called '${param}_scale_factor' and + '${param}_scale_pow', defining the scaling and power-law of each. + + Each scale will be modified as `param = param_scale_factor * param**param_scale_pow`. + + For example, if we want to jointly constrain the `sigma0` and `Rs` parameters + of some lens models, we can add: + + ``` + 'general_scaling': { + 'sigma0': [False]*num_halo + [1]*nmembers, + 'Rs': [False]*num_halo + [1]*nmembers, + } + ``` + + Then we can choose to fix the power-law and vary the scale factor like so: + + ``` + fixed_special = {'sigma0_scale_pow': [alpha*2], 'Rs_scale_pow': [beta]} + kwargs_special_init = {'sigma0_scale_factor': [17.0], 'Rs_scale_factor': [8]} + kwargs_special_sigma = {'sigma0_scale_factor': [10.0], 'Rs_scale_factor': [3]} + kwargs_lower_special = {'sigma0_scale_factor': [0.5], 'Rs_scale_factor': [1]} + kwargs_upper_special = {'sigma0_scale_factor': [40], 'Rs_scale_factor': [20]} + + special_params = [kwargs_special_init, kwargs_special_sigma, fixed_special, kwargs_lower_special, kwargs_upper_special] + ``` + hierarchy is as follows: 1. Point source parameters are inferred 2. Lens light joint parameters are set diff --git a/lenstronomy/Sampling/special_param.py b/lenstronomy/Sampling/special_param.py index 84dc050ea..50669bab0 100644 --- a/lenstronomy/Sampling/special_param.py +++ b/lenstronomy/Sampling/special_param.py @@ -12,18 +12,27 @@ class DdtSamplingParam(SingleParam): + ''' + Time delay parameter + ''' param_names = ['D_dt'] _kwargs_lower = {'D_dt': 0} _kwargs_upper = {'D_dt': 100000} class SourceSizeParam(SingleParam): + ''' + Source size parameter + ''' param_names = ['source_size'] _kwargs_lower = {'source_size': 0} _kwargs_upper = {'source_size': 1} class SourceGridOffsetParam(SingleParam): + ''' + Source grid offset, both x and y. + ''' param_names = ['delta_x_source_grid', 'delta_y_source_grid'] _kwargs_lower = { 'delta_x_source_grid': -100, @@ -36,6 +45,9 @@ class SourceGridOffsetParam(SingleParam): class MassScalingParam(ArrayParam): + ''' + Mass scaling. Can scale the masses of arbitrary subsets of lens models + ''' _kwargs_lower = {'scale_factor': 0} _kwargs_upper = {'scale_factor': 1000} def __init__(self, num_scale_factor): @@ -44,6 +56,9 @@ def __init__(self, num_scale_factor): class PointSourceOffsetParam(ArrayParam): + ''' + Point source offset, both x and y + ''' _kwargs_lower = {'delta_x_image': -1, 'delta_y_image': -1} _kwargs_upper = {'delta_x_image': 1, 'delta_y_image': 1} def __init__(self, offset, num_images): @@ -55,6 +70,9 @@ def __init__(self, offset, num_images): class Tau0ListParam(ArrayParam): + ''' + Optical depth renormalization parameters + ''' _kwargs_lower = {'tau0_list': 0} _kwargs_upper = {'tau0_list': 1000} def __init__(self, num_tau0): @@ -63,6 +81,9 @@ def __init__(self, num_tau0): class ZSamplingParam(ArrayParam): + ''' + Redshift sampling. + ''' _kwargs_lower = {'z_sampling': 0} _kwargs_upper = {'z_sampling': 1000} def __init__(self, num_z_sampling): @@ -71,9 +92,13 @@ def __init__(self, num_z_sampling): class GeneralScalingParam(ArrayParam): - # Mass scaling needs: - # - name of scaled param - # - number of scaled params + ''' + General lens scaling. + + For each scaled lens parameter, adds a `{param}_scale_factor` and + `{param}_scale_pow` special parameter, and updates the scaled param + as `param = param_scale_factor * param**param_scale_pow`. + ''' def __init__(self, params: dict): # params is a dictionary self.param_names = {} diff --git a/test/test_Sampling/test_param_groups.py b/test/test_Sampling/test_param_groups.py new file mode 100644 index 000000000..bedba9f02 --- /dev/null +++ b/test/test_Sampling/test_param_groups.py @@ -0,0 +1,91 @@ +__author__ = 'jhodonnell' + + +import numpy as np +import numpy.testing as npt +import unittest +import pytest + +from lenstronomy.Sampling.param_group import ( + ModelParamGroup, SingleParam, ArrayParam + ) + + +class ExampleSingleParam(SingleParam): + param_names = ['sp1', 'sp2'] + _kwargs_lower = {'sp1': 0, 'sp2': 0} + _kwargs_upper = {'sp1': 10, 'sp2': 10} + + +class ExampleArrayParam(ArrayParam): + param_names = {'ap1': 1, 'ap2': 3} + _kwargs_lower = {'ap1': [0], 'ap2': [0]*3} + _kwargs_upper = {'ap1': [10], 'ap2': [10]*3} + + def __init__(self, on): + self.on = bool(on) + + +class TestParamGroup(object): + def setup(self): + pass + + def test_single_param(self): + sp = ExampleSingleParam(on=True) + + num, names = sp.num_params({}) + assert num == 2 + assert names == ['sp1', 'sp2'] + + result = sp.set_params({'sp1': 2}, {'sp2': 3}) + assert result == [2] + + result = sp.set_params({'sp1': 2, 'sp2': 3}, {}) + assert result == [2, 3] + + kwargs, i = sp.get_params(result, i=0, kwargs_fixed={}) + assert kwargs['sp1'] == 2 + assert kwargs['sp2'] == 3 + + def test_array_param(self): + ap = ExampleArrayParam(on=True) + + num, names = ap.num_params({}) + assert num == 4 + assert names == ['ap1'] + ['ap2']*3 + + result = ap.set_params({'ap1': [2]}, {'ap2': [1, 1, 1]}) + assert result == [2] + + result = ap.set_params({'ap1': [2], 'ap2': [1, 2, 3]}, {}) + assert result == [2, 1, 2, 3] + + kwargs, i = ap.get_params(result, i=0, kwargs_fixed={}) + assert kwargs['ap1'] == [2] + assert kwargs['ap2'] == [1, 2, 3] + + def test_compose(self): + sp = ExampleSingleParam(on=True) + ap = ExampleArrayParam(on=True) + + num, names = ModelParamGroup.compose_num_params([sp, ap], kwargs_fixed={}) + assert num == 6 + assert names == sp.num_params({})[1] + ap.num_params({})[1] + + result = ModelParamGroup.compose_set_params( + [sp, ap], + { + 'sp1': 1, 'sp2': 2, + 'ap1': [3], 'ap2': [4, 5, 6] + }, + kwargs_fixed={} + ) + assert result == [1, 2, 3, 4, 5, 6] + + kwargs, i = ModelParamGroup.compose_get_params([sp, ap], result, i=0, kwargs_fixed={}) + assert kwargs['sp1'] == 1 + assert kwargs['ap2'] == [4, 5, 6] + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test_Sampling/test_parameters.py b/test/test_Sampling/test_parameters.py index 0eef85324..0066a42d8 100644 --- a/test/test_Sampling/test_parameters.py +++ b/test/test_Sampling/test_parameters.py @@ -192,23 +192,23 @@ def test_general_scaling(self): kwargs_return = param_class.args2kwargs(args) kwargs_lens = kwargs_return['kwargs_lens'] print('kwargs_lens:', kwargs_lens) - np.testing.assert_almost_equal(kwargs_lens[0]['Rs'], 2.0 * 2.0**1.1) - np.testing.assert_almost_equal(kwargs_lens[0]['sigma0'], 3) - np.testing.assert_almost_equal(kwargs_lens[1]['Rs'], 3.0) - np.testing.assert_almost_equal(kwargs_lens[1]['sigma0'], 3.0 * 2.0**2.0) - np.testing.assert_almost_equal(kwargs_lens[2]['alpha_Rs'], 0.3 * 0.1**0.5) - np.testing.assert_almost_equal(kwargs_lens[3]['Rs'], 2.0 * 1.5**1.1) - np.testing.assert_almost_equal(kwargs_lens[3]['sigma0'], 3.0 * 3**2.0) - np.testing.assert_almost_equal(kwargs_lens[4]['alpha_Rs'], 0.3 * 4**0.5) + npt.assert_almost_equal(kwargs_lens[0]['Rs'], 2.0 * 2.0**1.1) + npt.assert_almost_equal(kwargs_lens[0]['sigma0'], 3) + npt.assert_almost_equal(kwargs_lens[1]['Rs'], 3.0) + npt.assert_almost_equal(kwargs_lens[1]['sigma0'], 3.0 * 2.0**2.0) + npt.assert_almost_equal(kwargs_lens[2]['alpha_Rs'], 0.3 * 0.1**0.5) + npt.assert_almost_equal(kwargs_lens[3]['Rs'], 2.0 * 1.5**1.1) + npt.assert_almost_equal(kwargs_lens[3]['sigma0'], 3.0 * 3**2.0) + npt.assert_almost_equal(kwargs_lens[4]['alpha_Rs'], 0.3 * 4**0.5) kwargs_return = param_class.args2kwargs(args, bijective=True) kwargs_lens = kwargs_return['kwargs_lens'] - np.testing.assert_almost_equal(kwargs_lens[0]['Rs'], 2.0) - np.testing.assert_almost_equal(kwargs_lens[1]['sigma0'], 2.0) - np.testing.assert_almost_equal(kwargs_lens[2]['alpha_Rs'], 0.1) - np.testing.assert_almost_equal(kwargs_lens[3]['Rs'], 1.5) - np.testing.assert_almost_equal(kwargs_lens[3]['sigma0'], 3) - np.testing.assert_almost_equal(kwargs_lens[4]['alpha_Rs'], 4) + npt.assert_almost_equal(kwargs_lens[0]['Rs'], 2.0) + npt.assert_almost_equal(kwargs_lens[1]['sigma0'], 2.0) + npt.assert_almost_equal(kwargs_lens[2]['alpha_Rs'], 0.1) + npt.assert_almost_equal(kwargs_lens[3]['Rs'], 1.5) + npt.assert_almost_equal(kwargs_lens[3]['sigma0'], 3) + npt.assert_almost_equal(kwargs_lens[4]['alpha_Rs'], 4) def test_joint_lens_with_light(self): kwargs_model = {'lens_model_list': ['CHAMELEON'], 'lens_light_model_list': ['CHAMELEON']} From 02d6ea06701e9f8c9835347c8ad389af3c4741f4 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Tue, 23 Aug 2022 10:01:09 -0700 Subject: [PATCH 24/67] specific test for nautilus prior --- lenstronomy/Sampling/Samplers/nautilus.py | 1 + test/test_Sampling/test_Samplers/test_nautilus.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lenstronomy/Sampling/Samplers/nautilus.py b/lenstronomy/Sampling/Samplers/nautilus.py index 5dd15e3dc..a0ea776e8 100644 --- a/lenstronomy/Sampling/Samplers/nautilus.py +++ b/lenstronomy/Sampling/Samplers/nautilus.py @@ -44,6 +44,7 @@ def nautilus_sampling(self, prior_type='uniform', mpi=False, thread_count=1, ver if prior_type == 'uniform': for i in range(self._num_param): prior.add_parameter(dist=(self._lower_limit[i], self._upper_limit[i])) + assert self._num_param == prior.dimensionality() print(self._num_param, prior.dimensionality(), 'number of param, dimensionality') else: raise ValueError('prior_type %s is not supported for Nautilus wrapper.' % prior_type) diff --git a/test/test_Sampling/test_Samplers/test_nautilus.py b/test/test_Sampling/test_Samplers/test_nautilus.py index 2694cb7a7..c15afce16 100644 --- a/test/test_Sampling/test_Samplers/test_nautilus.py +++ b/test/test_Sampling/test_Samplers/test_nautilus.py @@ -11,7 +11,7 @@ def import_fixture(simple_einstein_ring_likelihood_2d): """ - :param simple_einstein_ring_likelihood: fixture + :param simple_einstein_ring_likelihood_2d: fixture :return: """ likelihood, kwargs_truths = simple_einstein_ring_likelihood_2d @@ -46,6 +46,16 @@ def test_sampler(self, import_fixture): kwargs_run_fail['prior_type'] = 'wrong' assert_raises(ValueError, sampler.nautilus_sampling, **kwargs_run_fail) + def test_prior(self): + + num_param = 10 + from nautilus import Prior + prior = Prior() + + for i in range(num_param): + prior.add_parameter(dist=(0, 1)) + assert num_param == prior.dimensionality() + if __name__ == '__main__': pytest.main() From 9de26dbafb400abdc32650a2411d7f256aa8b97d Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Tue, 23 Aug 2022 10:25:00 -0700 Subject: [PATCH 25/67] add myself as contributor! --- AUTHORS.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index bbcbed768..bb58bd0ec 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -22,6 +22,7 @@ Contributors (alphabetic) * Vikram Bhamre `vikramb1 `_ * Xuheng Ding `dartoon `_ * Sydney Erickson `smericks `_ +* Andreas Filipp `andreasfilipp `_ * Kevin Fusshoeller * Aymeric Galan `aymgal `_ * Matthew R. Gomer `mattgomer `_ @@ -34,7 +35,7 @@ Contributors (alphabetic) * Brian Nord `bnord `_ * Giulia Pagano * Ji Won Park `jiwoncpark `_ -* Andreas Filipp `andreasfilipp `_ +* Jackson O'Donnell `jhod0 `_ * Thomas Schmidt `Thomas-01 `_ * Dominique Sluse * Luca Teodori `lucateo `_ @@ -54,4 +55,4 @@ Past development lead The initial source code of lenstronomy was developed by Simon Birrer (`sibirrer `_) in 2014-2018 and made public in 2018. From 2018-2022 the development of lenstronomy was hosted on Simon Birrer's repository with increased contributions from many people. -The lenstronomy development moved to the `project repository `_ in 2022. \ No newline at end of file +The lenstronomy development moved to the `project repository `_ in 2022. From b620cef656a8dd95ed9b990e4cc2e4aac39cd048 Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Tue, 23 Aug 2022 10:36:02 -0700 Subject: [PATCH 26/67] Working on Simon's suggestions: renaming and adding documentation --- lenstronomy/PointSource/point_source_param.py | 19 ++++--- lenstronomy/Sampling/param_group.py | 50 +++++++++++++++++-- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/lenstronomy/PointSource/point_source_param.py b/lenstronomy/PointSource/point_source_param.py index 143047d24..e744256aa 100644 --- a/lenstronomy/PointSource/point_source_param.py +++ b/lenstronomy/PointSource/point_source_param.py @@ -17,6 +17,8 @@ class SourcePositionParam(SingleParam): class LensedPosition(ArrayParam): ''' Represents lensed positions, possibly many. ra_image and dec_image + + :param num_images: integer. The number of lensed positions to model. ''' _kwargs_lower = {'ra_image': -100, 'dec_image': -100, } _kwargs_upper = {'ra_image': 100, 'dec_image': 100, } @@ -34,9 +36,12 @@ class SourceAmp(SingleParam): _kwargs_upper = {'source_amp': 100} -class PointAmp(ArrayParam): +class ImageAmp(ArrayParam): ''' - Point amplification, possibly many + Observed amplification of lensed images of a point source. Can model + arbitrarily many magnified images + + :param fixed_magnification: integer. The number of lensed images with known magnification to fit. ''' _kwargs_lower = {'point_amp': 0} _kwargs_upper = {'point_amp': 100} @@ -88,7 +93,7 @@ def __init__(self, model_list, kwargs_fixed, num_point_source_list=None, linear_ if fixed_magnification_list[i] and model in ['LENSED_POSITION', 'SOURCE_POSITION']: params.append(SourceAmp(True)) else: - params.append(PointAmp(num)) + params.append(ImageAmp(num)) self.param_groups.append(params) @@ -96,16 +101,16 @@ def __init__(self, model_list, kwargs_fixed, num_point_source_list=None, linear_ kwargs_lower = [] for model_params in self.param_groups: fixed_lower = {} - for grp in model_params: - fixed_lower = dict(fixed_lower, **grp.kwargs_lower) + for param_group in model_params: + fixed_lower = dict(fixed_lower, **param_group.kwargs_lower) kwargs_lower.append(fixed_lower) if kwargs_upper is None: kwargs_upper = [] for model_params in self.param_groups: fixed_upper = {} - for grp in model_params: - fixed_upper = dict(fixed_upper, **grp.kwargs_upper) + for param_group in model_params: + fixed_upper = dict(fixed_upper, **param_group.kwargs_upper) kwargs_upper.append(fixed_upper) self.lower_limit = kwargs_lower diff --git a/lenstronomy/Sampling/param_group.py b/lenstronomy/Sampling/param_group.py index d5cb64432..6fa01eccc 100644 --- a/lenstronomy/Sampling/param_group.py +++ b/lenstronomy/Sampling/param_group.py @@ -19,7 +19,7 @@ def num_params(self): ''' Tells the number of parameters that this group samples and theri names. - returns: 2-tuple of (num param, list of names) + :returns: 2-tuple of (num param, list of names) ''' raise NotImplementedError @@ -30,6 +30,8 @@ def set_params(self, kwargs): The flattened array is for use in optimization algorithms, e.g. MCMC, Particle swarm, etc. + + :returns: flattened array of parameters as floats ''' raise NotImplementedError @@ -38,13 +40,25 @@ def get_params(self, args, i): Converts a flattened array of parameters back into a lenstronomy dictionary, starting at index i. - args: list of floats - returns: dictionary of parameters + :param args: flattened arguments to convert to lenstronomy format + :type args: list + :returns: dictionary of parameters ''' raise NotImplementedError @staticmethod def compose_num_params(each_group, *args, **kwargs): + ''' + Aggregates the number of parameters for a group of parameter groups, + calling each instance's `num_params()` method and combining the results + + :param each_group: collection of parameter groups. Should each be subclasses of ModelParamGroup. + :type each_group: list + :param args: Extra arguments to be passed to each call of `num_params()` + :param kwargs: Extra keyword arguments to be passed to each call of `num_params()` + + :returns: As in each individual `num_params()`, a 2-tuple of (num params, list of param names) + ''' tot_param = 0 param_names = [] for group in each_group: @@ -55,6 +69,20 @@ def compose_num_params(each_group, *args, **kwargs): @staticmethod def compose_set_params(each_group, param_kwargs, *args, **kwargs): + ''' + Converts lenstronomy semantic arguments in dictionary format to a + flattened list of floats for use in optimization/fitting algorithms. + Combines the results for a set of arbitrarily many parameter groups. + + :param each_group: collection of parameter groups. Should each be subclasses of ModelParamGroup. + :type each_group: list + :param param_kwargs: the kwargs to process + :type param_kwargs: dict + :param args: Extra arguments to be passed to each call of `set_params()` + :param kwargs: Extra keyword arguments to be passed to each call of `set_params()` + + :returns: As in each individual `set_params()`, a list of floats + ''' output_args = [] for group in each_group: output_args += group.set_params(param_kwargs, *args, **kwargs) @@ -62,6 +90,22 @@ def compose_set_params(each_group, param_kwargs, *args, **kwargs): @staticmethod def compose_get_params(each_group, flat_args, i, *args, **kwargs): + ''' + Converts a flattened array of parameters to lenstronomy semantic + parameters in dictionary format. + Combines the results for a set of arbitrarily many parameter groups. + + :param each_group: collection of parameter groups. Should each be subclasses of ModelParamGroup. + :type each_group: list + :param flat_args: the input array of parameters + :type flat_args: list + :param i: the index in `flat_args` to start at + :type i: int + :param args: Extra arguments to be passed to each call of `set_params()` + :param kwargs: Extra keyword arguments to be passed to each call of `set_params()` + + :returns: As in each individual `get_params()`, a 2-tuple of (dictionary of params, new index) + ''' output_kwargs = {} for group in each_group: kwargs_grp, i = group.get_params(flat_args, i, *args, **kwargs) From e096f6410607305385060f0cfb2dffd0c1f6140a Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Tue, 23 Aug 2022 11:04:04 -0700 Subject: [PATCH 27/67] improve documentation, checked it looks ok in sphinx output --- docs/lenstronomy.Sampling.rst | 8 +++++ lenstronomy/Sampling/param_group.py | 26 +++++++++++----- lenstronomy/Sampling/parameters.py | 46 ++++++++++++++++++----------- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/docs/lenstronomy.Sampling.rst b/docs/lenstronomy.Sampling.rst index 1aeaacc8c..08a211d8a 100644 --- a/docs/lenstronomy.Sampling.rst +++ b/docs/lenstronomy.Sampling.rst @@ -45,6 +45,14 @@ lenstronomy.Sampling.special\_param module :undoc-members: :show-inheritance: +lenstronomy.Sampling.param\_group module +------------------------------------------ + +.. automodule:: lenstronomy.Sampling.param_group + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/lenstronomy/Sampling/param_group.py b/lenstronomy/Sampling/param_group.py index 6fa01eccc..d47da32f1 100644 --- a/lenstronomy/Sampling/param_group.py +++ b/lenstronomy/Sampling/param_group.py @@ -2,6 +2,13 @@ __all__ = ['ModelParamGroup', 'SingleParam', 'ArrayParam'] +''' +This module provides helper classes for managing sample parameters. This is +for internal use, if you are not modifying lenstronomy sampling to include +new parameters you can safely ignore this. +''' + + class ModelParamGroup: ''' This abstract class represents any lenstronomy fitting parameters used @@ -119,10 +126,11 @@ class SingleParam(ModelParamGroup): Subclasses should define: - on: (bool) Whether this parameter is sampled - param_names: List of strings, the name of each parameter - _kwargs_lower: Dictionary. Lower bounds of each parameter - _kwargs_upper: Dictionary. Upper bounds of each parameter + :param on: Whether this parameter is sampled + :type on: bool + :param param_names: List of strings, the name of each parameter + :param _kwargs_lower: Dictionary. Lower bounds of each parameter + :param _kwargs_upper: Dictionary. Upper bounds of each parameter ''' def __init__(self, on): self.on = bool(on) @@ -169,6 +177,7 @@ def kwargs_upper(self): return {} return self._kwargs_upper + class ArrayParam(ModelParamGroup): ''' Helper for handling parameters which are an array of values. Examples @@ -177,10 +186,11 @@ class ArrayParam(ModelParamGroup): Subclasses should define: - on: (bool) Whether this parameter is sampled - param_names: Dictionary mapping the name of each parameter to the number of values needed. - _kwargs_lower: Dictionary. Lower bounds of each parameter - _kwargs_upper: Dictionary. Upper bounds of each parameter + :param on: Whether this parameter is sampled + :type on: bool + :param param_names: Dictionary mapping the name of each parameter to the number of values needed. + :param _kwargs_lower: Dictionary. Lower bounds of each parameter + :param _kwargs_upper: Dictionary. Upper bounds of each parameter ''' def num_params(self, kwargs_fixed): if not self.on: diff --git a/lenstronomy/Sampling/parameters.py b/lenstronomy/Sampling/parameters.py index 27997d218..4f07bf117 100644 --- a/lenstronomy/Sampling/parameters.py +++ b/lenstronomy/Sampling/parameters.py @@ -61,27 +61,33 @@ class that handles the parameter constraints. In particular when different model Each scale will be modified as `param = param_scale_factor * param**param_scale_pow`. - For example, if we want to jointly constrain the `sigma0` and `Rs` parameters - of some lens models, we can add: + For example, say we want to jointly constrain the `sigma0` and `Rs` parameters + of some lens models indexed by `i`, like so: - ``` - 'general_scaling': { - 'sigma0': [False]*num_halo + [1]*nmembers, - 'Rs': [False]*num_halo + [1]*nmembers, - } - ``` + .. math:: + + \\sigma_{0,i} = \\sigma_0^{ref} L_i^\\alpha \\\\ + r_{cut,i} = r_{cut}^{ref} L_i^\\beta + + To do this we can add the following. The lens models corresponding to + entries of `1` will be scaled together, and those corresponding to `False` + will not be. As in `mass_scaling_list`, subsets of models can be scaled + independently by marking them `2`, `3`, etc. + + >>> 'general_scaling': { + >>> 'sigma0': [False, 1, 1, False, 1, ...], + >>> 'Rs': [False, 1, 1, False, 1, ...], + >>> } Then we can choose to fix the power-law and vary the scale factor like so: - ``` - fixed_special = {'sigma0_scale_pow': [alpha*2], 'Rs_scale_pow': [beta]} - kwargs_special_init = {'sigma0_scale_factor': [17.0], 'Rs_scale_factor': [8]} - kwargs_special_sigma = {'sigma0_scale_factor': [10.0], 'Rs_scale_factor': [3]} - kwargs_lower_special = {'sigma0_scale_factor': [0.5], 'Rs_scale_factor': [1]} - kwargs_upper_special = {'sigma0_scale_factor': [40], 'Rs_scale_factor': [20]} + >>> fixed_special = {'sigma0_scale_pow': [alpha*2], 'Rs_scale_pow': [beta]} + >>> kwargs_special_init = {'sigma0_scale_factor': [17.0], 'Rs_scale_factor': [8]} + >>> kwargs_special_sigma = {'sigma0_scale_factor': [10.0], 'Rs_scale_factor': [3]} + >>> kwargs_lower_special = {'sigma0_scale_factor': [0.5], 'Rs_scale_factor': [1]} + >>> kwargs_upper_special = {'sigma0_scale_factor': [40], 'Rs_scale_factor': [20]} - special_params = [kwargs_special_init, kwargs_special_sigma, fixed_special, kwargs_lower_special, kwargs_upper_special] - ``` + >>> special_params = [kwargs_special_init, kwargs_special_sigma, fixed_special, kwargs_lower_special, kwargs_upper_special] hierarchy is as follows: 1. Point source parameters are inferred @@ -113,7 +119,6 @@ def __init__(self, kwargs_model, joint_source_with_source=[], joint_lens_with_light=[], joint_source_with_point_source=[], joint_lens_light_with_point_source=[], joint_extinction_with_lens_light=[], joint_lens_with_source_light=[], mass_scaling_list=None, point_source_offset=False, - # General scaling: need names of params and number of individual params general_scaling=None, num_point_source_list=None, image_plane_source_list=None, solver_type='NONE', Ddt_sampling=None, source_size=False, num_tau0=0, lens_redshift_sampling_indexes=None, @@ -163,6 +168,7 @@ def __init__(self, kwargs_model, joint parameter between lens model and source light model. Samples light model parameter only. :param mass_scaling_list: boolean list of length of lens model list (optional) models with identical integers will be scaled with the same additional scaling factor. First integer starts with 1 (not 0) + :param general_scaling: { 'param_1': [list of booleans/integers defining which model to fit], 'param_2': [..], ..} :param point_source_offset: bool, if True, adds relative offsets ot the modeled image positions relative to the time-delay and lens equation solver :param num_point_source_list: list of number of point sources per point source model class @@ -252,6 +258,10 @@ def __init__(self, kwargs_model, if general_scaling is not None: self._general_scaling = True + # FIXME TODO: check that the scaled parameters are actually used + # by each lens model. For example, NFW has no theta_E parameter, + # if a user tries to scale theta_E for an NFW we should throw an + # error self._general_scaling_masks = dict(general_scaling) else: self._general_scaling = False @@ -595,6 +605,8 @@ def update_lens_scaling(self, kwargs_special, kwargs_lens, inverse=False): if not (self._mass_scaling or self._general_scaling): return kwargs_lens_updated + # TODO: remove separate logic for mass scaling. either deprecate it + # entirely, implement the details as a special case of general_scaling if self._mass_scaling: scale_factor_list = np.array(kwargs_special['scale_factor']) if inverse is True: From bb0364a1a6f159d6d9e43fcf3e9ca7940b48f340 Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Tue, 23 Aug 2022 12:18:09 -0700 Subject: [PATCH 28/67] add documentation for Single/ArrayParam members, improve parameter printing --- lenstronomy/Sampling/param_group.py | 79 +++++++++++++++++++++++++++-- lenstronomy/Sampling/parameters.py | 2 + 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lenstronomy/Sampling/param_group.py b/lenstronomy/Sampling/param_group.py index d47da32f1..38645f26b 100644 --- a/lenstronomy/Sampling/param_group.py +++ b/lenstronomy/Sampling/param_group.py @@ -1,13 +1,12 @@ -__author__ = 'jhodonnell' -__all__ = ['ModelParamGroup', 'SingleParam', 'ArrayParam'] - - ''' This module provides helper classes for managing sample parameters. This is for internal use, if you are not modifying lenstronomy sampling to include new parameters you can safely ignore this. ''' +__author__ = 'jhodonnell' +__all__ = ['ModelParamGroup', 'SingleParam', 'ArrayParam'] + class ModelParamGroup: ''' @@ -49,6 +48,8 @@ def get_params(self, args, i): :param args: flattened arguments to convert to lenstronomy format :type args: list + :param i: index to begin at in args + :type i: int :returns: dictionary of parameters ''' raise NotImplementedError @@ -136,6 +137,14 @@ def __init__(self, on): self.on = bool(on) def num_params(self, kwargs_fixed): + ''' + Tells the number of parameters that this group samples and theri names. + + :param kwargs_fixed: Dictionary of fixed arguments + :type kwargs_fixed: dict + + :returns: 2-tuple of (num param, list of names) + ''' if self.on: npar, names = 0, [] for name in self.param_names: @@ -146,6 +155,20 @@ def num_params(self, kwargs_fixed): return 0, [] def set_params(self, kwargs, kwargs_fixed): + ''' + Converts lenstronomy semantic parameters in dictionary format into a + flattened array of parameters. + + The flattened array is for use in optimization algorithms, e.g. MCMC, + Particle swarm, etc. + + :param kwargs: lenstronomy parameters to flatten + :type kwargs: dict + :param kwargs_fixed: Dictionary of fixed arguments + :type kwargs_fixed: dict + + :returns: flattened array of parameters as floats + ''' if self.on: output = [] for name in self.param_names: @@ -155,6 +178,19 @@ def set_params(self, kwargs, kwargs_fixed): return [] def get_params(self, args, i, kwargs_fixed): + ''' + Converts a flattened array of parameters back into a lenstronomy dictionary, + starting at index i. + + :param args: flattened arguments to convert to lenstronomy format + :type args: list + :param i: index to begin at in args + :type i: int + :param kwargs_fixed: Dictionary of fixed arguments + :type kwargs_fixed: dict + + :returns: dictionary of parameters + ''' out = {} if self.on: for name in self.param_names: @@ -193,6 +229,14 @@ class ArrayParam(ModelParamGroup): :param _kwargs_upper: Dictionary. Upper bounds of each parameter ''' def num_params(self, kwargs_fixed): + ''' + Tells the number of parameters that this group samples and theri names. + + :param kwargs_fixed: Dictionary of fixed arguments + :type kwargs_fixed: dict + + :returns: 2-tuple of (num param, list of names) + ''' if not self.on: return 0, [] @@ -206,6 +250,20 @@ def num_params(self, kwargs_fixed): return npar, names def set_params(self, kwargs, kwargs_fixed): + ''' + Converts lenstronomy semantic parameters in dictionary format into a + flattened array of parameters. + + The flattened array is for use in optimization algorithms, e.g. MCMC, + Particle swarm, etc. + + :param kwargs: lenstronomy parameters to flatten + :type kwargs: dict + :param kwargs_fixed: Dictionary of fixed arguments + :type kwargs_fixed: dict + + :returns: flattened array of parameters as floats + ''' if not self.on: return [] @@ -216,6 +274,19 @@ def set_params(self, kwargs, kwargs_fixed): return args def get_params(self, args, i, kwargs_fixed): + ''' + Converts a flattened array of parameters back into a lenstronomy dictionary, + starting at index i. + + :param args: flattened arguments to convert to lenstronomy format + :type args: list + :param i: index to begin at in args + :type i: int + :param kwargs_fixed: Dictionary of fixed arguments + :type kwargs_fixed: dict + + :returns: dictionary of parameters + ''' if not self.on: return {}, i diff --git a/lenstronomy/Sampling/parameters.py b/lenstronomy/Sampling/parameters.py index 4f07bf117..3cab1acc7 100644 --- a/lenstronomy/Sampling/parameters.py +++ b/lenstronomy/Sampling/parameters.py @@ -738,6 +738,8 @@ def print_setting(self): print("Joint lens with light:", self._joint_lens_with_light) print("Joint source with point source:", self._joint_source_with_point_source) print("Joint lens light with point source:", self._joint_lens_light_with_point_source) + print("Mass scaling:", self._num_scale_factor, "groups") + print("General lens scaling:", self._general_scaling_masks) print("===================") print("Number of non-linear parameters being sampled: ", num) print("Parameters being sampled: ", param_list) From 74743c58e5bb0ab57d21a158ddf1d1a7412a9b4f Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Tue, 23 Aug 2022 13:26:07 -0700 Subject: [PATCH 29/67] change fixed magnification name to correct point source number --- lenstronomy/PointSource/point_source_param.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lenstronomy/PointSource/point_source_param.py b/lenstronomy/PointSource/point_source_param.py index e744256aa..baca87528 100644 --- a/lenstronomy/PointSource/point_source_param.py +++ b/lenstronomy/PointSource/point_source_param.py @@ -41,13 +41,13 @@ class ImageAmp(ArrayParam): Observed amplification of lensed images of a point source. Can model arbitrarily many magnified images - :param fixed_magnification: integer. The number of lensed images with known magnification to fit. + :param num_point_sources: integer. The number of lensed images without fixed magnification. ''' _kwargs_lower = {'point_amp': 0} _kwargs_upper = {'point_amp': 100} - def __init__(self, fixed_magnification): - self.on = int(fixed_magnification) > 0 - self.param_names = {'point_amp': int(fixed_magnification)} + def __init__(self, num_point_sources): + self.on = int(num_point_sources) > 0 + self.param_names = {'point_amp': int(num_point_sources)} class PointSourceParam(object): From 4f650a54159fc7dee3c1c76a38a0188e9114ffde Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Tue, 23 Aug 2022 15:05:21 -0700 Subject: [PATCH 30/67] Apply Anowar's changes: fix typo, and make `on` read-only --- lenstronomy/PointSource/point_source_param.py | 4 ++-- lenstronomy/Sampling/param_group.py | 23 +++++++++++++++++-- lenstronomy/Sampling/special_param.py | 14 +++++------ test/test_Sampling/test_param_groups.py | 3 --- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/lenstronomy/PointSource/point_source_param.py b/lenstronomy/PointSource/point_source_param.py index baca87528..8d6448880 100644 --- a/lenstronomy/PointSource/point_source_param.py +++ b/lenstronomy/PointSource/point_source_param.py @@ -23,7 +23,7 @@ class LensedPosition(ArrayParam): _kwargs_lower = {'ra_image': -100, 'dec_image': -100, } _kwargs_upper = {'ra_image': 100, 'dec_image': 100, } def __init__(self, num_images): - self.on = int(num_images) > 0 + super().__init__(int(num_images) > 0) self.param_names = {'ra_image': int(num_images), 'dec_image': int(num_images)} @@ -46,7 +46,7 @@ class ImageAmp(ArrayParam): _kwargs_lower = {'point_amp': 0} _kwargs_upper = {'point_amp': 100} def __init__(self, num_point_sources): - self.on = int(num_point_sources) > 0 + super().__init__(int(num_point_sources) > 0) self.param_names = {'point_amp': int(num_point_sources)} diff --git a/lenstronomy/Sampling/param_group.py b/lenstronomy/Sampling/param_group.py index 38645f26b..10b0ef951 100644 --- a/lenstronomy/Sampling/param_group.py +++ b/lenstronomy/Sampling/param_group.py @@ -23,7 +23,7 @@ class ModelParamGroup: ''' def num_params(self): ''' - Tells the number of parameters that this group samples and theri names. + Tells the number of parameters that this group samples and their names. :returns: 2-tuple of (num param, list of names) ''' @@ -134,7 +134,11 @@ class SingleParam(ModelParamGroup): :param _kwargs_upper: Dictionary. Upper bounds of each parameter ''' def __init__(self, on): - self.on = bool(on) + ''' + :param on: Whether this paramter should be sampled + :type on: bool + ''' + self._on = bool(on) def num_params(self, kwargs_fixed): ''' @@ -213,6 +217,10 @@ def kwargs_upper(self): return {} return self._kwargs_upper + @property + def on(self): + return self._on + class ArrayParam(ModelParamGroup): ''' @@ -228,6 +236,13 @@ class ArrayParam(ModelParamGroup): :param _kwargs_lower: Dictionary. Lower bounds of each parameter :param _kwargs_upper: Dictionary. Upper bounds of each parameter ''' + def __init__(self, on): + ''' + :param on: Whether this paramter should be sampled + :type on: bool + ''' + self._on = bool(on) + def num_params(self, kwargs_fixed): ''' Tells the number of parameters that this group samples and theri names. @@ -319,3 +334,7 @@ def kwargs_upper(self): for name, count in self.param_names.items(): out[name] = [self._kwargs_upper[name]] * count return out + + @property + def on(self): + return self._on diff --git a/lenstronomy/Sampling/special_param.py b/lenstronomy/Sampling/special_param.py index 50669bab0..7cc8e7391 100644 --- a/lenstronomy/Sampling/special_param.py +++ b/lenstronomy/Sampling/special_param.py @@ -51,7 +51,7 @@ class MassScalingParam(ArrayParam): _kwargs_lower = {'scale_factor': 0} _kwargs_upper = {'scale_factor': 1000} def __init__(self, num_scale_factor): - self.on = int(num_scale_factor) > 0 + super().__init__(on=int(num_scale_factor) > 0) self.param_names = {'scale_factor': int(num_scale_factor)} @@ -62,7 +62,7 @@ class PointSourceOffsetParam(ArrayParam): _kwargs_lower = {'delta_x_image': -1, 'delta_y_image': -1} _kwargs_upper = {'delta_x_image': 1, 'delta_y_image': 1} def __init__(self, offset, num_images): - self.on = offset and (int(num_images) > 0) + super().__init__(on=offset and (int(num_images) > 0)) self.param_names = { 'delta_x_image': int(num_images), 'delta_y_image': int(num_images), @@ -76,7 +76,7 @@ class Tau0ListParam(ArrayParam): _kwargs_lower = {'tau0_list': 0} _kwargs_upper = {'tau0_list': 1000} def __init__(self, num_tau0): - self.on = int(num_tau0) > 0 + super().__init__(on=int(num_tau0) > 0) self.param_names = {'tau0_list': int(num_tau0)} @@ -87,7 +87,7 @@ class ZSamplingParam(ArrayParam): _kwargs_lower = {'z_sampling': 0} _kwargs_upper = {'z_sampling': 1000} def __init__(self, num_z_sampling): - self.on = int(num_z_sampling) > 0 + super().__init__(on=int(num_z_sampling) > 0) self.param_names = {'z_sampling': int(num_z_sampling)} @@ -105,10 +105,8 @@ def __init__(self, params: dict): self._kwargs_lower = {} self._kwargs_upper = {} - if params: - self.on = True - else: - self.on = False + super().__init__(params) + if not self.on: return for name, array in params.items(): diff --git a/test/test_Sampling/test_param_groups.py b/test/test_Sampling/test_param_groups.py index bedba9f02..3f164196d 100644 --- a/test/test_Sampling/test_param_groups.py +++ b/test/test_Sampling/test_param_groups.py @@ -22,9 +22,6 @@ class ExampleArrayParam(ArrayParam): _kwargs_lower = {'ap1': [0], 'ap2': [0]*3} _kwargs_upper = {'ap1': [10], 'ap2': [10]*3} - def __init__(self, on): - self.on = bool(on) - class TestParamGroup(object): def setup(self): From 0d1cbc826ca6f429b65f83f021c6bc09c60af96a Mon Sep 17 00:00:00 2001 From: Jack O'Donnell Date: Tue, 23 Aug 2022 15:06:27 -0700 Subject: [PATCH 31/67] fix rest of typos --- lenstronomy/Sampling/param_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lenstronomy/Sampling/param_group.py b/lenstronomy/Sampling/param_group.py index 10b0ef951..d85628622 100644 --- a/lenstronomy/Sampling/param_group.py +++ b/lenstronomy/Sampling/param_group.py @@ -142,7 +142,7 @@ def __init__(self, on): def num_params(self, kwargs_fixed): ''' - Tells the number of parameters that this group samples and theri names. + Tells the number of parameters that this group samples and their names. :param kwargs_fixed: Dictionary of fixed arguments :type kwargs_fixed: dict @@ -245,7 +245,7 @@ def __init__(self, on): def num_params(self, kwargs_fixed): ''' - Tells the number of parameters that this group samples and theri names. + Tells the number of parameters that this group samples and their names. :param kwargs_fixed: Dictionary of fixed arguments :type kwargs_fixed: dict From 58c7af483b046d4aeb4324b708510e018278dbf9 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Tue, 23 Aug 2022 22:35:25 -0700 Subject: [PATCH 32/67] reformatted some point source parameter classes and down-graded to nautilus==0.1.0 again --- lenstronomy/PointSource/point_source_param.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lenstronomy/PointSource/point_source_param.py b/lenstronomy/PointSource/point_source_param.py index 8d6448880..84bf989e5 100644 --- a/lenstronomy/PointSource/point_source_param.py +++ b/lenstronomy/PointSource/point_source_param.py @@ -6,47 +6,49 @@ class SourcePositionParam(SingleParam): - ''' + """ Source position parameter, ra_source and dec_source - ''' + """ param_names = ['ra_source', 'dec_source'] _kwargs_lower = {'ra_source': -100, 'dec_source': -100} _kwargs_upper = {'ra_source': 100, 'dec_source': 100} class LensedPosition(ArrayParam): - ''' + """ Represents lensed positions, possibly many. ra_image and dec_image :param num_images: integer. The number of lensed positions to model. - ''' + """ _kwargs_lower = {'ra_image': -100, 'dec_image': -100, } _kwargs_upper = {'ra_image': 100, 'dec_image': 100, } + def __init__(self, num_images): - super().__init__(int(num_images) > 0) + ArrayParam.__init__(self, int(num_images) > 0) self.param_names = {'ra_image': int(num_images), 'dec_image': int(num_images)} class SourceAmp(SingleParam): - ''' + """ Source amplification - ''' + """ param_names = ['source_amp'] _kwargs_lower = {'source_amp': 0} _kwargs_upper = {'source_amp': 100} class ImageAmp(ArrayParam): - ''' + """ Observed amplification of lensed images of a point source. Can model arbitrarily many magnified images :param num_point_sources: integer. The number of lensed images without fixed magnification. - ''' + """ _kwargs_lower = {'point_amp': 0} _kwargs_upper = {'point_amp': 100} + def __init__(self, num_point_sources): - super().__init__(int(num_point_sources) > 0) + ArrayParam.__init__(self, int(num_point_sources) > 0) self.param_names = {'point_amp': int(num_point_sources)} From 9b4aa83dfc25dd3a2b362e91f5ab246ae30b9d1f Mon Sep 17 00:00:00 2001 From: sibirrer Date: Tue, 23 Aug 2022 22:37:44 -0700 Subject: [PATCH 33/67] down-graded to nautilus==0.1.0 again --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4efbf8b51..883bb3ed3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ pyyaml pyxdg h5py zeus-mcmc -nautilus-sampler>=0.2.0 +nautilus-sampler==0.1.0 schwimmbad diff --git a/setup.py b/setup.py index 07d354fde..e547a199c 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def run_tests(self): ] tests_require = ['pytest>=2.3', "mock", 'colossus==1.3.0', 'slitronomy==0.3.2', 'emcee>=3.0.0', 'dynesty', 'nestcheck', 'pymultinest', 'zeus-mcmc>=2.4.0', - 'nautilus-sampler>=0.2.0', + 'nautilus-sampler==0.1.0', ] PACKAGE_PATH = os.path.abspath(os.path.join(__file__, os.pardir)) From bec265d087a844c3550d150baf92f50fe0de5c98 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 13:39:55 -0700 Subject: [PATCH 34/67] revert nautilus testing to hopefully pass tests --- lenstronomy/Sampling/Samplers/nautilus.py | 4 ++-- test/test_Sampling/test_Samplers/test_nautilus.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lenstronomy/Sampling/Samplers/nautilus.py b/lenstronomy/Sampling/Samplers/nautilus.py index a0ea776e8..903f5bd10 100644 --- a/lenstronomy/Sampling/Samplers/nautilus.py +++ b/lenstronomy/Sampling/Samplers/nautilus.py @@ -44,8 +44,8 @@ def nautilus_sampling(self, prior_type='uniform', mpi=False, thread_count=1, ver if prior_type == 'uniform': for i in range(self._num_param): prior.add_parameter(dist=(self._lower_limit[i], self._upper_limit[i])) - assert self._num_param == prior.dimensionality() - print(self._num_param, prior.dimensionality(), 'number of param, dimensionality') + # assert self._num_param == prior.dimensionality() + # print(self._num_param, prior.dimensionality(), 'number of param, dimensionality') else: raise ValueError('prior_type %s is not supported for Nautilus wrapper.' % prior_type) # loop through prior diff --git a/test/test_Sampling/test_Samplers/test_nautilus.py b/test/test_Sampling/test_Samplers/test_nautilus.py index c15afce16..92fbae558 100644 --- a/test/test_Sampling/test_Samplers/test_nautilus.py +++ b/test/test_Sampling/test_Samplers/test_nautilus.py @@ -46,15 +46,15 @@ def test_sampler(self, import_fixture): kwargs_run_fail['prior_type'] = 'wrong' assert_raises(ValueError, sampler.nautilus_sampling, **kwargs_run_fail) - def test_prior(self): + # def test_prior(self): - num_param = 10 - from nautilus import Prior - prior = Prior() + # num_param = 10 + # from nautilus import Prior + # prior = Prior() - for i in range(num_param): - prior.add_parameter(dist=(0, 1)) - assert num_param == prior.dimensionality() + # for i in range(num_param): + # prior.add_parameter(dist=(0, 1)) + # assert num_param == prior.dimensionality() if __name__ == '__main__': From c50d63453c391f87940f5cf1c61fb242d5e259a2 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 13:56:06 -0700 Subject: [PATCH 35/67] revert nautilus testing to hopefully pass tests, added logo --- README.rst | 3 +++ docs/figures/logo_text.png | Bin 0 -> 66988 bytes lenstronomy/Sampling/Samplers/nautilus.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 docs/figures/logo_text.png diff --git a/README.rst b/README.rst index 19618f3a2..f8654b554 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,9 @@ lenstronomy - gravitational lensing software package ==================================================== +.. + .. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png + :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png .. image:: https://github.com/lenstronomy/lenstronomy/workflows/Tests/badge.svg :target: https://github.com/lenstronomy/lenstronomy/actions diff --git a/docs/figures/logo_text.png b/docs/figures/logo_text.png new file mode 100644 index 0000000000000000000000000000000000000000..ca914406a9af768eb7fddffb919e6c574580732e GIT binary patch literal 66988 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 literal 0 HcmV?d00001 diff --git a/lenstronomy/Sampling/Samplers/nautilus.py b/lenstronomy/Sampling/Samplers/nautilus.py index 903f5bd10..bf7dc97d0 100644 --- a/lenstronomy/Sampling/Samplers/nautilus.py +++ b/lenstronomy/Sampling/Samplers/nautilus.py @@ -50,7 +50,7 @@ def nautilus_sampling(self, prior_type='uniform', mpi=False, thread_count=1, ver raise ValueError('prior_type %s is not supported for Nautilus wrapper.' % prior_type) # loop through prior pool = choose_pool(mpi=mpi, processes=thread_count, use_dill=True) - sampler = Sampler(prior, likelihood=self.likelihood, pool=pool, pass_struct=False, **kwargs_nautilus) + sampler = Sampler(prior, likelihood=self.likelihood, pool=pool, **kwargs_nautilus) time_start = time.time() if one_step is True: sampler.add_bound() From 2271135780ae6d1e784c0c5e24acb2b61170459a Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 13:57:39 -0700 Subject: [PATCH 36/67] update to latest nautilus version --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 883bb3ed3..4efbf8b51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ pyyaml pyxdg h5py zeus-mcmc -nautilus-sampler==0.1.0 +nautilus-sampler>=0.2.0 schwimmbad diff --git a/setup.py b/setup.py index e547a199c..07d354fde 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def run_tests(self): ] tests_require = ['pytest>=2.3', "mock", 'colossus==1.3.0', 'slitronomy==0.3.2', 'emcee>=3.0.0', 'dynesty', 'nestcheck', 'pymultinest', 'zeus-mcmc>=2.4.0', - 'nautilus-sampler==0.1.0', + 'nautilus-sampler>=0.2.0', ] PACKAGE_PATH = os.path.abspath(os.path.join(__file__, os.pardir)) From 19ab3f19eb8412288174d23bb9b51616600b8c12 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 14:10:15 -0700 Subject: [PATCH 37/67] added logo to readme --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f8654b554..e9e30ba7a 100644 --- a/README.rst +++ b/README.rst @@ -2,9 +2,9 @@ lenstronomy - gravitational lensing software package ==================================================== -.. - .. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png - :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png + +.. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png + :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png .. image:: https://github.com/lenstronomy/lenstronomy/workflows/Tests/badge.svg :target: https://github.com/lenstronomy/lenstronomy/actions From 6d1d5c7f55b73c8396783ada8d4f65ecbe4d215a Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 14:11:07 -0700 Subject: [PATCH 38/67] changed temporarily the link to see how the logo looks --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e9e30ba7a..772044aa9 100644 --- a/README.rst +++ b/README.rst @@ -3,8 +3,8 @@ lenstronomy - gravitational lensing software package ==================================================== -.. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png - :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png +.. image:: https://raw.githubusercontent.com/sibirrer/lenstronomy/main/docs/figures/logo_text.png + :target: https://raw.githubusercontent.com/sibirrer/lenstronomy/main/docs/figures/logo_text.png .. image:: https://github.com/lenstronomy/lenstronomy/workflows/Tests/badge.svg :target: https://github.com/lenstronomy/lenstronomy/actions From 7d40a2b4fe86869a8757eb63d9b78fefd861fa2d Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 14:13:18 -0700 Subject: [PATCH 39/67] remove title and other graphics --- README.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 772044aa9..4712da69d 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,3 @@ -==================================================== -lenstronomy - gravitational lensing software package -==================================================== - .. image:: https://raw.githubusercontent.com/sibirrer/lenstronomy/main/docs/figures/logo_text.png :target: https://raw.githubusercontent.com/sibirrer/lenstronomy/main/docs/figures/logo_text.png @@ -29,6 +25,7 @@ lenstronomy - gravitational lensing software package .. image:: https://img.shields.io/badge/arXiv-1803.09746%20-yellowgreen.svg :target: https://arxiv.org/abs/1803.09746 +.. .. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/readme_fig.png :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/readme_fig.png From 9af686300b67efdfe4b6ab9ab248aa2b54574eac Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 14:14:10 -0700 Subject: [PATCH 40/67] remove other graphics --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 4712da69d..1dfa2cc82 100644 --- a/README.rst +++ b/README.rst @@ -26,8 +26,8 @@ :target: https://arxiv.org/abs/1803.09746 .. -.. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/readme_fig.png - :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/readme_fig.png + .. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/readme_fig.png + :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/readme_fig.png ``lenstronomy`` is a multi-purpose software package to model strong gravitational lenses. From 2c1a2cc6701e125955bc65a85dcbaeb0ae91ba1d Mon Sep 17 00:00:00 2001 From: sibirrer Date: Wed, 24 Aug 2022 14:16:25 -0700 Subject: [PATCH 41/67] changed links back to lenstronomy project --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1dfa2cc82..35266a40c 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -.. image:: https://raw.githubusercontent.com/sibirrer/lenstronomy/main/docs/figures/logo_text.png - :target: https://raw.githubusercontent.com/sibirrer/lenstronomy/main/docs/figures/logo_text.png +.. image:: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png + :target: https://raw.githubusercontent.com/lenstronomy/lenstronomy/main/docs/figures/logo_text.png .. image:: https://github.com/lenstronomy/lenstronomy/workflows/Tests/badge.svg :target: https://github.com/lenstronomy/lenstronomy/actions From 4b662f106aa72c6ff1a1f2a4e88d35418a6a6f00 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 26 Aug 2022 14:12:03 -0700 Subject: [PATCH 42/67] minor more informative verbose print statement in lens equation solver --- .../LensModel/Solver/lens_equation_solver.py | 3 ++- lenstronomy/Plots/plot_quasar_images.py | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lenstronomy/LensModel/Solver/lens_equation_solver.py b/lenstronomy/LensModel/Solver/lens_equation_solver.py index 3782b3588..4ad57b535 100644 --- a/lenstronomy/LensModel/Solver/lens_equation_solver.py +++ b/lenstronomy/LensModel/Solver/lens_equation_solver.py @@ -189,7 +189,8 @@ def image_position_lenstronomy(self, sourcePos_x, sourcePos_y, kwargs_lens, min_ # pixel width x_mins, y_mins, delta_map, pixel_width = self.candidate_solutions(sourcePos_x, sourcePos_y, kwargs_lens, min_distance, search_window, verbose, x_center, y_center) if verbose: - print("There are %s regions identified that could contain a solution of the lens equation" % len(x_mins)) + print("There are %s regions identified that could contain a solution of the lens equation with" + "coordinates %s and %s " % (len(x_mins), x_mins, y_mins)) if len(x_mins) < 1: return x_mins, y_mins if initial_guess_cut: diff --git a/lenstronomy/Plots/plot_quasar_images.py b/lenstronomy/Plots/plot_quasar_images.py index a4ae9c236..a8f034e80 100644 --- a/lenstronomy/Plots/plot_quasar_images.py +++ b/lenstronomy/Plots/plot_quasar_images.py @@ -23,19 +23,19 @@ def plot_quasar_images(lens_model, x_image, y_image, source_x, source_y, kwargs_ :param z_source: the source redshift :param cosmo: (optional) an instance of astropy.cosmology; if not specified, a default cosmology will be used :param grid_resolution: the grid resolution in units arcsec/pixel; if not specified, an appropriate value will - be estimated from the source size + be estimated from the source size :param grid_radius_arcsec: (optional) the size of the ray tracing region in arcsec; if not specified, an appropriate value - will be estimated from the source size + will be estimated from the source size :param source_light_model: the model for background source light; currently implemented are 'SINGLE_GAUSSIAN' and - 'DOUBLE_GAUSSIAN'. + 'DOUBLE_GAUSSIAN'. :param dx: used with source model 'DOUBLE_GAUSSIAN', the offset of the second source light profile from the first - [arcsec] + [arcsec] :param dy: used with source model 'DOUBLE_GAUSSIAN', the offset of the second source light profile from the first - [arcsec] + [arcsec] :param size_scale: used with source model 'DOUBLE_GAUSSIAN', the size of the second source light profile relative - to the first + to the first :param amp_scale: used with source model 'DOUBLE_GAUSSIAN', the peak brightness of the second source light profile - relative to the first + relative to the first :return: Four images of the background source in the image plane """ From a12ac12266e0d413d1f573403fb6cf617ee1d502 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 26 Aug 2022 14:29:59 -0700 Subject: [PATCH 43/67] local minima search simplified with more candidates possible --- .../LensModel/Solver/lens_equation_solver.py | 2 +- lenstronomy/Util/util.py | 36 ++++++++++++++++++- test/test_Util/test_util.py | 15 ++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/lenstronomy/LensModel/Solver/lens_equation_solver.py b/lenstronomy/LensModel/Solver/lens_equation_solver.py index 4ad57b535..7d259511c 100644 --- a/lenstronomy/LensModel/Solver/lens_equation_solver.py +++ b/lenstronomy/LensModel/Solver/lens_equation_solver.py @@ -100,7 +100,7 @@ def candidate_solutions(self, sourcePos_x, sourcePos_y, kwargs_lens, min_distanc absmapped = util.displaceAbs(x_mapped, y_mapped, sourcePos_x, sourcePos_y) # select minima in the grid points and select grid points that do not deviate more than the # width of the grid point to a solution of the lens equation - x_mins, y_mins, delta_map = util.neighborSelect(absmapped, x_grid, y_grid) + x_mins, y_mins, delta_map = util.local_minima_2d(absmapped, x_grid, y_grid) # pixel width pixel_width = x_grid[1]-x_grid[0] diff --git a/lenstronomy/Util/util.py b/lenstronomy/Util/util.py index 1572d707e..e1d7b1fbb 100644 --- a/lenstronomy/Util/util.py +++ b/lenstronomy/Util/util.py @@ -485,6 +485,40 @@ def points_on_circle(radius, num_points, connect_ends=True): y_coord = np.sin(angle) * radius return x_coord, y_coord +@export +@jit() +def local_minima_2d(a, x, y): + """ + finds (local) minima in a 2d grid + applies less rigid criteria for maximum without second-order tangential minima criteria + + :param a: 1d array of displacements from the source positions + :type a: numpy array with length numPix**2 in float + :param x: 1d coordinate grid in x-direction + :type x: numpy array with length numPix**2 in float + :param y: 1d coordinate grid in x-direction + :type y: numpy array with length numPix**2 in float + :returns: array of indices of local minima, values of those minima + :raises: AttributeError, KeyError + """ + dim = int(np.sqrt(len(a))) + values = [] + x_mins = [] + y_mins = [] + for i in range(dim + 1, len(a) - dim - 1): + if (a[i] < a[i - 1] + and a[i] < a[i + 1] + and a[i] < a[i - dim] + and a[i] < a[i + dim] + and a[i] < a[i - (dim - 1)] + and a[i] < a[i - (dim + 1)] + and a[i] < a[i + (dim - 1)] + and a[i] < a[i + (dim + 1)]): + x_mins.append(x[i]) + y_mins.append(y[i]) + values.append(a[i]) + return np.array(x_mins), np.array(y_mins), np.array(values) + @export @jit() @@ -545,7 +579,7 @@ def neighborSelect(a, x, y): def fwhm2sigma(fwhm): """ - :param fwhm: full-widt-half-max value + :param fwhm: full-width-half-max value :return: gaussian sigma (sqrt(var)) """ sigma = fwhm / (2 * np.sqrt(2 * np.log(2))) diff --git a/test/test_Util/test_util.py b/test/test_Util/test_util.py index 9ca203e52..624e617fb 100644 --- a/test/test_Util/test_util.py +++ b/test/test_Util/test_util.py @@ -322,11 +322,22 @@ def test_min_square_dist(): assert dist[1] == 1 +def test_neighbor_select_fast(): + a = np.ones(100) + a[41] = 0 + x = np.linspace(0, 99, 100) + y = np.linspace(0, 99, 100) + x_mins, y_mins, values = util.local_minima_2d(a, x, y) + assert x_mins[0] == 41 + assert y_mins[0] == 41 + assert values[0] == 0 + + def test_neighborSelect(): a = np.ones(100) a[41] = 0 - x = np.linspace(0,99,100) - y = np.linspace(0,99,100) + x = np.linspace(0, 99, 100) + y = np.linspace(0, 99, 100) x_mins, y_mins, values = util.neighborSelect(a, x, y) assert x_mins[0] == 41 assert y_mins[0] == 41 From 8f1af8e2066058cd48a3953773918f144d4247b3 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 26 Aug 2022 18:09:17 -0700 Subject: [PATCH 44/67] dynesty sampler less sampling around the minima for tests --- lenstronomy/LensModel/Solver/lens_equation_solver.py | 2 +- test/test_Workflow/test_fitting_sequence.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lenstronomy/LensModel/Solver/lens_equation_solver.py b/lenstronomy/LensModel/Solver/lens_equation_solver.py index 7d259511c..64f0931cd 100644 --- a/lenstronomy/LensModel/Solver/lens_equation_solver.py +++ b/lenstronomy/LensModel/Solver/lens_equation_solver.py @@ -124,7 +124,7 @@ def image_position_analytical(self, x, y, kwargs_lens, arrival_time_sort=True, m """ lens_model_list = list(self.lensModel.lens_model_list) if lens_model_list not in (['SIE', 'SHEAR'], ['SIE'], ['EPL_NUMBA', 'SHEAR'], ['EPL_NUMBA'], ['EPL', 'SHEAR'], ['EPL']): - raise ValueError("Only SIE or PEMD (+shear) supported in the analytical solver for now") + raise ValueError("Only SIE, EPL, EPL_NUMBA (+shear) supported in the analytical solver for now.") x_mins, y_mins = solve_lenseq_pemd((x, y), kwargs_lens, **kwargs_solver) if arrival_time_sort: diff --git a/test/test_Workflow/test_fitting_sequence.py b/test/test_Workflow/test_fitting_sequence.py index b5036e5b8..86cdbd0de 100644 --- a/test/test_Workflow/test_fitting_sequence.py +++ b/test/test_Workflow/test_fitting_sequence.py @@ -278,7 +278,6 @@ def test_zeus(self): chain_list = fittingSequence.fit_sequence(fitting_list) - def test_multinest(self): # Nested sampler tests # further decrease the parameter space for nested samplers to run faster @@ -321,8 +320,9 @@ def test_multinest(self): assert kwargs_out['kwargs_lens'] == 1 def test_dynesty(self): + np.random.seed(42) kwargs_params = copy.deepcopy(self.kwargs_params) - kwargs_params['lens_model'][0][0]['theta_E'] += 0.01 + kwargs_params['lens_model'][0][0]['theta_E'] += 0.2 fittingSequence = FittingSequence(self.kwargs_data_joint, self.kwargs_model, self.kwargs_constraints, self.kwargs_likelihood, kwargs_params) @@ -341,6 +341,7 @@ def test_dynesty(self): chain_list = fittingSequence.fit_sequence(fitting_list) def test_nautilus(self): + np.random.seed(42) kwargs_params = copy.deepcopy(self.kwargs_params) fittingSequence = FittingSequence(self.kwargs_data_joint, self.kwargs_model, self.kwargs_constraints, self.kwargs_likelihood, kwargs_params) From 8889928db82f8cb91bbfb4979147668b0b9efbab Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 26 Aug 2022 18:28:21 -0700 Subject: [PATCH 45/67] zeus sampler potentially more stable --- .../LensModel/Solver/epl_shear_solver.py | 4 ++++ .../test_Solver/test_lens_equation_solver.py | 18 ++++++++++++++++++ test/test_Workflow/test_fitting_sequence.py | 9 ++++----- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lenstronomy/LensModel/Solver/epl_shear_solver.py b/lenstronomy/LensModel/Solver/epl_shear_solver.py index 68b6762b0..a6fe046dc 100644 --- a/lenstronomy/LensModel/Solver/epl_shear_solver.py +++ b/lenstronomy/LensModel/Solver/epl_shear_solver.py @@ -88,6 +88,7 @@ def _getphi(thpl, args): """ Finds all roots to both versions the 1-dimensional lens equation in phi, by doing a grid search for sign changes on the supplied thpl. In the case of extrema, refine at the relevant location. + :param thpl: What points to calculate the equation on use for detecting sign changes :param args: Parameters to be passed to the lens equation :return: an array containing all roots @@ -163,6 +164,9 @@ def _check_center(kwargs_lens): if kwargs_lens[1]['ra_0'] != kwargs_lens[0]['center_x'] or kwargs_lens[1]['dec_0'] != kwargs_lens[0]['center_y']: raise ValueError("Center of lens (center_{x,y}) must be the same as center of shear ({ra,dec}_0). " "This can be ensured by supplying a dictionary-style joint_setting_list to the model.") + # TODO: calculate (inverse) displacement caused by the offset between shear and lens centroid + # this shift needs to be added to the source position such that the solution of the lens equation + # without this shift in the shear is the correct one def solve_lenseq_pemd(pos_, kwargs_lens, Nmeas=400, Nmeas_extra=80, **kwargs): diff --git a/test/test_LensModel/test_Solver/test_lens_equation_solver.py b/test/test_LensModel/test_Solver/test_lens_equation_solver.py index 90c677f50..37c0dacd2 100644 --- a/test/test_LensModel/test_Solver/test_lens_equation_solver.py +++ b/test/test_LensModel/test_Solver/test_lens_equation_solver.py @@ -145,6 +145,24 @@ def test_analytical_lens_equation_solver(self): for x, y in zip(x_pos_ls, y_pos_ls): # Check if it found all solutions lenstronomy found assert np.sqrt((x-x_pos)**2+(y-y_pos)**2).min() < 1e-8 + # here we test with shear and mass profile centroids not aligned + """ + + lensModel = LensModel(['EPL_NUMBA', 'SHEAR']) + lensEquationSolver = LensEquationSolver(lensModel) + sourcePos_x = 0.03 + sourcePos_y = 0.0 + kwargs_lens = [{'theta_E': 1., 'gamma': 2.2, 'center_x': 0.01, 'center_y': 0.02, 'e1': 0.01, 'e2': 0.05}, + {'gamma1': -0.04, 'gamma2': -0.1, 'ra_0': 0.0, 'dec_0': 0.0}] + + x_pos, y_pos = lensEquationSolver.image_position_from_source(sourcePos_x, sourcePos_y, kwargs_lens, + solver='analytical') + source_x, source_y = lensModel.ray_shooting(x_pos, y_pos, kwargs_lens) + assert len(source_x) == len(source_y) >= 4 + npt.assert_almost_equal(sourcePos_x, source_x, decimal=10) + npt.assert_almost_equal(sourcePos_y, source_y, decimal=10) + """ + def test_caustics(self): lm = LensModel(['EPL_NUMBA', 'SHEAR']) leqs = LensEquationSolver(lm) diff --git a/test/test_Workflow/test_fitting_sequence.py b/test/test_Workflow/test_fitting_sequence.py index 86cdbd0de..ed6f983ac 100644 --- a/test/test_Workflow/test_fitting_sequence.py +++ b/test/test_Workflow/test_fitting_sequence.py @@ -198,6 +198,7 @@ def test_fitting_sequence(self): assert kwargs_set['kwargs_ps'][0]['ra_source'] == 0.007 def test_zeus(self): + np.random.seed(42) # we make a very basic lens+source model to feed to check zeus can be run through fitting sequence # we don't use the kwargs defined in setup() as those are modified during the tests; using unique kwargs here is safer @@ -214,8 +215,6 @@ def test_zeus(self): data_class = ImageData(**kwargs_data) kwargs_psf_gaussian = {'psf_type': 'GAUSSIAN', 'fwhm': fwhm, 'pixel_size': deltaPix, 'truncation': 3} psf_gaussian = PSF(**kwargs_psf_gaussian) - kwargs_psf = {'psf_type': 'PIXEL', 'kernel_point_source': psf_gaussian.kernel_point_source, 'psf_error_map': np.zeros_like(psf_gaussian.kernel_point_source)} - psf_class = PSF(**kwargs_psf) # make a lens lens_model_list = ['EPL'] @@ -232,7 +231,7 @@ def test_zeus(self): kwargs_numerics = {'supersampling_factor': 1, 'supersampling_convolution': False} - imageModel = ImageModel(data_class, psf_class, lens_model_class, source_model_class, kwargs_numerics=kwargs_numerics) + imageModel = ImageModel(data_class, psf_gaussian, lens_model_class, source_model_class, kwargs_numerics=kwargs_numerics) image_sim = sim_util.simulate_simple(imageModel, kwargs_lens, kwargs_source) data_class.update_data(image_sim) @@ -260,12 +259,12 @@ def test_zeus(self): kwargs_constraints = {} - multi_band_list = [[kwargs_data, kwargs_psf, kwargs_numerics]] + multi_band_list = [[kwargs_data, kwargs_psf_gaussian, kwargs_numerics]] kwargs_data_joint = {'multi_band_list': multi_band_list, 'multi_band_type': 'multi-linear'} - kwargs_likelihood = {'source_marg': True} + kwargs_likelihood = {'source_marg': False} fittingSequence = FittingSequence(kwargs_data_joint, kwargs_model, kwargs_constraints, kwargs_likelihood, From 79243137d51a69c4dae1fd93e9ebe9e5405fa02a Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 26 Aug 2022 19:05:56 -0700 Subject: [PATCH 46/67] added non-aligned shear and mass profile centroid in analytic solver solution --- .../LensModel/Solver/epl_shear_solver.py | 26 ++++++++++++------- .../test_Solver/test_lens_equation_solver.py | 13 ++++------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lenstronomy/LensModel/Solver/epl_shear_solver.py b/lenstronomy/LensModel/Solver/epl_shear_solver.py index a6fe046dc..55d16bc9f 100644 --- a/lenstronomy/LensModel/Solver/epl_shear_solver.py +++ b/lenstronomy/LensModel/Solver/epl_shear_solver.py @@ -7,6 +7,7 @@ from lenstronomy.LensModel.Profiles.epl_numba import alpha, omega from lenstronomy.Util.numba_util import jit from lenstronomy.Util.param_util import ellipticity2phi_q, shear_cartesian2polar, shear_polar2cartesian +from lenstronomy.LensModel.Profiles.shear import Shear @jit() @@ -161,13 +162,15 @@ def solvelenseq_majoraxis(args, Nmeas=200, Nmeas_extra=50): def _check_center(kwargs_lens): """Checks if the shear-at-center convention is properly used.""" - if kwargs_lens[1]['ra_0'] != kwargs_lens[0]['center_x'] or kwargs_lens[1]['dec_0'] != kwargs_lens[0]['center_y']: - raise ValueError("Center of lens (center_{x,y}) must be the same as center of shear ({ra,dec}_0). " - "This can be ensured by supplying a dictionary-style joint_setting_list to the model.") - # TODO: calculate (inverse) displacement caused by the offset between shear and lens centroid + # calculate (inverse) displacement caused by the offset between shear and lens centroid # this shift needs to be added to the source position such that the solution of the lens equation # without this shift in the shear is the correct one + shear = Shear() + # calculate shift from the deflector centroid from the shear field + alpha_x, alpha_y = shear.derivatives(kwargs_lens[0]['center_x'], kwargs_lens[0]['center_y'], **kwargs_lens[1]) + return alpha_x, alpha_y + def solve_lenseq_pemd(pos_, kwargs_lens, Nmeas=400, Nmeas_extra=80, **kwargs): """ @@ -186,14 +189,16 @@ def solve_lenseq_pemd(pos_, kwargs_lens, Nmeas=400, Nmeas_extra=80, **kwargs): theta_ell, q = ellipticity2phi_q(kwargs_lens[0]['e1'], kwargs_lens[0]['e2']) b = kwargs_lens[0]['theta_E']*np.sqrt(q) - - cen = kwargs_lens[0]['center_x']+1j*kwargs_lens[0]['center_y'] - p = pos[0]+1j*pos[1]-cen if len(kwargs_lens) > 1: gamma = kwargs_lens[1]['gamma1']+1j*kwargs_lens[1]['gamma2'] - _check_center(kwargs_lens) + shift_x, shift_y = _check_center(kwargs_lens) else: gamma = 0+0j + shift_x, shift_y = 0, 0 + shift = shift_x + 1j * shift_y + cen = kwargs_lens[0]['center_x']+1j*kwargs_lens[0]['center_y'] + p = pos[0]+1j*pos[1] - cen + shift + rotfact = np.exp(-1j*theta_ell) gamma *= rotfact**2 p *= rotfact @@ -222,12 +227,13 @@ def caustics_epl_shear(kwargs_lens, num_th=500, maginf=0, sourceplane=True, retu gamma1unr, gamma2unr = kwargs_lens[1]['gamma1'], kwargs_lens[1]['gamma2'] else: gamma1unr, gamma2unr = 0, 0 - _check_center(kwargs_lens) + shift_x, shift_y = _check_center(kwargs_lens) t = kwargs_lens[0]['gamma']-1 if 'gamma' in kwargs_lens[0] else 1 theta_ell, q = ellipticity2phi_q(e1, e2) theta_gamma, gamma_mag = shear_cartesian2polar(gamma1unr, gamma2unr) b = np.sqrt(q)*kwargs_lens[0]['theta_E'] - cen = np.expand_dims(np.array([kwargs_lens[0]['center_x'], kwargs_lens[0]['center_y']]), 1) + # TODO: check whether shear shift is applied in the correct direction + cen = np.expand_dims(np.array([kwargs_lens[0]['center_x']-shift_x, kwargs_lens[0]['center_y']-shift_y]), 1) theta_gamma -= theta_ell gamma1, gamma2 = shear_polar2cartesian(theta_gamma, gamma_mag) M = rotmat(-theta_ell) diff --git a/test/test_LensModel/test_Solver/test_lens_equation_solver.py b/test/test_LensModel/test_Solver/test_lens_equation_solver.py index 37c0dacd2..69f4565de 100644 --- a/test/test_LensModel/test_Solver/test_lens_equation_solver.py +++ b/test/test_LensModel/test_Solver/test_lens_equation_solver.py @@ -146,22 +146,19 @@ def test_analytical_lens_equation_solver(self): assert np.sqrt((x-x_pos)**2+(y-y_pos)**2).min() < 1e-8 # here we test with shear and mass profile centroids not aligned - """ - lensModel = LensModel(['EPL_NUMBA', 'SHEAR']) lensEquationSolver = LensEquationSolver(lensModel) sourcePos_x = 0.03 sourcePos_y = 0.0 kwargs_lens = [{'theta_E': 1., 'gamma': 2.2, 'center_x': 0.01, 'center_y': 0.02, 'e1': 0.01, 'e2': 0.05}, - {'gamma1': -0.04, 'gamma2': -0.1, 'ra_0': 0.0, 'dec_0': 0.0}] + {'gamma1': -0.04, 'gamma2': -0.1, 'ra_0': 1.0, 'dec_0': 1.0}] x_pos, y_pos = lensEquationSolver.image_position_from_source(sourcePos_x, sourcePos_y, kwargs_lens, solver='analytical') source_x, source_y = lensModel.ray_shooting(x_pos, y_pos, kwargs_lens) - assert len(source_x) == len(source_y) >= 4 + assert len(source_x) == len(source_y) >= 2 npt.assert_almost_equal(sourcePos_x, source_x, decimal=10) npt.assert_almost_equal(sourcePos_y, source_y, decimal=10) - """ def test_caustics(self): lm = LensModel(['EPL_NUMBA', 'SHEAR']) @@ -231,9 +228,9 @@ def test_assertions(self): with pytest.raises(ValueError): lensEquationSolver.image_position_from_source(0.1, 0., kwargs_lens, solver='nonexisting') - with pytest.raises(ValueError): - kwargs_lens[1]['ra_0']=0.1 - lensEquationSolver.image_position_from_source(0.1, 0., kwargs_lens, solver='analytical') + # with pytest.raises(ValueError): + # kwargs_lens[1]['ra_0']=0.1 + # lensEquationSolver.image_position_from_source(0.1, 0., kwargs_lens, solver='analytical') From 76e150b9ecc1b4c016079c20f29860b7cfed94ef Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 26 Aug 2022 19:36:58 -0700 Subject: [PATCH 47/67] analytical caustics with shear offset debugged --- .../LensModel/Solver/epl_shear_solver.py | 19 +++++++++---------- .../test_Solver/test_lens_equation_solver.py | 10 +++------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lenstronomy/LensModel/Solver/epl_shear_solver.py b/lenstronomy/LensModel/Solver/epl_shear_solver.py index 55d16bc9f..c6fc69928 100644 --- a/lenstronomy/LensModel/Solver/epl_shear_solver.py +++ b/lenstronomy/LensModel/Solver/epl_shear_solver.py @@ -165,11 +165,13 @@ def _check_center(kwargs_lens): # calculate (inverse) displacement caused by the offset between shear and lens centroid # this shift needs to be added to the source position such that the solution of the lens equation # without this shift in the shear is the correct one - - shear = Shear() - # calculate shift from the deflector centroid from the shear field - alpha_x, alpha_y = shear.derivatives(kwargs_lens[0]['center_x'], kwargs_lens[0]['center_y'], **kwargs_lens[1]) - return alpha_x, alpha_y + if len(kwargs_lens) > 1: + shear = Shear() + # calculate shift from the deflector centroid from the shear field + alpha_x, alpha_y = shear.derivatives(kwargs_lens[0]['center_x'], kwargs_lens[0]['center_y'], **kwargs_lens[1]) + return alpha_x, alpha_y + else: + return 0, 0 def solve_lenseq_pemd(pos_, kwargs_lens, Nmeas=400, Nmeas_extra=80, **kwargs): @@ -191,10 +193,9 @@ def solve_lenseq_pemd(pos_, kwargs_lens, Nmeas=400, Nmeas_extra=80, **kwargs): b = kwargs_lens[0]['theta_E']*np.sqrt(q) if len(kwargs_lens) > 1: gamma = kwargs_lens[1]['gamma1']+1j*kwargs_lens[1]['gamma2'] - shift_x, shift_y = _check_center(kwargs_lens) else: gamma = 0+0j - shift_x, shift_y = 0, 0 + shift_x, shift_y = _check_center(kwargs_lens) shift = shift_x + 1j * shift_y cen = kwargs_lens[0]['center_x']+1j*kwargs_lens[0]['center_y'] p = pos[0]+1j*pos[1] - cen + shift @@ -227,13 +228,11 @@ def caustics_epl_shear(kwargs_lens, num_th=500, maginf=0, sourceplane=True, retu gamma1unr, gamma2unr = kwargs_lens[1]['gamma1'], kwargs_lens[1]['gamma2'] else: gamma1unr, gamma2unr = 0, 0 - shift_x, shift_y = _check_center(kwargs_lens) t = kwargs_lens[0]['gamma']-1 if 'gamma' in kwargs_lens[0] else 1 theta_ell, q = ellipticity2phi_q(e1, e2) theta_gamma, gamma_mag = shear_cartesian2polar(gamma1unr, gamma2unr) b = np.sqrt(q)*kwargs_lens[0]['theta_E'] - # TODO: check whether shear shift is applied in the correct direction - cen = np.expand_dims(np.array([kwargs_lens[0]['center_x']-shift_x, kwargs_lens[0]['center_y']-shift_y]), 1) + cen = np.expand_dims(np.array([kwargs_lens[0]['center_x'], kwargs_lens[0]['center_y']]), 1) theta_gamma -= theta_ell gamma1, gamma2 = shear_polar2cartesian(theta_gamma, gamma_mag) M = rotmat(-theta_ell) diff --git a/test/test_LensModel/test_Solver/test_lens_equation_solver.py b/test/test_LensModel/test_Solver/test_lens_equation_solver.py index 69f4565de..b45b88e68 100644 --- a/test/test_LensModel/test_Solver/test_lens_equation_solver.py +++ b/test/test_LensModel/test_Solver/test_lens_equation_solver.py @@ -151,7 +151,7 @@ def test_analytical_lens_equation_solver(self): sourcePos_x = 0.03 sourcePos_y = 0.0 kwargs_lens = [{'theta_E': 1., 'gamma': 2.2, 'center_x': 0.01, 'center_y': 0.02, 'e1': 0.01, 'e2': 0.05}, - {'gamma1': -0.04, 'gamma2': -0.1, 'ra_0': 1.0, 'dec_0': 1.0}] + {'gamma1': -0.04, 'gamma2': -0.1, 'ra_0': 0.0, 'dec_0': 1.0}] x_pos, y_pos = lensEquationSolver.image_position_from_source(sourcePos_x, sourcePos_y, kwargs_lens, solver='analytical') @@ -174,7 +174,8 @@ def test_caustics(self): lensplane_cut = caustics_epl_shear(kwargs, return_which='cut', sourceplane=False) twoimg = caustics_epl_shear(kwargs, return_which='double') fourimg = caustics_epl_shear(kwargs, return_which='quad') - assert np.abs(lm.magnification(*lensplane_caus, kwargs)).min() > 1e12 + min_mag = np.abs(lm.magnification(*lensplane_caus, kwargs)).min() + assert min_mag > 1e12 assert np.abs(lm.magnification(*lensplane_cut, kwargs)).min() > 1e12 # Test whether the caustics indeed the number of images they say @@ -228,11 +229,6 @@ def test_assertions(self): with pytest.raises(ValueError): lensEquationSolver.image_position_from_source(0.1, 0., kwargs_lens, solver='nonexisting') - # with pytest.raises(ValueError): - # kwargs_lens[1]['ra_0']=0.1 - # lensEquationSolver.image_position_from_source(0.1, 0., kwargs_lens, solver='analytical') - - if __name__ == '__main__': pytest.main() From be3a1cee455864c7e21da10b3c383bc44d807067 Mon Sep 17 00:00:00 2001 From: mattgomer Date: Wed, 31 Aug 2022 14:00:39 +0200 Subject: [PATCH 48/67] Created ProductAvg version of CSE class, NFW_CSE mismatch now only 2% instead of 30% (at q=0.3). Remainder comes from s --- .../Profiles/cored_steep_ellipsoid.py | 142 +++++++++++++++++- .../LensModel/Profiles/nfw_ellipse_cse.py | 8 +- .../test_cored_steep_ellipsoid.py | 20 ++- .../test_Profiles/test_nfw_ellipse_cse.py | 20 +-- 4 files changed, 167 insertions(+), 23 deletions(-) diff --git a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py index 65b59f970..24dbe0611 100644 --- a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py +++ b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py @@ -11,6 +11,7 @@ class CSE(LensProfileBase): """ Cored steep ellipsoid (CSE) + :param axis: 'major' or 'product_avg' ; whether to evaluate corresponding to r= major axis or r= sqrt(ab) source: Keeton and Kochanek (1998) Oguri 2021: https://arxiv.org/pdf/2106.11464.pdf @@ -28,8 +29,13 @@ class CSE(LensProfileBase): lower_limit_default = {'A': -1000, 's': 0, 'e1': -0.5, 'e2': -0.5, 'center_x': -100, 'center_y': -100} upper_limit_default = {'A': 1000, 's': 10000, 'e1': 0.5, 'e2': 0.5, 'center_x': -100, 'center_y': -100} - def __init__(self): - self.major_axis_model = CSEMajorAxis() + def __init__(self, axis='product_avg'): + if axis=='major': + self.major_axis_model = CSEMajorAxis() + elif axis=='product_avg': + self.major_axis_model = CSEProductAvg() + else: + raise ValueError("axis must be set to'major' or 'product_avg'") super(CSE, self).__init__() def function(self, x, y, a, s, e1, e2, center_x, center_y): @@ -251,3 +257,135 @@ def hessian(self, x, y, a_list, s_list, q): f_xy += f_xy_ f_yy += f_yy_ return f_xx, f_xy, f_xy, f_yy + +class CSEProductAvg(LensProfileBase): + """ + Cored steep ellipsoid (CSE) evaluated at the product-averaged radius sqrt(ab), + such that mass is not changed when increasing ellipticity + + Same as CSEMajorAxis but evalulated at r=sqrt(q)*r_original + + """ + param_names = ['A', 's', 'q', 'center_x', 'center_y'] + lower_limit_default = {'A': -1000, 's': 0, 'q': 0.001, 'center_x': -100, 'center_y': -100} + upper_limit_default = {'A': 1000, 's': 10000, 'q': 0.99999, 'e2': 0.5, 'center_x': -100, 'center_y': -100} + + def function(self, x, y, a, s, q): + """ + + :param x: coordinate in image plane (angle) + :param y: coordinate in image plane (angle) + :param a: lensing strength + :param s: core radius + :param q: axis ratio + :return: lensing potential + """ + x = x * np.sqrt(q) + y = y * np.sqrt(q) + # potential calculation + psi = np.sqrt(q**2*(s**2 + x**2) + y**2) + Phi = (psi + s)**2 + (1-q**2) * x**2 + phi = q/(2*s) * np.log(Phi) - q/s * np.log((1+q) * s) + return a * phi + + def derivatives(self, x, y, a, s, q): + """ + + :param x: coordinate in image plane (angle) + :param y: coordinate in image plane (angle) + :param a: lensing strength + :param s: core radius + :param q: axis ratio + :return: deflection in x- and y-direction + """ + x = x * np.sqrt(q) + y = y * np.sqrt(q) + + psi = np.sqrt(q ** 2 * (s ** 2 + x ** 2) + y ** 2) + Phi = (psi + s) ** 2 + (1 - q ** 2) * x ** 2 + f_x = q * x * (psi + q**2*s) / (s * psi * Phi) + f_y = q * y * (psi + s) / (s * psi * Phi) + + return a * f_x, a * f_y + + def hessian(self, x, y, a, s, q): + """ + + :param x: coordinate in image plane (angle) + :param y: coordinate in image plane (angle) + :param a: lensing strength + :param s: core radius + :param q: axis ratio + :return: hessian elements f_xx, f_xy, f_yx, f_yy + """ + x = x * np.sqrt(q) + y = y * np.sqrt(q) + + # equations 21-23 in Oguri 2021 + psi = np.sqrt(q ** 2 * (s ** 2 + x ** 2) + y ** 2) + Phi = (psi + s) ** 2 + (1 - q ** 2) * x ** 2 + f_xx = q/(s * Phi) * (1 + q**2*s*(q**2 * s**2 + y**2)/psi**3 - 2*x**2*(psi + q**2*s)**2/(psi**2 * Phi)) + f_yy = q/(s * Phi) * (1 + q**2 * s * (s**2 + x**2)/psi**3 - 2*y**2*(psi + s)**2/(psi**2 * Phi)) + f_xy = - q * x*y / (s * Phi) * (q**2 * s / psi**3 + 2 * (psi + q**2*s) * (psi + s) / (psi**2 * Phi)) + + return a * f_xx, a * f_xy, a * f_xy, a * f_yy + + +class CSEProductAvgSet(LensProfileBase): + """ + a set of CSE profiles along a joint center and axis + """ + + def __init__(self): + self.major_axis_model = CSEProductAvg() + super(CSEProductAvgSet, self).__init__() + + def function(self, x, y, a_list, s_list, q): + """ + + :param x: coordinate in image plane (angle) + :param y: coordinate in image plane (angle) + :param a_list: list of lensing strength + :param s_list: list of core radius + :param q: axis ratio + :return: lensing potential + """ + f_ = np.zeros_like(x) + for a, s in zip(a_list, s_list): + f_ += self.major_axis_model.function(x, y, a, s, q) + return f_ + + def derivatives(self, x, y, a_list, s_list, q): + """ + + :param x: coordinate in image plane (angle) + :param y: coordinate in image plane (angle) + :param a_list: list of lensing strength + :param s_list: list of core radius + :param q: axis ratio + :return: deflection in x- and y-direction + """ + f_x, f_y = np.zeros_like(x), np.zeros_like(y) + for a, s in zip(a_list, s_list): + f_x_, f_y_ = self.major_axis_model.derivatives(x, y, a, s, q) + f_x += f_x_ + f_y += f_y_ + return f_x, f_y + + def hessian(self, x, y, a_list, s_list, q): + """ + + :param x: coordinate in image plane (angle) + :param y: coordinate in image plane (angle) + :param a_list: list of lensing strength + :param s_list: list of core radius + :param q: axis ratio + :return: hessian elements f_xx, f_xy, f_yx, f_yy + """ + f_xx, f_xy, f_yy = np.zeros_like(x), np.zeros_like(x), np.zeros_like(x) + for a, s in zip(a_list, s_list): + f_xx_, f_xy_, _, f_yy_ = self.major_axis_model.hessian(x, y, a, s, q) + f_xx += f_xx_ + f_xy += f_xy_ + f_yy += f_yy_ + return f_xx, f_xy, f_xy, f_yy diff --git a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py index 64e2e65a0..1f0f2751f 100644 --- a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py +++ b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py @@ -4,6 +4,7 @@ from lenstronomy.Util import util from lenstronomy.LensModel.Profiles.nfw import NFW from lenstronomy.LensModel.Profiles.nfw_ellipse import NFW_ELLIPSE +from lenstronomy.LensModel.Profiles.cored_steep_ellipsoid import CSEProductAvgSet from lenstronomy.LensModel.Profiles.cored_steep_ellipsoid import CSEMajorAxisSet import lenstronomy.Util.param_util as param_util @@ -30,7 +31,8 @@ def __init__(self, high_accuracy=True): :param high_accuracy: boolean, if True uses a more accurate larger set of CSE profiles (see Oguri 2021) """ - self.cse_major_axis_set = CSEMajorAxisSet() + # self.cse_major_axis_set = CSEMajorAxisSet() + self.cse_major_axis_set = CSEProductAvgSet() self.nfw = NFW() if high_accuracy is True: # Table 1 in Oguri 2021 @@ -168,6 +170,6 @@ def _normalization(self, alpha_Rs, Rs, q): """ rho0 = self.nfw.alpha2rho0(alpha_Rs, Rs) c = 2 * q / (1 + q) # this is the same as 1 - e with e = (1. - q) / (1. + q) - rs_ = Rs # / np.sqrt(q) - const = 4 * rho0 * rs_ ** 3 / c + rs_ = Rs #/ np.sqrt(q) + const = 4 * rho0 * rs_ ** 3 #/ c return const diff --git a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py index 0b76f3233..aeefc4db2 100644 --- a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py +++ b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py @@ -4,6 +4,7 @@ import numpy as np import pytest import numpy.testing as npt +from lenstronomy.Util import param_util class TestCSP(object): @@ -12,7 +13,7 @@ class TestCSP(object): """ def setup(self): from lenstronomy.LensModel.Profiles.cored_steep_ellipsoid import CSE - self.CSP = CSE() + self.CSP = CSE(axis='product_avg') def test_function(self): @@ -48,7 +49,7 @@ def test_ellipticity(self): test the definition of the ellipticity normalization (along major axis or product averaged axes) """ x, y = np.linspace(start=0.001, stop=10, num=100), np.zeros(100) - kwargs_round = {'a': 2, 's': 1, 'e1': 0.3, 'e2': 0., 'center_x': 0, 'center_y': 0} + kwargs_round = {'a': 2, 's': 1, 'e1': 0., 'e2': 0., 'center_x': 0, 'center_y': 0} kwargs = {'a': 2, 's': 1, 'e1': 0.3, 'e2': 0., 'center_x': 0, 'center_y': 0} f_xx, f_xy, f_yx, f_yy = self.CSP.hessian(x, y, **kwargs_round) @@ -60,16 +61,19 @@ def test_ellipticity(self): f_xx, f_xy, f_yx, f_yy = self.CSP.hessian(y, x, **kwargs) kappa_minor = 1. / 2 * (f_xx + f_yy) - npt.assert_almost_equal(kappa_major, kappa_round, decimal=4) - - # import matplotlib.pyplot as plt + import matplotlib.pyplot as plt # plt.plot(x, kappa_round, ':', label='round', alpha=0.5) # plt.plot(x, kappa_major, ',-', label='major', alpha=0.5) # plt.plot(x, kappa_minor, '--', label='minor', alpha=0.5) - # plt.legend() - # plt.show() - # assert 1 == 0 + # plt.plot(x, np.sqrt(kappa_minor*kappa_major), '-', label='square', alpha=0.5) + + plt.plot(x, np.sqrt(kappa_minor*kappa_major)/kappa_round,label='prod/kappa_round') + plt.legend() + plt.show() + npt.assert_almost_equal(kappa_round,np.sqrt(kappa_minor*kappa_major), decimal=5) + #I think I need to modify s too +#test that it breaks if product average or major axis not given if __name__ == '__main__': pytest.main() diff --git a/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py b/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py index 24cbeae38..64af10396 100644 --- a/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py +++ b/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py @@ -73,7 +73,7 @@ def test_ellipticity(self): """ x, y = np.linspace(start=0.001, stop=10, num=100), np.zeros(100) kwargs_round = {'alpha_Rs': 0.5, 'Rs': 2, 'center_x': 0, 'center_y': 0, 'e1': 0, 'e2': 0} - kwargs = {'alpha_Rs': 0.5, 'Rs': 2, 'center_x': 0, 'center_y': 0, 'e1': 0.5, 'e2': 0} + kwargs = {'alpha_Rs': 0.5, 'Rs': 2, 'center_x': 0, 'center_y': 0, 'e1': 0.3, 'e2': 0} f_xx, f_xy, f_yx, f_yy = self.nfw_cse.hessian(x, y, **kwargs_round) kappa_round = 1. / 2 * (f_xx + f_yy) @@ -84,16 +84,16 @@ def test_ellipticity(self): f_xx, f_xy, f_yx, f_yy = self.nfw_cse.hessian(y, x, **kwargs) kappa_minor = 1. / 2 * (f_xx + f_yy) - # npt.assert_almost_equal(np.sqrt(kappa_minor**2 + kappa_major**2), kappa_round, decimal=4) + npt.assert_almost_equal(np.sqrt(kappa_minor * kappa_major),kappa_round, decimal=4) - # import matplotlib.pyplot as plt - # plt.plot(x, kappa_round/kappa_round, ':', label='round', alpha=0.5) - # plt.plot(x, kappa_major/kappa_round, ',-', label='major', alpha=0.5) - # plt.plot(x, kappa_minor/kappa_round, '--', label='minor', alpha=0.5) - # plt.plot(x, np.sqrt(kappa_minor * kappa_major)/kappa_round, '--', label='prod', alpha=0.5) - # plt.plot(x, np.sqrt(kappa_minor**2 + kappa_major**2) / kappa_round / 2, '--', label='square', alpha=0.5) - # plt.legend() - # plt.show() + import matplotlib.pyplot as plt + plt.plot(x, kappa_round/kappa_round, ':', label='round', alpha=0.5) + plt.plot(x, kappa_major/kappa_round, ',-', label='major', alpha=0.5) + plt.plot(x, kappa_minor/kappa_round, '--', label='minor', alpha=0.5) + plt.plot(x, np.sqrt(kappa_minor * kappa_major)/kappa_round, '--', label='prod', alpha=0.5) + plt.plot(x, np.sqrt(kappa_minor**2 + kappa_major**2) / kappa_round / 2, '--', label='square', alpha=0.5) + plt.legend() + plt.show() # assert 1 == 0 From acc5a4d4d9a010b33c0ed98b2d8820d2d01c1963 Mon Sep 17 00:00:00 2001 From: mattgomer Date: Wed, 31 Aug 2022 14:14:08 +0200 Subject: [PATCH 49/67] plot created in test_ellipticity --- .../test_Profiles/test_cored_steep_ellipsoid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py index aeefc4db2..851e59c98 100644 --- a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py +++ b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py @@ -50,6 +50,7 @@ def test_ellipticity(self): """ x, y = np.linspace(start=0.001, stop=10, num=100), np.zeros(100) kwargs_round = {'a': 2, 's': 1, 'e1': 0., 'e2': 0., 'center_x': 0, 'center_y': 0} + phi_q, q = param_util.ellipticity2phi_q(0.3, 0) kwargs = {'a': 2, 's': 1, 'e1': 0.3, 'e2': 0., 'center_x': 0, 'center_y': 0} f_xx, f_xy, f_yx, f_yy = self.CSP.hessian(x, y, **kwargs_round) @@ -63,16 +64,15 @@ def test_ellipticity(self): import matplotlib.pyplot as plt # plt.plot(x, kappa_round, ':', label='round', alpha=0.5) - # plt.plot(x, kappa_major, ',-', label='major', alpha=0.5) - # plt.plot(x, kappa_minor, '--', label='minor', alpha=0.5) + plt.plot(x, kappa_major/kappa_round, ',-', label='major/round', alpha=0.5) + plt.plot(x, kappa_minor/kappa_round, '--', label='minor/round', alpha=0.5) # plt.plot(x, np.sqrt(kappa_minor*kappa_major), '-', label='square', alpha=0.5) plt.plot(x, np.sqrt(kappa_minor*kappa_major)/kappa_round,label='prod/kappa_round') plt.legend() plt.show() - npt.assert_almost_equal(kappa_round,np.sqrt(kappa_minor*kappa_major), decimal=5) - #I think I need to modify s too + # npt.assert_almost_equal(kappa_round,np.sqrt(kappa_minor*kappa_major), decimal=5) #test that it breaks if product average or major axis not given if __name__ == '__main__': From 4b35d27ca6cae4aaafa38af1d8b653b6f1d22c8f Mon Sep 17 00:00:00 2001 From: mattgomer Date: Wed, 31 Aug 2022 14:23:42 +0200 Subject: [PATCH 50/67] updated __all__ --- lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py index 24dbe0611..0aee36f27 100644 --- a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py +++ b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py @@ -5,7 +5,7 @@ from lenstronomy.Util import param_util from lenstronomy.Util import util -__all__ = ['CSE', 'CSEMajorAxis', 'CSEMajorAxisSet'] +__all__ = ['CSE', 'CSEMajorAxis', 'CSEMajorAxisSet','CSEProductAvg','CSEProductAvgSet'] class CSE(LensProfileBase): From 4eed569740071aaf49afb5c3867d5e0f98b332ee Mon Sep 17 00:00:00 2001 From: Jelle Aalbers Date: Wed, 31 Aug 2022 15:14:57 -0700 Subject: [PATCH 51/67] Fix link to tutorials in usage docs --- docs/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index c33f7b182..509610d4e 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -17,7 +17,7 @@ user to directly access a lot of tools and each module can also be used as stand Example notebooks ----------------- -We have made an extension module available at `https://github.com/lenstronomy/lenstronom-tutorials `_. +We have made an extension module available at `https://github.com/lenstronomy/lenstronomy-tutorials `_. You can find simple example notebooks for various cases. The latest versions of the notebooks should be compatible with the recent pip version of lenstronomy. From e788694ec2a4b14f87384746d478a4222dcaab1e Mon Sep 17 00:00:00 2001 From: mattgomer Date: Fri, 2 Sep 2022 15:07:03 +0200 Subject: [PATCH 52/67] added test function to check einstein radius --- .../LensModel/Profiles/nfw_ellipse_cse.py | 2 +- .../test_cored_steep_ellipsoid.py | 21 +++++------- .../test_Profiles/test_nfw_ellipse_cse.py | 34 +++++++++++++------ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py index 1f0f2751f..542d4eff9 100644 --- a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py +++ b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py @@ -169,7 +169,7 @@ def _normalization(self, alpha_Rs, Rs, q): :return: normalization (m) """ rho0 = self.nfw.alpha2rho0(alpha_Rs, Rs) - c = 2 * q / (1 + q) # this is the same as 1 - e with e = (1. - q) / (1. + q) + # c = 2 * q / (1 + q) # this is the same as 1 - e with e = (1. - q) / (1. + q) rs_ = Rs #/ np.sqrt(q) const = 4 * rho0 * rs_ ** 3 #/ c return const diff --git a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py index 851e59c98..fdbecf81e 100644 --- a/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py +++ b/test/test_LensModel/test_Profiles/test_cored_steep_ellipsoid.py @@ -62,18 +62,15 @@ def test_ellipticity(self): f_xx, f_xy, f_yx, f_yy = self.CSP.hessian(y, x, **kwargs) kappa_minor = 1. / 2 * (f_xx + f_yy) - import matplotlib.pyplot as plt - # plt.plot(x, kappa_round, ':', label='round', alpha=0.5) - plt.plot(x, kappa_major/kappa_round, ',-', label='major/round', alpha=0.5) - plt.plot(x, kappa_minor/kappa_round, '--', label='minor/round', alpha=0.5) - # plt.plot(x, np.sqrt(kappa_minor*kappa_major), '-', label='square', alpha=0.5) - - plt.plot(x, np.sqrt(kappa_minor*kappa_major)/kappa_round,label='prod/kappa_round') - plt.legend() - plt.show() - - # npt.assert_almost_equal(kappa_round,np.sqrt(kappa_minor*kappa_major), decimal=5) -#test that it breaks if product average or major axis not given + # import matplotlib.pyplot as plt + # plt.plot(x, kappa_major/kappa_round, ',-', label='major/round', alpha=0.5) + # plt.plot(x, kappa_minor/kappa_round, '--', label='minor/round', alpha=0.5) + # + # plt.plot(x, np.sqrt(kappa_minor*kappa_major)/kappa_round,label='prod/kappa_round') + # plt.legend() + # plt.show() + + npt.assert_almost_equal(kappa_round,np.sqrt(kappa_minor*kappa_major), decimal=1) if __name__ == '__main__': pytest.main() diff --git a/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py b/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py index 64af10396..f8749f888 100644 --- a/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py +++ b/test/test_LensModel/test_Profiles/test_nfw_ellipse_cse.py @@ -2,6 +2,8 @@ from lenstronomy.LensModel.Profiles.nfw import NFW from lenstronomy.LensModel.Profiles.nfw_ellipse_cse import NFW_ELLIPSE_CSE +from lenstronomy.Analysis.lens_profile import LensProfileAnalysis +from lenstronomy.LensModel.lens_model import LensModel import numpy as np import numpy.testing as npt @@ -84,17 +86,27 @@ def test_ellipticity(self): f_xx, f_xy, f_yx, f_yy = self.nfw_cse.hessian(y, x, **kwargs) kappa_minor = 1. / 2 * (f_xx + f_yy) - npt.assert_almost_equal(np.sqrt(kappa_minor * kappa_major),kappa_round, decimal=4) - - import matplotlib.pyplot as plt - plt.plot(x, kappa_round/kappa_round, ':', label='round', alpha=0.5) - plt.plot(x, kappa_major/kappa_round, ',-', label='major', alpha=0.5) - plt.plot(x, kappa_minor/kappa_round, '--', label='minor', alpha=0.5) - plt.plot(x, np.sqrt(kappa_minor * kappa_major)/kappa_round, '--', label='prod', alpha=0.5) - plt.plot(x, np.sqrt(kappa_minor**2 + kappa_major**2) / kappa_round / 2, '--', label='square', alpha=0.5) - plt.legend() - plt.show() - # assert 1 == 0 + npt.assert_almost_equal(np.sqrt(kappa_minor * kappa_major),kappa_round, decimal=2) + + # import matplotlib.pyplot as plt + # plt.plot(x, kappa_round/kappa_round, ':', label='round', alpha=0.5) + # plt.plot(x, kappa_major/kappa_round, ',-', label='major', alpha=0.5) + # plt.plot(x, kappa_minor/kappa_round, '--', label='minor', alpha=0.5) + # plt.plot(x, np.sqrt(kappa_minor * kappa_major)/kappa_round, '--', label='prod', alpha=0.5) + # plt.plot(x, np.sqrt(kappa_minor**2 + kappa_major**2) / kappa_round / 2, '--', label='square', alpha=0.5) + # plt.legend() + # plt.show() + def test_einstein_rad(self): + """ + test that the Einstein radius doesn't change significantly with ellipticity + """ + kwargs_round = {'alpha_Rs': 0.5, 'Rs': 2, 'center_x': 0, 'center_y': 0, 'e1': 0, 'e2': 0} + kwargs = {'alpha_Rs': 0.5, 'Rs': 2, 'center_x': 0, 'center_y': 0, 'e1': 0.3, 'e2': 0} + LensMod=LensModel(['NFW_ELLIPSE_CSE']) + LensAn=LensProfileAnalysis(LensMod) + r_Ein_round=LensAn.effective_einstein_radius([kwargs_round]) + r_Ein_ell=LensAn.effective_einstein_radius([kwargs]) + npt.assert_almost_equal(r_Ein_round,r_Ein_ell,decimal=1) if __name__ == '__main__': From 338ab25a0c9767080bccd894c78f515f2d7df100 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sat, 3 Sep 2022 09:44:42 -0700 Subject: [PATCH 53/67] upgraded to nautilus 0.2.1 that should solve the issue #328 --- requirements.txt | 2 +- setup.py | 2 +- test/test_Sampling/test_Samplers/test_nautilus.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4efbf8b51..4b67d92c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ pyyaml pyxdg h5py zeus-mcmc -nautilus-sampler>=0.2.0 +nautilus-sampler>=0.2.1 schwimmbad diff --git a/setup.py b/setup.py index 07d354fde..79ff446c3 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def run_tests(self): ] tests_require = ['pytest>=2.3', "mock", 'colossus==1.3.0', 'slitronomy==0.3.2', 'emcee>=3.0.0', 'dynesty', 'nestcheck', 'pymultinest', 'zeus-mcmc>=2.4.0', - 'nautilus-sampler>=0.2.0', + 'nautilus-sampler>=0.2.1', ] PACKAGE_PATH = os.path.abspath(os.path.join(__file__, os.pardir)) diff --git a/test/test_Sampling/test_Samplers/test_nautilus.py b/test/test_Sampling/test_Samplers/test_nautilus.py index 92fbae558..c15afce16 100644 --- a/test/test_Sampling/test_Samplers/test_nautilus.py +++ b/test/test_Sampling/test_Samplers/test_nautilus.py @@ -46,15 +46,15 @@ def test_sampler(self, import_fixture): kwargs_run_fail['prior_type'] = 'wrong' assert_raises(ValueError, sampler.nautilus_sampling, **kwargs_run_fail) - # def test_prior(self): + def test_prior(self): - # num_param = 10 - # from nautilus import Prior - # prior = Prior() + num_param = 10 + from nautilus import Prior + prior = Prior() - # for i in range(num_param): - # prior.add_parameter(dist=(0, 1)) - # assert num_param == prior.dimensionality() + for i in range(num_param): + prior.add_parameter(dist=(0, 1)) + assert num_param == prior.dimensionality() if __name__ == '__main__': From 1262bd4c978c1c1f80c63d85d9ac8b762029adbf Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sat, 3 Sep 2022 09:48:39 -0700 Subject: [PATCH 54/67] fix bug in initialization and remove comments --- lenstronomy/Sampling/Samplers/nautilus.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lenstronomy/Sampling/Samplers/nautilus.py b/lenstronomy/Sampling/Samplers/nautilus.py index bf7dc97d0..e6948827d 100644 --- a/lenstronomy/Sampling/Samplers/nautilus.py +++ b/lenstronomy/Sampling/Samplers/nautilus.py @@ -44,13 +44,11 @@ def nautilus_sampling(self, prior_type='uniform', mpi=False, thread_count=1, ver if prior_type == 'uniform': for i in range(self._num_param): prior.add_parameter(dist=(self._lower_limit[i], self._upper_limit[i])) - # assert self._num_param == prior.dimensionality() - # print(self._num_param, prior.dimensionality(), 'number of param, dimensionality') else: raise ValueError('prior_type %s is not supported for Nautilus wrapper.' % prior_type) # loop through prior pool = choose_pool(mpi=mpi, processes=thread_count, use_dill=True) - sampler = Sampler(prior, likelihood=self.likelihood, pool=pool, **kwargs_nautilus) + sampler = Sampler(prior, likelihood=self.likelihood, pool=pool, pass_struct=False, **kwargs_nautilus) time_start = time.time() if one_step is True: sampler.add_bound() From 58efb13835686f8665ed3dbce527a2b07a9853b0 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sat, 3 Sep 2022 10:17:13 -0700 Subject: [PATCH 55/67] test precisions updated with numpy.testing --- test/test_Data/test_psf.py | 4 ++-- test/test_LensModel/test_Profiles/test_gaussian.py | 6 +++--- .../test_Profiles/test_shapelet_pot_cartesian.py | 8 ++++---- test/test_LensModel/test_single_plane.py | 2 +- test/test_LightModel/test_Profiles/test_shapelets.py | 2 +- test/test_Util/test_analysis_util.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/test_Data/test_psf.py b/test/test_Data/test_psf.py index 40e19dd58..1084f4779 100644 --- a/test/test_Data/test_psf.py +++ b/test/test_Data/test_psf.py @@ -31,8 +31,8 @@ def test_kernel_point_source(self): kernel_point_source = psf_class.kernel_point_source assert len(kernel_point_source) == 13 kernel_super = psf_class.kernel_point_source_supersampled(supersampling_factor=3) - assert np.sum(kernel_point_source) == np.sum(kernel_super) - assert np.sum(kernel_point_source) == 1 + npt.assert_almost_equal(np.sum(kernel_point_source), np.sum(kernel_super), decimal=9) + npt.assert_almost_equal(np.sum(kernel_point_source), 1, decimal=9) def test_kernel_subsampled(self): deltaPix = 0.05 # pixel size of image diff --git a/test/test_LensModel/test_Profiles/test_gaussian.py b/test/test_LensModel/test_Profiles/test_gaussian.py index 1464dcb4c..0efcb56d6 100644 --- a/test/test_LensModel/test_Profiles/test_gaussian.py +++ b/test/test_LensModel/test_Profiles/test_gaussian.py @@ -68,9 +68,9 @@ def test_hessian(self): assert values[0][0] == 0. assert values[3][0] == -np.exp(-1./2) assert values[1][0] == 0. - assert values[0][1] == 0.40600584970983811 - assert values[3][1] == -0.1353352832366127 - assert values[1][1] == 0. + npt.assert_almost_equal(values[0][1], 0.40600584970983811, decimal=9) + npt.assert_almost_equal(values[3][1], -0.1353352832366127, decimal=9) + npt.assert_almost_equal(values[1][1], 0, decimal=9) class TestGaussianKappa(object): diff --git a/test/test_LensModel/test_Profiles/test_shapelet_pot_cartesian.py b/test/test_LensModel/test_Profiles/test_shapelet_pot_cartesian.py index cdb8f16ff..a75f109c6 100644 --- a/test/test_LensModel/test_Profiles/test_shapelet_pot_cartesian.py +++ b/test/test_LensModel/test_Profiles/test_shapelet_pot_cartesian.py @@ -22,14 +22,14 @@ def test_function(self): beta = 1. coeffs = (1., 1.) values = self.cartShapelets.function(x, y, coeffs, beta) - assert values[0] == 0.11180585426466891 + npt.assert_almost_equal(values[0], 0.11180585426466888, decimal=9) x = 1. y = 2. beta = 1. coeffs = (1., 1.) values = self.cartShapelets.function(x, y, coeffs, beta) - assert values == 0.11180585426466891 + npt.assert_almost_equal(values, 0.11180585426466891, decimal=9) x = np.array([0]) y = np.array([0]) @@ -40,11 +40,11 @@ def test_function(self): coeffs = (1, 1., 0, 0, 1, 1) values = self.cartShapelets.function(x, y, coeffs, beta) - assert values[0] == 0.16524730314632363 + npt.assert_almost_equal(values[0], 0.16524730314632363, decimal=9) coeffs = (1, 1., 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) values = self.cartShapelets.function(x, y, coeffs, beta) - assert values[0] == 0.16524730314632363 + npt.assert_almost_equal(values[0], 0.16524730314632363, decimal=9) coeffs = (0., 0., 0, 0, 0., 0., 0, 0, 0, 0, 0, 0, 0, 0, 0) values = self.cartShapelets.function(x, y, coeffs, beta) diff --git a/test/test_LensModel/test_single_plane.py b/test/test_LensModel/test_single_plane.py index e873eb492..915afe654 100644 --- a/test/test_LensModel/test_single_plane.py +++ b/test/test_LensModel/test_single_plane.py @@ -40,7 +40,7 @@ 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 + npt.assert_almost_equal(output, 0.11750309741540453, decimal=9) def test_density(self): theta_E = 1 diff --git a/test/test_LightModel/test_Profiles/test_shapelets.py b/test/test_LightModel/test_Profiles/test_shapelets.py index eb07446a9..8e7e5aa9d 100644 --- a/test/test_LightModel/test_Profiles/test_shapelets.py +++ b/test/test_LightModel/test_Profiles/test_shapelets.py @@ -50,7 +50,7 @@ def test_shapelet_basis(self): beta = 1 numPix = 10 kernel_list = self.shapeletSet.shapelet_basis_2d(num_order, beta, numPix) - assert kernel_list[0][4, 4] == 0.43939128946772255 + npt.assert_almost_equal(kernel_list[0][4, 4], 0.4393912894677224, decimal=9) def test_decomposition(self): """ diff --git a/test/test_Util/test_analysis_util.py b/test/test_Util/test_analysis_util.py index d91c64e1e..c7e677dcf 100644 --- a/test/test_Util/test_analysis_util.py +++ b/test/test_Util/test_analysis_util.py @@ -47,8 +47,8 @@ def test_half_light_radius(self): assert r_half == -1 def test_bic_model(self): - bic=analysis_util.bic_model(0,np.e,1) - assert bic == 1 + bic=analysis_util.bic_model(0, np.e, 1) + npt.assert_almost_equal(bic, 1, decimal=8) def test_azimuthalAverage(self): num_pix = 101 From b7246959c0eb88574a092e8d0ee14497df931e9f Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sat, 3 Sep 2022 11:32:34 -0700 Subject: [PATCH 56/67] merged with main branch and added contributions --- lenstronomy/LensModel/Solver/epl_shear_solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lenstronomy/LensModel/Solver/epl_shear_solver.py b/lenstronomy/LensModel/Solver/epl_shear_solver.py index c6fc69928..73089677f 100644 --- a/lenstronomy/LensModel/Solver/epl_shear_solver.py +++ b/lenstronomy/LensModel/Solver/epl_shear_solver.py @@ -1,4 +1,4 @@ -__author__ = 'ewoudwempe' +__author__ = 'ewoudwempe', 'sibirrer' import numpy as np from lenstronomy.LensModel.Util.epl_util import min_approx, pol_to_cart, cart_to_pol, cdot, ps, rotmat, solvequadeq, brentq_inline From 4afd8cdf584e568ee75ecf692fcd5b4dc9908639 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sun, 4 Sep 2022 12:26:45 -0700 Subject: [PATCH 57/67] added two publications --- PUBLISHED.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PUBLISHED.rst b/PUBLISHED.rst index 60882370d..7c1f5618e 100644 --- a/PUBLISHED.rst +++ b/PUBLISHED.rst @@ -235,6 +235,10 @@ Galaxy formation and evolution * Early results from GLASS-JWST. V: the first rest-frame optical size-luminosity relation of galaxies at z>7; `Yang et al. 2022b `_ *galaxy size measurement from JWST data with Galight/lenstronomy* +* A New Polar Ring Galaxy Discovered in the COSMOS Field; `Nishimura et al. 2022 `_ + +* Webb's PEARLS: dust attenuation and gravitational lensing in the backlit-galaxy system VV 191; `Keel et al. 2022 `_ + Automatized Lens Modeling From ef59ee2e6075efc1d88a7b2bba6ff4dd4d05925c Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sun, 4 Sep 2022 21:59:39 -0700 Subject: [PATCH 58/67] ADS query added and marked as non-recent publication --- PUBLISHED.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/PUBLISHED.rst b/PUBLISHED.rst index 7c1f5618e..c1106be72 100644 --- a/PUBLISHED.rst +++ b/PUBLISHED.rst @@ -2,10 +2,9 @@ Published work with lenstronomy =============================== -In this section you can find the concept papers **lenstronomy** is based on the list of science publications that made -use of **lenstronomy**. Please let the developers know when you publish a paper that made use of **lenstronomy**. -We are happy to include your publication in this list. - +In this section you can find the concept papers **lenstronomy** is based on and a list of science publications that made +use of **lenstronomy** before 09/2022. +For a more complete and current list of publications using lenstronomy we refer to the `NASA/ADS query `_,. Core lenstronomy methodology and software publications @@ -44,6 +43,10 @@ Related software publications +===================================== +Scientific publication before 09/2022 +===================================== + @@ -351,7 +354,6 @@ Large scale structure - Others ------ From 2f82df08e20e8924e07b846032f3f0f8dbe6c441 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sun, 4 Sep 2022 22:31:51 -0700 Subject: [PATCH 59/67] random seed added for tests passing --- PUBLISHED.rst | 3 ++- test/test_Workflow/test_fitting_sequence.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/PUBLISHED.rst b/PUBLISHED.rst index c1106be72..5efaa72d3 100644 --- a/PUBLISHED.rst +++ b/PUBLISHED.rst @@ -4,7 +4,8 @@ Published work with lenstronomy In this section you can find the concept papers **lenstronomy** is based on and a list of science publications that made use of **lenstronomy** before 09/2022. -For a more complete and current list of publications using lenstronomy we refer to the `NASA/ADS query `_,. +For a more complete and current list of publications using lenstronomy we refer to the `NASA/ADS query `_ +(this incudes all publications citing lenstronomy papers, which is not the same as publications making active use of the software). Core lenstronomy methodology and software publications diff --git a/test/test_Workflow/test_fitting_sequence.py b/test/test_Workflow/test_fitting_sequence.py index b5036e5b8..8efe04bc6 100644 --- a/test/test_Workflow/test_fitting_sequence.py +++ b/test/test_Workflow/test_fitting_sequence.py @@ -321,6 +321,7 @@ def test_multinest(self): assert kwargs_out['kwargs_lens'] == 1 def test_dynesty(self): + np.random.seed(42) kwargs_params = copy.deepcopy(self.kwargs_params) kwargs_params['lens_model'][0][0]['theta_E'] += 0.01 fittingSequence = FittingSequence(self.kwargs_data_joint, self.kwargs_model, self.kwargs_constraints, From 9195569d6cc4c60be6237885c1a9d0d8f6940c44 Mon Sep 17 00:00:00 2001 From: mattgomer Date: Mon, 5 Sep 2022 17:22:57 +0200 Subject: [PATCH 60/67] updated CSEProductAvg class, corrected normalizations for hessian and derivatives --- .../Profiles/cored_steep_ellipsoid.py | 51 ++++++++----------- .../LensModel/Profiles/nfw_ellipse_cse.py | 7 ++- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py index 0aee36f27..8fdf65304 100644 --- a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py +++ b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py @@ -270,9 +270,19 @@ class CSEProductAvg(LensProfileBase): lower_limit_default = {'A': -1000, 's': 0, 'q': 0.001, 'center_x': -100, 'center_y': -100} upper_limit_default = {'A': 1000, 's': 10000, 'q': 0.99999, 'e2': 0.5, 'center_x': -100, 'center_y': -100} + def __init__(self): + self.MA_class = CSEMajorAxis() + + def _convert2prodavg(self, x, y, a, s, q): + """ + converts coordinates and renormalizes major-axis parameterization to instead be wrt. product-averaged + """ + a = a / q + x = x * np.sqrt(q) + y = y * np.sqrt(q) + return x, y, a, s, q def function(self, x, y, a, s, q): """ - :param x: coordinate in image plane (angle) :param y: coordinate in image plane (angle) :param a: lensing strength @@ -280,17 +290,11 @@ def function(self, x, y, a, s, q): :param q: axis ratio :return: lensing potential """ - x = x * np.sqrt(q) - y = y * np.sqrt(q) - # potential calculation - psi = np.sqrt(q**2*(s**2 + x**2) + y**2) - Phi = (psi + s)**2 + (1-q**2) * x**2 - phi = q/(2*s) * np.log(Phi) - q/s * np.log((1+q) * s) - return a * phi + x, y, a, s, q = self._convert2prodavg(x, y, a, s, q) + return self.MA_class.function(x, y, a, s, q) def derivatives(self, x, y, a, s, q): """ - :param x: coordinate in image plane (angle) :param y: coordinate in image plane (angle) :param a: lensing strength @@ -298,19 +302,13 @@ def derivatives(self, x, y, a, s, q): :param q: axis ratio :return: deflection in x- and y-direction """ - x = x * np.sqrt(q) - y = y * np.sqrt(q) - - psi = np.sqrt(q ** 2 * (s ** 2 + x ** 2) + y ** 2) - Phi = (psi + s) ** 2 + (1 - q ** 2) * x ** 2 - f_x = q * x * (psi + q**2*s) / (s * psi * Phi) - f_y = q * y * (psi + s) / (s * psi * Phi) - - return a * f_x, a * f_y + x, y, a, s, q = self._convert2prodavg(x, y, a, s, q) + af_x,af_y=self.MA_class.derivatives(x, y, a, s, q) + #extra sqrt(q) factor from taking derivative of transformed coordinate + return np.sqrt(q)* af_x, np.sqrt(q)* af_y def hessian(self, x, y, a, s, q): """ - :param x: coordinate in image plane (angle) :param y: coordinate in image plane (angle) :param a: lensing strength @@ -318,17 +316,10 @@ def hessian(self, x, y, a, s, q): :param q: axis ratio :return: hessian elements f_xx, f_xy, f_yx, f_yy """ - x = x * np.sqrt(q) - y = y * np.sqrt(q) - - # equations 21-23 in Oguri 2021 - psi = np.sqrt(q ** 2 * (s ** 2 + x ** 2) + y ** 2) - Phi = (psi + s) ** 2 + (1 - q ** 2) * x ** 2 - f_xx = q/(s * Phi) * (1 + q**2*s*(q**2 * s**2 + y**2)/psi**3 - 2*x**2*(psi + q**2*s)**2/(psi**2 * Phi)) - f_yy = q/(s * Phi) * (1 + q**2 * s * (s**2 + x**2)/psi**3 - 2*y**2*(psi + s)**2/(psi**2 * Phi)) - f_xy = - q * x*y / (s * Phi) * (q**2 * s / psi**3 + 2 * (psi + q**2*s) * (psi + s) / (psi**2 * Phi)) - - return a * f_xx, a * f_xy, a * f_xy, a * f_yy + x, y, a, s, q = self._convert2prodavg(x, y, a, s, q) + af_xx, af_xy, af_xy, af_yy=self.MA_class.hessian(x, y, a, s, q) + #two sqrt(q) factors from taking derivatives of transformed coordinate + return q* af_xx, q * af_xy, q * af_xy, q * af_yy class CSEProductAvgSet(LensProfileBase): diff --git a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py index 542d4eff9..a79917445 100644 --- a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py +++ b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py @@ -16,6 +16,7 @@ class NFW_ELLIPSE_CSE(NFW_ELLIPSE): this class contains functions concerning the NFW profile with an ellipticity defined in the convergence parameterization of alpha_Rs and Rs is the same as for the spherical NFW profile Approximation with CSE profile introduced by Oguri 2021: https://arxiv.org/pdf/2106.11464.pdf + Match to NFW using CSEs is approximate: kappa matches to ~1-2% relation are: R_200 = c * Rs @@ -31,7 +32,6 @@ def __init__(self, high_accuracy=True): :param high_accuracy: boolean, if True uses a more accurate larger set of CSE profiles (see Oguri 2021) """ - # self.cse_major_axis_set = CSEMajorAxisSet() self.cse_major_axis_set = CSEProductAvgSet() self.nfw = NFW() if high_accuracy is True: @@ -169,7 +169,6 @@ def _normalization(self, alpha_Rs, Rs, q): :return: normalization (m) """ rho0 = self.nfw.alpha2rho0(alpha_Rs, Rs) - # c = 2 * q / (1 + q) # this is the same as 1 - e with e = (1. - q) / (1. + q) - rs_ = Rs #/ np.sqrt(q) - const = 4 * rho0 * rs_ ** 3 #/ c + rs_ = Rs + const = 4 * rho0 * rs_ ** 3 return const From 6c118b6f4374316e269c2a4dfcd132ce1cebce32 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Mon, 5 Sep 2022 10:51:50 -0700 Subject: [PATCH 61/67] minor updates in documentation and PEP8 --- .../Profiles/cored_steep_ellipsoid.py | 43 +++++++++++++------ .../LensModel/Profiles/nfw_ellipse_cse.py | 4 +- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py index 8fdf65304..7ecec92ef 100644 --- a/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py +++ b/lenstronomy/LensModel/Profiles/cored_steep_ellipsoid.py @@ -5,7 +5,7 @@ from lenstronomy.Util import param_util from lenstronomy.Util import util -__all__ = ['CSE', 'CSEMajorAxis', 'CSEMajorAxisSet','CSEProductAvg','CSEProductAvgSet'] +__all__ = ['CSE', 'CSEMajorAxis', 'CSEMajorAxisSet', 'CSEProductAvg', 'CSEProductAvgSet'] class CSE(LensProfileBase): @@ -30,12 +30,12 @@ class CSE(LensProfileBase): upper_limit_default = {'A': 1000, 's': 10000, 'e1': 0.5, 'e2': 0.5, 'center_x': -100, 'center_y': -100} def __init__(self, axis='product_avg'): - if axis=='major': + if axis == 'major': self.major_axis_model = CSEMajorAxis() - elif axis=='product_avg': + elif axis == 'product_avg': self.major_axis_model = CSEProductAvg() else: - raise ValueError("axis must be set to'major' or 'product_avg'") + raise ValueError("axis must be set to 'major' or 'product_avg'. Input is %s ." % axis) super(CSE, self).__init__() def function(self, x, y, a, s, e1, e2, center_x, center_y): @@ -258,12 +258,26 @@ def hessian(self, x, y, a_list, s_list, q): f_yy += f_yy_ return f_xx, f_xy, f_xy, f_yy + class CSEProductAvg(LensProfileBase): """ Cored steep ellipsoid (CSE) evaluated at the product-averaged radius sqrt(ab), such that mass is not changed when increasing ellipticity - Same as CSEMajorAxis but evalulated at r=sqrt(q)*r_original + Same as CSEMajorAxis but evaluated at r=sqrt(q)*r_original + + Keeton and Kochanek (1998) + Oguri 2021: https://arxiv.org/pdf/2106.11464.pdf + + .. math:: + \\kappa(u;s) = \\frac{A}{2(s^2 + \\xi^2)^{3/2}} + + with + + .. math:: + \\xi(x, y) = \\sqrt{qx^2 + \\frac{y^2}{q}} + + """ param_names = ['A', 's', 'q', 'center_x', 'center_y'] @@ -271,16 +285,19 @@ class CSEProductAvg(LensProfileBase): upper_limit_default = {'A': 1000, 's': 10000, 'q': 0.99999, 'e2': 0.5, 'center_x': -100, 'center_y': -100} def __init__(self): + super(CSEProductAvg, self).__init__() self.MA_class = CSEMajorAxis() - def _convert2prodavg(self, x, y, a, s, q): + @staticmethod + def _convert2prodavg(x, y, a, s, q): """ - converts coordinates and renormalizes major-axis parameterization to instead be wrt. product-averaged + converts coordinates and re-normalizes major-axis parameterization to instead be wrt. product-averaged """ a = a / q x = x * np.sqrt(q) y = y * np.sqrt(q) return x, y, a, s, q + def function(self, x, y, a, s, q): """ :param x: coordinate in image plane (angle) @@ -303,9 +320,9 @@ def derivatives(self, x, y, a, s, q): :return: deflection in x- and y-direction """ x, y, a, s, q = self._convert2prodavg(x, y, a, s, q) - af_x,af_y=self.MA_class.derivatives(x, y, a, s, q) - #extra sqrt(q) factor from taking derivative of transformed coordinate - return np.sqrt(q)* af_x, np.sqrt(q)* af_y + af_x, af_y = self.MA_class.derivatives(x, y, a, s, q) + # extra sqrt(q) factor from taking derivative of transformed coordinate + return np.sqrt(q) * af_x, np.sqrt(q) * af_y def hessian(self, x, y, a, s, q): """ @@ -317,9 +334,9 @@ def hessian(self, x, y, a, s, q): :return: hessian elements f_xx, f_xy, f_yx, f_yy """ x, y, a, s, q = self._convert2prodavg(x, y, a, s, q) - af_xx, af_xy, af_xy, af_yy=self.MA_class.hessian(x, y, a, s, q) - #two sqrt(q) factors from taking derivatives of transformed coordinate - return q* af_xx, q * af_xy, q * af_xy, q * af_yy + af_xx, af_xy, af_xy, af_yy = self.MA_class.hessian(x, y, a, s, q) + # two sqrt(q) factors from taking derivatives of transformed coordinate + return q * af_xx, q * af_xy, q * af_xy, q * af_yy class CSEProductAvgSet(LensProfileBase): diff --git a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py index a79917445..98e74375f 100644 --- a/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py +++ b/lenstronomy/LensModel/Profiles/nfw_ellipse_cse.py @@ -5,7 +5,6 @@ from lenstronomy.LensModel.Profiles.nfw import NFW from lenstronomy.LensModel.Profiles.nfw_ellipse import NFW_ELLIPSE from lenstronomy.LensModel.Profiles.cored_steep_ellipsoid import CSEProductAvgSet -from lenstronomy.LensModel.Profiles.cored_steep_ellipsoid import CSEMajorAxisSet import lenstronomy.Util.param_util as param_util __all__ = ['NFW_ELLIPSE_CSE'] @@ -30,7 +29,8 @@ class NFW_ELLIPSE_CSE(NFW_ELLIPSE): def __init__(self, high_accuracy=True): """ - :param high_accuracy: boolean, if True uses a more accurate larger set of CSE profiles (see Oguri 2021) + :param high_accuracy: if True uses a more accurate larger set of CSE profiles (see Oguri 2021) + :type high_accuracy: boolean """ self.cse_major_axis_set = CSEProductAvgSet() self.nfw = NFW() From ec1b5e5b41e69a1281fa374323d86ea029f742d9 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Tue, 6 Sep 2022 18:01:10 -0700 Subject: [PATCH 62/67] updated new logo and added contributors and acknowledgement to logo --- AUTHORS.rst | 6 ++++++ docs/figures/logo_text.png | Bin 66988 -> 60191 bytes 2 files changed, 6 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index bb58bd0ec..d1bc3c296 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,6 +27,7 @@ Contributors (alphabetic) * Aymeric Galan `aymgal `_ * Matthew R. Gomer `mattgomer `_ * Natalie B. Hogg `nataliehogg `_ +* Tyler Hughes * Felix A. Kuhn * Felix Mayor * Martin Millon `martin-millon `_ @@ -56,3 +57,8 @@ The initial source code of lenstronomy was developed by Simon Birrer (`sibirrer in 2014-2018 and made public in 2018. From 2018-2022 the development of lenstronomy was hosted on Simon Birrer's repository with increased contributions from many people. The lenstronomy development moved to the `project repository `_ in 2022. + + +Lenstronomy logo +---------------- +The lenstronomy logo was designed by Zoe Alexandre. \ No newline at end of file diff --git a/docs/figures/logo_text.png b/docs/figures/logo_text.png index ca914406a9af768eb7fddffb919e6c574580732e..f4a828d9c052127e10e5d1088da1df066bfc1a8b 100644 GIT binary patch literal 60191 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 From 506ecf05e4c4358b9dad87dd75375ba4da9b4b9f Mon Sep 17 00:00:00 2001 From: Matt Gomer <69860755+mattgomer@users.noreply.github.com> Date: Wed, 7 Sep 2022 17:29:38 +0200 Subject: [PATCH 63/67] Update AUTHORS.rst Edited logo design acknowledgement --- AUTHORS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index d1bc3c296..e3ebc766c 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -61,4 +61,4 @@ The lenstronomy development moved to the `project repository `_. From 8cc0168da972245c544895c3bc6b33ea2fd1c82e Mon Sep 17 00:00:00 2001 From: sibirrer Date: Fri, 9 Sep 2022 14:05:30 -0700 Subject: [PATCH 64/67] updated documentation in EPL and TODO for testing of 'INTERPOL' light model --- lenstronomy/LensModel/Profiles/epl.py | 2 +- test/test_LightModel/test_Profiles/test_interpolation.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lenstronomy/LensModel/Profiles/epl.py b/lenstronomy/LensModel/Profiles/epl.py index 15499ab7b..f33063b2f 100644 --- a/lenstronomy/LensModel/Profiles/epl.py +++ b/lenstronomy/LensModel/Profiles/epl.py @@ -26,7 +26,7 @@ class EPL(LensProfileBase): In terms of eccentricities, this profile is defined as .. math:: - \\kappa(r) = \\frac{3-\\gamma}{2} \\left(\\frac{\\theta'_{E}}{r \\sqrt{1 − e*\\cos(2*\\phi)}} \\right)^{\\gamma-1} + \\kappa(r) = \\frac{3-\\gamma}{2} \\left(\\frac{\\theta'_{E}}{r \\sqrt{1 - e*\\cos(2*\\phi)}} \\right)^{\\gamma-1} with :math:`\\epsilon` is the ellipticity defined as diff --git a/test/test_LightModel/test_Profiles/test_interpolation.py b/test/test_LightModel/test_Profiles/test_interpolation.py index 265a99933..21dc26db9 100644 --- a/test/test_LightModel/test_Profiles/test_interpolation.py +++ b/test/test_LightModel/test_Profiles/test_interpolation.py @@ -42,6 +42,9 @@ def test_function(self): out = interp.function(x=1000, y=0, **kwargs_interp) assert out == 0 + # TODO: test for scale !=1 + + # test change of center without re-doing interpolation out = interp.function(x=0, y=0, image=image, scale=1., phi_G=0, center_x=0, center_y=0) out_shift = interp.function(x=1, y=0, image=image, scale=1., phi_G=0, center_x=1, center_y=0) From 10c219671e75a69460063b05a0d253acf81c6eba Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sat, 10 Sep 2022 20:40:18 -0700 Subject: [PATCH 65/67] minor update in 'INTERPOL' light model documentation and testing, no changes to actual calculations --- .../LightModel/Profiles/interpolation.py | 18 ++++++----- .../test_Profiles/test_interpolation.py | 31 +++++++++++++++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/lenstronomy/LightModel/Profiles/interpolation.py b/lenstronomy/LightModel/Profiles/interpolation.py index aed35addb..2c438aba1 100644 --- a/lenstronomy/LightModel/Profiles/interpolation.py +++ b/lenstronomy/LightModel/Profiles/interpolation.py @@ -13,7 +13,7 @@ class Interpol(object): class which uses an interpolation of an image to compute the surface brightness parameters are - 'image': 2d numpy array of surface brightness (not integrated flux per pixel!) + 'image': 2d numpy array of surface brightness per square arc second (not integrated flux per pixel!) 'center_x': coordinate of center of image in angular units (i.e. arc seconds) 'center_y': coordinate of center of image in angular units (i.e. arc seconds) 'phi_G': rotation of image relative to the rectangular ra-to-dec orientation @@ -32,8 +32,10 @@ def function(self, x, y, image=None, amp=1, center_x=0, center_y=0, phi_G=0, sca :param x: x-coordinate to evaluate surface brightness :param y: y-coordinate to evaluate surface brightness - :param image: 2d numpy array (image) to be used to interpolate - :param amp: amplitude of surface brightness scaling in respect of original image + :param image: pixelized surface brightness (an image) to be used to interpolate in units of surface brightness + (flux per square arc seconds, not flux per pixel!) + :type image: 2d numpy array + :param amp: amplitude of surface brightness scaling in respect of original input image :param center_x: center of interpolated image :param center_y: center of interpolated image :param phi_G: rotation angle of simulated image in respect to input gird @@ -60,12 +62,14 @@ def image_interp(self, x, y, image): # (try reversing, the unit tests will fail) return self._image_interp(y, x, grid=False) - def total_flux(self, image, scale, amp=1, center_x=0, center_y=0, phi_G=0): + @staticmethod + def total_flux(image, scale, amp=1, center_x=0, center_y=0, phi_G=0): """ - sums up all the image surface brightness (image pixels defined in surface brightness at the coordinate of the pixel) - times pixel area + sums up all the image surface brightness (image pixels defined in surface brightness at the coordinate of the + pixel) times pixel area - :param image: pixelized surface brightness + :param image: pixelized surface brightness used to interpolate in units of surface brightness + (flux per square arc seconds, not flux per pixel!) :param scale: scale of the pixel in units of angle :param amp: linear scaling parameter of the surface brightness multiplicative with the initial image :param center_x: center of image in angular coordinates diff --git a/test/test_LightModel/test_Profiles/test_interpolation.py b/test/test_LightModel/test_Profiles/test_interpolation.py index 21dc26db9..7f40e6afd 100644 --- a/test/test_LightModel/test_Profiles/test_interpolation.py +++ b/test/test_LightModel/test_Profiles/test_interpolation.py @@ -3,6 +3,7 @@ from lenstronomy.LightModel.Profiles.interpolation import Interpol from lenstronomy.LightModel.Profiles.gaussian import Gaussian import lenstronomy.Util.util as util +import numpy as np class TestInterpol(object): @@ -42,9 +43,6 @@ def test_function(self): out = interp.function(x=1000, y=0, **kwargs_interp) assert out == 0 - # TODO: test for scale !=1 - - # test change of center without re-doing interpolation out = interp.function(x=0, y=0, image=image, scale=1., phi_G=0, center_x=0, center_y=0) out_shift = interp.function(x=1, y=0, image=image, scale=1., phi_G=0, center_x=1, center_y=0) @@ -58,6 +56,33 @@ def test_function(self): out_scaled = interp.function(x=2., y=0, image=image, scale=2, phi_G=0, center_x=0, center_y=0) assert out_scaled == out + def test_flux_normalization(self): + interp = Interpol() + delta_pix = 0.1 + len_x, len_y = 21, 21 + x, y = util.make_grid(numPix=(len_x, len_y), deltapix=delta_pix) + gauss = Gaussian() + flux = gauss.function(x, y, amp=1., center_x=0., center_y=0., sigma=0.3) + image = util.array2image(flux, nx=len_y, ny=len_x) + flux_total = np.sum(image) + + kwargs_interp = {'image': image, 'scale': delta_pix, 'phi_G': 0., 'center_x': 0., 'center_y': 0.} + image_interp = interp.function(x, y, **kwargs_interp) + flux_interp = np.sum(image_interp) + npt.assert_almost_equal(flux_interp, flux_total, decimal=3) + + # test for scale !=1 + # demands same surface brightness values. We rescale the pixel grid by the same amount as the image + scale = 0.5 + x, y = util.make_grid(numPix=(len_x, len_y), deltapix=delta_pix * scale) + kwargs_interp = {'image': image, 'scale': delta_pix * scale, 'phi_G': 0., 'center_x': 0., 'center_y': 0.} + output = interp.function(x, y, **kwargs_interp) + + npt.assert_almost_equal(output / image_interp, 1, decimal=5) + + from lenstronomy.ImSim.image_model import ImageModel + + def test_delete_cache(self): x, y = util.make_grid(numPix=20, deltapix=1.) gauss = Gaussian() From 4dc39b1e5d00a4e6e4990f5bfc32d0f59715094c Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sat, 10 Sep 2022 21:01:01 -0700 Subject: [PATCH 66/67] removed unused line in testing --- test/test_LightModel/test_Profiles/test_interpolation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/test_LightModel/test_Profiles/test_interpolation.py b/test/test_LightModel/test_Profiles/test_interpolation.py index 7f40e6afd..25f316491 100644 --- a/test/test_LightModel/test_Profiles/test_interpolation.py +++ b/test/test_LightModel/test_Profiles/test_interpolation.py @@ -80,9 +80,6 @@ def test_flux_normalization(self): npt.assert_almost_equal(output / image_interp, 1, decimal=5) - from lenstronomy.ImSim.image_model import ImageModel - - def test_delete_cache(self): x, y = util.make_grid(numPix=20, deltapix=1.) gauss = Gaussian() From 80c2df05af82990521969f7b16fa62b60c3ac584 Mon Sep 17 00:00:00 2001 From: sibirrer Date: Sun, 11 Sep 2022 18:12:52 -0700 Subject: [PATCH 67/67] changed the PSO return from chi2_list to log_likelihood list, to be consistent with other outputs of e.g. emcee and the chain_plot routines. --- lenstronomy/Sampling/Samplers/pso.py | 21 +++++++++++---------- lenstronomy/Sampling/sampler.py | 16 +++++++++------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lenstronomy/Sampling/Samplers/pso.py b/lenstronomy/Sampling/Samplers/pso.py index e180cf5cf..f541e0aa3 100644 --- a/lenstronomy/Sampling/Samplers/pso.py +++ b/lenstronomy/Sampling/Samplers/pso.py @@ -105,6 +105,7 @@ def set_global_best(self, position, velocity, fitness): def _init_swarm(self): """ Initiate the swarm. + :return: :rtype: """ @@ -125,10 +126,9 @@ def sample(self, max_iter=1000, c1=1.193, c2=1.193, p=0.7, m=1e-3, n=1e-2, early :param c1: cognitive weight :param c2: social weight :param p: stop criterion, percentage of particles to use - :param m: stop criterion, difference between mean fitness and global - best + :param m: stop criterion, difference between mean fitness and global best :param n: stop criterion, difference between norm of the particle - vector and norm of the global best + vector and norm of the global best :param early_stop_tolerance: will terminate at the given value (should be specified as a chi^2) """ @@ -194,19 +194,18 @@ def optimize(self, max_iter=1000, verbose=True, c1=1.193, c2=1.193, :param c1: cognitive weight :param c2: social weight :param p: stop criterion, percentage of particles to use - :param m: stop criterion, difference between mean fitness and global - best + :param m: stop criterion, difference between mean fitness and global best :param n: stop criterion, difference between norm of the particle - vector and norm of the global best + vector and norm of the global best :param early_stop_tolerance: will terminate at the given value (should be specified as a chi^2) """ - chi2_list = [] + log_likelihood_list = [] vel_list = [] pos_list = [] num_iter = 0 for _ in self.sample(max_iter, c1, c2, p, m, n, early_stop_tolerance): - chi2_list.append(self.global_best.fitness * 2) + log_likelihood_list.append(self.global_best.fitness) vel_list.append(self.global_best.velocity) pos_list.append(self.global_best.position) num_iter += 1 @@ -215,11 +214,12 @@ def optimize(self, max_iter=1000, verbose=True, c1=1.193, c2=1.193, if num_iter % 10 == 0: print(num_iter) - return self.global_best.position, [chi2_list, pos_list, vel_list] + return self.global_best.position, [log_likelihood_list, pos_list, vel_list] def _get_fitness(self, swarm): """ Set fitness (probability) of the particles in swarm. + :param swarm: PSO state :type swarm: list of Particle() instances of the swarm :return: @@ -239,6 +239,7 @@ def _get_fitness(self, swarm): def _converged(self, it, p, m, n): """ Check for convergence. + :param it: :type it: :param p: @@ -274,7 +275,7 @@ def _converged_fit(self, it, p, m): best_sort = np.sort([particle.personal_best.fitness for particle in self.swarm])[::-1] mean_fit = np.mean(best_sort[1:int(math.floor(self.particleCount * p))]) - #print( "best %f, mean_fit %f, ration %f"%( self.global_best[0], + # print( "best %f, mean_fit %f, ration %f"%( self.global_best[0], # mean_fit, abs((self.global_best[0]-mean_fit)))) return abs(self.global_best.fitness - mean_fit) < m diff --git a/lenstronomy/Sampling/sampler.py b/lenstronomy/Sampling/sampler.py index 9e68b2e92..7e0ffd672 100644 --- a/lenstronomy/Sampling/sampler.py +++ b/lenstronomy/Sampling/sampler.py @@ -44,7 +44,7 @@ def simplex(self, init_pos, n_iterations, method, print_key='SIMPLEX'): kwargs_return = self.chain.param.args2kwargs(result['x']) print(-logL * 2 / (max(self.chain.effective_num_data_points(**kwargs_return), 1)), 'reduced X^2 of best position') - print(logL, 'logL') + print(logL, 'log likelihood') print(self.chain.effective_num_data_points(**kwargs_return), 'effective number of data points') print(kwargs_return.get('kwargs_lens', None), 'lens result') print(kwargs_return.get('kwargs_source', None), 'source result') @@ -100,13 +100,13 @@ def pso(self, n_particles, n_iterations, lower_start=None, upper_start=None, time_start = time.time() - result, [chi2_list, pos_list, vel_list] = pso.optimize(n_iterations) + result, [log_likelihood_list, pos_list, vel_list] = pso.optimize(n_iterations) if pool.is_master(): kwargs_return = self.chain.param.args2kwargs(result) print(pso.global_best.fitness * 2 / (max( self.chain.effective_num_data_points(**kwargs_return), 1)), 'reduced X^2 of best position') - print(pso.global_best.fitness, 'logL') + print(pso.global_best.fitness, 'log likelihood') print(self.chain.effective_num_data_points(**kwargs_return), 'effective number of data points') print(kwargs_return.get('kwargs_lens', None), 'lens result') print(kwargs_return.get('kwargs_source', None), 'source result') @@ -116,7 +116,7 @@ def pso(self, n_particles, n_iterations, lower_start=None, upper_start=None, time_end = time.time() print(time_end - time_start, 'time used for ', print_key) print('===================') - return result, [chi2_list, pos_list, vel_list] + return result, [log_likelihood_list, pos_list, vel_list] def mcmc_emcee(self, n_walkers, n_run, n_burn, mean_start, sigma_start, mpi=False, progress=False, threadCount=1, @@ -217,12 +217,14 @@ def mcmc_zeus(self, n_walkers, n_run, n_burn, mean_start, sigma_start, :type mean_start: numpy array of length the number of parameters :param sigma_start: spread of the parameter values (uncorrelated in each dimension) of the initialising sample :type sigma_start: numpy array of length the number of parameters + :param mpi: if True, initializes an MPIPool to allow for MPI execution of the sampler + :type mpi: bool :param progress: :type progress: bool :param initpos: initial walker position to start sampling (optional) :type initpos: numpy array of size num param x num walkser - :param backup_filename: name of the HDF5 file where sampling state is saved (through zeus callback function) - :type backup_filename: string + :param backend_filename: name of the HDF5 file where sampling state is saved (through zeus callback function) + :type backend_filename: string :return: samples, ln likelihood value of samples :rtype: numpy 2d array, numpy 1d array """ @@ -251,7 +253,7 @@ def mcmc_zeus(self, n_walkers, n_run, n_burn, mean_start, sigma_start, blobs_dtype=blobs_dtype, verbose=verbose, check_walkers=check_walkers, shuffle_ensemble=shuffle_ensemble, light_mode=light_mode) - sampler.run_mcmc(initpos, n_run_eff, progress=progress, callbacks = backend) + sampler.run_mcmc(initpos, n_run_eff, progress=progress, callbacks=backend) flat_samples = sampler.get_chain(flat=True, thin=1, discard=n_burn)