Skip to content

fix: Apparent unphysical behaviour in the cNFWMCRLudlowSph class #451

@mwiet

Description

@mwiet

Description

The cNFWMCRLudlowSph returns zero convergence_2d_from and an over-amplified deflections_yx_2d_from for  dimensionless core fractions f_c greather than 0.1 The convergence is also negative in this regime.

The cored-NFW family in autogalaxy.profiles.mass.dark.cnfw / cnfw_mcr suffers three related numerical issues at the core fractions (f_c = r_core / r_s ≈ 0.1 – 0.3) most relevant to self-interacting dark matter (SIDM) lensing applications:

  1. convergence_2d_from returns identically zero everywhere on a uniform image-plane grid, for any combination of mass_at_200, redshift, and f_c we tested.
  2. deflections_yx_2d_from scales non-monotonically with f_c: the deflection magnitude blows up by roughly an order of magnitude near f_c ≈ 0.2, and the implied kappa_s becomes negative for f_c ≳ 0.5. Compared to an equivalent NFWTruncatedSph (same mass_at_200, autolens-shipped MCR concentration), cNFWMCRLudlowSph with f_c = 0.2 produces deflections ~20× larger at radii ~r_s.
  3. The deflection vector field flips sign (points INWARD insteadof outward) for f_c ≳ 0.22-0.30, well before kappa_s becomes negative. A cored halo in this regime acts as a negative-mass perturber: it deflects light toward its centre. The previous draft of this report only checked deflection magnitudes via np.linalg.norm and missed this — the inward-flip happens silently within the "positive kappa_s" range, so any user checking only kappa_s > 0 to validate a cored halo is unaware of the qualitative rendering error.

Steps to Reproduce 1

import numpy as np
import autolens as al
import autogalaxy as ag

mass = 1.0e9
# Four probe points 1 arcsec from a halo at the origin, on the +/-x/y axes.
probes = [(0.0, 1.0), (0.0, -1.0), (1.0, 0.0), (-1.0, 0.0)]
probe_grid = al.Grid2DIrregular(values=probes)

for f_c in (0.01, 0.10, 0.20, 0.30, 0.50):
    cnfw = ag.profiles.mass.dark.cnfw_mcr.cNFWMCRLudlowSph(
        centre=(0.0, 0.0), mass_at_200=mass, f_c=f_c,
        redshift_object=0.5, redshift_source=1.0,
    )
    defl = np.asarray(cnfw.deflections_yx_2d_from(grid=probe_grid))
    print(f"f_c={f_c}  kappa_s={cnfw.kappa_s:+.3e}")
    for (y, x), (ay, ax) in zip(probes, defl):
        outward = (ax * x >= 0) and (ay * y >= 0)
        print(f"  probe ({y:+.1f}, {x:+.1f}):  α=({ay:+.3e}, {ax:+.3e})  "
              f"{'OUTWARD (OK)' if outward else 'INWARD (sign-flipped)'}")

Expected Behaviour 1

For f_c ∈ (0, 1):

  • convergence_2d_from should produce a finite, positive, smoothly varying field that integrates to the projected mass within the evaluation aperture.
  • deflections_yx_2d_from magnitudes should vary continuously with f_c and approach the NFW value as f_c → 0. Beyond f_c ≈ 0.1 they currently jump by 10× before falling back to NFW-comparable values at f_c ≈ 0.5, which is not physically plausible.
  • kappa_s should remain positive across the full f_c ∈ (0, 1) range.

Actual Behaviour 1

f_c=0.01  kappa_s=+8.132e-03   OUTWARD at every probe
f_c=0.10  kappa_s=+1.754e-02   OUTWARD at every probe
f_c=0.20  kappa_s=+4.125e-01   OUTWARD at every probe  (but magnitude 20× NFW)
f_c=0.30  kappa_s=-2.433e-02   INWARD at every probe   ← qualitatively wrong
f_c=0.50  kappa_s=-8.388e-03   INWARD at every probe   ← qualitatively wrong

The sign flip is uniform across the field (every probe point on every axis flips at the same f_c threshold), which rules out a local numerical artifact and points at an analytic-formula sign error in one of the branches of the cNFW deflection. The threshold is somewhere in f_c ∈ (0.20, 0.30).

Steps to reproduce 2

import numpy as np
import autogalaxy as ag

