From 591bb27cf44ca87708d780c4fdd1879395e82c1f Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Mon, 13 Apr 2026 09:36:42 +0100 Subject: [PATCH] refactor: use transform(rotate_back=True) for deflection methods Replace manual rotated_grid_from_reference_frame_from calls in deflection methods with the new automatic back-rotation decorator parameter. Net reduction of ~47 lines across 12 profile files. Co-Authored-By: Claude Opus 4.6 --- autogalaxy/profiles/mass/abstract/cse.py | 2 +- autogalaxy/profiles/mass/dark/nfw.py | 10 +- .../profiles/mass/sheets/external_shear.py | 7 +- autogalaxy/profiles/mass/stellar/chameleon.py | 6 +- autogalaxy/profiles/mass/stellar/gaussian.py | 9 +- autogalaxy/profiles/mass/stellar/sersic.py | 2 +- .../mass/total/dual_pseudo_isothermal_mass.py | 27 +-- .../total/dual_pseudo_isothermal_potential.py | 9 +- autogalaxy/profiles/mass/total/isothermal.py | 8 +- autogalaxy/profiles/mass/total/power_law.py | 6 +- .../profiles/mass/total/power_law_broken.py | 9 +- .../profiles/mass/total/power_law_core.py | 6 +- .../mass/test_transform_rotate_back.py | 208 ++++++++++++++++++ 13 files changed, 235 insertions(+), 74 deletions(-) create mode 100644 test_autogalaxy/profiles/mass/test_transform_rotate_back.py diff --git a/autogalaxy/profiles/mass/abstract/cse.py b/autogalaxy/profiles/mass/abstract/cse.py index 32af87396..c7a832fa8 100644 --- a/autogalaxy/profiles/mass/abstract/cse.py +++ b/autogalaxy/profiles/mass/abstract/cse.py @@ -192,4 +192,4 @@ def _deflections_2d_via_cse_from(self, grid: np.ndarray, **kwargs) -> np.ndarray for amplitude, core_radius in zip(amplitude_list, core_radius_list) ) - return self.rotated_grid_from_reference_frame_from(deflections_2d.T) + return deflections_2d.T diff --git a/autogalaxy/profiles/mass/dark/nfw.py b/autogalaxy/profiles/mass/dark/nfw.py index 092affc27..614af2e73 100644 --- a/autogalaxy/profiles/mass/dark/nfw.py +++ b/autogalaxy/profiles/mass/dark/nfw.py @@ -46,7 +46,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return self.deflections_2d_via_analytic_from(grid=grid, xp=xp, **kwargs) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) def deflections_2d_via_analytic_from( self, grid: aa.type.Grid2DLike, xp=np, **kwargs ): @@ -103,14 +103,10 @@ def deflections_2d_via_analytic_from( ) deflection_y *= prefactor - return self.rotated_grid_from_reference_frame_from( - xp.multiply(self.scale_radius, xp.vstack((deflection_y, deflection_x)).T), - xp=xp, - **kwargs, - ) + return xp.multiply(self.scale_radius, xp.vstack((deflection_y, deflection_x)).T) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) def deflections_2d_via_cse_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return self._deflections_2d_via_cse_from(grid=grid, **kwargs) diff --git a/autogalaxy/profiles/mass/sheets/external_shear.py b/autogalaxy/profiles/mass/sheets/external_shear.py index f0c762958..b13386f17 100644 --- a/autogalaxy/profiles/mass/sheets/external_shear.py +++ b/autogalaxy/profiles/mass/sheets/external_shear.py @@ -57,7 +57,7 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return -0.5 * shear_amp * rcoord**2 * xp.cos(2 * (phicoord - phig)) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): """ Calculate the deflection angles at a given set of arc-second gridded coordinates. @@ -70,7 +70,4 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): """ deflection_y = -xp.multiply(self.magnitude(xp=xp), grid.array[:, 0]) deflection_x = xp.multiply(self.magnitude(xp=xp), grid.array[:, 1]) - return self.rotated_grid_from_reference_frame_from( - grid=xp.vstack((deflection_y, deflection_x)).T, - xp=xp, - ) + return xp.vstack((deflection_y, deflection_x)).T diff --git a/autogalaxy/profiles/mass/stellar/chameleon.py b/autogalaxy/profiles/mass/stellar/chameleon.py index 2ba95146e..a8f767d98 100644 --- a/autogalaxy/profiles/mass/stellar/chameleon.py +++ b/autogalaxy/profiles/mass/stellar/chameleon.py @@ -51,7 +51,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return self.deflections_2d_via_analytic_from(grid=grid, xp=xp, **kwargs) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) def deflections_2d_via_analytic_from( self, grid: aa.type.Grid2DLike, xp=np, **kwargs ): @@ -128,9 +128,7 @@ def deflections_2d_via_analytic_from( deflection_y = xp.subtract(deflection_y0, deflection_y1) deflection_x = xp.subtract(deflection_x0, deflection_x1) - return self.rotated_grid_from_reference_frame_from( - xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T), xp=xp - ) + return xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T) @aa.over_sample @aa.grid_dec.to_array diff --git a/autogalaxy/profiles/mass/stellar/gaussian.py b/autogalaxy/profiles/mass/stellar/gaussian.py index b5946d7f7..255e63767 100644 --- a/autogalaxy/profiles/mass/stellar/gaussian.py +++ b/autogalaxy/profiles/mass/stellar/gaussian.py @@ -51,7 +51,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return self.deflections_2d_via_analytic_from(grid=grid, xp=xp, **kwargs) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) def deflections_2d_via_analytic_from( self, grid: aa.type.Grid2DLike, xp=np, **kwargs ): @@ -73,12 +73,7 @@ def deflections_2d_via_analytic_from( * self.zeta_from(grid=grid, xp=xp) ) - return self.rotated_grid_from_reference_frame_from( - xp.multiply( - 1.0, xp.vstack((-1.0 * xp.imag(deflections), xp.real(deflections))).T - ), - xp=xp, - ) + return xp.vstack((-1.0 * xp.imag(deflections), xp.real(deflections))).T @aa.over_sample @aa.grid_dec.to_array diff --git a/autogalaxy/profiles/mass/stellar/sersic.py b/autogalaxy/profiles/mass/stellar/sersic.py index c058729aa..3709a0187 100644 --- a/autogalaxy/profiles/mass/stellar/sersic.py +++ b/autogalaxy/profiles/mass/stellar/sersic.py @@ -123,7 +123,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return self.deflections_2d_via_cse_from(grid=grid, xp=xp, **kwargs) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) def deflections_2d_via_cse_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): """ Calculate the projected 2D deflection angles from a grid of (y,x) arc second coordinates, by computing and diff --git a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py index a981526ec..7cf04df0b 100644 --- a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py +++ b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py @@ -263,7 +263,7 @@ def _ellip(self, xp=np): return xp.min(xp.array([ellip, MAX_ELLIP])) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) 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. @@ -283,12 +283,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): deflection_x = zis.real deflection_y = zis.imag - # And here we convert back to the real axes - return self.rotated_grid_from_reference_frame_from( - grid=xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T), - xp=xp, - **kwargs, - ) + return xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T) def _convergence(self, radii, xp=np): @@ -418,7 +413,7 @@ def _ellip(self, xp=np): return xp.min(xp.array([ellip, MAX_ELLIP])) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) 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. @@ -443,12 +438,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): deflection_x = zis.real deflection_y = zis.imag - # And here we convert back to the real axes - return self.rotated_grid_from_reference_frame_from( - grid=xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T), - xp=xp, - **kwargs, - ) + return xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T) def _convergence(self, radii, xp=np): @@ -594,7 +584,7 @@ def __init__( self.b0 = b0 @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) 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. @@ -627,12 +617,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): deflection_x = grid.array[:, 1] * factor deflection_y = grid.array[:, 0] * factor - # And here we convert back to the real axes - return self.rotated_grid_from_reference_frame_from( - grid=xp.multiply(1.0, xp.vstack((deflection_y, deflection_x)).T), - xp=xp, - **kwargs, - ) + return xp.vstack((deflection_y, deflection_x)).T @aa.grid_dec.to_array @aa.grid_dec.transform diff --git a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py index 9f3ec14c9..c925922c8 100644 --- a/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py +++ b/autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py @@ -94,7 +94,7 @@ def _convergence(self, radii, xp=np): ) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) 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. @@ -116,12 +116,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): deflection_y = alpha_circ * xp.sqrt(1 + ellip) * (grid.array[:, 0] / grid_radii) deflection_x = alpha_circ * xp.sqrt(1 - ellip) * (grid.array[:, 1] / grid_radii) - # And here we convert back to the real axes - return self.rotated_grid_from_reference_frame_from( - grid=xp.multiply(1.0, xp.vstack((deflection_y, deflection_x)).T), - xp=xp, - **kwargs, - ) + return xp.vstack((deflection_y, deflection_x)).T @aa.grid_dec.to_vector_yx @aa.grid_dec.transform diff --git a/autogalaxy/profiles/mass/total/isothermal.py b/autogalaxy/profiles/mass/total/isothermal.py index e0fca8d23..8abcedec3 100644 --- a/autogalaxy/profiles/mass/total/isothermal.py +++ b/autogalaxy/profiles/mass/total/isothermal.py @@ -71,7 +71,7 @@ def axis_ratio(self, xp=np): return xp.minimum(axis_ratio, 0.99999) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) 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. @@ -105,11 +105,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): psi, ) ) - return self.rotated_grid_from_reference_frame_from( - grid=xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T), - xp=xp, - **kwargs, - ) + return xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T) @aa.grid_dec.to_vector_yx @aa.grid_dec.transform diff --git a/autogalaxy/profiles/mass/total/power_law.py b/autogalaxy/profiles/mass/total/power_law.py index ac2cfeefa..036c10610 100644 --- a/autogalaxy/profiles/mass/total/power_law.py +++ b/autogalaxy/profiles/mass/total/power_law.py @@ -53,7 +53,7 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return (x * alpha_x + y * alpha_y) / (3 - self.slope) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) 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. @@ -119,9 +119,7 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): deflection_y *= rescale_factor deflection_x *= rescale_factor - return self.rotated_grid_from_reference_frame_from( - grid=xp.vstack((deflection_y, deflection_x)).T, xp=xp - ) + return xp.vstack((deflection_y, deflection_x)).T def convergence_func(self, grid_radius: float, xp=np) -> float: return self.einstein_radius_rescaled(xp) * grid_radius.array ** ( diff --git a/autogalaxy/profiles/mass/total/power_law_broken.py b/autogalaxy/profiles/mass/total/power_law_broken.py index 577049787..1d57c3283 100644 --- a/autogalaxy/profiles/mass/total/power_law_broken.py +++ b/autogalaxy/profiles/mass/total/power_law_broken.py @@ -74,7 +74,7 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return xp.zeros(shape=grid.shape[0]) @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) def deflections_yx_2d_from(self, grid, xp=np, max_terms=20, **kwargs): """ Returns the complex deflection angle from eq. 18 and 19 @@ -135,12 +135,7 @@ def deflections_yx_2d_from(self, grid, xp=np, max_terms=20, **kwargs): inner_part * (R <= self.break_radius) + outer_part * (R > self.break_radius) ).conjugate() - return self.rotated_grid_from_reference_frame_from( - grid=xp.multiply( - 1.0, xp.vstack((xp.imag(deflections), xp.real(deflections))).T - ), - xp=xp, - ) + return xp.vstack((xp.imag(deflections), xp.real(deflections))).T @staticmethod def hyp2f1_series(t, q, r, z, max_terms=20, xp=np): diff --git a/autogalaxy/profiles/mass/total/power_law_core.py b/autogalaxy/profiles/mass/total/power_law_core.py index 98ae899de..6f352112a 100644 --- a/autogalaxy/profiles/mass/total/power_law_core.py +++ b/autogalaxy/profiles/mass/total/power_law_core.py @@ -98,7 +98,7 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): return self.einstein_radius_rescaled(xp) * self.axis_ratio(xp) * potential_grid @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform + @aa.grid_dec.transform(rotate_back=True) 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. @@ -139,9 +139,7 @@ def calculate_deflection_component(npow, index): deflection_y = calculate_deflection_component(1.0, 0) deflection_x = calculate_deflection_component(0.0, 1) - return self.rotated_grid_from_reference_frame_from( - grid=np.multiply(1.0, np.vstack((deflection_y, deflection_x)).T), xp=xp - ) + return np.vstack((deflection_y, deflection_x)).T def convergence_func(self, grid_radius: float, xp=np) -> float: return self.einstein_radius_rescaled(xp) * ( diff --git a/test_autogalaxy/profiles/mass/test_transform_rotate_back.py b/test_autogalaxy/profiles/mass/test_transform_rotate_back.py new file mode 100644 index 000000000..f75ddc5bf --- /dev/null +++ b/test_autogalaxy/profiles/mass/test_transform_rotate_back.py @@ -0,0 +1,208 @@ +""" +Tests for the automatic back-rotation feature of the transform decorator (Phase 2). + +These tests capture the expected deflection values from the current manual back-rotation +approach. After switching to automatic back-rotation via @aa.grid_dec.transform(rotate_back=True), +the same values must be produced. +""" +import numpy as np +import pytest + +import autogalaxy as ag + + +@pytest.fixture +def grid(): + return ag.Grid2D.uniform(shape_native=(3, 3), pixel_scales=0.5) + + +@pytest.fixture +def grid_irregular(): + return ag.Grid2DIrregular(values=[(1.0, 1.0), (0.5, -0.5), (-1.0, 0.3)]) + + +class TestIsothermalBackRotation: + def test__deflections__spherical__no_rotation_needed(self, grid): + """Spherical profile: back-rotation is identity (angle=0).""" + profile = ag.mp.Isothermal( + centre=(0.0, 0.0), ell_comps=(0.0, 0.0), einstein_radius=1.0 + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + # Symmetry: for centred spherical, deflections should point radially + # Centre pixel should have ~zero deflection + assert deflections.array[4, 0] == pytest.approx(0.0, abs=1e-10) + assert deflections.array[4, 1] == pytest.approx(0.0, abs=1e-10) + + def test__deflections__elliptical__correct_values(self, grid): + """Elliptical profile: back-rotation must rotate deflection vectors.""" + profile = ag.mp.Isothermal( + centre=(0.0, 0.0), ell_comps=(0.17647, 0.0), einstein_radius=1.0 + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + + # Store reference values for regression + expected_y = deflections.array[:, 0].copy() + expected_x = deflections.array[:, 1].copy() + + # Recompute and verify stability + deflections_2 = profile.deflections_yx_2d_from(grid=grid) + np.testing.assert_array_almost_equal( + deflections_2.array[:, 0], expected_y, decimal=10 + ) + np.testing.assert_array_almost_equal( + deflections_2.array[:, 1], expected_x, decimal=10 + ) + + def test__deflections__off_centre_elliptical(self, grid): + """Off-centre elliptical: both translation and rotation matter.""" + profile = ag.mp.Isothermal( + centre=(0.3, -0.2), ell_comps=(0.1, 0.2), einstein_radius=1.5 + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + # No pixel should have NaN + assert not np.any(np.isnan(deflections.array)) + + def test__deflections__irregular_grid(self, grid_irregular): + """Irregular grid: decorator must handle non-uniform grids.""" + profile = ag.mp.Isothermal( + centre=(0.0, 0.0), ell_comps=(0.1, 0.15), einstein_radius=1.0 + ) + deflections = profile.deflections_yx_2d_from(grid=grid_irregular) + assert deflections.shape == (3, 2) + assert not np.any(np.isnan(deflections.array)) + + +class TestNFWBackRotation: + def test__deflections__spherical(self, grid): + profile = ag.mp.NFW( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.0), + kappa_s=0.05, + scale_radius=1.0, + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + assert not np.any(np.isnan(deflections.array)) + + def test__deflections__elliptical(self, grid): + profile = ag.mp.NFW( + centre=(0.0, 0.0), + ell_comps=(0.15, 0.05), + kappa_s=0.05, + scale_radius=1.0, + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + assert not np.any(np.isnan(deflections.array)) + + # Verify determinism + deflections_2 = profile.deflections_yx_2d_from(grid=grid) + np.testing.assert_array_almost_equal( + deflections.array, deflections_2.array, decimal=10 + ) + + +class TestPowerLawBackRotation: + def test__deflections__elliptical(self, grid): + profile = ag.mp.PowerLaw( + centre=(0.0, 0.0), + ell_comps=(0.1, 0.2), + einstein_radius=1.0, + slope=2.3, + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + assert not np.any(np.isnan(deflections.array)) + + deflections_2 = profile.deflections_yx_2d_from(grid=grid) + np.testing.assert_array_almost_equal( + deflections.array, deflections_2.array, decimal=10 + ) + + +class TestExternalShearBackRotation: + def test__deflections(self, grid): + profile = ag.mp.ExternalShear(gamma_1=0.05, gamma_2=0.03) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + assert not np.any(np.isnan(deflections.array)) + + +class TestGaussianMassBackRotation: + def test__deflections__elliptical(self, grid): + profile = ag.mp.Gaussian( + centre=(0.0, 0.0), + ell_comps=(0.1, 0.2), + intensity=1.0, + sigma=0.5, + mass_to_light_ratio=1.0, + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + assert deflections.shape == (9, 2) + assert not np.any(np.isnan(deflections.array)) + + +class TestRegressionValues: + """ + Exact numerical values from the manual back-rotation implementation. + Any drift after switching to automatic back-rotation is a bug. + """ + + def test__isothermal_regression(self): + grid = ag.Grid2D.uniform(shape_native=(3, 3), pixel_scales=1.0) + profile = ag.mp.Isothermal( + centre=(0.0, 0.0), ell_comps=(0.17647, 0.0), einstein_radius=1.0 + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + + expected = np.array([ + [7.302765158231814e-01, -7.302765158231813e-01], + [9.780567592278144e-01, -1.147703673995887e-01], + [6.485808825578577e-01, 6.485808825578578e-01], + [1.147703673995890e-01, -9.780567592278144e-01], + [0.0, 0.0], + [-1.147703673995890e-01, 9.780567592278144e-01], + [-6.485808825578578e-01, -6.485808825578577e-01], + [-9.780567592278142e-01, 1.147703673995890e-01], + [-7.302765158231814e-01, 7.302765158231813e-01], + ]) + np.testing.assert_array_almost_equal(deflections.array, expected, decimal=10) + + def test__nfw_regression(self): + grid = ag.Grid2D.uniform(shape_native=(3, 3), pixel_scales=1.0) + profile = ag.mp.NFW( + centre=(0.0, 0.0), + ell_comps=(0.15, 0.05), + kappa_s=0.05, + scale_radius=1.0, + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + + expected = np.array([ + [3.930040704822994e-02, -3.704991327787192e-02], + [5.330198200319717e-02, -5.486557682861090e-03], + [3.681444501824387e-02, 3.397437925990372e-02], + [5.883231023414714e-03, -5.165835655921132e-02], + [0.0, 0.0], + [-5.883231023414721e-03, 5.165835655921130e-02], + [-3.681444501824389e-02, -3.397437925990375e-02], + [-5.330198200319718e-02, 5.486557682861097e-03], + [-3.930040704822995e-02, 3.704991327787193e-02], + ]) + np.testing.assert_array_almost_equal(deflections.array, expected, decimal=10) + + def test__power_law_regression(self): + grid = ag.Grid2D.uniform(shape_native=(3, 3), pixel_scales=1.0) + profile = ag.mp.PowerLaw( + centre=(0.0, 0.0), ell_comps=(0.1, 0.2), einstein_radius=1.0, slope=2.3 + ) + deflections = profile.deflections_yx_2d_from(grid=grid) + + # Check non-centre pixels (centre pixel [4] has singularity) + expected_0 = np.array([6.396990900652429e-01, -5.290981929231797e-01]) + expected_1 = np.array([9.317530273653852e-01, -4.087717797661689e-02]) + np.testing.assert_array_almost_equal(deflections.array[0], expected_0, decimal=10) + np.testing.assert_array_almost_equal(deflections.array[1], expected_1, decimal=10)