Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions autogalaxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,10 @@ def __getattr__(name):

globals()["plot"] = plot
return plot
if name == "interop":
import importlib

interop = importlib.import_module(f"{__name__}.interop")
globals()["interop"] = interop
return interop
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
1 change: 1 addition & 0 deletions autogalaxy/interop/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from autogalaxy.interop import coolest
37 changes: 37 additions & 0 deletions autogalaxy/interop/coolest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Conversion between PyAutoGalaxy profiles and the COOLEST standard
(https://github.com/aymgal/COOLEST).

This package converts profile *parameterisations* only — every function is
dict-in / dict-out on plain floats, so ``autogalaxy`` gains no dependency on
the ``coolest`` package. Reading and writing COOLEST JSON template files is
performed one layer up, in ``autolens.interop.coolest``, which consumes the
converters defined here.

COOLEST conventions (see https://coolest.readthedocs.io -> Conventions):

- Cartesian coordinates (x to the right, y up), positions in arcseconds.
- Position angles ``phi`` measured counter-clockwise from the positive y axis
("East-of-North"), in degrees, in the interval (-90, +90].
- Ellipticity described by the axis ratio ``q = b / a`` and ``phi``.
- Characteristic radii (Einstein radius, effective radius, scale radius) are
defined along the *intermediate axis* of the ellipse, r = sqrt(a * b).

PyAutoGalaxy conventions:

- (y, x) coordinates in arcseconds.
- Position angles measured counter-clockwise from the positive x axis.
- Ellipticity described by the elliptical components ``ell_comps`` (e1, e2).
- Radius conventions are profile specific (e.g. the ``PowerLaw`` Einstein
radius follows an average-axis convention, the ``Sersic`` effective radius
is already an intermediate-axis radius) — see the converters in
``mass.py`` / ``light.py`` for the exact factor each profile requires.
"""

from autogalaxy.interop.coolest import conventions
from autogalaxy.interop.coolest import light
from autogalaxy.interop.coolest import mass
from autogalaxy.interop.coolest.light import coolest_dict_from_light
from autogalaxy.interop.coolest.light import light_profile_from
from autogalaxy.interop.coolest.mass import coolest_dict_from_mass
from autogalaxy.interop.coolest.mass import mass_profile_from
129 changes: 129 additions & 0 deletions autogalaxy/interop/coolest/conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from typing import Tuple

import numpy as np

from autogalaxy import convert


def phi_coolest_from(angle: float) -> float:
"""
Convert a PyAutoGalaxy position angle to a COOLEST position angle.

PyAutoGalaxy position angles are measured counter-clockwise from the
positive x axis; COOLEST position angles are measured counter-clockwise
from the positive y axis ("East-of-North") and lie in (-90, +90].

Parameters
----------
angle
Position angle in degrees, counter-clockwise from the positive x axis.

Returns
-------
Position angle in degrees, counter-clockwise from the positive y axis,
normalized to (-90, +90].
"""
phi = float(angle) - 90.0
while phi <= -90.0:
phi += 180.0
while phi > 90.0:
phi -= 180.0
return phi


def angle_from_phi_coolest(phi: float) -> float:
"""
Convert a COOLEST position angle (counter-clockwise from the positive y
axis) to a PyAutoGalaxy position angle (counter-clockwise from the
positive x axis).

Parameters
----------
phi
Position angle in degrees, counter-clockwise from the positive y axis.
"""
return float(phi) + 90.0


def q_phi_from_ell_comps(ell_comps: Tuple[float, float]) -> Tuple[float, float]:
"""
Convert PyAutoGalaxy elliptical components (e1, e2) to the COOLEST axis
ratio ``q`` and position angle ``phi`` (East-of-North, degrees).

Parameters
----------
ell_comps
The elliptical components (e1, e2) of a light or mass profile.
"""
axis_ratio, angle = convert.axis_ratio_and_angle_from(ell_comps=ell_comps)
return float(axis_ratio), phi_coolest_from(angle=float(angle))


def ell_comps_from_q_phi(q: float, phi: float) -> Tuple[float, float]:
"""
Convert a COOLEST axis ratio ``q`` and position angle ``phi``
(East-of-North, degrees) to PyAutoGalaxy elliptical components (e1, e2).

Parameters
----------
q
The axis ratio (b/a) of the ellipse.
phi
Position angle in degrees, counter-clockwise from the positive y axis.
"""
ell_comps = convert.ell_comps_from(
axis_ratio=q, angle=angle_from_phi_coolest(phi=phi)
)
return float(ell_comps[0]), float(ell_comps[1])


def center_from_centre(centre: Tuple[float, float]) -> Tuple[float, float]:
"""
Convert a PyAutoGalaxy (y, x) profile centre to a COOLEST
(center_x, center_y) position. Both are arcseconds with x increasing to
the right and y increasing upwards, so only the ordering changes.

Parameters
----------
centre
The (y, x) arc-second coordinates of the profile centre.
"""
return float(centre[1]), float(centre[0])


def centre_from_center(center_x: float, center_y: float) -> Tuple[float, float]:
"""
Convert a COOLEST (center_x, center_y) position to a PyAutoGalaxy (y, x)
profile centre.
"""
return float(center_y), float(center_x)


def radius_intermediate_from(radius_major: float, q: float) -> float:
"""
Convert a major-axis radius to the COOLEST intermediate-axis radius
r = sqrt(a * b) = sqrt(q) * a of the same elliptical contour.

Parameters
----------
radius_major
The radius measured along the ellipse's major axis.
q
The axis ratio (b/a) of the ellipse.
"""
return float(radius_major) * np.sqrt(q)


def radius_major_from(radius_intermediate: float, q: float) -> float:
"""
Convert a COOLEST intermediate-axis radius r = sqrt(a * b) to the
major-axis radius of the same elliptical contour.

Parameters
----------
radius_intermediate
The intermediate-axis radius sqrt(a * b) of the elliptical contour.
q
The axis ratio (b/a) of the ellipse.
"""
return float(radius_intermediate) / np.sqrt(q)
111 changes: 111 additions & 0 deletions autogalaxy/interop/coolest/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""
Bidirectional parameter conversion between PyAutoGalaxy light profiles and the
COOLEST standard's light profiles.

The PyAutoGalaxy ``Sersic`` evaluates its image on *eccentric* radii
``sqrt(q) * sqrt(x^2 + (y/q)^2)`` (``geometry_profiles.py``), which is exactly
the COOLEST intermediate-axis radius ``sqrt(q x^2 + y^2 / q)``, and its
``intensity`` is the amplitude at ``effective_radius``. The Sersic mapping is
therefore an identity on (I_eff, theta_eff, n) — only the centre ordering and
the ellipticity / position-angle conventions change.
"""

from typing import Callable, Dict

from autogalaxy import exc
from autogalaxy.interop.coolest import conventions
from autogalaxy.profiles.light.standard.sersic import Sersic
from autogalaxy.profiles.light.standard.sersic import SersicSph


def _sersic_dict_from(profile, **kwargs) -> Dict:
q, phi = conventions.q_phi_from_ell_comps(ell_comps=profile.ell_comps)
center_x, center_y = conventions.center_from_centre(centre=profile.centre)
return {
"type": "Sersic",
"parameters": {
"I_eff": float(profile.intensity),
"theta_eff": float(profile.effective_radius),
"n": float(profile.sersic_index),
"q": q,
"phi": phi,
"center_x": center_x,
"center_y": center_y,
},
}


def _sersic_from(parameters: Dict) -> Sersic:
q = parameters["q"]
centre = conventions.centre_from_center(
center_x=parameters["center_x"], center_y=parameters["center_y"]
)
if q == 1.0:
return SersicSph(
centre=centre,
intensity=float(parameters["I_eff"]),
effective_radius=float(parameters["theta_eff"]),
sersic_index=float(parameters["n"]),
)
return Sersic(
centre=centre,
ell_comps=conventions.ell_comps_from_q_phi(q=q, phi=parameters["phi"]),
intensity=float(parameters["I_eff"]),
effective_radius=float(parameters["theta_eff"]),
sersic_index=float(parameters["n"]),
)


_TO_COOLEST: Dict[type, Callable] = {
Sersic: _sersic_dict_from,
SersicSph: _sersic_dict_from,
}

_FROM_COOLEST: Dict[str, Callable] = {
"Sersic": _sersic_from,
}


def coolest_dict_from_light(profile) -> Dict:
"""
Convert a PyAutoGalaxy light profile to its COOLEST representation, a dict
``{"type": <COOLEST profile name>, "parameters": {<name>: <float>}}`` with
all parameters in COOLEST conventions.

Parameters
----------
profile
The light profile to convert. Supported: ``Sersic`` / ``SersicSph``.
"""
try:
to_coolest = _TO_COOLEST[type(profile)]
except KeyError:
raise exc.ProfileException(
f"The light profile {type(profile).__name__} has no COOLEST "
f"converter. Supported profiles: "
f"{sorted(cls.__name__ for cls in _TO_COOLEST)}."
)
return to_coolest(profile)


def light_profile_from(profile_type: str, parameters: Dict):
"""
Build a PyAutoGalaxy light profile from a COOLEST profile type name and
its parameter dict (in COOLEST conventions).

Parameters
----------
profile_type
The COOLEST profile name, e.g. "Sersic".
parameters
The COOLEST parameters of the profile, e.g. point-estimate values read
from a COOLEST template file.
"""
try:
from_coolest = _FROM_COOLEST[profile_type]
except KeyError:
raise exc.ProfileException(
f"The COOLEST light profile '{profile_type}' has no PyAutoGalaxy "
f"converter. Supported: {sorted(_FROM_COOLEST)}."
)
return from_coolest(parameters)
Loading
Loading