Skip to content
Merged
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
4 changes: 4 additions & 0 deletions autogalaxy/galaxy/galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from autogalaxy.profiles.light.linear import LightProfileLinear
from autogalaxy.profiles.light.snr.abstract import LightProfileSNR
from autogalaxy.profiles.mass.abstract.abstract import MassProfile
from autogalaxy.profiles import validate


class Galaxy(af.ModelObject, OperateImageList):
Expand All @@ -49,6 +50,9 @@ def __init__(self, redshift: float, **kwargs):
The pixelization of the galaxy used to reconstruct an observed image using an inversion.
"""
super().__init__()

validate.validate_redshift(redshift=redshift)

self.redshift = redshift

for name, val in kwargs.items():
Expand Down
3 changes: 3 additions & 0 deletions autogalaxy/profiles/geometry_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
light and mass profiles inherit, including translating a grid to the profile centre and rotating it to the
profile's position angle.
"""

import numpy as np

from typing import Optional, Tuple, Type

import autoarray as aa

from autogalaxy import convert
from autogalaxy.profiles import validate


class GeometryProfile:
Expand Down Expand Up @@ -232,6 +234,7 @@ def __init__(
"""
super().__init__(centre=centre)

validate.validate_ell_comps(ell_comps=ell_comps)
self.ell_comps = ell_comps

def axis_ratio(self, xp=np) -> float:
Expand Down
3 changes: 3 additions & 0 deletions autogalaxy/profiles/light/standard/sersic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

This module provides both elliptical (`Sersic`) and spherical (`SersicSph`) variants.
"""

import numpy as np

from numpy import seterr
Expand All @@ -24,6 +25,7 @@
from autogalaxy.profiles.light.decorators import (
check_operated_only,
)
from autogalaxy.profiles import validate


class AbstractSersic(LightProfile):
Expand Down Expand Up @@ -54,6 +56,7 @@ def __init__(
"""
super().__init__(centre=centre, ell_comps=ell_comps, intensity=intensity)
self.effective_radius = effective_radius
validate.validate_sersic_index(sersic_index=sersic_index)
self.sersic_index = sersic_index

@property
Expand Down
2 changes: 2 additions & 0 deletions autogalaxy/profiles/mass/dark/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@


from autogalaxy import exc
from autogalaxy.profiles import validate


class DarkProfile:
Expand Down Expand Up @@ -92,6 +93,7 @@ def __init__(
super().__init__(centre=centre, ell_comps=ell_comps)

self.kappa_s = kappa_s
validate.validate_scale_radius(scale_radius=scale_radius)
self.scale_radius = scale_radius
self.inner_slope = inner_slope

Expand Down
3 changes: 3 additions & 0 deletions autogalaxy/profiles/mass/dark/cnfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from autogalaxy.profiles.mass.abstract.mge import MGEDecomposer

import autoarray as aa
from autogalaxy.profiles import validate


def F_func_from(theta, radius, xp=np):
Expand Down Expand Up @@ -126,6 +127,7 @@ def __init__(
super().__init__(centre=centre, ell_comps=ell_comps)

self.kappa_s = kappa_s
validate.validate_scale_radius(scale_radius=scale_radius)
self.scale_radius = scale_radius
self.core_radius = core_radius

Expand Down Expand Up @@ -275,6 +277,7 @@ def __init__(
super().__init__(centre=centre, ell_comps=(0.0, 0.0))

self.kappa_s = kappa_s
validate.validate_scale_radius(scale_radius=scale_radius)
self.scale_radius = scale_radius
self.core_radius = core_radius

Expand Down
2 changes: 2 additions & 0 deletions autogalaxy/profiles/mass/dark/kaplinghat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from autogalaxy.profiles.mass.dark.abstract import DarkProfile
from autogalaxy.profiles.mass.dark.nfw import NFWSph
from autogalaxy.profiles.mass.abstract.abstract import MassProfile
from autogalaxy.profiles import validate


@functools.lru_cache(maxsize=1)
Expand Down Expand Up @@ -183,6 +184,7 @@ def __init__(
super().__init__(centre=centre, ell_comps=(0.0, 0.0))

self.kappa_s = kappa_s
validate.validate_scale_radius(scale_radius=scale_radius)
self.scale_radius = scale_radius
self.sigma_over_m = sigma_over_m
self.t_age = t_age
Expand Down
2 changes: 2 additions & 0 deletions autogalaxy/profiles/mass/dark/yang24.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
_trapezoid_from,
)
from autogalaxy.profiles.mass.abstract.abstract import MassProfile
from autogalaxy.profiles import validate


def _yang24_parameter_ratios_from(tau):
Expand Down Expand Up @@ -114,6 +115,7 @@ def __init__(
super().__init__(centre=centre, ell_comps=(0.0, 0.0))

self.kappa_s = kappa_s
validate.validate_scale_radius(scale_radius=scale_radius)
self.scale_radius = scale_radius
self.tau = min(max(float(tau), 0.0), 1.0)

Expand Down
2 changes: 2 additions & 0 deletions autogalaxy/profiles/mass/stellar/sersic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
MassProfileCSE,
)
from autogalaxy.profiles.mass.stellar.abstract import StellarProfile
from autogalaxy.profiles import validate


def cse_settings_from(
Expand Down Expand Up @@ -171,6 +172,7 @@ def __init__(
self.mass_to_light_ratio = mass_to_light_ratio
self.intensity = intensity
self.effective_radius = effective_radius
validate.validate_sersic_index(sersic_index=sersic_index)
self.sersic_index = sersic_index

def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):
Expand Down
163 changes: 163 additions & 0 deletions autogalaxy/profiles/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
Profile constructor guards.

These are thin, named wrappers over the shared helpers in ``autoarray.validate``
(landed by PyAutoArray#440 for PyAutoArray#333). PyAutoArray is the floor both
PyAutoGalaxy and PyAutoLens build on, so the *rules* and the *message shape* are
defined once there and reused here — the failure mode being avoided is three repos
telling a user three different things about the same mistake.

What lives here is only the per-parameter explanation, written once per parameter
rather than once per profile class. Every message therefore reads the same way:
name the parameter, state the rule, show the received value, then explain why.

__Tracer safety__

Profile parameters are free model parameters, so under a JAX-traced fit a
constructor is handed a tracer rather than a number, and a plain Python
``if value <= 0`` would raise ``TracerBoolConversionError``. The shared helpers gate
every comparison on ``autoarray.validate.is_concrete_scalar`` and pass non-concrete
values straight through, so these guards catch hand-written mistakes and cost
nothing inside a trace.
"""

import numpy as np

from autoarray import validate


def validate_scale_radius(scale_radius, name: str = "scale_radius"):
"""
Raise if a dark-matter halo scale radius is a concrete scalar which is not finite
and positive.

A ``scale_radius`` of zero divides the grid by zero in the very first step of the
deflection-angle calculation, so every returned value is NaN rather than an error —
the profile appears to work and quietly poisons the whole fit.

Parameters
----------
scale_radius
The scale radius to validate.
name
The parameter's name, used in the error message.
"""
validate.validate_positive_finite(
value=scale_radius,
name=name,
extra=(
"The scale radius is the angular radius at which the halo's log-slope "
"changes, so it must be above zero. A value of 0.0 divides the grid by "
"zero when computing deflection angles, which returns an all-NaN result "
"instead of raising"
),
)


def validate_sersic_index(sersic_index, name: str = "sersic_index"):
"""
Raise if a Sersic index is a concrete scalar which is not finite and positive.

A ``sersic_index`` of zero reaches a division by ``n`` deep inside the profile's
``image_2d_from``, surfacing as a bare ``ZeroDivisionError`` several calls away
from the constructor that accepted it.

Parameters
----------
sersic_index
The Sersic index to validate.
name
The parameter's name, used in the error message.
"""
validate.validate_positive_finite(
value=sersic_index,
name=name,
extra=(
"The Sersic index controls the concentration of the profile and appears "
"as a divisor in its normalisation, so it must be above zero. A value of "
"0.0 raises ZeroDivisionError from inside image_2d_from rather than at "
"construction"
),
)


def validate_redshift(redshift, name: str = "redshift"):
"""
Raise if a galaxy redshift is a concrete scalar which is negative or non-finite.

A negative redshift is unphysical and produces meaningless angular diameter
distances in every multi-plane calculation that consumes it.

Zero is permitted: ``redshift=0.0`` is a legitimate way to place a galaxy at the
observer, and is used in single-plane work where the redshift is a label rather
than a cosmological quantity.

**This does not touch the ``z_lens > z_source`` question.** Multi-plane lensing
genuinely supports geometries that look wrong under two-plane naming, so that
case must warn at most, never raise, and is held pending the reporter's answer on
PyAutoLens#532.

Parameters
----------
redshift
The redshift to validate.
name
The parameter's name, used in the error message.
"""
validate.validate_non_negative_finite(
value=redshift,
name=name,
extra=(
"A redshift is a cosmological distance measure and cannot be negative — "
"a negative value yields meaningless angular diameter distances in every "
"multi-plane calculation that consumes it. Note that a lens redshift "
"above a source redshift is NOT rejected: multi-plane lensing supports "
"geometries that look inverted under two-plane naming"
),
)


def validate_ell_comps(ell_comps, name: str = "ell_comps"):
"""
Raise if the elliptical components are concrete scalars whose magnitude is not
below one.

The axis ratio is defined as ``q = (1 - f) / (1 + f)`` with
``f = sqrt(e_y**2 + e_x**2)``. That is a valid axis ratio in ``(0, 1]`` only while
``f < 1``; at ``f == 1`` the ellipse degenerates to ``q == 0``, and beyond it ``q``
goes negative and the profile has no geometric meaning. Today such a profile is
accepted and returns a finite but non-physical image.

Applied at ``EllProfile``, the single base every elliptical light and mass profile
inherits, so the rule is stated once rather than per subclass.

Parameters
----------
ell_comps
The (e_y, e_x) elliptical components to validate.
name
The parameter's name, used in the error message.
"""
if ell_comps is None:
return

try:
ell_y, ell_x = ell_comps
except (TypeError, ValueError):
return

if not validate.is_concrete_scalar(ell_y) or not validate.is_concrete_scalar(ell_x):
return

magnitude_squared = ell_y * ell_y + ell_x * ell_x

if not np.isfinite(magnitude_squared) or magnitude_squared >= 1.0:
raise ValueError(
f"{name} must satisfy {name}[0]**2 + {name}[1]**2 < 1; got "
f"{tuple(ell_comps)!r}, whose magnitude is "
f"{np.sqrt(magnitude_squared) if np.isfinite(magnitude_squared) else magnitude_squared!r}. "
f"The axis ratio is q = (1 - f) / (1 + f) with f the magnitude of "
f"{name}, so f must be below 1 for q to be a valid axis ratio — at f = 1 "
f"the ellipse degenerates to q = 0 and beyond it q is negative and the "
f"profile has no geometric meaning"
)
Loading
Loading