# Copyright (c) 2012-2023 by the GalSim developers team on GitHub
# https://github.com/GalSim-developers
#
# This file is part of GalSim: The modular galaxy image simulation toolkit.
# https://github.com/GalSim-developers/GalSim
#
# GalSim is free software: redistribution and use in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions, and the disclaimer given in the accompanying LICENSE
# file.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the disclaimer given in the documentation
# and/or other materials provided with the distribution.
#
# Define the class hierarchy for errors and warnings emitted by GalSim that aren't
# obviously one of the standard python errors.
__all__ = [ 'GalSimError', 'GalSimValueError', 'GalSimKeyError', 'GalSimIndexError',
'GalSimRangeError', 'GalSimBoundsError', 'GalSimUndefinedBoundsError',
'GalSimImmutableError', 'GalSimIncompatibleValuesError',
'GalSimSEDError', 'GalSimHSMError', 'GalSimFFTSizeError',
'GalSimConfigError', 'GalSimConfigValueError',
'GalSimNotImplementedError',
'GalSimWarning', 'GalSimDeprecationWarning',
'convert_cpp_errors', 'galsim_warn', ]
import warnings
from contextlib import contextmanager
# Note to developers about which exception to throw.
#
# Aside from the below classes, which should be preferred for most errors, we also
# throw the following in some cases.
#
# TypeError: Use this for errors that in a more strongly typed language would probably
# be a compiler error. For instance, it is used for the following errors:
# - a parameter with the wrong type
# - the wrong number of unnamed args when processing `*args` by hand.
# - missing or invalid kwargs when processing `**kwargs` by hand.
#
# OSError: Use this for errors related to I/O, disk access, etc.
#
# NotImplementedError: Use this for code that is not implemented by design and which will never
# be implemented. E.g. GSObject and Position use this for their __init__
# implementations, since it is invalid to instantiate the base class.
# Use GalSimNotImplementedError for features that might someday be
# implemented.
#
# AttributeError: Use this only for an attempt to access an attribute that an object does not
# have. Like TypeError, this should be reserved for things that a more
# strongly typed language would catch at compile time. We don't currently
# raise this anywhere in GalSim.
#
# RuntimeError: Don't use this. Use GalSimError (or a subclass) for any run-time errors.
#
# ValueError: Don't use this. Use one of the below exceptions that derive from ValueError.
#
# KeyError: Don't use this. Use GalSimKeyError instead
#
# IndexError: Don't use this. Use GalSimIndexError instead.
#
# std::runtime_error: Use this for errors in the C++ layer, and use the convert_cpp_errors()
# context to convert these errors into GalSimErrors. E.g. GSFitsWCS._invert_pv
# uses this for non-convergence, which gets converted into GalSimError in
# the Python layer.
# When possible, it is preferable to guard against any such events by making
# appropriate checks in the Python layer before dropping down into C++.
# E.g. Image checks for anything that might cause the C++ Image class to
# throw an exception and raises some kind of GalSim exception first.
# Nonetheless, it is good practice to use the `with convert_cpp_errors()`
# context for all calls to the C++ layer, just in case.
#
# GalSim-specific error classes:
# ------------------------------
#
# GalSimError: Use this for what would normally be a RuntimeError. Usually some exceptional
# occurrence in otherwise correct code. E.g. an algorithm not converging or
# a singular matrix encountered. It can also be used when the program does
# things out of order; e.g. PowerSpectrum raises this when getShear and the
# like are called before `buildGrid`. This is also the catch-all exception
# to use when none of the other GalSim exceptions are appropriate.
#
# GalSimValueError: Use this when a user provides an invalid value for a parameter.
# Note: it has an optional argument to give a list of allowed values when
# that is appropriate.
#
# GalSimKeyError Use this for accessing a dict-like object with an invalid key. E.g.
# FitsHeader and Catalog raise this for accessing invalid columns.
#
# GalSimIndexError Use this for the equivalent of accessing a list-like object with an
# invalid index. E.g. RealGalaxyCatalog and Catalog raise this for accessing
# invalid rows.
#
# GalSimRangeError: Use this when a user provides an value outside of some allowed range.
# You should also give the min/max values of the allowed range. The max
# is optional, because it's not uncommon for there to be no upper limit.
# If only the upper limit is relevant and not the lower limit, you may
# use min=None to indicate this.
#
# GalSimBoundsError: Use this when a position is outside its allowed bounds. It's basically
# the same as GalSimRangeError, but in two dimensions.
#
# GalSimUndefinedBoundsError: Use this when the user tries to perform an operation on an
# Image with undefined bounds (and which requires the bounds to be
# defined).
#
# GalSimImmutableError: Use this when the user tries to modify an immutable Image in some way.
#
# GalSimIncompatibleValuesError: Use this when two or more parameters are invalid when used
# in combination. E.g. providing more than one size parameter
# to Moffat, Sersic, Gaussian, etc. The conflicting values
# should be given as extra keywords to the constructor, which
# are mentioned in the error message.
# Note: if one of the conflicting values is self (e.g. adding two
# SEDs with different redshifts), then don't name the kwarg self.
# Instead use something like `self_sed=self`.
#
# GalSimSEDError: Use this when an SED is required to be either spectral or dimensionless,
# and the other kind of SED is provided.
#
# GalSimHSMError: Use this for errors from the HSM algorithm. They are emitted in C++, but
# we use `with convert_cpp_errors(GalSimHSMError):` to convert them.
#
# GalSimFFTSizeError: Use this when a requested FFT would exceed the relevant maximum_fft_size
# for the object, so the recommendation is raise this parameter if that
# is possible.
#
# GalSimConfigError: Use this for errors processing a config dict.
#
# GalSimConfigValueError: Use this when a config dict has a value that is invalid. Basically,
# whenever you would normally use GalSimValueError when processing
# a config dict, you should use this instead.
#
# GalSimNotImplementedError: Use this for features that we have not yet implemented, but which may
# be implemented someday. So it's not a necessarily invalid usage, just
# something that doesn't work currently.
[docs]class GalSimError(RuntimeError):
"""The base class for GalSim-specific run-time errors.
"""
# Minimal version of these to make GalSimError reprable and picklable.
def __repr__(self): return 'galsim.GalSimError(%r)'%(str(self))
def __eq__(self, other): return self is other or repr(self) == repr(other)
def __hash__(self): return hash(repr(self))
[docs]class GalSimValueError(GalSimError, ValueError):
"""A GalSim-specific exception class indicating that some user-input value is invalid.
Attributes:
value: The invalid value
allowed_values: A list of allowed values if appropriate (may be None)
"""
def __init__(self, message, value, allowed_values=None):
self.message = message
self.value = value
self.allowed_values = allowed_values
message += " Value {0!s}".format(value)
if allowed_values:
message += " not in {0!s}".format(allowed_values)
super(GalSimValueError, self).__init__(message)
def __repr__(self):
return 'galsim.GalSimValueError(%r,%r,%r)'%(self.message, self.value, self.allowed_values)
def __reduce__(self): # Need to override this whenever constructor take extra params
return GalSimValueError, (self.message, self.value, self.allowed_values)
[docs]class GalSimKeyError(GalSimError, KeyError):
"""A GalSim-specific exception class indicating an attempt to access a dict-like object
with an invalid key.
Attributes:
key: The invalid key
"""
def __init__(self, message, key):
self.message = message
self.key = key
super(GalSimKeyError, self).__init__(message, key) # Need to pass key or pickle fails.
def __str__(self):
return self.message + " Key {0!s}".format(self.key)
def __repr__(self):
return 'galsim.GalSimKeyError(%r,%r)'%(self.message, self.key)
[docs]class GalSimIndexError(GalSimError, IndexError):
"""A GalSim-specific exception class indicating an attempt to access a list-like object
with an invalid index.
Attributes:
index: The invalid index
"""
def __init__(self, message, index):
self.message = message
self.index = index
super(GalSimIndexError, self).__init__(message, index)
def __str__(self):
return self.message + " Index {0!s}".format(self.index)
def __repr__(self):
return 'galsim.GalSimIndexError(%r,%r)'%(self.message, self.index)
[docs]class GalSimRangeError(GalSimError, ValueError):
"""A GalSim-specific exception class indicating that some user-input value is
outside of the allowed range of values.
Attributes:
value: The invalid value
min: The minimum allowed value (may be None)
max: The maximum allowed value (may be None)
"""
def __init__(self, message, value, min, max=None):
self.message = message
self.value = value
self.min = min
self.max = max
message += " Value {0!s} not in range [{1!s}, {2!s}]".format(value, min, max)
super(GalSimRangeError, self).__init__(message)
def __repr__(self):
return 'galsim.GalSimRangeError(%r,%r,%r,%r)'%(self.message, self.value, self.min, self.max)
def __reduce__(self):
return GalSimRangeError, (self.message, self.value, self.min, self.max)
[docs]class GalSimBoundsError(GalSimError, ValueError):
"""A GalSim-specific exception class indicating that some user-input position is
outside of the allowed bounds.
Attributes:
pos: The invalid position
bounds: The bounds in which it was expected to fall
"""
def __init__(self, message, pos, bounds):
self.message = message
self.pos = pos
self.bounds = bounds
message += " {0!s} not in {1!s}".format(pos, bounds)
super(GalSimBoundsError, self).__init__(message)
def __repr__(self):
return 'galsim.GalSimBoundsError(%r,%r,%r)'%(self.message, self.pos, self.bounds)
def __reduce__(self):
return GalSimBoundsError, (self.message, self.pos, self.bounds)
[docs]class GalSimUndefinedBoundsError(GalSimError):
"""A GalSim-specific exception class indicating an attempt to access the extent of
a `Bounds` instance that has not yet been defined.
"""
def __repr__(self):
return 'galsim.GalSimUndefinedBoundsError(%r)'%(str(self))
[docs]class GalSimImmutableError(GalSimError):
"""A GalSim-specific exception class indicating an attempt to modify an immutable image.
Attributes:
image: The image that the user attempted to modify
"""
def __init__(self, message, image):
self.message = message
self.image = image
message += " Image: {0!s}".format(image)
super(GalSimImmutableError, self).__init__(message)
def __repr__(self):
return 'galsim.GalSimImmutableError(%r,%r)'%(self.message, self.image)
def __reduce__(self):
return GalSimImmutableError, (self.message, self.image)
[docs]class GalSimIncompatibleValuesError(GalSimError, ValueError, TypeError):
"""A GalSim-specific exception class indicating that 2 or more user-input values are
incompatible as given.
Attributes:
values: A dict of {name : value} giving the values that in combination are invalid.
"""
def __init__(self, message, values={}, **kwargs):
self.message = message
self.values = dict(values, **kwargs)
message += " Values {0!s}".format(self.values)
super(GalSimIncompatibleValuesError, self).__init__(message)
# Note: the repr of values can rearrange the items, but the dicts should compare equal.
def __eq__(self, other):
return (self is other or
(isinstance(other, GalSimIncompatibleValuesError) and
self.message == other.message and
self.values == other.values))
def __repr__(self):
return 'galsim.GalSimIncompatibleValuesError(%r,%r)'%(self.message, self.values)
def __reduce__(self):
return GalSimIncompatibleValuesError, (self.message, self.values)
[docs]class GalSimSEDError(GalSimError, TypeError):
"""A GalSim-specific exception class indicating an attempt to do something invalid for the
kind of `SED` that is present. Typically involving a dimensionless `SED` where a spectral
`SED` is required (or vice versa).
Attributes:
sed: The invalid `SED`
"""
def __init__(self, message, sed):
self.message = message
self.sed = sed
message += " SED: {0!s}".format(sed)
super(GalSimSEDError, self).__init__(message)
def __repr__(self):
return 'galsim.GalSimSEDError(%r,%r)'%(self.message, self.sed)
def __reduce__(self):
return GalSimSEDError, (self.message, self.sed)
[docs]class GalSimHSMError(GalSimError):
"""A GalSim-specific exception class indicating some kind of failure of the HSM algorithms
"""
def __repr__(self):
return 'galsim.GalSimHSMError(%r)'%(str(self))
[docs]class GalSimFFTSizeError(GalSimError):
"""A GalSim-specific exception class indicating that a requested FFT exceeds the relevant
maximum_fft_size.
Attributes:
size: The size that was deemed too large
mem: The estimated memory that would be required (in GB) for the FFT.
"""
def __init__(self, message, size):
self.message = message
self.size = size
self.mem = size * size * 24. / 1024**3
message += "\nThe required FFT size would be {0} x {0}, which requires ".format(size)
message += "{0:.2f} GB of memory.\n".format(self.mem)
message += "If you can handle the large FFT, you may update gsparams.maximum_fft_size."
super(GalSimFFTSizeError, self).__init__(message)
def __repr__(self):
return 'galsim.GalSimFFTSizeError(%r,%r)'%(self.message, self.size)
def __reduce__(self):
return GalSimFFTSizeError, (self.message, self.size)
[docs]class GalSimConfigError(GalSimError, ValueError):
"""A GalSim-specific exception class indicating some kind of failure processing a
configuration file.
"""
def __repr__(self):
return 'galsim.GalSimConfigError(%r)'%(str(self))
[docs]class GalSimConfigValueError(GalSimValueError, GalSimConfigError):
"""A GalSim-specific exception class indicating that a config entry has an invalid value.
Attributes:
value: The invalid value
allowed_values: A list of allowed values if appropriate (may be None)
"""
def __repr__(self):
return 'galsim.GalSimConfigValueError(%r,%r,%r)'%(
self.message, self.value, self.allowed_values)
def __reduce__(self):
return GalSimConfigValueError, (self.message, self.value, self.allowed_values)
[docs]class GalSimNotImplementedError(GalSimError, NotImplementedError):
"""A GalSim-specific exception class indicating that the feature being attempted is not
currently implemented.
If this is a feature you feel you need, please open an issue about it at
https://github.com/GalSim-developers/GalSim/issues
Even better, feel free to offer to contribute code to implement the feature.
"""
def __repr__(self):
return 'galsim.GalSimNotImplementedError(%r)'%(str(self))
# Note: Can use galsim_warn to raise warnings with this warning class.
[docs]class GalSimWarning(UserWarning):
"""The base class for GalSim-emitted warnings.
"""
def __repr__(self): return 'galsim.GalSimWarning(%r)'%(str(self))
def __eq__(self, other): return self is other or repr(self) == repr(other)
def __hash__(self): return hash(repr(self))
# Note: By default python ignores DeprecationWarnings. Apparently they are really
# for python system deprecations. GalSim deprecations are thus only subclassed from
# GalSimWarning, not DeprecationWarning.
[docs]class GalSimDeprecationWarning(GalSimWarning):
"""A GalSim-specific warning class used for deprecation warnings.
"""
def __repr__(self): return 'galsim.GalSimDeprecationWarning(%r)'%(str(self))
@contextmanager
def convert_cpp_errors(error_type=GalSimError):
try:
yield
except RuntimeError as err:
raise error_type(str(err)) from None
def galsim_warn(message):
"""A helper function for emitting a GalSimWarning with the given message
"""
warnings.warn(message, GalSimWarning)