Skip to content

Commit 9c6811d

Browse files
Jammy2211claude
authored andcommitted
refactor(light): split multipole module + add ag.lp_linear variants
Refactors the multipole light profile module added in #420: - autogalaxy/profiles/light/standard/multipole.py is split into per-class modules: - _multipole_mixin.py — private, holds the shared _LightProfileMultipoleMixin - sersic_multipole.py — SersicMultipole - gaussian_multipole.py — GaussianMultipole - test_autogalaxy/profiles/light/standard/test_multipole.py is split into test_sersic_multipole.py + test_gaussian_multipole.py - standard/__init__.py updated to the two new imports Adds linear-light-profile counterparts: - autogalaxy/profiles/light/linear/sersic_multipole.py ag.lp_linear.SersicMultipole(lp.SersicMultipole, LightProfileLinear) hardcodes intensity=1.0 (solved via inversion) and drops intensity from the constructor signature. - autogalaxy/profiles/light/linear/gaussian_multipole.py — same pattern. - linear/__init__.py exports both. - test_autogalaxy/profiles/light/linear/test_sersic_multipole.py and test_gaussian_multipole.py cover: class identity (LightProfileLinear + the standard subclass), intensity-not-a-constructor-parameter, multipole_*_comps propagation, parity with the linear base profile at zero perturbation, and image parity with the standard variant at intensity=1.0. Docs: - docs/api/light.rst Linear [ag.lp_linear] section now lists SersicMultipole and GaussianMultipole. 895 unit tests pass. Follows up #418. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6151902 commit 9c6811d

13 files changed

Lines changed: 615 additions & 320 deletions

File tree

autogalaxy/profiles/light/linear/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from .dev_vaucouleurs import DevVaucouleurs, DevVaucouleursSph
77
from .sersic_core import SersicCore, SersicCoreSph
88
from .exponential_core import ExponentialCore, ExponentialCoreSph
9+
from .sersic_multipole import SersicMultipole
10+
from .gaussian_multipole import GaussianMultipole
911
from .shapelets.polar import ShapeletPolarSph, ShapeletPolar
1012
from .shapelets.cartesian import ShapeletCartesianSph, ShapeletCartesian
1113
from .shapelets.exponential import ShapeletExponentialSph, ShapeletExponential
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Linear elliptical Gaussian light profile with m=3 and m=4 Fourier multipole
3+
perturbations on the eccentric radius.
4+
5+
This is the linear-light-profile counterpart of ``autogalaxy.lp.GaussianMultipole``:
6+
``intensity`` is hardcoded to 1.0 in the constructor because it is solved
7+
analytically via the linear inversion at fit time.
8+
"""
9+
10+
from typing import Tuple
11+
12+
from autogalaxy.profiles.light.linear.abstract import LightProfileLinear
13+
14+
from autogalaxy.profiles.light import standard as lp
15+
16+
17+
class GaussianMultipole(lp.GaussianMultipole, LightProfileLinear):
18+
def __init__(
19+
self,
20+
centre: Tuple[float, float] = (0.0, 0.0),
21+
ell_comps: Tuple[float, float] = (0.0, 0.0),
22+
sigma: float = 1.0,
23+
multipole_3_comps: Tuple[float, float] = (0.0, 0.0),
24+
multipole_4_comps: Tuple[float, float] = (0.0, 0.0),
25+
):
26+
"""
27+
The linear elliptical Gaussian light profile with m=3 and m=4 Fourier multipole
28+
perturbations on the eccentric radius.
29+
30+
Parameters
31+
----------
32+
centre
33+
The (y,x) arc-second coordinates of the profile centre.
34+
ell_comps
35+
The first and second ellipticity components of the elliptical coordinate
36+
system.
37+
sigma
38+
The sigma value of the Gaussian.
39+
multipole_3_comps
40+
The ``(cos, sin)`` components of the m=3 Fourier perturbation. Defaults to
41+
``(0.0, 0.0)`` which reduces the profile to ``lp_linear.Gaussian``.
42+
multipole_4_comps
43+
The ``(cos, sin)`` components of the m=4 Fourier perturbation. Defaults to
44+
``(0.0, 0.0)`` which reduces the profile to ``lp_linear.Gaussian``.
45+
"""
46+
super().__init__(
47+
centre=centre,
48+
ell_comps=ell_comps,
49+
intensity=1.0,
50+
sigma=sigma,
51+
multipole_3_comps=multipole_3_comps,
52+
multipole_4_comps=multipole_4_comps,
53+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
Linear elliptical Sersic light profile with m=3 and m=4 Fourier multipole
3+
perturbations on the eccentric radius.
4+
5+
This is the linear-light-profile counterpart of ``autogalaxy.lp.SersicMultipole``:
6+
``intensity`` is hardcoded to 1.0 in the constructor because it is solved
7+
analytically via the linear inversion at fit time.
8+
"""
9+
10+
from typing import Tuple
11+
12+
from autogalaxy.profiles.light.linear.abstract import LightProfileLinear
13+
14+
from autogalaxy.profiles.light import standard as lp
15+
16+
17+
class SersicMultipole(lp.SersicMultipole, LightProfileLinear):
18+
def __init__(
19+
self,
20+
centre: Tuple[float, float] = (0.0, 0.0),
21+
ell_comps: Tuple[float, float] = (0.0, 0.0),
22+
effective_radius: float = 0.6,
23+
sersic_index: float = 4.0,
24+
multipole_3_comps: Tuple[float, float] = (0.0, 0.0),
25+
multipole_4_comps: Tuple[float, float] = (0.0, 0.0),
26+
):
27+
"""
28+
The linear elliptical Sersic light profile with m=3 and m=4 Fourier multipole
29+
perturbations on the eccentric radius.
30+
31+
Parameters
32+
----------
33+
centre
34+
The (y,x) arc-second coordinates of the profile centre.
35+
ell_comps
36+
The first and second ellipticity components of the elliptical coordinate
37+
system.
38+
effective_radius
39+
The circular radius containing half the light of this profile.
40+
sersic_index
41+
Controls the concentration of the profile.
42+
multipole_3_comps
43+
The ``(cos, sin)`` components of the m=3 Fourier perturbation. Defaults to
44+
``(0.0, 0.0)`` which reduces the profile to ``lp_linear.Sersic``.
45+
multipole_4_comps
46+
The ``(cos, sin)`` components of the m=4 Fourier perturbation. Defaults to
47+
``(0.0, 0.0)`` which reduces the profile to ``lp_linear.Sersic``.
48+
"""
49+
super().__init__(
50+
centre=centre,
51+
ell_comps=ell_comps,
52+
intensity=1.0,
53+
effective_radius=effective_radius,
54+
sersic_index=sersic_index,
55+
multipole_3_comps=multipole_3_comps,
56+
multipole_4_comps=multipole_4_comps,
57+
)

