From 39aab61c01d999d0382047d0ba8141994636fddc Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Tue, 13 Jan 2026 15:56:30 +0000 Subject: [PATCH 01/38] Added cNFW --- autogalaxy/profiles/mass/dark/cnfw.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 autogalaxy/profiles/mass/dark/cnfw.py diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py new file mode 100644 index 000000000..e69de29bb From 3397bd45026515ca2439078aaa3478f1dcea9f10 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 14 Jan 2026 14:57:15 +0000 Subject: [PATCH 02/38] added cnfw.yaml to config --- autogalaxy/config/priors/mass/dark/cnfw.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 autogalaxy/config/priors/mass/dark/cnfw.yaml diff --git a/autogalaxy/config/priors/mass/dark/cnfw.yaml b/autogalaxy/config/priors/mass/dark/cnfw.yaml new file mode 100644 index 000000000..e69de29bb From c0e0e9898f13afc764838b7c5086a701397aa6af Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 14 Jan 2026 15:10:30 +0000 Subject: [PATCH 03/38] included cnfw in __init__.py --- autogalaxy/profiles/mass/dark/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/autogalaxy/profiles/mass/dark/__init__.py b/autogalaxy/profiles/mass/dark/__init__.py index 37c26c344..c193245d7 100644 --- a/autogalaxy/profiles/mass/dark/__init__.py +++ b/autogalaxy/profiles/mass/dark/__init__.py @@ -9,3 +9,4 @@ from .nfw_truncated_mcr import NFWTruncatedMCRLudlowSph, NFWTruncatedMCRDuffySph from .nfw_truncated_mcr_scatter import NFWTruncatedMCRScatterLudlowSph from .nfw_virial_mass_conc import NFWVirialMassConcSph +from .cnfw import cNFW From 8748cc5906f18ad4996e051d5f53805d99e99fe7 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 14 Jan 2026 15:15:22 +0000 Subject: [PATCH 04/38] included cnfw in __init__.py of mass folder --- autogalaxy/profiles/mass/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/autogalaxy/profiles/mass/__init__.py b/autogalaxy/profiles/mass/__init__.py index a4b358d0d..bbf619605 100644 --- a/autogalaxy/profiles/mass/__init__.py +++ b/autogalaxy/profiles/mass/__init__.py @@ -36,6 +36,7 @@ NFWMCRLudlow, gNFWMCRLudlow, NFWVirialMassConcSph, + cNFW, ) from .stellar import ( Gaussian, From 19fb5093d8de2c0ad89082cf31c83db0190fc8d8 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 14 Jan 2026 15:42:04 +0000 Subject: [PATCH 05/38] added cnfw.py --- autogalaxy/profiles/mass/dark/cnfw.py | 114 ++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index e69de29bb..1516256ca 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -0,0 +1,114 @@ +import numpy as np + +from typing import Tuple + +import autoarray as aa + +from autogalaxy.profiles.mass.abstract.abstract import MassProfile + +class cNFW(MassProfile): + def __init__( + self, + centre: Tuple[float, float] = (0.0, 0.0), + kappa_s: float = 0.05, + scale_radius: float = 1.0, + core_radius: float = 0.5, + ): + """ + Represents a spherical cored NFW density distribution + + Parameters + ---------- + centre + The (y,x) arc-second coordinates of the profile centre. + kappa_s + The overall normalization of the dark matter halo \| + (kappa_s = (rho_0 * scale_radius)/lensing_critical_density) + scale_radius + The cored NFW scale radius `theta_s`, as an angle on the sky in arcseconds. + core_radius + The cored NFW core radius `theta_c`, as an angle on the sky in arcseconds. + """ + + super().__init__( + centre=centre, + ell_comps=(0.0, 0.0)) + + self.kappa_s = kappa_s + self.scale_radius = scale_radius + self.core_radius = core_radius + + + @aa.grid_dec.to_vector_yx + @aa.grid_dec.transform + def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): + """ + Calculate the deflection angles on a grid of (y,x) arc-second coordinates. + + The input grid of (y,x) coordinates are transformed to a coordinate system centred on the profile centre with + and rotated based on the position angle defined from its `ell_comps` (this is described fully below). + + The numerical backend can be selected via the ``xp`` argument, allowing this + method to be used with both NumPy and JAX (e.g. inside ``jax.jit``-compiled + code). This is described fully later in this example. + + Parameters + ---------- + grid + The grid of (y,x) arc-second coordinates the deflection angles are computed on. + xp + The numerical backend to use, either `numpy` or `jax.numpy`. + """ + theta = self.radial_grid_from(grid=grid, xp=xp, **kwargs).array + + factor = ( + 4.0 + * self.kappa_s + * self.scale_radius**2 + ) + + deflection_r = ( + factor + * (self.F_func(theta, self.scale_radius) - self.F_func(theta, self.core_radius) + - (self.scale_radius - self.core_radius) * self.dev_F_func(theta, self.scale_radius) + ) + / (theta * (self.scale_radius - self.core_radius)**2) + ) + + + return self._cartesian_grid_via_radial_from( + grid=grid, + radius=deflection_r, + xp=xp, + **kwargs, + ) + + def F_func(self, theta, radius, xp=np): + if theta == 0: + F = 0 + elif theta < radius: + F = (radius / 2 * xp.log(2 * radius / theta) - xp.sqrt(radius ** 2 - theta ** 2) + * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) + ) + else: + F = (radius / 2 * xp.log(2 * radius / theta) + xp.sqrt(theta ** 2 - radius ** 2) + * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) + ) + return 2 * radius * F + + def dev_F_func(self, theta, radius, xp=np): + if theta == 0: + dev_F = 0 + elif theta < radius: + dev_F = (radius * xp.log(2 * radius / theta) + - (2 * radius ** 2 - theta ** 2) / xp.sqrt(radius ** 2 - theta ** 2) + * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) + ) + elif theta == radius: + dev_F = (radius * (xp.log(2) - 1 / 2)) + else: + dev_F = (radius * xp.log(2 * radius / theta) + + (theta ** 2 - 2 * radius ** 2) / xp.sqrt(theta ** 2 - radius ** 2) + * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) + ) + return 2 * dev_F \ No newline at end of file From efbb0d9d0d9e006fd5fd1e1b26921cac3f5551d6 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 14 Jan 2026 15:54:52 +0000 Subject: [PATCH 06/38] cnfw.yaml added --- autogalaxy/config/priors/mass/dark/cnfw.yaml | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/autogalaxy/config/priors/mass/dark/cnfw.yaml b/autogalaxy/config/priors/mass/dark/cnfw.yaml index e69de29bb..984aa7f56 100644 --- a/autogalaxy/config/priors/mass/dark/cnfw.yaml +++ b/autogalaxy/config/priors/mass/dark/cnfw.yaml @@ -0,0 +1,51 @@ +cNFW: + centre_0: + type: Gaussian + mean: 0.0 + sigma: 0.1 + width_modifier: + type: Absolute + value: 0.05 + limits: + lower: -inf + upper: inf + centre_1: + type: Gaussian + mean: 0.0 + sigma: 0.1 + width_modifier: + type: Absolute + value: 0.05 + limits: + lower: -inf + upper: inf + kappa_s: + type: Uniform + lower_limit: 0.0 + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.2 + limits: + lower: 0.0 + upper: inf + scale_radius: + type: Uniform + lower_limit: 0.0 + upper_limit: 30.0 + width_modifier: + type: Relative + value: 0.2 + limits: + lower: 0.0 + upper: inf + core_radius: + type: Uniform + lower_limit: 0.0 + upper_limit: 30.0 + width_modifier: + type: Relative + value: 0.2 + limits: + lower: 0.0 + upper: inf \ No newline at end of file From b9d974e05c139b6c7af5bc338e830f6d0a60732d Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 14 Jan 2026 16:09:46 +0000 Subject: [PATCH 07/38] fix of F functions, remove if loop --- autogalaxy/profiles/mass/dark/cnfw.py | 141 +++++++++++++++++++++----- 1 file changed, 116 insertions(+), 25 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 1516256ca..bca525aa8 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -84,31 +84,122 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): ) def F_func(self, theta, radius, xp=np): - if theta == 0: - F = 0 - elif theta < radius: - F = (radius / 2 * xp.log(2 * radius / theta) - xp.sqrt(radius ** 2 - theta ** 2) - * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) - ) - else: - F = (radius / 2 * xp.log(2 * radius / theta) + xp.sqrt(theta ** 2 - radius ** 2) - * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) - ) + + theta = xp.asarray(theta) + + F = xp.zeros_like(theta) + + # theta == 0 + mask0 = theta == 0 + + # theta < radius + mask1 = (theta > 0) & (theta < radius) + + # theta > radius + mask2 = theta > radius + + F = xp.where( + mask0, + ( + 0.0 + ), + F, + ) + + F = xp.where( + mask1, + ( + radius / 2 * xp.log(2 * radius / theta) + - xp.sqrt(radius ** 2 - theta ** 2) + * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) + ), + F, + ) + + F = xp.where( + mask2, + ( + radius / 2 * xp.log(2 * radius / theta) + + xp.sqrt(theta ** 2 - radius ** 2) + * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) + ), + F, + ) + return 2 * radius * F def dev_F_func(self, theta, radius, xp=np): - if theta == 0: - dev_F = 0 - elif theta < radius: - dev_F = (radius * xp.log(2 * radius / theta) - - (2 * radius ** 2 - theta ** 2) / xp.sqrt(radius ** 2 - theta ** 2) - * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) - ) - elif theta == radius: - dev_F = (radius * (xp.log(2) - 1 / 2)) - else: - dev_F = (radius * xp.log(2 * radius / theta) - + (theta ** 2 - 2 * radius ** 2) / xp.sqrt(theta ** 2 - radius ** 2) - * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) - ) - return 2 * dev_F \ No newline at end of file + theta = xp.asarray(theta) + + dev_F = xp.zeros_like(theta) + + mask0 = theta == 0 + mask1 = (theta > 0) & (theta < radius) + mask2 = theta == radius + mask3 = theta > radius + + dev_F = xp.where( + mask0, + ( + 0.0 + ), + dev_F, + ) + + dev_F = xp.where( + mask1, + ( + radius * xp.log(2 * radius / theta) + - (2 * radius ** 2 - theta ** 2) / xp.sqrt(radius ** 2 - theta ** 2) + * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) + ), + dev_F, + ) + + dev_F = xp.where( + mask2, + radius * (xp.log(2) - 1 / 2), + dev_F, + ) + + dev_F = xp.where( + mask3, + ( + radius * xp.log(2 * radius / theta) + + (theta ** 2 - 2 * radius ** 2) / xp.sqrt(theta ** 2 - radius ** 2) + * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) + ), + dev_F, + ) + + return 2 * dev_F + + # def F_func(self, theta, radius, xp=np): + # if theta == 0: + # F = 0 + # elif theta < radius: + # F = (radius / 2 * xp.log(2 * radius / theta) - xp.sqrt(radius ** 2 - theta ** 2) + # * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) + # ) + # else: + # F = (radius / 2 * xp.log(2 * radius / theta) + xp.sqrt(theta ** 2 - radius ** 2) + # * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) + # ) + # return 2 * radius * F + + # def dev_F_func(self, theta, radius, xp=np): + # if theta == 0: + # dev_F = 0 + # elif theta < radius: + # dev_F = (radius * xp.log(2 * radius / theta) + # - (2 * radius ** 2 - theta ** 2) / xp.sqrt(radius ** 2 - theta ** 2) + # * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) + # ) + # elif theta == radius: + # dev_F = (radius * (xp.log(2) - 1 / 2)) + # else: + # dev_F = (radius * xp.log(2 * radius / theta) + # + (theta ** 2 - 2 * radius ** 2) / xp.sqrt(theta ** 2 - radius ** 2) + # * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) + # ) + # return 2 * dev_F \ No newline at end of file From a1a3e1542a8f406fbb1d758bcccdb6dd27bce9d6 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 15 Jan 2026 09:09:27 +0000 Subject: [PATCH 08/38] removed xp.asarray(theta) --- autogalaxy/profiles/mass/dark/cnfw.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index bca525aa8..92ac68be6 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -85,8 +85,6 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): def F_func(self, theta, radius, xp=np): - theta = xp.asarray(theta) - F = xp.zeros_like(theta) # theta == 0 @@ -129,7 +127,6 @@ def F_func(self, theta, radius, xp=np): return 2 * radius * F def dev_F_func(self, theta, radius, xp=np): - theta = xp.asarray(theta) dev_F = xp.zeros_like(theta) From a779a767296681a0b705380b79f7101770f7b656 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 15 Jan 2026 10:38:33 +0000 Subject: [PATCH 09/38] change xp.zeros_like to theta * 0.0 --- autogalaxy/profiles/mass/dark/cnfw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 92ac68be6..0b919c9c9 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -85,7 +85,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): def F_func(self, theta, radius, xp=np): - F = xp.zeros_like(theta) + F = theta * 0.0 # theta == 0 mask0 = theta == 0 @@ -128,7 +128,7 @@ def F_func(self, theta, radius, xp=np): def dev_F_func(self, theta, radius, xp=np): - dev_F = xp.zeros_like(theta) + dev_F = theta * 0.0 mask0 = theta == 0 mask1 = (theta > 0) & (theta < radius) From 05798466193ef058515cab92923d96311dec6951 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 15 Jan 2026 10:42:12 +0000 Subject: [PATCH 10/38] pass on xp=xp to F functions --- autogalaxy/profiles/mass/dark/cnfw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 0b919c9c9..515cb7e82 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -69,8 +69,8 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): deflection_r = ( factor - * (self.F_func(theta, self.scale_radius) - self.F_func(theta, self.core_radius) - - (self.scale_radius - self.core_radius) * self.dev_F_func(theta, self.scale_radius) + * (self.F_func(theta, self.scale_radius, xp=xp) - self.F_func(theta, self.core_radius, xp=xp) + - (self.scale_radius - self.core_radius) * self.dev_F_func(theta, self.scale_radius, xp=xp) ) / (theta * (self.scale_radius - self.core_radius)**2) ) From bb41d60b5e7bc3c88a260c7eb28b28fa5361d86e Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 15 Jan 2026 10:43:28 +0000 Subject: [PATCH 11/38] prevent theta=0 --- autogalaxy/profiles/mass/dark/cnfw.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 515cb7e82..648b6a7f1 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -60,7 +60,8 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): The numerical backend to use, either `numpy` or `jax.numpy`. """ theta = self.radial_grid_from(grid=grid, xp=xp, **kwargs).array - + theta = xp.maximum(theta, 1e-8) + factor = ( 4.0 * self.kappa_s From 36750ab225d65b98380a62cd8fefaea19e1e361e Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 15 Jan 2026 10:44:55 +0000 Subject: [PATCH 12/38] remove unnecessary mask0 --- autogalaxy/profiles/mass/dark/cnfw.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 648b6a7f1..a112174de 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -61,7 +61,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): """ theta = self.radial_grid_from(grid=grid, xp=xp, **kwargs).array theta = xp.maximum(theta, 1e-8) - + factor = ( 4.0 * self.kappa_s @@ -88,23 +88,12 @@ def F_func(self, theta, radius, xp=np): F = theta * 0.0 - # theta == 0 - mask0 = theta == 0 - # theta < radius mask1 = (theta > 0) & (theta < radius) # theta > radius mask2 = theta > radius - F = xp.where( - mask0, - ( - 0.0 - ), - F, - ) - F = xp.where( mask1, ( @@ -131,19 +120,10 @@ def dev_F_func(self, theta, radius, xp=np): dev_F = theta * 0.0 - mask0 = theta == 0 mask1 = (theta > 0) & (theta < radius) mask2 = theta == radius mask3 = theta > radius - dev_F = xp.where( - mask0, - ( - 0.0 - ), - dev_F, - ) - dev_F = xp.where( mask1, ( From 189d401f3d716e1aba4b266a4b17a2e4c565194f Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 15 Jan 2026 14:07:47 +0000 Subject: [PATCH 13/38] added convergence_2d_from and potential_2d_from with zeros --- autogalaxy/profiles/mass/dark/cnfw.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index a112174de..73d55d3a7 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -152,6 +152,14 @@ def dev_F_func(self, theta, radius, xp=np): return 2 * dev_F + @aa.grid_dec.to_array + def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): + return xp.zeros(shape=grid.shape[0]) + + @aa.grid_dec.to_array + def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): + return xp.zeros(shape=grid.shape[0]) + # def F_func(self, theta, radius, xp=np): # if theta == 0: # F = 0 From 700f4fc9518732480a5713edfbd089a24b8211fd Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 15 Jan 2026 14:33:57 +0000 Subject: [PATCH 14/38] docstring: corrected kappa_s --- autogalaxy/profiles/mass/dark/cnfw.py | 2 +- test_autogalaxy/profiles/mass/dark/test_cnfw.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 test_autogalaxy/profiles/mass/dark/test_cnfw.py diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 73d55d3a7..c29a8be46 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -23,7 +23,7 @@ def __init__( The (y,x) arc-second coordinates of the profile centre. kappa_s The overall normalization of the dark matter halo \| - (kappa_s = (rho_0 * scale_radius)/lensing_critical_density) + (kappa_s = (rho_0 * D_d * scale_radius)/lensing_critical_density) scale_radius The cored NFW scale radius `theta_s`, as an angle on the sky in arcseconds. core_radius diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py new file mode 100644 index 000000000..e69de29bb From 728bcb8d35e3c36cbeabe25babc5a9a13ee423a2 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Fri, 16 Jan 2026 11:49:02 +0000 Subject: [PATCH 15/38] added test_nfw.py --- .../profiles/mass/dark/test_cnfw.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py index e69de29bb..99e228092 100644 --- a/test_autogalaxy/profiles/mass/dark/test_cnfw.py +++ b/test_autogalaxy/profiles/mass/dark/test_cnfw.py @@ -0,0 +1,28 @@ +import numpy as np +import pytest + +import autogalaxy as ag + +def test__deflections_yx_2d_from(): + cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + + deflection_2d = cnfw.deflections_yx_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) + deflection_r = np.sqrt(deflection_2d[0]**2 + deflection_2d[1]**2) + + assert deflection_r == pytest.approx(0.006034319441107217, 1.0e-8) + +def test_convergence_2d_from(): + cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + + convergence_2d = cnfw.convergence_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) + convergence_r = np.sqrt(convergence_2d[0]**2 + convergence_2d[1]**2) + + assert convergence_r == pytest.approx(0.0, 1.0e-4) + +def potential_2d_from(): + cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + + potential_2d = cnfw.potential_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) + potential_r = np.sqrt(potential_2d[0] ** 2 + potential_2d[1] ** 2) + + assert potential_r == pytest.approx(0.0, 1.0e-4) \ No newline at end of file From e2791a5bd42da1de87e53a060383f409d69fd790 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Fri, 16 Jan 2026 11:59:08 +0000 Subject: [PATCH 16/38] fix convergence and potential test --- test_autogalaxy/profiles/mass/dark/test_cnfw.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py index 99e228092..2f03a5c80 100644 --- a/test_autogalaxy/profiles/mass/dark/test_cnfw.py +++ b/test_autogalaxy/profiles/mass/dark/test_cnfw.py @@ -14,15 +14,13 @@ def test__deflections_yx_2d_from(): def test_convergence_2d_from(): cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) - convergence_2d = cnfw.convergence_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) - convergence_r = np.sqrt(convergence_2d[0]**2 + convergence_2d[1]**2) + convergence = cnfw.convergence_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) - assert convergence_r == pytest.approx(0.0, 1.0e-4) + assert convergence == pytest.approx(0.0, 1.0e-4) -def potential_2d_from(): +def test_potential_2d_from(): cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) - potential_2d = cnfw.potential_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) - potential_r = np.sqrt(potential_2d[0] ** 2 + potential_2d[1] ** 2) + potential = cnfw.potential_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) - assert potential_r == pytest.approx(0.0, 1.0e-4) \ No newline at end of file + assert potential == pytest.approx(0.0, 1.0e-4) \ No newline at end of file From 1505abf8b9cdcb5c2dbe194d366c1138a7560224 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Fri, 16 Jan 2026 12:09:42 +0000 Subject: [PATCH 17/38] fix deflection test --- test_autogalaxy/profiles/mass/dark/test_cnfw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py index 2f03a5c80..5fcd99097 100644 --- a/test_autogalaxy/profiles/mass/dark/test_cnfw.py +++ b/test_autogalaxy/profiles/mass/dark/test_cnfw.py @@ -7,7 +7,7 @@ def test__deflections_yx_2d_from(): cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) deflection_2d = cnfw.deflections_yx_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) - deflection_r = np.sqrt(deflection_2d[0]**2 + deflection_2d[1]**2) + deflection_r = np.sqrt(deflection_2d[0, 0]**2 + deflection_2d[0, 1]**2) assert deflection_r == pytest.approx(0.006034319441107217, 1.0e-8) From 980e655f5612c89fe8143e5fc290f79911f070b5 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 10:27:29 +0000 Subject: [PATCH 18/38] changed class cNFW to cNFWsph --- autogalaxy/config/priors/mass/dark/cnfw.yaml | 2 +- autogalaxy/profiles/mass/__init__.py | 2 +- autogalaxy/profiles/mass/dark/__init__.py | 2 +- autogalaxy/profiles/mass/dark/cnfw.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/autogalaxy/config/priors/mass/dark/cnfw.yaml b/autogalaxy/config/priors/mass/dark/cnfw.yaml index 984aa7f56..a4683ba63 100644 --- a/autogalaxy/config/priors/mass/dark/cnfw.yaml +++ b/autogalaxy/config/priors/mass/dark/cnfw.yaml @@ -1,4 +1,4 @@ -cNFW: +cNFWsph: centre_0: type: Gaussian mean: 0.0 diff --git a/autogalaxy/profiles/mass/__init__.py b/autogalaxy/profiles/mass/__init__.py index bbf619605..bf3607470 100644 --- a/autogalaxy/profiles/mass/__init__.py +++ b/autogalaxy/profiles/mass/__init__.py @@ -36,7 +36,7 @@ NFWMCRLudlow, gNFWMCRLudlow, NFWVirialMassConcSph, - cNFW, + cNFWsph, ) from .stellar import ( Gaussian, diff --git a/autogalaxy/profiles/mass/dark/__init__.py b/autogalaxy/profiles/mass/dark/__init__.py index c193245d7..b2062df9e 100644 --- a/autogalaxy/profiles/mass/dark/__init__.py +++ b/autogalaxy/profiles/mass/dark/__init__.py @@ -9,4 +9,4 @@ from .nfw_truncated_mcr import NFWTruncatedMCRLudlowSph, NFWTruncatedMCRDuffySph from .nfw_truncated_mcr_scatter import NFWTruncatedMCRScatterLudlowSph from .nfw_virial_mass_conc import NFWVirialMassConcSph -from .cnfw import cNFW +from .cnfw import cNFWsph diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index c29a8be46..ee14a687e 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -6,7 +6,7 @@ from autogalaxy.profiles.mass.abstract.abstract import MassProfile -class cNFW(MassProfile): +class cNFWsph(MassProfile): def __init__( self, centre: Tuple[float, float] = (0.0, 0.0), From 16d2dc2ce27446a7b63f17c2b176752792164a1f Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 10:35:08 +0000 Subject: [PATCH 19/38] change cNFW to cNFWsph in tests --- test_autogalaxy/profiles/mass/dark/test_cnfw.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py index 5fcd99097..20ee101d5 100644 --- a/test_autogalaxy/profiles/mass/dark/test_cnfw.py +++ b/test_autogalaxy/profiles/mass/dark/test_cnfw.py @@ -4,7 +4,7 @@ import autogalaxy as ag def test__deflections_yx_2d_from(): - cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + cnfw = ag.mp.cNFWsph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) deflection_2d = cnfw.deflections_yx_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) deflection_r = np.sqrt(deflection_2d[0, 0]**2 + deflection_2d[0, 1]**2) @@ -12,14 +12,14 @@ def test__deflections_yx_2d_from(): assert deflection_r == pytest.approx(0.006034319441107217, 1.0e-8) def test_convergence_2d_from(): - cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + cnfw = ag.mp.cNFWsph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) convergence = cnfw.convergence_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) assert convergence == pytest.approx(0.0, 1.0e-4) def test_potential_2d_from(): - cnfw = ag.mp.cNFW(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + cnfw = ag.mp.cNFWshp(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) potential = cnfw.potential_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) From 1dcf3d0eaca7f315666e4f7f89538826fb9d1de6 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 10:41:24 +0000 Subject: [PATCH 20/38] corrected typo --- test_autogalaxy/profiles/mass/dark/test_cnfw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py index 20ee101d5..d52a1c295 100644 --- a/test_autogalaxy/profiles/mass/dark/test_cnfw.py +++ b/test_autogalaxy/profiles/mass/dark/test_cnfw.py @@ -19,7 +19,7 @@ def test_convergence_2d_from(): assert convergence == pytest.approx(0.0, 1.0e-4) def test_potential_2d_from(): - cnfw = ag.mp.cNFWshp(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + cnfw = ag.mp.cNFWsph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) potential = cnfw.potential_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) From cfea2c0ff733a171f194c19a522a5d1567e6b232 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 11:47:46 +0000 Subject: [PATCH 21/38] added kappa_s_and_scale_radius_and_core_radius --- autogalaxy/profiles/mass/dark/mcr_util.py | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/autogalaxy/profiles/mass/dark/mcr_util.py b/autogalaxy/profiles/mass/dark/mcr_util.py index 64468c039..b8c82025f 100644 --- a/autogalaxy/profiles/mass/dark/mcr_util.py +++ b/autogalaxy/profiles/mass/dark/mcr_util.py @@ -58,6 +58,56 @@ def kappa_s_and_scale_radius_for_duffy(mass_at_200, redshift_object, redshift_so return kappa_s, scale_radius, radius_at_200 +def kappa_s_and_scale_radius_and_core_radius(mass_at_200, f_c, redshift_object, redshift_source): + """ + Computes the AutoGalaxy cNFW parameters (kappa_s, scale_radius, core_radius) for an NFW halo of the given + mass, enforcing the Penarrubia '12 mass-concentration relation. + + Interprets mass as *`M_{200c}`*, not `M_{200m}`. + + f_c = core_radius / scale radius + """ + + from astropy import units + + from autogalaxy.cosmology.wrap import Planck15 + + cosmology = Planck15() + + cosmic_average_density = ( + cosmology.critical_density(redshift_object).to(units.solMass / units.kpc**3) + ).value + + critical_surface_density = ( + cosmology.critical_surface_density_between_redshifts_solar_mass_per_kpc2_from( + redshift_0=redshift_object, redshift_1=redshift_source + ) + ) + + kpc_per_arcsec = cosmology.kpc_per_arcsec_from(redshift=redshift_object) + + radius_at_200 = ( + mass_at_200 / (200.0 * cosmic_average_density * (4.0 * np.pi / 3.0)) + ) ** ( + 1.0 / 3.0 + ) # r200 + coefficient = 5.71 * (1.0 + redshift_object) ** ( + -0.47 + ) # The coefficient of Duffy mass-concentration (Duffy+2008) + concentration = coefficient * (mass_at_200 / 2.952465309e12) ** ( + -0.084 + ) # mass-concentration relation. (Duffy+2008) + mass_concentration_relation = ((f_c**2 * np.log(1 + concentration / f_c) + (1 - 2 * f_c) * np.log(1 + concentration)) / (1 + f_c)**2 + - concentration / ((1+concentration) * (1-f_c))) #mass concentration relation (Penarrubia+2012) + + scale_radius_kpc = radius_at_200 / concentration # scale radius in kpc + rho_0 = mass_at_200 / (4 * np.pi * scale_radius_kpc**3 * mass_concentration_relation) + kappa_s = rho_0 * scale_radius_kpc / critical_surface_density # kappa_s + scale_radius = scale_radius_kpc / kpc_per_arcsec # scale radius in arcsec + core_radius = f_c * scale_radius # core radius in arcsec + + return kappa_s, scale_radius, core_radius, radius_at_200 + def _ludlow16_cosmology_callback( mass_at_200, From 7e8bdda6dc7a57d3ed2e2e30c86c32fc7aba3fa0 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 11:52:41 +0000 Subject: [PATCH 22/38] changed naming cNFWsph to cNFWSph --- autogalaxy/config/priors/mass/dark/cnfw.yaml | 2 +- autogalaxy/profiles/mass/__init__.py | 2 +- autogalaxy/profiles/mass/dark/__init__.py | 2 +- autogalaxy/profiles/mass/dark/cnfw.py | 2 +- test_autogalaxy/profiles/mass/dark/test_cnfw.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/autogalaxy/config/priors/mass/dark/cnfw.yaml b/autogalaxy/config/priors/mass/dark/cnfw.yaml index a4683ba63..49aa82d00 100644 --- a/autogalaxy/config/priors/mass/dark/cnfw.yaml +++ b/autogalaxy/config/priors/mass/dark/cnfw.yaml @@ -1,4 +1,4 @@ -cNFWsph: +cNFWSph: centre_0: type: Gaussian mean: 0.0 diff --git a/autogalaxy/profiles/mass/__init__.py b/autogalaxy/profiles/mass/__init__.py index bf3607470..9fcd1c9c7 100644 --- a/autogalaxy/profiles/mass/__init__.py +++ b/autogalaxy/profiles/mass/__init__.py @@ -36,7 +36,7 @@ NFWMCRLudlow, gNFWMCRLudlow, NFWVirialMassConcSph, - cNFWsph, + cNFWSph, ) from .stellar import ( Gaussian, diff --git a/autogalaxy/profiles/mass/dark/__init__.py b/autogalaxy/profiles/mass/dark/__init__.py index b2062df9e..66293c2b3 100644 --- a/autogalaxy/profiles/mass/dark/__init__.py +++ b/autogalaxy/profiles/mass/dark/__init__.py @@ -9,4 +9,4 @@ from .nfw_truncated_mcr import NFWTruncatedMCRLudlowSph, NFWTruncatedMCRDuffySph from .nfw_truncated_mcr_scatter import NFWTruncatedMCRScatterLudlowSph from .nfw_virial_mass_conc import NFWVirialMassConcSph -from .cnfw import cNFWsph +from .cnfw import cNFWSph diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index ee14a687e..4700fdbb4 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -6,7 +6,7 @@ from autogalaxy.profiles.mass.abstract.abstract import MassProfile -class cNFWsph(MassProfile): +class cNFWSph(MassProfile): def __init__( self, centre: Tuple[float, float] = (0.0, 0.0), diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py index d52a1c295..e4e19d4ee 100644 --- a/test_autogalaxy/profiles/mass/dark/test_cnfw.py +++ b/test_autogalaxy/profiles/mass/dark/test_cnfw.py @@ -4,7 +4,7 @@ import autogalaxy as ag def test__deflections_yx_2d_from(): - cnfw = ag.mp.cNFWsph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + cnfw = ag.mp.cNFWSph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) deflection_2d = cnfw.deflections_yx_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) deflection_r = np.sqrt(deflection_2d[0, 0]**2 + deflection_2d[0, 1]**2) @@ -12,14 +12,14 @@ def test__deflections_yx_2d_from(): assert deflection_r == pytest.approx(0.006034319441107217, 1.0e-8) def test_convergence_2d_from(): - cnfw = ag.mp.cNFWsph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + cnfw = ag.mp.cNFWSph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) convergence = cnfw.convergence_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) assert convergence == pytest.approx(0.0, 1.0e-4) def test_potential_2d_from(): - cnfw = ag.mp.cNFWsph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) + cnfw = ag.mp.cNFWSph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) potential = cnfw.potential_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) From 5daf88d03b956cc7bef4bd2afe2de676fa11c11f Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 11:57:19 +0000 Subject: [PATCH 23/38] changed kappa_s_and_scale_radius_and_core_radius to kappa_s_scale_radius_and_core_radius_for_duffy --- autogalaxy/profiles/mass/dark/cnfw_mcr.py | 0 autogalaxy/profiles/mass/dark/mcr_util.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 autogalaxy/profiles/mass/dark/cnfw_mcr.py diff --git a/autogalaxy/profiles/mass/dark/cnfw_mcr.py b/autogalaxy/profiles/mass/dark/cnfw_mcr.py new file mode 100644 index 000000000..e69de29bb diff --git a/autogalaxy/profiles/mass/dark/mcr_util.py b/autogalaxy/profiles/mass/dark/mcr_util.py index b8c82025f..d317f4704 100644 --- a/autogalaxy/profiles/mass/dark/mcr_util.py +++ b/autogalaxy/profiles/mass/dark/mcr_util.py @@ -58,7 +58,7 @@ def kappa_s_and_scale_radius_for_duffy(mass_at_200, redshift_object, redshift_so return kappa_s, scale_radius, radius_at_200 -def kappa_s_and_scale_radius_and_core_radius(mass_at_200, f_c, redshift_object, redshift_source): +def kappa_s_scale_radius_and_core_radius_for_duffy(mass_at_200, f_c, redshift_object, redshift_source): """ Computes the AutoGalaxy cNFW parameters (kappa_s, scale_radius, core_radius) for an NFW halo of the given mass, enforcing the Penarrubia '12 mass-concentration relation. From 791d7f92ef85b319a3e7bfd02bda781203edba0b Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 12:04:04 +0000 Subject: [PATCH 24/38] added cnfw_mcr.py and added it in init files --- autogalaxy/profiles/mass/__init__.py | 1 + autogalaxy/profiles/mass/dark/__init__.py | 1 + autogalaxy/profiles/mass/dark/cnfw_mcr.py | 33 +++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/autogalaxy/profiles/mass/__init__.py b/autogalaxy/profiles/mass/__init__.py index 9fcd1c9c7..8ffa5fd53 100644 --- a/autogalaxy/profiles/mass/__init__.py +++ b/autogalaxy/profiles/mass/__init__.py @@ -37,6 +37,7 @@ gNFWMCRLudlow, NFWVirialMassConcSph, cNFWSph, + cNFWMCRDuffySph, ) from .stellar import ( Gaussian, diff --git a/autogalaxy/profiles/mass/dark/__init__.py b/autogalaxy/profiles/mass/dark/__init__.py index 66293c2b3..89a74ee96 100644 --- a/autogalaxy/profiles/mass/dark/__init__.py +++ b/autogalaxy/profiles/mass/dark/__init__.py @@ -10,3 +10,4 @@ from .nfw_truncated_mcr_scatter import NFWTruncatedMCRScatterLudlowSph from .nfw_virial_mass_conc import NFWVirialMassConcSph from .cnfw import cNFWSph +from .cnfw_mcr import cNFWMCRDuffySph diff --git a/autogalaxy/profiles/mass/dark/cnfw_mcr.py b/autogalaxy/profiles/mass/dark/cnfw_mcr.py index e69de29bb..cf2673847 100644 --- a/autogalaxy/profiles/mass/dark/cnfw_mcr.py +++ b/autogalaxy/profiles/mass/dark/cnfw_mcr.py @@ -0,0 +1,33 @@ +from typing import Tuple + +from autogalaxy.profiles.mass.dark.cnfw import cNFWSph + +from autogalaxy.profiles.mass.dark import mcr_util + +class cNFWMCRDuffySph(cNFWSph): + def __init__( + self, + centre: Tuple[float, float] = (0.0, 0.0), + mass_at_200: float = 1e9, + f_c = 0.01, + redshift_object: float = 0.5, + redshift_source: float = 1.0, + ): + self.mass_at_200 = mass_at_200 + self.f_c = f_c + self.redshift_object = redshift_object + self.redshift_source = redshift_source + + ( + kappa_s, + scale_radius, + core_radius, + radius_at_200, + ) = mcr_util.kappa_s_scale_radius_and_core_radius_for_duffy( + mass_at_200=mass_at_200, + f_c=f_c, + redshift_object=redshift_object, + redshift_source=redshift_source, + ) + + super().__init__(centre=centre, kappa_s=kappa_s, scale_radius=scale_radius, core_radius=core_radius) \ No newline at end of file From e719e444798643252671dab35d4388d1ac497384 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 12:07:17 +0000 Subject: [PATCH 25/38] added cnfw_mcr.yaml --- .../config/priors/mass/dark/cnfw_mcr.yaml | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml diff --git a/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml new file mode 100644 index 000000000..5643ba2bc --- /dev/null +++ b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml @@ -0,0 +1,61 @@ +cNFWMCRDuffySph: + centre_0: + type: Gaussian + mean: 0.0 + sigma: 0.1 + width_modifier: + type: Absolute + value: 0.05 + limits: + lower: -inf + upper: inf + centre_1: + type: Gaussian + mean: 0.0 + sigma: 0.1 + width_modifier: + type: Absolute + value: 0.05 + limits: + lower: -inf + upper: inf + mass_at_200: + type: LogUniform + lower_limit: 100000000.0 + upper_limit: 1000000000000000.0 + width_modifier: + type: Relative + value: 0.5 + limits: + lower: 0.0 + upper: inf + f_c: + type: Uniform + lower_limit: 0.0 + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.2 + limits: + lower: 0.0 + upper: inf + redshift_object: + type: Uniform + lower_limit: 0.0 + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.5 + limits: + lower: 0.0 + upper: inf + redshift_source: + type: Uniform + lower_limit: 0.0 + upper_limit: 1.0 + width_modifier: + type: Relative + value: 0.5 + limits: + lower: 0.0 + upper: inf \ No newline at end of file From ec3f05ced5f93498f0ffca2758bf24bd64ec330e Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 15:06:15 +0000 Subject: [PATCH 26/38] xp=np in kappa_s_scale_radius_and_core_radius_for_duffy --- autogalaxy/profiles/mass/dark/cnfw.py | 2 +- autogalaxy/profiles/mass/dark/mcr_util.py | 8 +-- .../profiles/mass/dark/test_nfw_mcr.py | 54 ++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 4700fdbb4..a22f68b9f 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -12,7 +12,7 @@ def __init__( centre: Tuple[float, float] = (0.0, 0.0), kappa_s: float = 0.05, scale_radius: float = 1.0, - core_radius: float = 0.5, + core_radius: float = 0.01, ): """ Represents a spherical cored NFW density distribution diff --git a/autogalaxy/profiles/mass/dark/mcr_util.py b/autogalaxy/profiles/mass/dark/mcr_util.py index d317f4704..f25364400 100644 --- a/autogalaxy/profiles/mass/dark/mcr_util.py +++ b/autogalaxy/profiles/mass/dark/mcr_util.py @@ -58,7 +58,7 @@ def kappa_s_and_scale_radius_for_duffy(mass_at_200, redshift_object, redshift_so return kappa_s, scale_radius, radius_at_200 -def kappa_s_scale_radius_and_core_radius_for_duffy(mass_at_200, f_c, redshift_object, redshift_source): +def kappa_s_scale_radius_and_core_radius_for_duffy(mass_at_200, f_c, redshift_object, redshift_source, xp=np): """ Computes the AutoGalaxy cNFW parameters (kappa_s, scale_radius, core_radius) for an NFW halo of the given mass, enforcing the Penarrubia '12 mass-concentration relation. @@ -87,7 +87,7 @@ def kappa_s_scale_radius_and_core_radius_for_duffy(mass_at_200, f_c, redshift_ob kpc_per_arcsec = cosmology.kpc_per_arcsec_from(redshift=redshift_object) radius_at_200 = ( - mass_at_200 / (200.0 * cosmic_average_density * (4.0 * np.pi / 3.0)) + mass_at_200 / (200.0 * cosmic_average_density * (4.0 * xp.pi / 3.0)) ) ** ( 1.0 / 3.0 ) # r200 @@ -97,11 +97,11 @@ def kappa_s_scale_radius_and_core_radius_for_duffy(mass_at_200, f_c, redshift_ob concentration = coefficient * (mass_at_200 / 2.952465309e12) ** ( -0.084 ) # mass-concentration relation. (Duffy+2008) - mass_concentration_relation = ((f_c**2 * np.log(1 + concentration / f_c) + (1 - 2 * f_c) * np.log(1 + concentration)) / (1 + f_c)**2 + mass_concentration_relation = ((f_c**2 * xp.log(1 + concentration / f_c) + (1 - 2 * f_c) * xp.log(1 + concentration)) / (1 + f_c)**2 - concentration / ((1+concentration) * (1-f_c))) #mass concentration relation (Penarrubia+2012) scale_radius_kpc = radius_at_200 / concentration # scale radius in kpc - rho_0 = mass_at_200 / (4 * np.pi * scale_radius_kpc**3 * mass_concentration_relation) + rho_0 = mass_at_200 / (4 * xp.pi * scale_radius_kpc**3 * mass_concentration_relation) kappa_s = rho_0 * scale_radius_kpc / critical_surface_density # kappa_s scale_radius = scale_radius_kpc / kpc_per_arcsec # scale radius in arcsec core_radius = f_c * scale_radius # core radius in arcsec diff --git a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py index 143f6f108..06f426051 100644 --- a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py +++ b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py @@ -38,7 +38,7 @@ def test__mass_and_concentration_consistent_with_normal_nfw(): redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology ) - # We uare using the NFWTruncatedSph to check the mass gives a conosistnt kappa_s, given certain radii. + # We are using the NFWTruncatedSph to check the mass gives a consistnt kappa_s, given certain radii. assert mass_at_200_via_kappa_s == mass_at_200_via_mass assert concentration_via_kappa_s == concentration_via_mass @@ -53,6 +53,58 @@ def test__mass_and_concentration_consistent_with_normal_nfw(): assert mp.scale_radius == pytest.approx(0.273382, 1.0e-4) +# def test__mass_and_concentration_consistent_with_cored_nfw(): +# +# from autogalaxy.cosmology.model import FlatLambdaCDMWrap +# +# cosmology = FlatLambdaCDMWrap(H0=70.0, Om0=0.3) +# +# mp = ag.mp.cNFWMCRDuffySph( +# centre=(1.0, 2.0), +# mass_at_200=1.0e9, +# f_c=0.01, +# redshift_object=0.6, +# redshift_source=2.5, +# ) +# +# mass_at_200_via_mass = mp.mass_at_200_solar_masses( +# redshift_object=0.6, redshift_source=2.5, cosmology=cosmology +# ) +# concentration_via_mass = mp.concentration( +# redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology +# ) +# +# cnfw_kappa_s = ag.mp.cNFWSph( +# centre=(1.0, 2.0), +# kappa_s=mp.kappa_s, +# scale_radius=mp.scale_radius, +# core_radius=mp.core_radius, +# ) +# +# mass_at_200_via_kappa_s = cnfw_kappa_s.mass_at_200_solar_masses( +# redshift_object=0.6, redshift_source=2.5, cosmology=cosmology +# ) +# concentration_via_kappa_s = cnfw_kappa_s.concentration( +# redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology +# ) +# +# # We are using the NFWTruncatedSph to check the mass gives a consistnt kappa_s, given certain radii. +# +# assert mass_at_200_via_kappa_s == mass_at_200_via_mass +# assert concentration_via_kappa_s == concentration_via_mass +# +# assert mp.centre == (1.0, 2.0) +# +# assert mp.axis_ratio() == 1.0 +# +# assert mp.angle() == 0.0 +# +# assert mp.inner_slope == 1.0 +# +# assert mp.scale_radius == pytest.approx(0.273382, 1.0e-4) +# +# assert mp.core_radius == pytest.approx(0.00273382, 1.0e-6) + def test__mass_and_concentration_consistent_with_normal_nfw__scatter_0(): From 0a8259852a5c49ad9dc99b5bbf87e2650ab7081e Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 15:38:31 +0000 Subject: [PATCH 27/38] converted cnfw with Duffy to Ludlow --- .../config/priors/mass/dark/cnfw_mcr.yaml | 2 +- autogalaxy/profiles/mass/__init__.py | 2 +- autogalaxy/profiles/mass/dark/__init__.py | 2 +- autogalaxy/profiles/mass/dark/cnfw_mcr.py | 4 +- autogalaxy/profiles/mass/dark/mcr_util.py | 61 ++++++++++++------- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml index 5643ba2bc..187484a84 100644 --- a/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml +++ b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml @@ -1,4 +1,4 @@ -cNFWMCRDuffySph: +cNFWMCRLudlowSph: centre_0: type: Gaussian mean: 0.0 diff --git a/autogalaxy/profiles/mass/__init__.py b/autogalaxy/profiles/mass/__init__.py index 8ffa5fd53..8ec0fa820 100644 --- a/autogalaxy/profiles/mass/__init__.py +++ b/autogalaxy/profiles/mass/__init__.py @@ -37,7 +37,7 @@ gNFWMCRLudlow, NFWVirialMassConcSph, cNFWSph, - cNFWMCRDuffySph, + cNFWMCRLudlowSph, ) from .stellar import ( Gaussian, diff --git a/autogalaxy/profiles/mass/dark/__init__.py b/autogalaxy/profiles/mass/dark/__init__.py index 89a74ee96..5a7b29895 100644 --- a/autogalaxy/profiles/mass/dark/__init__.py +++ b/autogalaxy/profiles/mass/dark/__init__.py @@ -10,4 +10,4 @@ from .nfw_truncated_mcr_scatter import NFWTruncatedMCRScatterLudlowSph from .nfw_virial_mass_conc import NFWVirialMassConcSph from .cnfw import cNFWSph -from .cnfw_mcr import cNFWMCRDuffySph +from .cnfw_mcr import cNFWMCRLudlowSph diff --git a/autogalaxy/profiles/mass/dark/cnfw_mcr.py b/autogalaxy/profiles/mass/dark/cnfw_mcr.py index cf2673847..ec3049eeb 100644 --- a/autogalaxy/profiles/mass/dark/cnfw_mcr.py +++ b/autogalaxy/profiles/mass/dark/cnfw_mcr.py @@ -4,7 +4,7 @@ from autogalaxy.profiles.mass.dark import mcr_util -class cNFWMCRDuffySph(cNFWSph): +class cNFWMCRLudlowSph(cNFWSph): def __init__( self, centre: Tuple[float, float] = (0.0, 0.0), @@ -23,7 +23,7 @@ def __init__( scale_radius, core_radius, radius_at_200, - ) = mcr_util.kappa_s_scale_radius_and_core_radius_for_duffy( + ) = mcr_util.kappa_s_scale_radius_and_core_radius_for_ludlow( mass_at_200=mass_at_200, f_c=f_c, redshift_object=redshift_object, diff --git a/autogalaxy/profiles/mass/dark/mcr_util.py b/autogalaxy/profiles/mass/dark/mcr_util.py index f25364400..50d639197 100644 --- a/autogalaxy/profiles/mass/dark/mcr_util.py +++ b/autogalaxy/profiles/mass/dark/mcr_util.py @@ -58,9 +58,9 @@ def kappa_s_and_scale_radius_for_duffy(mass_at_200, redshift_object, redshift_so return kappa_s, scale_radius, radius_at_200 -def kappa_s_scale_radius_and_core_radius_for_duffy(mass_at_200, f_c, redshift_object, redshift_source, xp=np): +def kappa_s_scale_radius_and_core_radius_for_ludlow(mass_at_200, scatter_sigma, f_c, redshift_object, redshift_source): """ - Computes the AutoGalaxy cNFW parameters (kappa_s, scale_radius, core_radius) for an NFW halo of the given + Computes the AutoGalaxy cNFW parameters (kappa_s, scale_radius, core_radius) for a cored NFW halo of the given mass, enforcing the Penarrubia '12 mass-concentration relation. Interprets mass as *`M_{200c}`*, not `M_{200m}`. @@ -68,40 +68,55 @@ def kappa_s_scale_radius_and_core_radius_for_duffy(mass_at_200, f_c, redshift_ob f_c = core_radius / scale radius """ - from astropy import units - - from autogalaxy.cosmology.wrap import Planck15 - - cosmology = Planck15() + if isinstance(mass_at_200, (float, np.ndarray, np.float64)): + xp = np + else: + xp = jnp - cosmic_average_density = ( - cosmology.critical_density(redshift_object).to(units.solMass / units.kpc**3) - ).value + # ------------------------------------ + # Cosmology + concentration (callback) + # ------------------------------------ - critical_surface_density = ( - cosmology.critical_surface_density_between_redshifts_solar_mass_per_kpc2_from( - redshift_0=redshift_object, redshift_1=redshift_source + if xp is np: + ( + concentration, + cosmic_average_density, + critical_surface_density, + kpc_per_arcsec, + ) = _ludlow16_cosmology_callback( + mass_at_200, + redshift_object, + redshift_source, + ) + else: + ( + concentration, + cosmic_average_density, + critical_surface_density, + kpc_per_arcsec, + ) = ludlow16_cosmology_jax( + mass_at_200, + redshift_object, + redshift_source, ) - ) - kpc_per_arcsec = cosmology.kpc_per_arcsec_from(redshift=redshift_object) + # Apply scatter (JAX-safe) + concentration = 10.0 ** (xp.log10(concentration) + scatter_sigma * 0.15) + # ------------------------------------ + # JAX-native algebra + # ------------------------------------ radius_at_200 = ( mass_at_200 / (200.0 * cosmic_average_density * (4.0 * xp.pi / 3.0)) ) ** ( 1.0 / 3.0 ) # r200 - coefficient = 5.71 * (1.0 + redshift_object) ** ( - -0.47 - ) # The coefficient of Duffy mass-concentration (Duffy+2008) - concentration = coefficient * (mass_at_200 / 2.952465309e12) ** ( - -0.084 - ) # mass-concentration relation. (Duffy+2008) - mass_concentration_relation = ((f_c**2 * xp.log(1 + concentration / f_c) + (1 - 2 * f_c) * xp.log(1 + concentration)) / (1 + f_c)**2 + + mcr_penarrubia = ((f_c**2 * xp.log(1 + concentration / f_c) + (1 - 2 * f_c) * xp.log(1 + concentration)) / (1 + f_c)**2 - concentration / ((1+concentration) * (1-f_c))) #mass concentration relation (Penarrubia+2012) scale_radius_kpc = radius_at_200 / concentration # scale radius in kpc - rho_0 = mass_at_200 / (4 * xp.pi * scale_radius_kpc**3 * mass_concentration_relation) + rho_0 = mass_at_200 / (4 * xp.pi * scale_radius_kpc**3 * mcr_penarrubia) kappa_s = rho_0 * scale_radius_kpc / critical_surface_density # kappa_s scale_radius = scale_radius_kpc / kpc_per_arcsec # scale radius in arcsec core_radius = f_c * scale_radius # core radius in arcsec From fec284751e6e9464f0c4328d1bdb44ef21bfde96 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 21 Jan 2026 15:45:42 +0000 Subject: [PATCH 28/38] cnfw_mcr.py fix, added scatter_sigma=0.0 --- autogalaxy/profiles/mass/dark/cnfw_mcr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/autogalaxy/profiles/mass/dark/cnfw_mcr.py b/autogalaxy/profiles/mass/dark/cnfw_mcr.py index ec3049eeb..1a22e1056 100644 --- a/autogalaxy/profiles/mass/dark/cnfw_mcr.py +++ b/autogalaxy/profiles/mass/dark/cnfw_mcr.py @@ -25,6 +25,7 @@ def __init__( radius_at_200, ) = mcr_util.kappa_s_scale_radius_and_core_radius_for_ludlow( mass_at_200=mass_at_200, + scatter_sigma=0.0, f_c=f_c, redshift_object=redshift_object, redshift_source=redshift_source, From 45162d19ab69bb29299a8c6029a778d721cf3a74 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 22 Jan 2026 09:40:29 +0000 Subject: [PATCH 29/38] changed r_c and f_c priors to half of r_s --- autogalaxy/config/priors/mass/dark/cnfw.yaml | 2 +- autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autogalaxy/config/priors/mass/dark/cnfw.yaml b/autogalaxy/config/priors/mass/dark/cnfw.yaml index 49aa82d00..265c9782d 100644 --- a/autogalaxy/config/priors/mass/dark/cnfw.yaml +++ b/autogalaxy/config/priors/mass/dark/cnfw.yaml @@ -42,7 +42,7 @@ cNFWSph: core_radius: type: Uniform lower_limit: 0.0 - upper_limit: 30.0 + upper_limit: 15.0 width_modifier: type: Relative value: 0.2 diff --git a/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml index 187484a84..acd67ae54 100644 --- a/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml +++ b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml @@ -32,7 +32,7 @@ cNFWMCRLudlowSph: f_c: type: Uniform lower_limit: 0.0 - upper_limit: 1.0 + upper_limit: 0.5 width_modifier: type: Relative value: 0.2 From 1de95e6fb0ced5ce0448bd7fa262ddb493417716 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 22 Jan 2026 10:09:56 +0000 Subject: [PATCH 30/38] added a test in test_nfw_mcr and changed the inheritance from MassProfile to AbstractgNFW in cnfw.py --- autogalaxy/profiles/mass/dark/cnfw.py | 6 +- .../profiles/mass/dark/test_nfw_mcr.py | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index a22f68b9f..c4111d236 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -2,11 +2,13 @@ from typing import Tuple +from build.lib.autogalaxy.profiles.mass.dark.abstract import AbstractgNFW + import autoarray as aa -from autogalaxy.profiles.mass.abstract.abstract import MassProfile +#from autogalaxy.profiles.mass.abstract.abstract import MassProfile -class cNFWSph(MassProfile): +class cNFWSph(AbstractgNFW): def __init__( self, centre: Tuple[float, float] = (0.0, 0.0), diff --git a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py index 06f426051..1e79f1e69 100644 --- a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py +++ b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py @@ -268,3 +268,58 @@ def test__same_as_above_but_generalized_elliptical(): deflections = nfw_kappa_s.deflections_yx_2d_from(grid=grid) assert (deflections_ludlow == deflections).all() + +def test__same_as_above_but_cored_nfw(): + + from autogalaxy.cosmology.model import FlatLambdaCDMWrap + + cosmology = FlatLambdaCDMWrap(H0=70.0, Om0=0.3) + + mp = ag.mp.cNFWMCRLudlowSph( + centre=(1.0, 2.0), + mass_at_200=1.0e9, + f_c=0.01, + redshift_object=0.6, + redshift_source=2.5, + ) + + mass_at_200_via_mass = mp.mass_at_200_solar_masses( + redshift_object=0.6, redshift_source=2.5, cosmology=cosmology + ) + concentration_via_mass = mp.concentration( + redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology + ) + + cnfw_kappa_s = ag.mp.cNFWSph( + centre=(1.0, 2.0), + kappa_s=mp.kappa_s, + scale_radius=mp.scale_radius, + core_radius=mp.core_radius, + ) + + mass_at_200_via_kappa_s = cnfw_kappa_s.mass_at_200_solar_masses( + redshift_object=0.6, redshift_source=2.5, cosmology=cosmology + ) + concentration_via_kappa_s = cnfw_kappa_s.concentration( + redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology + ) + + # We are using the NFWTruncatedSph to check the mass gives a consistnt kappa_s, given certain radii. + + assert mass_at_200_via_kappa_s == mass_at_200_via_mass + assert concentration_via_kappa_s == concentration_via_mass + + assert mp.centre == (1.0, 2.0) + + assert mp.axis_ratio() == 1.0 + + assert mp.angle() == 0.0 + + assert mp.scale_radius == pytest.approx(0.273382, 1.0e-4) + + assert mp.core_radius == pytest.approx(0.00273382, 1.0e-6) + + deflections_ludlow = mp.deflections_yx_2d_from(grid=grid) + deflections = cnfw_kappa_s.deflections_yx_2d_from(grid=grid) + + assert (deflections_ludlow == deflections).all() From c50d7f089681c234513b0e458120a702ef744cce Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 22 Jan 2026 10:16:50 +0000 Subject: [PATCH 31/38] fix import of AbstractgNFW in cnfw.py --- autogalaxy/profiles/mass/dark/cnfw.py | 2 +- test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index c4111d236..1ef912ee7 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -2,7 +2,7 @@ from typing import Tuple -from build.lib.autogalaxy.profiles.mass.dark.abstract import AbstractgNFW +from autogalaxy.profiles.mass.dark.abstract import AbstractgNFW import autoarray as aa diff --git a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py index 1e79f1e69..8538c680d 100644 --- a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py +++ b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py @@ -304,7 +304,7 @@ def test__same_as_above_but_cored_nfw(): redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology ) - # We are using the NFWTruncatedSph to check the mass gives a consistnt kappa_s, given certain radii. + # We are using the NFWTruncatedSph to check the mass gives a consistent kappa_s, given certain radii. assert mass_at_200_via_kappa_s == mass_at_200_via_mass assert concentration_via_kappa_s == concentration_via_mass From bfde04cda8286f7877961010ad6faa1194e5f342 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 22 Jan 2026 10:24:36 +0000 Subject: [PATCH 32/38] change in test assert --- test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py index 8538c680d..e29b27494 100644 --- a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py +++ b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py @@ -315,9 +315,9 @@ def test__same_as_above_but_cored_nfw(): assert mp.angle() == 0.0 - assert mp.scale_radius == pytest.approx(0.273382, 1.0e-4) + assert mp.scale_radius == pytest.approx(0.21158, 1.0e-4) - assert mp.core_radius == pytest.approx(0.00273382, 1.0e-6) + assert mp.core_radius == pytest.approx(0.0021158, 1.0e-6) deflections_ludlow = mp.deflections_yx_2d_from(grid=grid) deflections = cnfw_kappa_s.deflections_yx_2d_from(grid=grid) From ee1bc3c8dbaba6f36ac6a3ab0c8533f45410598e Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Thu, 22 Jan 2026 10:36:16 +0000 Subject: [PATCH 33/38] change in test assert --- test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py index e29b27494..19417db27 100644 --- a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py +++ b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py @@ -317,7 +317,7 @@ def test__same_as_above_but_cored_nfw(): assert mp.scale_radius == pytest.approx(0.21158, 1.0e-4) - assert mp.core_radius == pytest.approx(0.0021158, 1.0e-6) + assert mp.core_radius == pytest.approx(0.0021158, 1.0e-4) deflections_ludlow = mp.deflections_yx_2d_from(grid=grid) deflections = cnfw_kappa_s.deflections_yx_2d_from(grid=grid) From 7dea9165ca313c595e919bde8a5c8f900ebf2a74 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Mon, 26 Jan 2026 10:40:33 +0000 Subject: [PATCH 34/38] pre change commit --- autogalaxy/profiles/mass/dark/mcr_util.py | 130 +++++++++--------- autogalaxy/profiles/mass/dark/test.py | 16 +++ .../profiles/mass/dark/test_nfw_mcr.py | 52 ------- 3 files changed, 81 insertions(+), 117 deletions(-) create mode 100644 autogalaxy/profiles/mass/dark/test.py diff --git a/autogalaxy/profiles/mass/dark/mcr_util.py b/autogalaxy/profiles/mass/dark/mcr_util.py index 50d639197..9e76bca17 100644 --- a/autogalaxy/profiles/mass/dark/mcr_util.py +++ b/autogalaxy/profiles/mass/dark/mcr_util.py @@ -58,71 +58,6 @@ def kappa_s_and_scale_radius_for_duffy(mass_at_200, redshift_object, redshift_so return kappa_s, scale_radius, radius_at_200 -def kappa_s_scale_radius_and_core_radius_for_ludlow(mass_at_200, scatter_sigma, f_c, redshift_object, redshift_source): - """ - Computes the AutoGalaxy cNFW parameters (kappa_s, scale_radius, core_radius) for a cored NFW halo of the given - mass, enforcing the Penarrubia '12 mass-concentration relation. - - Interprets mass as *`M_{200c}`*, not `M_{200m}`. - - f_c = core_radius / scale radius - """ - - if isinstance(mass_at_200, (float, np.ndarray, np.float64)): - xp = np - else: - xp = jnp - - # ------------------------------------ - # Cosmology + concentration (callback) - # ------------------------------------ - - if xp is np: - ( - concentration, - cosmic_average_density, - critical_surface_density, - kpc_per_arcsec, - ) = _ludlow16_cosmology_callback( - mass_at_200, - redshift_object, - redshift_source, - ) - else: - ( - concentration, - cosmic_average_density, - critical_surface_density, - kpc_per_arcsec, - ) = ludlow16_cosmology_jax( - mass_at_200, - redshift_object, - redshift_source, - ) - - # Apply scatter (JAX-safe) - concentration = 10.0 ** (xp.log10(concentration) + scatter_sigma * 0.15) - - # ------------------------------------ - # JAX-native algebra - # ------------------------------------ - radius_at_200 = ( - mass_at_200 / (200.0 * cosmic_average_density * (4.0 * xp.pi / 3.0)) - ) ** ( - 1.0 / 3.0 - ) # r200 - - mcr_penarrubia = ((f_c**2 * xp.log(1 + concentration / f_c) + (1 - 2 * f_c) * xp.log(1 + concentration)) / (1 + f_c)**2 - - concentration / ((1+concentration) * (1-f_c))) #mass concentration relation (Penarrubia+2012) - - scale_radius_kpc = radius_at_200 / concentration # scale radius in kpc - rho_0 = mass_at_200 / (4 * xp.pi * scale_radius_kpc**3 * mcr_penarrubia) - kappa_s = rho_0 * scale_radius_kpc / critical_surface_density # kappa_s - scale_radius = scale_radius_kpc / kpc_per_arcsec # scale radius in arcsec - core_radius = f_c * scale_radius # core radius in arcsec - - return kappa_s, scale_radius, core_radius, radius_at_200 - def _ludlow16_cosmology_callback( mass_at_200, @@ -268,3 +203,68 @@ def kappa_s_and_scale_radius_for_ludlow( scale_radius = scale_radius_kpc / kpc_per_arcsec return kappa_s, scale_radius, radius_at_200 + +def kappa_s_scale_radius_and_core_radius_for_ludlow(mass_at_200, scatter_sigma, f_c, redshift_object, redshift_source): + """ + Computes the AutoGalaxy cNFW parameters (kappa_s, scale_radius, core_radius) for a cored NFW halo of the given + mass, enforcing the Penarrubia '12 mass-concentration relation. + + Interprets mass as *`M_{200c}`*, not `M_{200m}`. + + f_c = core_radius / scale radius + """ + + if isinstance(mass_at_200, (float, np.ndarray, np.float64)): + xp = np + else: + xp = jnp + + # ------------------------------------ + # Cosmology + concentration (callback) + # ------------------------------------ + + if xp is np: + ( + concentration, + cosmic_average_density, + critical_surface_density, + kpc_per_arcsec, + ) = _ludlow16_cosmology_callback( + mass_at_200, + redshift_object, + redshift_source, + ) + else: + ( + concentration, + cosmic_average_density, + critical_surface_density, + kpc_per_arcsec, + ) = ludlow16_cosmology_jax( + mass_at_200, + redshift_object, + redshift_source, + ) + + # Apply scatter (JAX-safe) + concentration = 10.0 ** (xp.log10(concentration) + scatter_sigma * 0.15) + + # ------------------------------------ + # JAX-native algebra + # ------------------------------------ + radius_at_200 = ( + mass_at_200 / (200.0 * cosmic_average_density * (4.0 * xp.pi / 3.0)) + ) ** ( + 1.0 / 3.0 + ) # r200 + + mcr_penarrubia = ((f_c**2 * xp.log(1 + concentration / f_c) + (1 - 2 * f_c) * xp.log(1 + concentration)) / (1 + f_c)**2 + - concentration / ((1+concentration) * (1-f_c))) #mass concentration relation (Penarrubia+2012) + + scale_radius_kpc = radius_at_200 / concentration # scale radius in kpc + rho_0 = mass_at_200 / (4 * xp.pi * scale_radius_kpc**3 * mcr_penarrubia) + kappa_s = rho_0 * scale_radius_kpc / critical_surface_density # kappa_s + scale_radius = scale_radius_kpc / kpc_per_arcsec # scale radius in arcsec + core_radius = f_c * scale_radius # core radius in arcsec + + return kappa_s, scale_radius, core_radius, radius_at_200 diff --git a/autogalaxy/profiles/mass/dark/test.py b/autogalaxy/profiles/mass/dark/test.py new file mode 100644 index 000000000..c90e62a48 --- /dev/null +++ b/autogalaxy/profiles/mass/dark/test.py @@ -0,0 +1,16 @@ +import mcr_util +import numpy as np + +concentration, cosmic_average_density, critical_surface_density, kpc_per_arcsec = mcr_util._ludlow16_cosmology_callback(mass_at_200=1.0e9,redshift_object=0.6, + redshift_source=2.5) + +print(concentration + , cosmic_average_density + , critical_surface_density + , kpc_per_arcsec) + +r_200 = (3 * 1.0e9 / (4 * 200 * cosmic_average_density * np.pi))**(1/3) +scale_radius = r_200 / concentration / kpc_per_arcsec + +print('r_200', r_200) +print('scale_radius', scale_radius) \ No newline at end of file diff --git a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py index 19417db27..5f47b7a3a 100644 --- a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py +++ b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py @@ -53,58 +53,6 @@ def test__mass_and_concentration_consistent_with_normal_nfw(): assert mp.scale_radius == pytest.approx(0.273382, 1.0e-4) -# def test__mass_and_concentration_consistent_with_cored_nfw(): -# -# from autogalaxy.cosmology.model import FlatLambdaCDMWrap -# -# cosmology = FlatLambdaCDMWrap(H0=70.0, Om0=0.3) -# -# mp = ag.mp.cNFWMCRDuffySph( -# centre=(1.0, 2.0), -# mass_at_200=1.0e9, -# f_c=0.01, -# redshift_object=0.6, -# redshift_source=2.5, -# ) -# -# mass_at_200_via_mass = mp.mass_at_200_solar_masses( -# redshift_object=0.6, redshift_source=2.5, cosmology=cosmology -# ) -# concentration_via_mass = mp.concentration( -# redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology -# ) -# -# cnfw_kappa_s = ag.mp.cNFWSph( -# centre=(1.0, 2.0), -# kappa_s=mp.kappa_s, -# scale_radius=mp.scale_radius, -# core_radius=mp.core_radius, -# ) -# -# mass_at_200_via_kappa_s = cnfw_kappa_s.mass_at_200_solar_masses( -# redshift_object=0.6, redshift_source=2.5, cosmology=cosmology -# ) -# concentration_via_kappa_s = cnfw_kappa_s.concentration( -# redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology -# ) -# -# # We are using the NFWTruncatedSph to check the mass gives a consistnt kappa_s, given certain radii. -# -# assert mass_at_200_via_kappa_s == mass_at_200_via_mass -# assert concentration_via_kappa_s == concentration_via_mass -# -# assert mp.centre == (1.0, 2.0) -# -# assert mp.axis_ratio() == 1.0 -# -# assert mp.angle() == 0.0 -# -# assert mp.inner_slope == 1.0 -# -# assert mp.scale_radius == pytest.approx(0.273382, 1.0e-4) -# -# assert mp.core_radius == pytest.approx(0.00273382, 1.0e-6) - def test__mass_and_concentration_consistent_with_normal_nfw__scatter_0(): From 3f1f8cc6240fc38059ab13f5e71898313b60c325 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 28 Jan 2026 10:32:01 +0000 Subject: [PATCH 35/38] remove test.py and clean up cnfw.py --- autogalaxy/profiles/mass/dark/cnfw.py | 2 -- autogalaxy/profiles/mass/dark/test.py | 16 ---------------- 2 files changed, 18 deletions(-) delete mode 100644 autogalaxy/profiles/mass/dark/test.py diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 1ef912ee7..9eb196d0d 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -6,8 +6,6 @@ import autoarray as aa -#from autogalaxy.profiles.mass.abstract.abstract import MassProfile - class cNFWSph(AbstractgNFW): def __init__( self, diff --git a/autogalaxy/profiles/mass/dark/test.py b/autogalaxy/profiles/mass/dark/test.py deleted file mode 100644 index c90e62a48..000000000 --- a/autogalaxy/profiles/mass/dark/test.py +++ /dev/null @@ -1,16 +0,0 @@ -import mcr_util -import numpy as np - -concentration, cosmic_average_density, critical_surface_density, kpc_per_arcsec = mcr_util._ludlow16_cosmology_callback(mass_at_200=1.0e9,redshift_object=0.6, - redshift_source=2.5) - -print(concentration - , cosmic_average_density - , critical_surface_density - , kpc_per_arcsec) - -r_200 = (3 * 1.0e9 / (4 * 200 * cosmic_average_density * np.pi))**(1/3) -scale_radius = r_200 / concentration / kpc_per_arcsec - -print('r_200', r_200) -print('scale_radius', scale_radius) \ No newline at end of file From 34efa98acbdbcc028579e9eda165a40cc8996b26 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 28 Jan 2026 11:35:51 +0000 Subject: [PATCH 36/38] changed f_c lower bound to 0.0001 --- autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml | 4 ++-- test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml index acd67ae54..4fd52d85d 100644 --- a/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml +++ b/autogalaxy/config/priors/mass/dark/cnfw_mcr.yaml @@ -31,13 +31,13 @@ cNFWMCRLudlowSph: upper: inf f_c: type: Uniform - lower_limit: 0.0 + lower_limit: 0.0001 upper_limit: 0.5 width_modifier: type: Relative value: 0.2 limits: - lower: 0.0 + lower: 0.0001 upper: inf redshift_object: type: Uniform diff --git a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py index 5f47b7a3a..47964eb69 100644 --- a/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py +++ b/test_autogalaxy/profiles/mass/dark/test_nfw_mcr.py @@ -38,7 +38,7 @@ def test__mass_and_concentration_consistent_with_normal_nfw(): redshift_profile=0.6, redshift_source=2.5, cosmology=cosmology ) - # We are using the NFWTruncatedSph to check the mass gives a consistnt kappa_s, given certain radii. + # We are using the NFWTruncatedSph to check that the mass gives a consistent kappa_s, given certain radii. assert mass_at_200_via_kappa_s == mass_at_200_via_mass assert concentration_via_kappa_s == concentration_via_mass From aa9ac34a807126464cf79b18a9aace4088da69f9 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 28 Jan 2026 11:40:04 +0000 Subject: [PATCH 37/38] deleted commented out code --- autogalaxy/profiles/mass/dark/cnfw.py | 32 +-------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index 9eb196d0d..da628e368 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -158,34 +158,4 @@ def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): @aa.grid_dec.to_array def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): - return xp.zeros(shape=grid.shape[0]) - - # def F_func(self, theta, radius, xp=np): - # if theta == 0: - # F = 0 - # elif theta < radius: - # F = (radius / 2 * xp.log(2 * radius / theta) - xp.sqrt(radius ** 2 - theta ** 2) - # * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) - # ) - # else: - # F = (radius / 2 * xp.log(2 * radius / theta) + xp.sqrt(theta ** 2 - radius ** 2) - # * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) - # ) - # return 2 * radius * F - - # def dev_F_func(self, theta, radius, xp=np): - # if theta == 0: - # dev_F = 0 - # elif theta < radius: - # dev_F = (radius * xp.log(2 * radius / theta) - # - (2 * radius ** 2 - theta ** 2) / xp.sqrt(radius ** 2 - theta ** 2) - # * xp.arctanh(xp.sqrt((radius - theta) / (radius + theta))) - # ) - # elif theta == radius: - # dev_F = (radius * (xp.log(2) - 1 / 2)) - # else: - # dev_F = (radius * xp.log(2 * radius / theta) - # + (theta ** 2 - 2 * radius ** 2) / xp.sqrt(theta ** 2 - radius ** 2) - # * xp.arctan(xp.sqrt((theta - radius) / (theta + radius))) - # ) - # return 2 * dev_F \ No newline at end of file + return xp.zeros(shape=grid.shape[0]) \ No newline at end of file From d867d6a722d98e6b9ea7e1454f1c800c2c40dd19 Mon Sep 17 00:00:00 2001 From: Niek Wielders Date: Wed, 28 Jan 2026 11:44:08 +0000 Subject: [PATCH 38/38] changed potential and convergence bein zeros to a warning that it isn't implemented --- autogalaxy/profiles/mass/dark/cnfw.py | 18 ++++++++++++++++-- .../profiles/mass/dark/test_cnfw.py | 14 -------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/autogalaxy/profiles/mass/dark/cnfw.py b/autogalaxy/profiles/mass/dark/cnfw.py index da628e368..c98ad451b 100644 --- a/autogalaxy/profiles/mass/dark/cnfw.py +++ b/autogalaxy/profiles/mass/dark/cnfw.py @@ -154,8 +154,22 @@ def dev_F_func(self, theta, radius, xp=np): @aa.grid_dec.to_array def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): - return xp.zeros(shape=grid.shape[0]) + """ + Convergence (dimensionless surface mass density) for the cored NFW profile. + This is not yet implemented for `cNFWSph`. + """ + raise NotImplementedError( + "convergence_2d_from is not implemented for cNFWSph; a physical cNFW " + "convergence expression must be added before use." + ) @aa.grid_dec.to_array def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): - return xp.zeros(shape=grid.shape[0]) \ No newline at end of file + """ + Lensing potential for the cored NFW profile. + This is not yet implemented for `cNFWSph`. + """ + raise NotImplementedError( + "potential_2d_from is not implemented for cNFWSph; a physical cNFW " + "potential expression must be added before use." + ) \ No newline at end of file diff --git a/test_autogalaxy/profiles/mass/dark/test_cnfw.py b/test_autogalaxy/profiles/mass/dark/test_cnfw.py index e4e19d4ee..2dae506c8 100644 --- a/test_autogalaxy/profiles/mass/dark/test_cnfw.py +++ b/test_autogalaxy/profiles/mass/dark/test_cnfw.py @@ -10,17 +10,3 @@ def test__deflections_yx_2d_from(): deflection_r = np.sqrt(deflection_2d[0, 0]**2 + deflection_2d[0, 1]**2) assert deflection_r == pytest.approx(0.006034319441107217, 1.0e-8) - -def test_convergence_2d_from(): - cnfw = ag.mp.cNFWSph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) - - convergence = cnfw.convergence_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) - - assert convergence == pytest.approx(0.0, 1.0e-4) - -def test_potential_2d_from(): - cnfw = ag.mp.cNFWSph(centre=(0.0, 0.0), kappa_s=0.01591814312464436, scale_radius=0.36, core_radius=0.036) - - potential = cnfw.potential_2d_from(grid=ag.Grid2DIrregular([[1.0, 0.0]])) - - assert potential == pytest.approx(0.0, 1.0e-4) \ No newline at end of file