Skip to content

Commit acdff49

Browse files
Jammy2211claude
authored andcommitted
Add mge_point_model_from to model_util for compact point-source modelling
Adds a new convenience function that constructs a linear MGE Basis of Gaussians whose sigma values span 0.01 arcseconds to 2*pixel_scales, with shared centre and ellipticity priors, suitable for modelling unresolved AGN, nuclear starbursts, or compact unresolved bulge components. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5850af2 commit acdff49

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

autogalaxy/analysis/model_util.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,63 @@ def mge_model_from(
129129
)
130130

131131

132+
def mge_point_model_from(
133+
pixel_scales: float,
134+
total_gaussians: int = 10,
135+
centre: Tuple[float, float] = (0.0, 0.0),
136+
) -> af.Model:
137+
"""
138+
Construct a Multi-Gaussian Expansion (MGE) model for a compact or unresolved
139+
point-like component (e.g. a nuclear starburst, AGN, or unresolved bulge).
140+
141+
The model is composed of ``total_gaussians`` linear Gaussians whose sigma values
142+
are logarithmically spaced between 0.01 arcseconds and twice the pixel scale.
143+
All Gaussians share the same centre and ellipticity components, keeping the
144+
parameter count low while capturing a realistic PSF-convolved point source.
145+
146+
Parameters
147+
----------
148+
pixel_scales
149+
The pixel scale of the image in arcseconds per pixel. The maximum Gaussian
150+
width is set to ``2 * pixel_scales`` so that the model is compact relative to
151+
the resolution of the data.
152+
total_gaussians
153+
Number of Gaussian components in the basis.
154+
centre
155+
(y, x) centre of the point source in arc-seconds. A ±0.1 arcsecond uniform
156+
prior is placed on each coordinate.
157+
158+
Returns
159+
-------
160+
af.Model
161+
An ``autofit.Model`` wrapping a ``Basis`` of linear Gaussians.
162+
"""
163+
164+
from autogalaxy.profiles.light.linear import Gaussian
165+
from autogalaxy.profiles.basis import Basis
166+
167+
log10_sigma_list = np.linspace(-2, np.log10(pixel_scales * 2.0), total_gaussians)
168+
169+
centre_0 = af.UniformPrior(
170+
lower_limit=centre[0] - 0.1, upper_limit=centre[0] + 0.1
171+
)
172+
centre_1 = af.UniformPrior(
173+
lower_limit=centre[1] - 0.1, upper_limit=centre[1] + 0.1
174+
)
175+
176+
gaussian_list = af.Collection(
177+
af.Model(Gaussian) for _ in range(total_gaussians)
178+
)
179+
180+
for i, gaussian in enumerate(gaussian_list):
181+
gaussian.centre.centre_0 = centre_0
182+
gaussian.centre.centre_1 = centre_1
183+
gaussian.ell_comps = gaussian_list[0].ell_comps
184+
gaussian.sigma = 10 ** log10_sigma_list[i]
185+
186+
return af.Model(Basis, profile_list=gaussian_list)
187+
188+
132189
def simulator_start_here_model_from():
133190

134191
from autogalaxy.profiles.light.snr import Sersic
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import pytest
2+
import numpy as np
3+
4+
import autofit as af
5+
import autogalaxy as ag
6+
7+
8+
def test__mge_point_model_from__returns_basis_model_with_correct_gaussians():
9+
"""
10+
mge_point_model_from should return an af.Model wrapping a Basis whose
11+
profile_list contains the requested number of linear Gaussian components.
12+
"""
13+
model = ag.model_util.mge_point_model_from(pixel_scales=0.1, total_gaussians=5)
14+
15+
instance = model.instance_from_prior_medians()
16+
17+
assert isinstance(instance, ag.lp_basis.Basis)
18+
assert len(instance.profile_list) == 5
19+
20+
21+
def test__mge_point_model_from__sigma_values_span_correct_range():
22+
"""
23+
Sigma values should run from 10^-2 = 0.01 arcseconds up to 2 * pixel_scales,
24+
logarithmically spaced.
25+
"""
26+
pixel_scales = 0.1
27+
total_gaussians = 10
28+
29+
model = ag.model_util.mge_point_model_from(
30+
pixel_scales=pixel_scales, total_gaussians=total_gaussians
31+
)
32+
33+
gaussian_list = list(model.profile_list)
34+
35+
assert gaussian_list[0].sigma == pytest.approx(0.01, rel=1.0e-4)
36+
assert gaussian_list[-1].sigma == pytest.approx(pixel_scales * 2.0, rel=1.0e-4)
37+
38+
39+
def test__mge_point_model_from__shared_centre_and_ell_comps():
40+
"""
41+
All Gaussians must share exactly the same centre prior objects and ell_comps
42+
prior objects so the model has only 4 free parameters total.
43+
"""
44+
model = ag.model_util.mge_point_model_from(pixel_scales=0.1, total_gaussians=5)
45+
46+
gaussian_list = list(model.profile_list)
47+
48+
# Centres are all the same prior objects
49+
for gaussian in gaussian_list[1:]:
50+
assert gaussian.centre.centre_0 is gaussian_list[0].centre.centre_0
51+
assert gaussian.centre.centre_1 is gaussian_list[0].centre.centre_1
52+
53+
# Ell_comps are all the same prior objects
54+
for gaussian in gaussian_list[1:]:
55+
assert gaussian.ell_comps is gaussian_list[0].ell_comps
56+
57+
# Only 4 free parameters: centre_0, centre_1, ell_comps_0, ell_comps_1
58+
assert model.prior_count == 4
59+
60+
61+
def test__mge_point_model_from__centre_prior_bounds():
62+
"""
63+
When a custom centre is supplied the UniformPrior limits shift by ±0.1
64+
arcseconds around that centre.
65+
"""
66+
centre = (0.3, -0.2)
67+
model = ag.model_util.mge_point_model_from(
68+
pixel_scales=0.1, total_gaussians=3, centre=centre
69+
)
70+
71+
gaussian_list = list(model.profile_list)
72+
centre_0_prior = gaussian_list[0].centre.centre_0
73+
centre_1_prior = gaussian_list[0].centre.centre_1
74+
75+
assert isinstance(centre_0_prior, af.UniformPrior)
76+
assert centre_0_prior.lower_limit == pytest.approx(centre[0] - 0.1, rel=1.0e-6)
77+
assert centre_0_prior.upper_limit == pytest.approx(centre[0] + 0.1, rel=1.0e-6)
78+
79+
assert isinstance(centre_1_prior, af.UniformPrior)
80+
assert centre_1_prior.lower_limit == pytest.approx(centre[1] - 0.1, rel=1.0e-6)
81+
assert centre_1_prior.upper_limit == pytest.approx(centre[1] + 0.1, rel=1.0e-6)

0 commit comments

Comments
 (0)