mass = 1.0e9
nfw = ag.mp.NFWTruncatedSph.from_m200_concentration(
    centre=(0.0, 0.0),
    m200_solar_mass=mass,
    concentration=12.0,
    redshift_halo=0.1,
    redshift_source=1.0,
    cosmology=ag.cosmology.Planck15(),
    truncation_factor=100.0,
)
grid = ag.Grid2D.uniform(shape_native=(101, 101), pixel_scales=0.1)

# 1. convergence_2d_from on cNFW returns identically zero
for f_c in (0.01, 0.1, 0.2, 0.5):
    cnfw = ag.profiles.mass.dark.cnfw_mcr.cNFWMCRLudlowSph(
        centre=(0.0, 0.0),
        mass_at_200=mass,
        f_c=f_c,
        redshift_object=0.1,
        redshift_source=1.0,
    )
    kappa = np.asarray(cnfw.convergence_2d_from(grid=grid).native)
    print(f"f_c={f_c:>4}: kappa_s={cnfw.kappa_s: .3e}  sum(kappa_grid)={kappa.sum(): .3e}")

# 2. Deflection magnitude scales non-monotonically with f_c
pt = ag.Grid2D.no_mask([[(1.0, 0.0)]], pixel_scales=1.0)
nfw_alpha = np.linalg.norm(np.asarray(nfw.deflections_yx_2d_from(grid=pt)))
print(f"NFW   |alpha|(r=1\")={nfw_alpha:.3e}")
for f_c in (0.01, 0.1, 0.2, 0.5):
    cnfw = ag.profiles.mass.dark.cnfw_mcr.cNFWMCRLudlowSph(
        centre=(0.0, 0.0), mass_at_200=mass, f_c=f_c,
        redshift_object=0.1, redshift_source=1.0,
    )
    a = np.linalg.norm(np.asarray(cnfw.deflections_yx_2d_from(grid=pt)))
    print(f"cNFW f_c={f_c:>4}: |alpha|(r=1\")={a:.3e}")

Expected Behaviour 2

For f_c ∈ (0, 1):

  • convergence_2d_from should produce a finite, positive, smoothly varying field that integrates to the projected mass within the evaluation aperture.
  • deflections_yx_2d_from magnitudes should vary continuously with f_c and approach the NFW value as f_c → 0. Beyond f_c ≈ 0.1 they currently jump by 10× before falling back to NFW-comparable values at f_c ≈ 0.5, which is not physically plausible.
  • kappa_s should remain positive across the full f_c ∈ (0, 1) range.

Actual Behaviour 2

f_c=0.01: kappa_s= 4.615e-03  sum(kappa_grid)= 0.000e+00
f_c= 0.1: kappa_s= 9.652e-03  sum(kappa_grid)= 0.000e+00
f_c= 0.2: kappa_s= 9.754e-02  sum(kappa_grid)= 0.000e+00
f_c= 0.5: kappa_s=-5.118e-03  sum(kappa_grid)= 0.000e+00

NFW   |alpha|(r=1")=3.762e-03
cNFW f_c=0.01: |alpha|(r=1")=4.346e-03   (1.16 × NFW)
cNFW f_c= 0.1: |alpha|(r=1")=8.156e-03   (2.17 × NFW)
cNFW f_c= 0.2: |alpha|(r=1")=7.490e-02   (19.9 × NFW)
cNFW f_c= 0.5: |alpha|(r=1")=3.156e-03   (0.84 × NFW)

The f_c → 0 limit reduces cleanly to NFW (within ~20 % of the same deflection at the same c), so the analytic formulae are presumably correct in the small-core limit. The f_c ≈ 0.2 regime — exactly the physically interesting one for SIDM core radii — produces both a silently zero convergence map and a deflection magnitude inflated by an order of magnitude, then a sign flip in kappa_s for larger cores.

Multiple RuntimeWarning: invalid value encountered in sqrt / arctanh / arctan messages are emitted from autogalaxy.profiles.mass.dark.cnfw at lines 182, 183, 192, 193, 213, 214, 230, 231 when these routines are evaluated, which suggests the integration-region masks (radius > theta vs radius < theta) or branch selection in the cored-NFW analytic formulae may be at fault.

Environment

  • OS: macOS arm64
  • Python version: Python 3.12
  • Package version: autolens 2026.5.21.1 (with autogalaxy dependency)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions