diff --git a/autogalaxy/exc.py b/autogalaxy/exc.py index 086abac3..ce3f91f1 100644 --- a/autogalaxy/exc.py +++ b/autogalaxy/exc.py @@ -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. diff --git a/autogalaxy/profiles/validate.py b/autogalaxy/profiles/validate.py index cd974bc2..68a485e7 100644 --- a/autogalaxy/profiles/validate.py +++ b/autogalaxy/profiles/validate.py @@ -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"): @@ -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 " @@ -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 " @@ -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 " @@ -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}. " diff --git a/test_autogalaxy/profiles/test_validate.py b/test_autogalaxy/profiles/test_validate.py index 0017919f..7a13deec 100644 --- a/test_autogalaxy/profiles/test_validate.py +++ b/test_autogalaxy/profiles/test_validate.py @@ -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 @@ -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 # ======================================================================================