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
12 changes: 12 additions & 0 deletions autogalaxy/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ class ProfileException(Exception):
pass


class ModelParameterException(ValueError, af.exc.FitException):
"""
Raised when a concrete model parameter is outside its physical domain.

Direct profile construction remains a conventional ``ValueError`` for users,
while PyAutoFit can recognize the same failure as a ``FitException`` and reject
that candidate instead of terminating a non-linear search.
"""

pass


class GalaxyException(Exception):
"""
Raises exceptions associated with the `galaxy` module and `Galaxy` class.
Expand Down
6 changes: 5 additions & 1 deletion autogalaxy/profiles/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import numpy as np

from autoarray import validate
from autogalaxy import exc


def validate_scale_radius(scale_radius, name: str = "scale_radius"):
Expand All @@ -45,6 +46,7 @@ def validate_scale_radius(scale_radius, name: str = "scale_radius"):
validate.validate_positive_finite(
value=scale_radius,
name=name,
exc_type=exc.ModelParameterException,
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 "
Expand Down Expand Up @@ -72,6 +74,7 @@ def validate_sersic_index(sersic_index, name: str = "sersic_index"):
validate.validate_positive_finite(
value=sersic_index,
name=name,
exc_type=exc.ModelParameterException,
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 "
Expand Down Expand Up @@ -107,6 +110,7 @@ def validate_redshift(redshift, name: str = "redshift"):
validate.validate_non_negative_finite(
value=redshift,
name=name,
exc_type=exc.ModelParameterException,
extra=(
"A redshift is a cosmological distance measure and cannot be negative — "
"a negative value yields meaningless angular diameter distances in every "
Expand Down Expand Up @@ -152,7 +156,7 @@ def validate_ell_comps(ell_comps, name: str = "ell_comps"):
magnitude_squared = ell_y * ell_y + ell_x * ell_x

if not np.isfinite(magnitude_squared) or magnitude_squared >= 1.0:
raise ValueError(
raise exc.ModelParameterException(
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}. "
Expand Down
46 changes: 46 additions & 0 deletions test_autogalaxy/profiles/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import numpy as np
import pytest

import autofit as af
import autogalaxy as ag
from autofit.non_linear.fitness import Fitness
from autogalaxy.profiles import validate


Expand Down Expand Up @@ -46,6 +48,50 @@ def __add__(self, other):
return self


@pytest.mark.parametrize(
"constructor",
[
lambda: ag.mp.NFW(scale_radius=0.0),
lambda: ag.lp.Sersic(sersic_index=0.0),
lambda: ag.lp.Sersic(ell_comps=(1.0, 1.0)),
lambda: ag.Galaxy(redshift=-1.0),
],
)
def test__invalid_model_parameters_are_value_errors_and_fit_exceptions(constructor):
"""
The same narrow error contract serves direct API calls and model fitting:
users see ``ValueError`` while PyAutoFit treats the candidate as resampleable.
"""
with pytest.raises(ag.exc.ModelParameterException) as error:
constructor()

assert isinstance(error.value, ValueError)
assert isinstance(error.value, af.exc.FitException)


def test__invalid_sampled_profile_is_returned_as_a_resample_figure_of_merit():
"""Exercise the PyAutoFit boundary that non-linear searches call."""

class Model:
@staticmethod
def instance_from_vector(vector, xp):
return ag.lp.Sersic(sersic_index=vector[0])

class Analysis(af.Analysis):
@staticmethod
def log_likelihood_function(instance):
return 1.0

fitness = Fitness(
model=Model(),
analysis=Analysis(),
resample_figure_of_merit=-1.0e99,
)

assert fitness.call([0.0]) == fitness.resample_figure_of_merit
assert fitness.call([1.0]) == 1.0


# ======================================================================================
# B9 — scale_radius must be finite and positive
# ======================================================================================
Expand Down
Loading