Skip to content

Commit 17c9314

Browse files
authored
Merge pull request #366 from PyAutoLabs/claude/list-admin-prompts-Mid1U
docs(weak-lensing): document shear API across LensCalc, Isothermal, ExternalShear
2 parents 3851fbf + 1b9535b commit 17c9314

5 files changed

Lines changed: 241 additions & 88 deletions

File tree

autogalaxy/operate/lens_calc.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -519,33 +519,53 @@ def convergence_2d_via_hessian_from(self, grid, xp=np) -> aa.ArrayIrregular:
519519
return convergence
520520

521521
def shear_yx_2d_via_hessian_from(self, grid, xp=np) -> ShearYX2DIrregular:
522-
"""
523-
Returns the 2D (y,x) shear vectors of the lensing object, which are computed from the 2D deflection angle map
524-
via the Hessian using the expressions (see equation 57 https://inspirehep.net/literature/419263):
522+
r"""
523+
Returns the 2D weak-lensing shear vector field of the lensing object, computed from the deflection-angle
524+
Hessian (see equation 57 of https://inspirehep.net/literature/419263):
525525
526-
`shear_y = hessian_{1,0} = hessian_{0,1} = hessian_yx = hessian_xy`
527-
`shear_x = 0.5 * (hessian_{0,0} - hessian_{1,1}) = 0.5 * (hessian_xx - hessian_yy)`
526+
.. math::
528527
529-
By going via the Hessian, the shear vectors can be calculated at any (y,x) coordinate, therefore using either a
530-
2D uniform or irregular grid.
528+
\gamma_1 = \tfrac{1}{2} (\partial^2 \psi / \partial x^2 - \partial^2 \psi / \partial y^2)
529+
= 0.5 \, (H_{xx} - H_{yy})
531530
532-
This calculation of the shear vectors is independent of analytic calculations defined within `MassProfile`
533-
objects and can therefore be used as a cross-check.
531+
\gamma_2 = \partial^2 \psi / \partial x \partial y
532+
= H_{xy} = H_{yx}
534533
535-
The result is returned as a `ShearYX2D` dats structure, which has shape [total_shear_vectors, 2], where
536-
entries for [:,0] are the gamma_2 values and entries for [:,1] are the gamma_1 values.
534+
where :math:`\psi(\theta)` is the lensing potential and :math:`H_{ij}` are its second partial derivatives,
535+
i.e. the components of the Hessian returned by ``hessian_from``. The two components together encode the
536+
full distortion tensor of a background source: :math:`\gamma_1` is the stretch along the x/y axes and
537+
:math:`\gamma_2` is the stretch along the diagonals.
537538
538-
Note therefore that this convention means the FIRST entries in the array are the gamma_2 values and the SECOND
539-
entries are the gamma_1 values.
539+
Because the Hessian is computed numerically (Richardson-extrapolated finite differences in NumPy, or
540+
``jax.jacfwd`` in JAX), this routine works for **any** ``MassProfile``, ``Galaxy``, ``Galaxies`` or
541+
``Tracer`` that exposes ``deflections_yx_2d_from`` — no per-profile analytic formula is required. It can
542+
therefore be used as a numerical cross-check of analytic shear methods such as
543+
``Isothermal.shear_yx_2d_from`` (see ``test_isothermal.py``). It also accepts both uniform ``Grid2D`` and
544+
irregular ``Grid2DIrregular`` grids, which makes it the natural choice for simulating a weak-lensing
545+
shear field on either a regular pixel grid or at the discrete positions of background source galaxies.
546+
547+
Convention
548+
----------
549+
The result is returned as a ``ShearYX2DIrregular`` data structure of shape ``[total_shear_vectors, 2]``,
550+
where:
551+
552+
- ``[:, 0]`` are the :math:`\gamma_2` values
553+
- ``[:, 1]`` are the :math:`\gamma_1` values
554+
555+
Note therefore that the FIRST column is :math:`\gamma_2` and the SECOND column is :math:`\gamma_1`. This
556+
ordering matches the ``[y, x]`` ordering used elsewhere in the project for vector fields, since
557+
:math:`\gamma_2` plays the role of the "y" component and :math:`\gamma_1` the "x" component when the shear
558+
field is treated as a 2D pseudo-vector.
540559
541560
Parameters
542561
----------
543-
grids
562+
grid
544563
The 2D grid of (y,x) arc-second coordinates the deflection angles and Hessian are computed on.
545564
xp
546-
The array module to use for the computation (e.g. `numpy` or `jax.numpy`). Passed through to
547-
`hessian_from`. When `xp` is not `numpy` (e.g. inside a `jax.jit` trace) the result is returned
548-
as a raw array of shape `(N, 2)` rather than a `ShearYX2DIrregular` wrapper.
565+
The array module to use for the computation (e.g. ``numpy`` or ``jax.numpy``). Passed through to
566+
``hessian_from``. When ``xp`` is not ``numpy`` (e.g. inside a ``jax.jit`` trace) the result is
567+
returned as a raw array of shape ``(N, 2)`` rather than a ``ShearYX2DIrregular`` wrapper, because
568+
``ShearYX2DIrregular`` is not a registered JAX pytree.
549569
"""
550570

551571
hessian_yy, hessian_xy, hessian_yx, hessian_xx = self.hessian_from(

autogalaxy/profiles/mass/sheets/external_shear.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,51 @@
99

1010
class ExternalShear(MassProfile):
1111
def __init__(self, gamma_1: float = 0.0, gamma_2: float = 0.0):
12-
"""
13-
An `ExternalShear` term, to model the line-of-sight contribution of other galaxies / satellites.
12+
r"""
13+
Constant external shear term used in strong-lens mass models.
14+
15+
``ExternalShear`` represents the line-of-sight contribution to the lensing potential from mass that is
16+
not part of the primary lens — typically nearby group/cluster members, large-scale structure, or
17+
unmodelled satellites. Because this contribution is approximately uniform across the small angular
18+
extent of a strong-lens system, it is parameterised as a constant shear with two components,
19+
:math:`\gamma_1` and :math:`\gamma_2`, in the same convention as
20+
``LensCalc.shear_yx_2d_via_hessian_from`` and ``ShearYX2DIrregular``:
21+
22+
- :math:`\gamma_1` produces stretching along the x/y axes
23+
- :math:`\gamma_2` produces stretching along the diagonals
24+
25+
The associated shear *magnitude* and *position angle* (degrees, anticlockwise from the +x axis) are:
1426
15-
The shear angle is defined in the direction of stretching of the image. Therefore, if an object located \
16-
outside the lens is responsible for the shear, it will be offset 90 degrees from the value of angle.
27+
.. math::
28+
29+
|\gamma| = \sqrt{\gamma_1^2 + \gamma_2^2}, \qquad
30+
\phi = \tfrac{1}{2} \, \mathrm{arctan2}(\gamma_2, \gamma_1).
31+
32+
Note that the shear *position angle* lies in the direction of image stretching. An external mass
33+
located in the *direction of compression* therefore appears at an angle offset by 90 degrees from
34+
:math:`\phi`.
1735
1836
Parameters
1937
----------
20-
gamma
38+
gamma_1
39+
The :math:`\gamma_1` shear component.
40+
gamma_2
41+
The :math:`\gamma_2` shear component.
2142
"""
2243

2344
super().__init__(centre=(0.0, 0.0), ell_comps=(0.0, 0.0))
2445
self.gamma_1 = gamma_1
2546
self.gamma_2 = gamma_2
2647

2748
def magnitude(self, xp=np):
49+
r"""Returns the shear magnitude :math:`|\gamma| = \sqrt{\gamma_1^2 + \gamma_2^2}`."""
2850
return convert.shear_magnitude_from(
2951
gamma_1=self.gamma_1, gamma_2=self.gamma_2, xp=xp
3052
)
3153

3254
def angle(self, xp=np):
55+
r"""Returns the shear position angle :math:`\phi = \tfrac{1}{2}\,\mathrm{arctan2}(\gamma_2, \gamma_1)`
56+
in degrees, in the [0, 180) convention used elsewhere in the package."""
3357
return convert.shear_angle_from(
3458
gamma_1=self.gamma_1, gamma_2=self.gamma_2, xp=xp
3559
)
@@ -42,13 +66,25 @@ def average_convergence_of_1_radius(self):
4266

4367
@aa.decorators.to_array
4468
def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
69+
"""A pure shear term has zero convergence at every grid point."""
4570
return xp.zeros(shape=grid.shape[0])
4671

4772
@aa.decorators.to_array
4873
def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
74+
r"""
75+
Returns the lensing potential of the constant external shear, given by:
76+
77+
.. math::
78+
79+
\psi(\theta) = -\tfrac{1}{2} |\gamma| \, r^2 \, \cos\!\big(2\,(\varphi - \phi_\gamma)\big)
80+
81+
where :math:`r, \varphi` are the polar coordinates of ``grid`` and :math:`\phi_\gamma` is the shear
82+
position angle (offset by ``-90 deg`` to remain consistent with the deflection-angle convention used
83+
elsewhere in PyAutoLens).
84+
"""
4985
shear_angle = (
5086
self.angle(xp) - 90
51-
) ##to be onsistent with autolens deflection angle calculation
87+
) # offset by -90 deg to match the deflection-angle convention used elsewhere
5288
phig = xp.deg2rad(shear_angle)
5389
shear_amp = self.magnitude(xp=xp)
5490
phicoord = xp.arctan2(grid.array[:, 0], grid.array[:, 1])
@@ -59,14 +95,28 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
5995
@aa.decorators.to_vector_yx
6096
@aa.decorators.transform(rotate_back=True)
6197
def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
62-
"""
63-
Calculate the deflection angles at a given set of arc-second gridded coordinates.
98+
r"""
99+
Returns the deflection angles of the constant external shear at each ``(y, x)`` arc-second
100+
coordinate.
101+
102+
In the profile's rotated reference frame (where the shear position angle is aligned with the x-axis)
103+
the deflection reduces to:
104+
105+
.. math::
106+
107+
\alpha_y(y, x) = -|\gamma| \, y, \qquad \alpha_x(y, x) = +|\gamma| \, x.
108+
109+
The ``@transform(rotate_back=True)`` decorator rotates the input grid into this aligned frame and
110+
rotates the resulting deflection vectors back into the original frame. Because the shear is a spin-2
111+
field, the *position angle* :math:`\phi` of the shear is encoded in the rotation of the grid and not
112+
as an extra factor in the deflection formula above.
64113
65114
Parameters
66115
----------
67116
grid
68117
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
69-
118+
xp
119+
The array module (``numpy`` or ``jax.numpy``).
70120
"""
71121
deflection_y = -xp.multiply(self.magnitude(xp=xp), grid.array[:, 0])
72122
deflection_x = xp.multiply(self.magnitude(xp=xp), grid.array[:, 1])

autogalaxy/profiles/mass/total/isothermal.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,45 @@ def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
110110
@aa.decorators.to_vector_yx
111111
@aa.decorators.transform
112112
def shear_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
113-
"""
114-
Calculate the (gamma_y, gamma_x) shear vector field on a grid of (y,x) arc-second coordinates.
113+
r"""
114+
Returns the analytic 2D weak-lensing shear vector field :math:`(\gamma_2, \gamma_1)` of the elliptical
115+
isothermal mass distribution on a grid of ``(y, x)`` arc-second coordinates.
116+
117+
For an axis-aligned isothermal profile centred on the origin the shear components reduce to:
118+
119+
.. math::
115120
116-
The result is returned as a `ShearYX2D` dats structure, which has shape [total_shear_vectors, 2], where
117-
entries for [:,0] are the gamma_2 values and entries for [:,1] are the gamma_1 values.
121+
\gamma_1 = -\kappa(\theta) \, \frac{x^2 - y^2}{x^2 + y^2}
118122
119-
Note therefore that this convention means the FIRST entries in the array are the gamma_2 values and the SECOND
120-
entries are the gamma_1 values.
123+
\gamma_2 = -2 \, \kappa(\theta) \, \frac{x \, y}{x^2 + y^2}
124+
125+
where :math:`\kappa(\theta)` is the convergence at the rotated grid coordinate. After evaluation in the
126+
profile's reference frame the shear vector field is rotated back into the original frame using the
127+
``2 * angle`` rotation appropriate for a spin-2 quantity (the shear transforms as a spin-2 field, so a
128+
coordinate rotation by ``angle`` rotates the components by ``2 * angle``).
129+
130+
This analytic path is mathematically equivalent to ``LensCalc.shear_yx_2d_via_hessian_from``, which
131+
derives the same shear from finite-difference (or JAX) derivatives of ``deflections_yx_2d_from``. The
132+
cross-check is exercised in
133+
``test_autogalaxy/profiles/mass/total/test_isothermal.py::test__shear_yx_2d_from__matches_via_hessian``.
134+
135+
Convention
136+
----------
137+
The result is returned as a vector-field with shape ``[total_shear_vectors, 2]`` where:
138+
139+
- ``[:, 0]`` are the :math:`\gamma_2` values
140+
- ``[:, 1]`` are the :math:`\gamma_1` values
141+
142+
i.e. the FIRST column is :math:`\gamma_2` and the SECOND column is :math:`\gamma_1`. This ordering
143+
matches the convention used by ``ShearYX2D`` / ``ShearYX2DIrregular`` and
144+
``LensCalc.shear_yx_2d_via_hessian_from``.
121145
122146
Parameters
123147
----------
124148
grid
125-
The grid of (y,x) arc-second coordinates the deflection angles are computed on.
126-
149+
The grid of (y,x) arc-second coordinates the shear vectors are computed on.
150+
xp
151+
The array module (``numpy`` or ``jax.numpy``).
127152
"""
128153

129154
convergence = self.convergence_2d_from(grid=grid, xp=xp, **kwargs)

0 commit comments

Comments
 (0)