autogalaxy/profiles/light/standard/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
ElsonFreeFall,
1414
ElsonFreeFallSph,
1515
)
16-
from .multipole import SersicMultipole, GaussianMultipole
16+
from .sersic_multipole import SersicMultipole
17+
from .gaussian_multipole import GaussianMultipole
1718
from .shapelets.polar import ShapeletPolarSph, ShapeletPolar
1819
from .shapelets.cartesian import ShapeletCartesianSph, ShapeletCartesian
1920
from .shapelets.exponential import ShapeletExponentialSph, ShapeletExponential
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Shared helper for light profiles with m=3 and m=4 Fourier perturbations on the
3+
eccentric radius.
4+
5+
Used by ``SersicMultipole`` (``sersic_multipole.py``) and ``GaussianMultipole``
6+
(``gaussian_multipole.py``). The leading underscore signals "implementation
7+
detail" — when the future generalisation lands (a single wrapper-style multipole
8+
that perturbs any radial profile, unified with the ellipse-fitting API), this
9+
module will be replaced rather than evolved in-place.
10+
"""
11+
12+
from typing import Tuple
13+
14+
import numpy as np
15+
16+
import autoarray as aa
17+
18+
19+
class _LightProfileMultipoleMixin:
20+
"""
21+
Provides ``perturbed_radii_from`` to light profiles that:
22+
23+
- set ``multipole_3_comps`` and ``multipole_4_comps`` on ``self`` in ``__init__``
24+
- inherit from a base profile providing ``eccentric_radii_grid_from``
25+
"""
26+
27+
multipole_3_comps: Tuple[float, float]
28+
multipole_4_comps: Tuple[float, float]
29+
30+
def perturbed_radii_from(
31+
self,
32+
grid: aa.type.Grid2DLike,
33+
xp=np,
34+
**kwargs,
35+
) -> np.ndarray:
36+
"""
37+
Returns the eccentric radii of ``grid`` perturbed by the m=3 and m=4 Fourier
38+
multipoles, as a raw backend array (numpy or jax.numpy).
39+
40+
The perturbation is
41+
42+
r' = r * (1 + c_3 cos(3 theta) + s_3 sin(3 theta)
43+
+ c_4 cos(4 theta) + s_4 sin(4 theta))
44+
45+
where ``theta = arctan2(y, x)`` is the polar angle in the profile's
46+
elliptical reference frame (the grid is already in that frame after the
47+
``@aa.decorators.transform`` decorator on the caller's ``image_2d_from``).
48+
49+
Result is floored at 1e-8 to keep ``r ** (1/n)`` evaluations finite when a
50+
large multipole component drives the radius through zero.
51+
"""
52+
grid_radii = self.eccentric_radii_grid_from(grid=grid, xp=xp, **kwargs)
53+
grid_radii_array = (
54+
grid_radii.array if hasattr(grid_radii, "array") else grid_radii
55+
)
56+
y = grid.array[:, 0]
57+
x = grid.array[:, 1]
58+
theta = xp.arctan2(y, x)
59+
c3, s3 = self.multipole_3_comps
60+
c4, s4 = self.multipole_4_comps
61+
perturbation = (
62+
1.0
63+
+ c3 * xp.cos(3.0 * theta)
64+
+ s3 * xp.sin(3.0 * theta)
65+
+ c4 * xp.cos(4.0 * theta)
66+
+ s4 * xp.sin(4.0 * theta)
67+
)
68+
return xp.maximum(xp.multiply(grid_radii_array, perturbation), 1e-8)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
Elliptical Gaussian light profile with m=3 and m=4 Fourier multipole perturbations
3+
applied to the eccentric radius before evaluating the Gaussian profile.
4+
5+
With both ``multipole_*_comps`` set to ``(0.0, 0.0)`` (the default), the profile
6+
reduces exactly to ``Gaussian``.
7+
"""
8+
9+
from typing import Optional, Tuple
10+
11+
import numpy as np
12+
13+
import autoarray as aa
14+
15+
from autogalaxy.profiles.light.decorators import check_operated_only
16+
from autogalaxy.profiles.light.standard._multipole_mixin import (
17+
_LightProfileMultipoleMixin,
18+
)
19+
from autogalaxy.profiles.light.standard.gaussian import Gaussian
20+
21+
22+
class GaussianMultipole(_LightProfileMultipoleMixin, Gaussian):
23+
def __init__(
24+
self,
25+
centre: Tuple[float, float] = (0.0, 0.0),
26+
ell_comps: Tuple[float, float] = (0.0, 0.0),
27+
intensity: float = 0.1,
28+
sigma: float = 1.0,
29+
multipole_3_comps: Tuple[float, float] = (0.0, 0.0),
30+
multipole_4_comps: Tuple[float, float] = (0.0, 0.0),
31+
):
32+
"""
33+
The elliptical Gaussian light profile with m=3 and m=4 Fourier multipole
34+
perturbations on the eccentric radius.
35+
36+
Parameters
37+
----------
38+
centre
39+
The (y,x) arc-second coordinates of the profile centre.
40+
ell_comps
41+
The first and second ellipticity components of the elliptical coordinate
42+
system. The multipole perturbation is applied to the eccentric radius and
43+
therefore follows this ellipticity.
44+
intensity
45+
Overall intensity normalisation of the light profile.
46+
sigma
47+
The sigma value of the Gaussian.
48+
multipole_3_comps
49+
The ``(cos, sin)`` components of the m=3 Fourier perturbation. Defaults to
50+
``(0.0, 0.0)`` which reduces the profile to ``Gaussian``.
51+
multipole_4_comps
52+
The ``(cos, sin)`` components of the m=4 Fourier perturbation. Defaults to
53+
``(0.0, 0.0)`` which reduces the profile to ``Gaussian``.
54+
"""
55+
super().__init__(
56+
centre=centre, ell_comps=ell_comps, intensity=intensity, sigma=sigma
57+
)
58+
self.multipole_3_comps = multipole_3_comps
59+
self.multipole_4_comps = multipole_4_comps
60+
61+
def image_2d_via_radii_from(
62+
self, grid_radii: np.ndarray, xp=np, **kwargs
63+
) -> np.ndarray:
64+
"""
65+
Returns the 2D Gaussian image evaluated at the input radial values.
66+
67+
Unlike ``Gaussian.image_2d_via_radii_from``, this override accepts a raw backend
68+
array (the output of ``perturbed_radii_from``) rather than an autoarray-wrapped
69+
grid, since the perturbation step strips the wrapper.
70+
"""
71+
return xp.multiply(
72+
self._intensity,
73+
xp.exp(
74+
-0.5
75+
* xp.square(
76+
xp.divide(grid_radii, self.sigma / xp.sqrt(self.axis_ratio(xp)))
77+
)
78+
),
79+
)
80+
81+
@aa.over_sample
82+
@aa.decorators.to_array
83+
@check_operated_only
84+
@aa.decorators.transform
85+
def image_2d_from(
86+
self,
87+
grid: aa.type.Grid2DLike,
88+
xp=np,
89+
operated_only: Optional[bool] = None,
90+
**kwargs,
91+
) -> aa.Array2D:
92+
"""
93+
Returns the 2D image of the multipole-perturbed Gaussian profile.
94+
"""
95+
perturbed_radii = self.perturbed_radii_from(grid=grid, xp=xp, **kwargs)
96+
return self.image_2d_via_radii_from(grid_radii=perturbed_radii, xp=xp, **kwargs)

0 commit comments

Comments
 (0)