Skip to content

Commit a73fc6b

Browse files
authored
Merge pull request #501 from PyAutoLabs/feature/coolest-standard-support
feat: COOLEST-standard profile parameter converters (autogalaxy.interop.coolest)
2 parents 36ce779 + 31100e8 commit a73fc6b

10 files changed

Lines changed: 1046 additions & 0 deletions

File tree

autogalaxy/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,10 @@ def __getattr__(name):
141141

142142
globals()["plot"] = plot
143143
return plot
144+
if name == "interop":
145+
import importlib
146+
147+
interop = importlib.import_module(f"{__name__}.interop")
148+
globals()["interop"] = interop
149+
return interop
144150
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

autogalaxy/interop/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from autogalaxy.interop import coolest
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Conversion between PyAutoGalaxy profiles and the COOLEST standard
3+
(https://github.com/aymgal/COOLEST).
4+
5+
This package converts profile *parameterisations* only — every function is
6+
dict-in / dict-out on plain floats, so ``autogalaxy`` gains no dependency on
7+
the ``coolest`` package. Reading and writing COOLEST JSON template files is
8+
performed one layer up, in ``autolens.interop.coolest``, which consumes the
9+
converters defined here.
10+
11+
COOLEST conventions (see https://coolest.readthedocs.io -> Conventions):
12+
13+
- Cartesian coordinates (x to the right, y up), positions in arcseconds.
14+
- Position angles ``phi`` measured counter-clockwise from the positive y axis
15+
("East-of-North"), in degrees, in the interval (-90, +90].
16+
- Ellipticity described by the axis ratio ``q = b / a`` and ``phi``.
17+
- Characteristic radii (Einstein radius, effective radius, scale radius) are
18+
defined along the *intermediate axis* of the ellipse, r = sqrt(a * b).
19+
20+
PyAutoGalaxy conventions:
21+
22+
- (y, x) coordinates in arcseconds.
23+
- Position angles measured counter-clockwise from the positive x axis.
24+
- Ellipticity described by the elliptical components ``ell_comps`` (e1, e2).
25+
- Radius conventions are profile specific (e.g. the ``PowerLaw`` Einstein
26+
radius follows an average-axis convention, the ``Sersic`` effective radius
27+
is already an intermediate-axis radius) — see the converters in
28+
``mass.py`` / ``light.py`` for the exact factor each profile requires.
29+
"""
30+
31+
from autogalaxy.interop.coolest import conventions
32+
from autogalaxy.interop.coolest import light
33+
from autogalaxy.interop.coolest import mass
34+
from autogalaxy.interop.coolest.light import coolest_dict_from_light
35+
from autogalaxy.interop.coolest.light import light_profile_from
36+
from autogalaxy.interop.coolest.mass import coolest_dict_from_mass
37+
from autogalaxy.interop.coolest.mass import mass_profile_from
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from typing import Tuple
2+
3+
import numpy as np
4+
5+
from autogalaxy import convert
6+
7+
8+
def phi_coolest_from(angle: float) -> float:
9+
"""
10+
Convert a PyAutoGalaxy position angle to a COOLEST position angle.
11+
12+
PyAutoGalaxy position angles are measured counter-clockwise from the
13+
positive x axis; COOLEST position angles are measured counter-clockwise
14+
from the positive y axis ("East-of-North") and lie in (-90, +90].
15+
16+
Parameters
17+
----------
18+
angle
19+
Position angle in degrees, counter-clockwise from the positive x axis.
20+
21+
Returns
22+
-------
23+
Position angle in degrees, counter-clockwise from the positive y axis,
24+
normalized to (-90, +90].
25+
"""
26+
phi = float(angle) - 90.0
27+
while phi <= -90.0:
28+
phi += 180.0
29+
while phi > 90.0:
30+
phi -= 180.0
31+
return phi
32+
33+
34+
def angle_from_phi_coolest(phi: float) -> float:
35+
"""
36+
Convert a COOLEST position angle (counter-clockwise from the positive y
37+
axis) to a PyAutoGalaxy position angle (counter-clockwise from the
38+
positive x axis).
39+
40+
Parameters
41+
----------
42+
phi
43+
Position angle in degrees, counter-clockwise from the positive y axis.
44+
"""
45+
return float(phi) + 90.0
46+
47+
48+
def q_phi_from_ell_comps(ell_comps: Tuple[float, float]) -> Tuple[float, float]:
49+
"""
50+
Convert PyAutoGalaxy elliptical components (e1, e2) to the COOLEST axis
51+
ratio ``q`` and position angle ``phi`` (East-of-North, degrees).
52+
53+
Parameters
54+
----------
55+
ell_comps
56+
The elliptical components (e1, e2) of a light or mass profile.
57+
"""
58+
axis_ratio, angle = convert.axis_ratio_and_angle_from(ell_comps=ell_comps)
59+
return float(axis_ratio), phi_coolest_from(angle=float(angle))
60+
61+
62+
def ell_comps_from_q_phi(q: float, phi: float) -> Tuple[float, float]:
63+
"""
64+
Convert a COOLEST axis ratio ``q`` and position angle ``phi``
65+
(East-of-North, degrees) to PyAutoGalaxy elliptical components (e1, e2).
66+
67+
Parameters
68+
----------
69+
q
70+
The axis ratio (b/a) of the ellipse.
71+
phi
72+
Position angle in degrees, counter-clockwise from the positive y axis.
73+
"""
74+
ell_comps = convert.ell_comps_from(
75+
axis_ratio=q, angle=angle_from_phi_coolest(phi=phi)
76+
)
77+
return float(ell_comps[0]), float(ell_comps[1])
78+
79+
80+
def center_from_centre(centre: Tuple[float, float]) -> Tuple[float, float]:
81+
"""
82+
Convert a PyAutoGalaxy (y, x) profile centre to a COOLEST
83+
(center_x, center_y) position. Both are arcseconds with x increasing to
84+
the right and y increasing upwards, so only the ordering changes.
85+
86+
Parameters
87+
----------
88+
centre
89+
The (y, x) arc-second coordinates of the profile centre.
90+
"""
91+
return float(centre[1]), float(centre[0])
92+
93+
94+
def centre_from_center(center_x: float, center_y: float) -> Tuple[float, float]:
95+
"""
96+
Convert a COOLEST (center_x, center_y) position to a PyAutoGalaxy (y, x)
97+
profile centre.
98+
"""
99+
return float(center_y), float(center_x)
100+
101+
102+
def radius_intermediate_from(radius_major: float, q: float) -> float:
103+
"""
104+
Convert a major-axis radius to the COOLEST intermediate-axis radius
105+
r = sqrt(a * b) = sqrt(q) * a of the same elliptical contour.
106+
107+
Parameters
108+
----------
109+
radius_major
110+
The radius measured along the ellipse's major axis.
111+
q
112+
The axis ratio (b/a) of the ellipse.
113+
"""
114+
return float(radius_major) * np.sqrt(q)
115+
116+
117+
def radius_major_from(radius_intermediate: float, q: float) -> float:
118+
"""
119+
Convert a COOLEST intermediate-axis radius r = sqrt(a * b) to the
120+
major-axis radius of the same elliptical contour.
121+
122+
Parameters
123+
----------
124+
radius_intermediate
125+
The intermediate-axis radius sqrt(a * b) of the elliptical contour.
126+
q
127+
The axis ratio (b/a) of the ellipse.
128+
"""
129+
return float(radius_intermediate) / np.sqrt(q)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
Bidirectional parameter conversion between PyAutoGalaxy light profiles and the
3+
COOLEST standard's light profiles.
4+
5+
The PyAutoGalaxy ``Sersic`` evaluates its image on *eccentric* radii
6+
``sqrt(q) * sqrt(x^2 + (y/q)^2)`` (``geometry_profiles.py``), which is exactly
7+
the COOLEST intermediate-axis radius ``sqrt(q x^2 + y^2 / q)``, and its
8+
``intensity`` is the amplitude at ``effective_radius``. The Sersic mapping is
9+
therefore an identity on (I_eff, theta_eff, n) — only the centre ordering and
10+
the ellipticity / position-angle conventions change.
11+
"""
12+
13+
from typing import Callable, Dict
14+
15+
from autogalaxy import exc
16+
from autogalaxy.interop.coolest import conventions
17+
from autogalaxy.profiles.light.standard.sersic import Sersic
18+
from autogalaxy.profiles.light.standard.sersic import SersicSph
19+
20+
21+
def _sersic_dict_from(profile, **kwargs) -> Dict:
22+
q, phi = conventions.q_phi_from_ell_comps(ell_comps=profile.ell_comps)
23+
center_x, center_y = conventions.center_from_centre(centre=profile.centre)
24+
return {
25+
"type": "Sersic",
26+
"parameters": {
27+
"I_eff": float(profile.intensity),
28+
"theta_eff": float(profile.effective_radius),
29+
"n": float(profile.sersic_index),
30+
"q": q,
31+
"phi": phi,
32+
"center_x": center_x,
33+
"center_y": center_y,
34+
},
35+
}
36+
37+
38+
def _sersic_from(parameters: Dict) -> Sersic:
39+
q = parameters["q"]
40+
centre = conventions.centre_from_center(
41+
center_x=parameters["center_x"], center_y=parameters["center_y"]
42+
)
43+
if q == 1.0:
44+
return SersicSph(
45+
centre=centre,
46+
intensity=float(parameters["I_eff"]),
47+
effective_radius=float(parameters["theta_eff"]),
48+
sersic_index=float(parameters["n"]),
49+
)
50+
return Sersic(
51+
centre=centre,
52+
ell_comps=conventions.ell_comps_from_q_phi(q=q, phi=parameters["phi"]),
53+
intensity=float(parameters["I_eff"]),
54+
effective_radius=float(parameters["theta_eff"]),
55+
sersic_index=float(parameters["n"]),
56+
)
57+
58+
59+
_TO_COOLEST: Dict[type, Callable] = {
60+
Sersic: _sersic_dict_from,
61+
SersicSph: _sersic_dict_from,
62+
}
63+
64+
_FROM_COOLEST: Dict[str, Callable] = {
65+
"Sersic": _sersic_from,
66+
}
67+
68+
69+
def coolest_dict_from_light(profile) -> Dict:
70+
"""
71+
Convert a PyAutoGalaxy light profile to its COOLEST representation, a dict
72+
``{"type": <COOLEST profile name>, "parameters": {<name>: <float>}}`` with
73+
all parameters in COOLEST conventions.
74+
75+
Parameters
76+
----------
77+
profile
78+
The light profile to convert. Supported: ``Sersic`` / ``SersicSph``.
79+
"""
80+
try:
81+
to_coolest = _TO_COOLEST[type(profile)]
82+
except KeyError:
83+
raise exc.ProfileException(
84+
f"The light profile {type(profile).__name__} has no COOLEST "
85+
f"converter. Supported profiles: "
86+
f"{sorted(cls.__name__ for cls in _TO_COOLEST)}."
87+
)
88+
return to_coolest(profile)
89+
90+
91+
def light_profile_from(profile_type: str, parameters: Dict):
92+
"""
93+
Build a PyAutoGalaxy light profile from a COOLEST profile type name and
94+
its parameter dict (in COOLEST conventions).
95+
96+
Parameters
97+
----------
98+
profile_type
99+
The COOLEST profile name, e.g. "Sersic".
100+
parameters
101+
The COOLEST parameters of the profile, e.g. point-estimate values read
102+
from a COOLEST template file.
103+
"""
104+
try:
105+
from_coolest = _FROM_COOLEST[profile_type]
106+
except KeyError:
107+
raise exc.ProfileException(
108+
f"The COOLEST light profile '{profile_type}' has no PyAutoGalaxy "
109+
f"converter. Supported: {sorted(_FROM_COOLEST)}."
110+
)
111+
return from_coolest(parameters)

0 commit comments

Comments
 (0)