diff --git a/autogalaxy/config/priors/mass/total/dual_pseudo_isothermal_mass.yaml b/autogalaxy/config/priors/mass/total/dual_pseudo_isothermal_mass.yaml index ed239ba9..470396ee 100644 --- a/autogalaxy/config/priors/mass/total/dual_pseudo_isothermal_mass.yaml +++ b/autogalaxy/config/priors/mass/total/dual_pseudo_isothermal_mass.yaml @@ -1,4 +1,4 @@ -dPIEMass: +dPIEMassB0: centre_0: type: Gaussian mean: 0.0 @@ -73,7 +73,7 @@ dPIEMass: limits: lower: 0.0 upper: inf -dPIEMassSph: +dPIEMassB0Sph: centre_0: type: Gaussian mean: 0.0 @@ -124,7 +124,7 @@ dPIEMassSph: limits: lower: 0.0 upper: inf -dPIEMassLenstool: +dPIEMass: centre_0: type: Gaussian mean: 0.0 @@ -235,7 +235,7 @@ dPIEMassLenstool: limits: lower: 0.0 upper: 1.0 -dPIEMassLenstoolSph: +dPIEMassSph: centre_0: type: Gaussian mean: 0.0 diff --git a/autogalaxy/profiles/mass/__init__.py b/autogalaxy/profiles/mass/__init__.py index 765dfe2b..19b4cd8f 100644 --- a/autogalaxy/profiles/mass/__init__.py +++ b/autogalaxy/profiles/mass/__init__.py @@ -4,8 +4,8 @@ from .total import ( dPIEMass, dPIEMassSph, - dPIEMassLenstool, - dPIEMassLenstoolSph, + dPIEMassB0, + dPIEMassB0Sph, PIEMass, dPIEPotential, dPIEPotentialSph, diff --git a/autogalaxy/profiles/mass/total/__init__.py b/autogalaxy/profiles/mass/total/__init__.py index 63ebb853..a0a1ba3d 100644 --- a/autogalaxy/profiles/mass/total/__init__.py +++ b/autogalaxy/profiles/mass/total/__init__.py @@ -1,14 +1,14 @@ -from .dual_pseudo_isothermal_potential import dPIEPotential, dPIEPotentialSph -from .dual_pseudo_isothermal_mass import ( - PIEMass, - dPIEMass, - dPIEMassSph, - dPIEMassLenstool, - dPIEMassLenstoolSph, -) -from .isothermal import Isothermal, IsothermalSph -from .isothermal_core import IsothermalCore, IsothermalCoreSph -from .power_law import PowerLaw, PowerLawIntermediate, PowerLawSph -from .power_law_broken import PowerLawBroken, PowerLawBrokenSph -from .power_law_core import PowerLawCore, PowerLawCoreSph -from .power_law_multipole import PowerLawMultipole +from .dual_pseudo_isothermal_potential import dPIEPotential, dPIEPotentialSph +from .dual_pseudo_isothermal_mass import ( + PIEMass, + dPIEMass, + dPIEMassSph, + dPIEMassB0, + dPIEMassB0Sph, +) +from .isothermal import Isothermal, IsothermalSph +from .isothermal_core import IsothermalCore, IsothermalCoreSph +from .power_law import PowerLaw, PowerLawIntermediate, PowerLawSph +from .power_law_broken import PowerLawBroken, PowerLawBrokenSph +from .power_law_core import PowerLawCore, PowerLawCoreSph +from .power_law_multipole import PowerLawMultipole diff --git a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py index 45804b8d..46d4f5e2 100644 --- a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py +++ b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py @@ -40,7 +40,8 @@ def _b0_from_lenstool_sigma( return 6.0 * 648000.0 * (sigma / c_km_s) ** 2 * (d_ls / d_s) -# Within this profile family, PIEMass, dPIEMass, and dPIEMassSph are directly ported from Lenstool's C code, and have been thoroughly annotated and adapted for PyAutoLens. +# Within this profile family, PIEMass, dPIEMassB0, and dPIEMassB0Sph are directly ported from Lenstool's C code, and have been thoroughly annotated and adapted for PyAutoLens. +# dPIEMass and dPIEMassSph (the default profiles) expose the same physics in Lenstool's native parameterization. # The dPIEPotential and dPIEPotentialSph profiles are modified from the original `dPIEPotential` and `dPIEPotentialSph`, which were implemented to PyAutoLens by Jackson O'Donnell. @@ -434,8 +435,16 @@ def analytical_magnification_2d_from( return aa.Array2D(values=1.0 / det_A, mask=grid.mask) -class dPIEMass(MassProfile): - r"""Dual pseudo-isothermal elliptical mass distribution (dPIE, mass parameterisation). +class dPIEMassB0(MassProfile): + r"""Dual pseudo-isothermal elliptical mass distribution in the internal + (``ra``, ``rs``, ``b0``) parameterisation. + + **This is the non-standard parameterisation.** The default dPIE profile is + :class:`dPIEMass`, whose free parameters are Lenstool's native ones + (``ellipticity``, ``angle_pos``, ``sigma``, ``r_core``, ``r_cut``) as used + by essentially every published cluster- and group-scale analysis. Use this + class (or ``dPIEMass.from_b0``) only when composing a model directly on the + lens strength ``b0``. A two-component PIE profile with both a core radius :math:`r_a` and a truncation radius :math:`r_s`. The three-dimensional density scales as @@ -545,12 +554,18 @@ def from_lenstool( redshift_object: float = 0.5, redshift_source: float = 1.0, cosmology: LensingCosmology = None, - ) -> "dPIEMass": + ) -> "dPIEMassB0": """ - Construct a ``dPIEMass`` from Lenstool's native dPIE / PIEMD parameterization, as + Construct a ``dPIEMassB0`` from Lenstool's native dPIE / PIEMD parameterization, as read directly out of a Lenstool ``.par`` file (``potentiel`` profil 81) or the parameter tables of Lenstool-based papers. + For *model-fitting* in the Lenstool parameters use :class:`dPIEMass`, whose + constructor takes the same inputs (with a flat H0/Om0 cosmology so priors can be + composed); this classmethod is the general converter accepting an arbitrary + ``cosmology`` object (e.g. ``Planck15``) and returning the internal + parameterization. + Three Lenstool conventions are converted (each verified against the Lenstool C source): @@ -792,10 +807,14 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return factor * (pot_core - pot_cut) -class dPIEMassSph(dPIEMass): - r"""Spherical dual pseudo-isothermal mass distribution (dPIE, mass parameterisation). +class dPIEMassB0Sph(dPIEMassB0): + r"""Spherical dual pseudo-isothermal mass distribution in the internal + (``ra``, ``rs``, ``b0``) parameterisation. - The spherical limit of :class:`dPIEMass`. The projected convergence is: + **This is the non-standard parameterisation** — the default spherical dPIE is + :class:`dPIEMassSph` (Lenstool-native parameters); see :class:`dPIEMassB0`. + + The spherical limit of :class:`dPIEMassB0`. The projected convergence is: .. math:: @@ -887,11 +906,12 @@ def from_lenstool( redshift_object: float = 0.5, redshift_source: float = 1.0, cosmology: LensingCosmology = None, - ) -> "dPIEMassSph": + ) -> "dPIEMassB0Sph": """ - Construct a ``dPIEMassSph`` from Lenstool's native dPIE / PIEMD parameterization - (circular case). See ``dPIEMass.from_lenstool`` for the full conversion - conventions; the ellipticity and angle inputs are absent here. + Construct a ``dPIEMassB0Sph`` from Lenstool's native dPIE / PIEMD parameterization + (circular case). See ``dPIEMassB0.from_lenstool`` for the full conversion + conventions; the ellipticity and angle inputs are absent here. For model-fitting + in the Lenstool parameters use :class:`dPIEMassSph`. Parameters ---------- @@ -1056,21 +1076,45 @@ def analytical_hessian_2d_from(self, grid: "aa.type.Grid2DLike", xp=np, **kwargs return hessian_yy, hessian_xy, hessian_yx, hessian_xx -class dPIEMassLenstool(dPIEMass): - """ - The dPIE mass profile in Lenstool's native parameterization, supporting model-fitting - with priors placed directly on the Lenstool parameters. - - This is a thin wrapper around :class:`dPIEMass` whose free parameters are - (``ellipticity``, ``angle_pos``, ``sigma``, ``r_core``, ``r_cut``) exactly as they appear - in a Lenstool ``.par`` file (``potentiel`` profil 81), rather than the internal - (``ell_comps``, ``ra``, ``rs``, ``b0``). Use it to fit a model whose posteriors read - like a Lenstool results table; see ``dPIEMass.from_lenstool`` for the full - conversion conventions (verified against the Lenstool C source). - - The distance ratio D_LS / D_S entering ``b0`` uses the ``Planck15`` cosmology - (matching the ``NFWMCRLudlow`` convention); for a different cosmology construct via - ``dPIEMass.from_lenstool(..., cosmology=...)`` instead. +class dPIEMass(dPIEMassB0): + r"""Dual pseudo-isothermal elliptical mass distribution (dPIE) in Lenstool's + native parameterization — **the default dPIE profile**. + + The dPIE (Elíasdóttir et al. 2007, App. A; also PIEMD, Kassiola & Kovner 1993) + is the standard profile of published cluster- and group-scale strong-lensing + analyses (Limousin et al. 2005; Bergamini et al. 2019; and essentially every + Lenstool-based paper). Its free parameters here are exactly those of a Lenstool + ``.par`` file (``potentiel`` profil 81) / paper results table, so a fitted + posterior reads like a Lenstool results table: + + - ``ellipticity`` — Lenstool ``ellipticite``, emass = (a^2 - b^2) / (a^2 + b^2) + (Elíasdóttir et al. 2007, Eq. A26). Converted internally to + epot = (1 - q) / (1 + q) exactly as Lenstool's ``set_lens.c`` does. + - ``angle_pos`` — position angle in degrees, counter-clockwise from the + positive x-axis (Lenstool's tangent plane; axis flips from WCS conventions + must be handled when ingesting real-data catalogues). + - ``sigma`` — Lenstool's **fiducial** velocity dispersion ``v_disp`` + (sigma_LT, km/s), *not* the central dispersion: + sigma_0 = sqrt(3/2) * sigma_LT (Elíasdóttir et al. 2007, App. A; + Bergamini et al. 2019, Eq. 5). Quoting a measured stellar dispersion here + overestimates the mass by 50%. + - ``r_core`` / ``r_cut`` — Lenstool ``core_radius`` / ``cut_radius`` in + arcseconds (the internal ``ra`` / ``rs``). For ``.par`` files using the kpc + variants, pre-convert with + ``r_core = r_core_kpc / cosmology.kpc_per_arcsec_from(redshift=redshift_object)``. + + The lens strength is fully normalized internally: + b0 = 6 * 648000 * (sigma_LT / c)^2 * (D_LS / D_S) arcsec — equivalently + E_0 = 6 pi (D_LS / D_S) (sigma_LT / c)^2 in radians (Elíasdóttir et al. 2007, + Eq. A24) with the E_0-to-b0 prefactor folded in. Lenstool stores its ``b0`` + without the distance ratio and applies D_LS / D_S at deflection time + (``e_grad.c``); the two conventions are verified equivalent against the + Lenstool C source and reference deflections + (``autolens_workspace_test/scripts/cluster/lenstool_parity.py``). + + The internal (``ell_comps``, ``ra``, ``rs``, ``b0``) parameterization — the + non-standard variant — remains available via :class:`dPIEMassB0` (for models + with priors on ``b0``) and :meth:`from_b0`. Parameters ---------- @@ -1090,7 +1134,21 @@ class dPIEMassLenstool(dPIEMass): redshift_object : float The redshift of the lens, used for the D_LS / D_S normalization of ``b0``. redshift_source : float - The redshift of the source used to normalize ``b0``. + The redshift of the source used to normalize ``b0``. For multi-plane cluster + models this is the reference source plane the Lenstool model was normalized to. + H0, Om0 : float + Flat-input cosmology (defaults are Planck15 values) so the profile is fully + constructable from flat inputs — priors configs, CSV rows — while a Lenstool + run's own cosmology (typically H0=70, Om0=0.3) can be matched exactly. Model + *constants* in practice. For an arbitrary cosmology object (e.g. ``Planck15`` + with massive neutrinos) use ``dPIEMassB0.from_lenstool(..., cosmology=...)``. + + References + ---------- + Kassiola & Kovner (1993), ApJ, 417, 450. + Elíasdóttir et al. (2007), arXiv:0710.5636 (App. A). + Limousin et al. (2005), MNRAS, 356, 309. + Bergamini et al. (2019), A&A, 631, A130. """ def __init__( @@ -1143,12 +1201,48 @@ def __init__( self.H0 = H0 self.Om0 = Om0 + @classmethod + def from_b0( + cls, + centre: Tuple[float, float] = (0.0, 0.0), + ell_comps: Tuple[float, float] = (0.0, 0.0), + ra: float = 0.0, + rs: float = 2.0, + b0: float = 0.1, + ) -> "dPIEMassB0": + """ + Construct a dPIE in the internal, non-standard (``ell_comps``, ``ra``, ``rs``, + ``b0``) parameterization — returns a :class:`dPIEMassB0` instance. + + This is the pre-2026-07 default parameterization of the profile, kept for + direct control of the lens strength ``b0`` (e.g. scaling relations composed + on ``b0``). Published Lenstool-based analyses parameterize in + (``ellipticity``, ``angle_pos``, ``sigma``, ``r_core``, ``r_cut``) — the + default :class:`dPIEMass` constructor. -class dPIEMassLenstoolSph(dPIEMassSph): + Parameters + ---------- + centre + The (y,x) arc-second coordinates of the profile centre. + ell_comps + Ellipticity components (e1, e2) of the elliptical coordinate system. + ra + The inner core radius in arcseconds. + rs + The outer truncation radius in arcseconds. + b0 + The lens strength in arcseconds. + """ + return dPIEMassB0(centre=centre, ell_comps=ell_comps, ra=ra, rs=rs, b0=b0) + + +class dPIEMassSph(dPIEMassB0Sph): """ - The spherical dPIE mass profile in Lenstool's native parameterization, supporting - model-fitting with priors placed directly on the Lenstool parameters - (``sigma``, ``r_core``, ``r_cut``). See :class:`dPIEMassLenstool`. + The spherical dPIE mass profile in Lenstool's native parameterization — **the + default spherical dPIE profile** — supporting model-fitting with priors placed + directly on the Lenstool parameters (``sigma``, ``r_core``, ``r_cut``). See + :class:`dPIEMass` for the full conventions; the internal non-standard variant + is :class:`dPIEMassB0Sph` / :meth:`from_b0`. Parameters ---------- @@ -1202,3 +1296,29 @@ def __init__( self.redshift_source = redshift_source self.H0 = H0 self.Om0 = Om0 + + @classmethod + def from_b0( + cls, + centre: Tuple[float, float] = (0.0, 0.0), + ra: float = 0.1, + rs: float = 2.0, + b0: float = 1.0, + ) -> "dPIEMassB0Sph": + """ + Construct a spherical dPIE in the internal, non-standard (``ra``, ``rs``, + ``b0``) parameterization — returns a :class:`dPIEMassB0Sph` instance. See + ``dPIEMass.from_b0``. + + Parameters + ---------- + centre + The (y,x) arc-second coordinates of the profile centre. + ra + The inner core radius in arcseconds. + rs + The outer truncation radius in arcseconds. + b0 + The lens strength in arcseconds. + """ + return dPIEMassB0Sph(centre=centre, ra=ra, rs=rs, b0=b0) diff --git a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py index e0e67b6e..36b51a2b 100644 --- a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py +++ b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py @@ -10,7 +10,7 @@ class dPIEPotential(MassProfile): A dPIE profile in which ellipticity is applied to the lensing potential rather than the mass distribution. The circularly symmetric convergence takes the - same functional form as :class:`dPIEMassSph`: + same functional form as :class:`dPIEMassB0Sph`: .. math:: @@ -220,7 +220,7 @@ class dPIEPotentialSph(dPIEPotential): \right) where :math:`r` is the circular projected radius. This profile is - mathematically identical to :class:`dPIEMassSph`. + mathematically identical to :class:`dPIEMassB0Sph`. Parameters ---------- @@ -271,7 +271,7 @@ def __init__( `b0` is not in the Intermediate-Axis-Convention for its r_{em}^2 = x^2 / (1 + \\epsilon)^2 + y^2 / (1 - \\epsilon)^2 Credit: Jackson O'Donnell for implementing this profile in PyAutoLens. - Note: This dPIEPotentialSph should be the same with dPIEMassSph for their same mathamatical formulations. + Note: This dPIEPotentialSph should be the same with dPIEMassB0Sph for their same mathamatical formulations. Parameters ---------- diff --git a/test_autogalaxy/galaxy/test_galaxy_model_csv.py b/test_autogalaxy/galaxy/test_galaxy_model_csv.py index 1153bfb7..843a34bb 100644 --- a/test_autogalaxy/galaxy/test_galaxy_model_csv.py +++ b/test_autogalaxy/galaxy/test_galaxy_model_csv.py @@ -16,10 +16,10 @@ def test__mass_single_class__round_trip(tmp_path): profiles_by_galaxy = { "lens_0": { - "mass": ag.mp.dPIEMassSph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) + "mass": ag.mp.dPIEMassB0Sph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) }, "lens_1": { - "mass": ag.mp.dPIEMassSph(centre=(10.0, 8.0), ra=5.0, rs=12.0, b0=1.2) + "mass": ag.mp.dPIEMassB0Sph(centre=(10.0, 8.0), ra=5.0, rs=12.0, b0=1.2) }, } @@ -35,7 +35,7 @@ def test__mass_single_class__round_trip(tmp_path): assert table.family == "mass" assert [r.galaxy for r in table.rows] == ["lens_0", "lens_1"] assert [r.attr_name for r in table.rows] == ["mass", "mass"] - assert all(r.profile_class is ag.mp.dPIEMassSph for r in table.rows) + assert all(r.profile_class is ag.mp.dPIEMassB0Sph for r in table.rows) assert table.rows[0].params == { "centre": (0.0, 0.0), "ra": 8.0, @@ -50,7 +50,7 @@ def test__mass_sparse_columns__dpie_plus_nfw__round_trip(tmp_path): profiles_by_galaxy = { "lens_0": { - "mass": ag.mp.dPIEMassSph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) + "mass": ag.mp.dPIEMassB0Sph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) }, "host_halo": { "dark": ag.mp.NFWMCRLudlowSph( @@ -96,7 +96,7 @@ def test__mass_sparse_columns__dpie_plus_nfw__round_trip(tmp_path): assert raw_rows[1]["b0"] == "" table = ag.galaxy_models_from_csv(file_path, family="mass") - assert table.rows[0].profile_class is ag.mp.dPIEMassSph + assert table.rows[0].profile_class is ag.mp.dPIEMassB0Sph assert table.rows[0].params == { "centre": (0.0, 0.0), "ra": 8.0, @@ -176,7 +176,7 @@ def test__cross_family_join__builds_named_galaxies(tmp_path): ag.galaxy_models_to_csv( profiles_by_galaxy={ "lens_0": { - "mass": ag.mp.dPIEMassSph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) + "mass": ag.mp.dPIEMassB0Sph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) }, }, file_path=mass_csv, @@ -224,7 +224,7 @@ def test__cross_family_join__builds_named_galaxies(tmp_path): assert set(galaxies.keys()) == {"lens_0", "source_0"} assert galaxies["lens_0"].redshift == 0.5 - assert isinstance(galaxies["lens_0"].mass, ag.mp.dPIEMassSph) + assert isinstance(galaxies["lens_0"].mass, ag.mp.dPIEMassB0Sph) assert isinstance(galaxies["lens_0"].bulge, ag.lp.SersicSph) assert galaxies["source_0"].redshift == 1.0 assert isinstance(galaxies["source_0"].bulge, ag.lp.SersicCore) @@ -237,7 +237,7 @@ def test__af_models_round_trip(tmp_path): ag.galaxy_models_to_csv( profiles_by_galaxy={ "lens_0": { - "mass": ag.mp.dPIEMassSph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) + "mass": ag.mp.dPIEMassB0Sph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) }, }, file_path=mass_csv, @@ -254,7 +254,7 @@ def test__af_models_round_trip(tmp_path): assert isinstance(galaxy_model, af.Model) assert galaxy_model.cls is ag.Galaxy assert galaxy_model.redshift == 0.5 - assert galaxy_model.mass.cls is ag.mp.dPIEMassSph + assert galaxy_model.mass.cls is ag.mp.dPIEMassB0Sph assert galaxy_model.mass.ra == 8.0 assert galaxy_model.mass.rs == 20.0 assert galaxy_model.mass.b0 == 3.0 @@ -291,7 +291,7 @@ def test__redshift_consistency_check__raises(tmp_path): ag.galaxy_models_to_csv( profiles_by_galaxy={ "lens_0": { - "mass": ag.mp.dPIEMassSph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) + "mass": ag.mp.dPIEMassB0Sph(centre=(0.0, 0.0), ra=8.0, rs=20.0, b0=3.0) } }, file_path=mass_csv, @@ -390,7 +390,7 @@ def test__lenstool_parameterized_mass_row__round_trips(tmp_path): galaxies_from_csv_tables, ) - profile = ag.mp.dPIEMassLenstool( + profile = ag.mp.dPIEMass( centre=(1.5, -3.0), ellipticity=0.68, angle_pos=8.97, diff --git a/test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_mass.py b/test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_mass.py index 98de5f1a..7ac95fb5 100644 --- a/test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_mass.py +++ b/test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_mass.py @@ -6,7 +6,7 @@ def test__deflections_yx_2d_from__sph_config_1(): - mp = ag.mp.dPIEMassSph(centre=(-0.7, 0.5), b0=5.2, ra=2.0, rs=3.0) + mp = ag.mp.dPIEMassB0Sph(centre=(-0.7, 0.5), b0=5.2, ra=2.0, rs=3.0) deflections = mp.deflections_yx_2d_from(grid=ag.Grid2DIrregular([[0.1875, 0.1625]])) @@ -15,7 +15,7 @@ def test__deflections_yx_2d_from__sph_config_1(): def test__deflections_yx_2d_from__sph_config_2(): - mp = ag.mp.dPIEMassSph(centre=(-0.1, 0.1), b0=20.0, ra=2.0, rs=3.0) + mp = ag.mp.dPIEMassB0Sph(centre=(-0.1, 0.1), b0=20.0, ra=2.0, rs=3.0) deflections = mp.deflections_yx_2d_from(grid=ag.Grid2DIrregular([[0.1875, 0.1625]])) @@ -26,7 +26,7 @@ def test__deflections_yx_2d_from__sph_config_2(): def test__deflections_yx_2d_from__elliptical(): # First deviation from potential case due to ellipticity - mp = ag.mp.dPIEMass( + mp = ag.mp.dPIEMassB0( centre=(0, 0), ell_comps=(0.0, 0.333333), b0=4.0, ra=2.0, rs=3.0 ) @@ -37,10 +37,10 @@ def test__deflections_yx_2d_from__elliptical(): def test__deflections_yx_2d_from__elliptical_vs_spherical(): - elliptical = ag.mp.dPIEMass( + elliptical = ag.mp.dPIEMassB0( centre=(1.1, 1.1), ell_comps=(0.000001, 0.0000001), b0=12.0, ra=2.0, rs=3.0 ) - spherical = ag.mp.dPIEMassSph(centre=(1.1, 1.1), b0=12.0, ra=2.0, rs=3.0) + spherical = ag.mp.dPIEMassB0Sph(centre=(1.1, 1.1), b0=12.0, ra=2.0, rs=3.0) assert elliptical.deflections_yx_2d_from(grid=grid).array == pytest.approx( spherical.deflections_yx_2d_from(grid=grid).array, 1e-1 @@ -48,7 +48,7 @@ def test__deflections_yx_2d_from__elliptical_vs_spherical(): def test__convergence_func__matches_private_helper(): - """Regression: dPIEMass must override the abstract `convergence_func` + """Regression: dPIEMassB0 must override the abstract `convergence_func` so MGEDecomposer.decompose_convergence_via_mge (which walks the convergence radially during MGE potential decomposition) doesn't fall through to the abstract NotImplementedError. The shim delegates @@ -57,7 +57,7 @@ def test__convergence_func__matches_private_helper(): import numpy as np - mp = ag.mp.dPIEMass(centre=(0.0, 0.0), b0=5.2, ra=2.0, rs=3.0) + mp = ag.mp.dPIEMassB0(centre=(0.0, 0.0), b0=5.2, ra=2.0, rs=3.0) # Scalar radius: equals the _convergence formula directly. assert mp.convergence_func(1.5) == pytest.approx(mp._convergence(1.5), 1e-12) @@ -69,8 +69,8 @@ def test__convergence_func__matches_private_helper(): assert actual.shape == radii.shape assert actual == pytest.approx(expected, 1e-12) - # dPIEMassSph inherits the override from dPIEMass. - sph = ag.mp.dPIEMassSph(centre=(0.0, 0.0), b0=5.2, ra=2.0, rs=3.0) + # dPIEMassB0Sph inherits the override from dPIEMassB0. + sph = ag.mp.dPIEMassB0Sph(centre=(0.0, 0.0), b0=5.2, ra=2.0, rs=3.0) assert sph.convergence_func(1.5) == pytest.approx(sph._convergence(1.5), 1e-12) @@ -81,7 +81,7 @@ def test__from_lenstool__b0_conversion__matches_isothermal_relation(): cosmology = ag.cosmo.Planck15() - mp = ag.mp.dPIEMassSph.from_lenstool( + mp = ag.mp.dPIEMassB0Sph.from_lenstool( sigma=1000.0, r_core=0.1, r_cut=20.0, @@ -103,7 +103,7 @@ def test__from_lenstool__b0_explicit_value(): cosmology = ag.cosmo.Planck15() - mp = ag.mp.dPIEMassSph.from_lenstool( + mp = ag.mp.dPIEMassB0Sph.from_lenstool( sigma=250.0, redshift_object=0.3, redshift_source=1.5, @@ -126,7 +126,7 @@ def test__from_lenstool__ellipticity_conversion__matches_lenstool_internal(): emass = 0.4 - mp = ag.mp.dPIEMass.from_lenstool(ellipticity=emass, angle_pos=0.0) + mp = ag.mp.dPIEMassB0.from_lenstool(ellipticity=emass, angle_pos=0.0) epot_lenstool = (1.0 - (1.0 - emass**2) ** 0.5) / emass @@ -140,7 +140,7 @@ def test__from_lenstool__ellipticity_conversion__matches_lenstool_internal(): def test__from_lenstool__angle_and_radii_passthrough(): - mp = ag.mp.dPIEMass.from_lenstool( + mp = ag.mp.dPIEMassB0.from_lenstool( centre=(0.1, -0.2), ellipticity=0.3, angle_pos=45.0, @@ -165,8 +165,8 @@ def test__from_lenstool__sph_matches_elliptical_zero_ellipticity(): sigma=300.0, r_core=0.2, r_cut=15.0, redshift_object=0.4, redshift_source=1.8 ) - sph = ag.mp.dPIEMassSph.from_lenstool(**kwargs) - ell = ag.mp.dPIEMass.from_lenstool(ellipticity=0.0, angle_pos=0.0, **kwargs) + sph = ag.mp.dPIEMassB0Sph.from_lenstool(**kwargs) + ell = ag.mp.dPIEMassB0.from_lenstool(ellipticity=0.0, angle_pos=0.0, **kwargs) grid_check = ag.Grid2DIrregular([[0.5, 0.8], [1.5, -0.3]]) @@ -181,7 +181,7 @@ def test__from_lenstool__isothermal_limit_deflection_equals_b0(): # ra -> 0, rs -> inf, e = 0: the dPIE tends to a SIS whose deflection magnitude is b0 # everywhere, so b0 is the Einstein radius in this limit. - mp = ag.mp.dPIEMassSph.from_lenstool( + mp = ag.mp.dPIEMassB0Sph.from_lenstool( sigma=800.0, r_core=1e-5, r_cut=1e6, redshift_object=0.5, redshift_source=2.0 ) @@ -194,7 +194,7 @@ def test__potential_2d_from__gradient_matches_deflections(): # The analytic potential (Lenstool pi05 port) must be exactly consistent with the # ci05f deflections: finite-difference grad(psi) = alpha, including rotation. - mp = ag.mp.dPIEMass( + mp = ag.mp.dPIEMassB0( centre=(0.1, -0.3), ell_comps=(0.15, 0.1), ra=1.5, rs=30.0, b0=10.0 ) @@ -227,7 +227,7 @@ def test__potential_2d_from__spherical_limit_matches_quadrature(): import numpy as np - mp = ag.mp.dPIEMassSph(centre=(0.0, 0.0), ra=2.0, rs=20.0, b0=5.0) + mp = ag.mp.dPIEMassB0Sph(centre=(0.0, 0.0), ra=2.0, rs=20.0, b0=5.0) eps_fd = 1e-6 R = 4.0 @@ -242,9 +242,9 @@ def test__potential_2d_from__spherical_limit_matches_quadrature(): assert alpha_fd == pytest.approx(alpha[0, 1], rel=1e-5) -def test__lenstool_wrapper__matches_from_lenstool_constructor(): - # The model-fittable wrapper class must produce the identical profile to the - # from_lenstool classmethod with the (fixed Planck15) cosmology. +def test__default_class__matches_from_lenstool_classmethod(): + # The default (Lenstool-native parameterized) class must produce the identical + # profile to the dPIEMassB0.from_lenstool classmethod converter. kwargs = dict( centre=(0.1, -0.2), @@ -257,11 +257,11 @@ def test__lenstool_wrapper__matches_from_lenstool_constructor(): redshift_source=1.8, ) - wrapper = ag.mp.dPIEMassLenstool(**kwargs) + wrapper = ag.mp.dPIEMass(**kwargs) # The wrapper's flat-input cosmology (H0/Om0 floats -> FlatLambdaCDM) matches the # classmethod when the classmethod is handed the same background cosmology; the # Planck15 subclass differs at the massive-neutrino level. - constructed = ag.mp.dPIEMass.from_lenstool( + constructed = ag.mp.dPIEMassB0.from_lenstool( cosmology=ag.cosmo.FlatLambdaCDM(), **kwargs ) @@ -281,22 +281,22 @@ def test__lenstool_wrapper__matches_from_lenstool_constructor(): k: v for k, v in kwargs.items() if k not in ("ellipticity", "angle_pos") } - wrapper_sph = ag.mp.dPIEMassLenstoolSph(**sph_kwargs) - constructed_sph = ag.mp.dPIEMassSph.from_lenstool( + wrapper_sph = ag.mp.dPIEMassSph(**sph_kwargs) + constructed_sph = ag.mp.dPIEMassB0Sph.from_lenstool( cosmology=ag.cosmo.FlatLambdaCDM(), **sph_kwargs ) assert wrapper_sph.b0 == pytest.approx(constructed_sph.b0, rel=1e-10) -def test__lenstool_wrapper__supports_model_composition(): +def test__default_class__supports_model_composition(): # Fitting in Lenstool parameters: af.Model must resolve priors for every __init__ # arg from the config, and fixing the redshifts must leave the Lenstool free # parameters (centre_0, centre_1, ellipticity, angle, sigma, r_core, r_cut). import autofit as af - model = af.Model(ag.mp.dPIEMassLenstool) + model = af.Model(ag.mp.dPIEMass) assert model.prior_count == 11 @@ -309,9 +309,29 @@ def test__lenstool_wrapper__supports_model_composition(): instance = model.instance_from_unit_vector([0.5] * model.prior_count) - assert isinstance(instance, ag.mp.dPIEMassLenstool) + assert isinstance(instance, ag.mp.dPIEMass) assert instance.b0 > 0.0 - model_sph = af.Model(ag.mp.dPIEMassLenstoolSph) + model_sph = af.Model(ag.mp.dPIEMassSph) assert model_sph.prior_count == 9 + + +def test__from_b0__returns_internal_parameterization(): + # The non-standard construction path: dPIEMass.from_b0 / dPIEMassSph.from_b0 + # return the internal (ra, rs, b0) classes with the inputs passed through. + + mp = ag.mp.dPIEMass.from_b0( + centre=(0.1, -0.2), ell_comps=(0.1, 0.05), ra=0.5, rs=10.0, b0=1.3 + ) + + assert isinstance(mp, ag.mp.dPIEMassB0) + assert mp.centre == (0.1, -0.2) + assert mp.ra == 0.5 + assert mp.rs == 10.0 + assert mp.b0 == 1.3 + + sph = ag.mp.dPIEMassSph.from_b0(centre=(0.0, 0.0), ra=0.5, rs=10.0, b0=1.3) + + assert isinstance(sph, ag.mp.dPIEMassB0Sph) + assert sph.b0 == 1.3