diff --git a/.codecov.yml b/.codecov.yml index ebcd2fc3b13..efcb6a70ea7 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,14 +9,14 @@ coverage: status: project: default: - target: 95% + target: 100% threshold: 0% branches: null patch: default: target: 100% - threshold: 1% + threshold: 0% branches: null changes: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 67501b16bdf..c05ac480858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,26 +3,38 @@ Changes from v1.6 to v2.0 The principal change in GalSim 2.0 is that it is now pip installable. See the updated INSTALL file for details on how to install GalSim using -either pip or setup.py. +either pip or setup.py. The functionality is essentially equivalent to +v1.6, although there are a few (mostly minor) API changes in some classes. Dependency Changes ------------------ +- Officially no longer support Python 2.6. (#755) +- No longer support pre-astropy versions of pyfits (now bundled in astropy + as astropy.io.fits). Nor astropy versions <1.0. (#755) +- No longer support pre-2016 version of the COSMOS catalog. You may be + asked to run galsim_download_cosmos again if your version is found to + be obsolete. (#755) - Added LSSTDESC.Coord, which contains the functionality that used to be in GalSim as the Angle and CelestialCoord classes. We moved it to a separate repo so people could more easily use this functionality without requiring all of GalSim as a dependency. (#809b) - Removed dependency on boost. (#809) - Removed dependency on TMV. (#809) -- Added dependency on pybind11. (#809) -- Added dependency on Eigen. (#809) +- Added dependency on pybind11. (You can still use boost if you want using + the SCons installation method.) (#809) +- Added dependency on Eigen. (You can still use TMV if you want using the + SCons installation method.) (#809) - FFTW is now the only dependency that pip cannot handle automatically. (#809) -- Officially no longer support Python 2.6. (Pretty sure no one cares.) API Changes ----------- +- Changed the default maximum_fft_size in GSParams to 8192 from 4096. This + increases the potential memory used by an FFT when drawing an object with + an FFT from 256 MB to 1 GB. (#755) +- Changed the order of arguments of galsim.wfirst.allDetectorEffects. (#755) - Most of the functionality associated with C++-layer objects has been redesigned or removed. These were non-public-API features, so if you have been using the public API, you should be fine. But if you have been relying @@ -55,6 +67,10 @@ API Changes Bug Fixes --------- +======= +- Removed the lsst module, which depended on the LSST stack and had gotten + quite out of sync and broken. (#964) +>>>>>>> Change default maximum_fft_size to 8192 Deprecated Features @@ -66,18 +82,13 @@ Deprecated Features New Features ------------ -- Added Zernike submodule. (#832, #951) -- Updated PhaseScreen wavefront and wavefront_gradient methods to accept `None` - as a valid time argument, which means to use the internally stored time in - the screen(s). (#864) -- Added SecondKick profile GSObject. (#864) -- Updated PhaseScreenPSFs to automatically include SecondKick objects when - being drawn with geometric photon shooting. (#864) -- Added option to use circular weight function in HSM adaptive moments code. - (#917) -- Added VonKarman profile GSObject. (#940) -- Added PhotonDCR surface op to apply DCR for photon shooting. (#955) -- Added astropy units as allowed values of wave_type in Bandpass. (#955) -- Added ability to get net pixel areas from the Silicon code for a given flux - image. (#963) -- Added ability to transpose the meaning of (x,y) in the Silicon class. (#963) +- Added a new class hierarchy for exceptions raised by GalSim with the base + class `GalSimError`, a subclass of `RuntimeError`. For complete details + about the various sub-classes within this hierarchy, see the file errors.py. + In most cases, if you were catching a specific exception such as ValueError + or RuntimeError, the new error will still be caught properly. However, some + cases have changed to an incompatible error type, so users who have written + `except` statements with specific error types should be careful to make + sure that the errors you wanted to catch are still being caught. (#755) +- Changed the type of warnings raised by GalSim to GalSimWarning, which is + a subclass of UserWarning. (#755) diff --git a/SConstruct b/SConstruct index f3945ab4644..d7970e28786 100644 --- a/SConstruct +++ b/SConstruct @@ -1328,8 +1328,8 @@ def GetPythonVersion(config): # there: if not result: py_version = '' - for v in ['2.7', '2,6', '3.4', '3.5', # supported versions first - '2.5', '2,4', '3.3', '3.2', '3.1', '3.0']: # these are mostly to give accurate logging and error messages + for v in ['2.7', '3.4', '3.5', '3.6', # supported versions first + '2.6', '2.5', '2,4', '3.3', '3.2', '3.1', '3.0']: # these are mostly to give accurate logging and error messages if v in py_inc or v in python: py_version = v break @@ -1601,6 +1601,9 @@ PyMODINIT_FUNC initcheck_tmv(void) def CheckEigen(config): eigen_source_file = """ +#if defined(__GNUC__) && __GNUC__ >= 6 +#pragma GCC diagnostic ignored "-Wint-in-bool-context" +#endif #include "Python.h" #include "Eigen/Core" #include "Eigen/Cholesky" @@ -1763,24 +1766,16 @@ PyMODINIT_FUNC initcheck_numpy(void) return 1 def CheckPyFITS(config): - config.Message('Checking for PyFITS... ') + config.Message('Checking for astropy.io.fits... ') - result, output = TryScript(config,"import pyfits",python) - astropy = False - if not result: - result, output = TryScript(config,"import astropy.io.fits",python) - astropy = True + result, output = TryScript(config,"import astropy.io.fits",python) if not result: - ErrorExit("Unable to import pyfits or astropy.io.fits using the python executable:\n" + + ErrorExit("Unable to import astropy.io.fits using the python executable:\n" + python) config.Result(1) - if astropy: - result, astropy_ver = TryScript(config,"import astropy; print(astropy.__version__)",python) - print('Astropy version is',astropy_ver) - else: - result, pyfits_ver = TryScript(config,"import pyfits; print(pyfits.__version__)",python) - print('PyFITS version is',pyfits_ver) + result, astropy_ver = TryScript(config,"import astropy; print(astropy.__version__)",python) + print('Astropy version is',astropy_ver) return 1 diff --git a/examples/check_diff.py b/examples/check_diff.py index b1e13dbad29..4f4cc3f3465 100644 --- a/examples/check_diff.py +++ b/examples/check_diff.py @@ -54,7 +54,7 @@ def report(file_name1, file_name2): try: f1 = pyfits.open(file_name1) f2 = pyfits.open(file_name2) - except IOError as e: + except (IOError, OSError) as e: # Then either at least one of the files doesn't exist, which diff can report for us, # or the files are txt files, which diff can also do. return report_txt(file_name1, file_name2) diff --git a/galsim/__init__.py b/galsim/__init__.py index 84af8749cf7..47fcdc7c445 100644 --- a/galsim/__init__.py +++ b/galsim/__init__.py @@ -101,6 +101,15 @@ from .scene import COSMOSCatalog from .table import LookupTable, LookupTable2D +# Exception and Warning classes +from .errors import GalSimError, GalSimRangeError, GalSimValueError +from .errors import GalSimKeyError, GalSimIndexError, GalSimNotImplementedError +from .errors import GalSimBoundsError, GalSimUndefinedBoundsError, GalSimImmutableError +from .errors import GalSimIncompatibleValuesError, GalSimSEDError, GalSimHSMError +from .errors import GalSimFFTSizeError +from .errors import GalSimConfigError, GalSimConfigValueError +from .errors import GalSimWarning, GalSimDeprecationWarning + # Image from .image import Image, ImageS, ImageI, ImageF, ImageD, ImageCF, ImageCD, ImageUS, ImageUI, _Image @@ -168,8 +177,8 @@ from .sensor import Sensor, SiliconSensor from . import detectors # Everything here is a method of Image, so nothing to import by name. -# Deprecation warning class -from .deprecated import GalSimDeprecationWarning +# Deprecated functionality +from . import deprecated # Packages we intentionally keep separate. E.g. requires galsim.fits.read(...) from . import fits diff --git a/galsim/__main__.py b/galsim/__main__.py index 9160b25fa02..85b8d1361fa 100644 --- a/galsim/__main__.py +++ b/galsim/__main__.py @@ -17,4 +17,5 @@ # from .main import main + main() diff --git a/galsim/_pyfits.py b/galsim/_pyfits.py index 2c73d9fd313..45750c24c42 100644 --- a/galsim/_pyfits.py +++ b/galsim/_pyfits.py @@ -16,18 +16,9 @@ # and/or other materials provided with the distribution. # -# Make it so we can use either pyfits or astropy.io.fits as pyfits. +# We used to support legacy pyfits in addition to astropy.io.fits. We still call +# astropy.io.fits pyfits in the code, but we have removed the legacy compatibility hacks. -try: - import astropy.io.fits as pyfits - # astropy started their versioning over at 0. (Understandably.) - # To make this seamless with pyfits versions, we add 4 to the astropy version. - from astropy import version as astropy_version - pyfits_version = str( (4 + astropy_version.major) + astropy_version.minor/10.) - pyfits_str = 'astropy.io.fits' -except ImportError: - import pyfits - pyfits_version = pyfits.__version__ - pyfits_str = 'pyfits' +import astropy.io.fits as pyfits diff --git a/galsim/airy.py b/galsim/airy.py index 874c95eff48..45785720c29 100644 --- a/galsim/airy.py +++ b/galsim/airy.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import GalSimIncompatibleValuesError, GalSimNotImplementedError, convert_cpp_errors class Airy(GSObject): @@ -127,11 +128,15 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, obscuration=0., flux # docstring. if lam_over_diam is not None: if lam is not None or diam is not None: - raise TypeError("If specifying lam_over_diam, then do not specify lam or diam") + raise GalSimIncompatibleValuesError( + "If specifying lam_over_diam, then do not specify lam or diam", + lam_over_diam=lam_over_diam, lam=lam, diam=diam) self._lod = float(lam_over_diam) else: if lam is None or diam is None: - raise TypeError("If not specifying lam_over_diam, then specify lam AND diam") + raise GalSimIncompatibleValuesError( + "If not specifying lam_over_diam, then specify lam AND diam", + lam_over_diam=lam_over_diam, lam=lam, diam=diam) # In this case we're going to use scale_unit, so parse it in case of string input: if isinstance(scale_unit, str): scale_unit = AngleUnit.from_name(scale_unit) @@ -141,7 +146,8 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, obscuration=0., flux @lazy_property def _sbp(self): - return _galsim.SBAiry(self._lod, self._obscuration, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBAiry(self._lod, self._obscuration, self._flux, self.gsparams._gsp) @property def lam_over_diam(self): return self._lod @@ -158,8 +164,9 @@ def half_light_radius(self): else: # In principle can find the half light radius as a function of lam_over_diam and # obscuration too, but it will be much more involved...! - raise NotImplementedError("Half light radius calculation not implemented for Airy "+ - "objects with non-zero obscuration.") + raise GalSimNotImplementedError( + "Half light radius calculation not implemented for Airy " + "objects with non-zero obscuration.") @property def fwhm(self): @@ -171,8 +178,9 @@ def fwhm(self): else: # In principle can find the FWHM as a function of lam_over_diam and obscuration too, # but it will be much more involved...! - raise NotImplementedError("FWHM calculation not implemented for Airy "+ - "objects with non-zero obscuration.") + raise GalSimNotImplementedError( + "FWHM calculation not implemented for Airy " + "objects with non-zero obscuration.") def __eq__(self, other): return (isinstance(other, Airy) and diff --git a/galsim/bandpass.py b/galsim/bandpass.py index 6f6721b6c9b..f3a7d4dfea1 100644 --- a/galsim/bandpass.py +++ b/galsim/bandpass.py @@ -29,6 +29,7 @@ from . import integ from . import meta_data from .utilities import WeakMethod, combine_wave_list +from .errors import GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError class Bandpass(object): """Simple bandpass object, which models the transmission fraction of incident light as a @@ -111,21 +112,22 @@ def __init__(self, throughput, wave_type, blue_limit=None, red_limit=None, # it can be supplied directly as a constructor argument. if blue_limit is not None and red_limit is not None and blue_limit >= red_limit: - raise ValueError("blue_limit must be less than red_limit") + raise GalSimRangeError("blue_limit must be less than red_limit", + blue_limit, None, red_limit) self.blue_limit = blue_limit # These may change as we go through this. self.red_limit = red_limit self.zeropoint = zeropoint # Parse the various options for wave_type if isinstance(wave_type, str): - if wave_type.lower() in ['nm', 'nanometer', 'nanometers']: + if wave_type.lower() in ('nm', 'nanometer', 'nanometers'): self.wave_type = 'nm' self.wave_factor = 1. - elif wave_type.lower() in ['a', 'ang', 'angstrom', 'angstroms']: + elif wave_type.lower() in ('a', 'ang', 'angstrom', 'angstroms'): self.wave_type = 'Angstrom' self.wave_factor = 10. else: - raise ValueError("Unknown wave_type '{0}'".format(wave_type)) + raise GalSimValueError("Invalid wave_type.", wave_type, ('nm', 'Angstrom')) else: self.wave_type = wave_type try: @@ -137,7 +139,7 @@ def __init__(self, throughput, wave_type, blue_limit=None, red_limit=None, self.wave_factor = 10. except units.UnitConversionError: # Unlike in SED, we require a distance unit for wave_type - raise ValueError("Unknown wave_type '{0}'".format(wave_type)) + raise GalSimValueError("Invalid wave_type. Must be a distance.", wave_type) # Convert string input into a real function (possibly a LookupTable) self._initialize_tp() @@ -167,19 +169,20 @@ def __init__(self, throughput, wave_type, blue_limit=None, red_limit=None, self.red_limit = float(self._tp.x_max)/self.wave_factor else: if self.blue_limit is None or self.red_limit is None: - raise TypeError( - "red_limit and blue_limit are required if throughput is not a LookupTable.") + raise GalSimIncompatibleValuesError( + "red_limit and blue_limit are required if throughput is not a LookupTable.", + blue_limit=blue_limit, red_limit=red_limit, throughput=throughput) # Sanity check blue/red limit and create self.wave_list if isinstance(self._tp, LookupTable): self.wave_list = np.array(self._tp.getArgs())/self.wave_factor # Make sure that blue_limit and red_limit are within LookupTable region of support. if self.blue_limit < (self._tp.x_min/self.wave_factor): - raise ValueError("Cannot set blue_limit to be less than throughput " - + "LookupTable.x_min") + raise GalSimRangeError("Cannot set blue_limit to be less than throughput x_min", + self.blue_limit, self._tp.x_min, self._tp.x_max) if self.red_limit > (self._tp.x_max/self.wave_factor): - raise ValueError("Cannot set red_limit to be greater than throughput " - + "LookupTable.x_max") + raise GalSimRangeError("Cannot set red_limit to be greater than throughput x_max", + self.red_limit, self._tp.x_min, self._tp.x_max) # Remove any values that are outside the limits self.wave_list = self.wave_list[np.logical_and(self.wave_list >= self.blue_limit, self.wave_list <= self.red_limit) ] @@ -217,23 +220,24 @@ def _initialize_tp(self): self._tp = LookupTable.from_file(filename, interpolant='linear') else: if self.blue_limit is None or self.red_limit is None: - raise TypeError( - "red_limit and blue_limit are required if throughput is not a LookupTable.") + raise GalSimIncompatibleValuesError( + "red_limit and blue_limit are required if throughput is not a LookupTable.", + blue_limit=None, red_limit=None, throughput=self._orig_tp) test_wave = self.blue_limit try: self._tp = utilities.math_eval('lambda wave : ' + self._orig_tp) test_value = self._tp(test_wave) except Exception as e: - raise ValueError( - "String throughput must either be a valid filename or something that "+ - "can eval to a function of wave.\n" + - "Input provided: {0!r}\n".format(self._orig_tp) + - "Caught error: {0}".format(e)) + raise GalSimValueError( + "String throughput must either be a valid filename or something that " + "can eval to a function of wave.\n Caught error: %s."%(e), + self._orig_tp) from numbers import Real if not isinstance(test_value, Real): - raise ValueError("The given throughput function, %r, did not return a valid" - " number at test wavelength %s: got %s"%( - self._orig_tp, test_wave, test_value)) + raise GalSimValueError( + "The given throughput function did not return a valid " + "number at test wavelength %s: got %s."%(test_wave, test_value), + self._orig_tp) else: self._tp = self._orig_tp @@ -312,8 +316,7 @@ def __div__(self, other): return Bandpass(tp, wave_type, self.blue_limit, self.red_limit, _wave_list=self.wave_list) - def __truediv__(self, other): - return self.__div__(other) + __truediv__ = __div__ def __call__(self, wave): """ Return dimensionless throughput of bandpass at given wavelength in nanometers. @@ -398,7 +401,8 @@ def withZeropoint(self, zeropoint): vegafile = os.path.join(meta_data.share_dir, "SEDs", "vega.txt") sed = SED(vegafile, wave_type='nm', flux_type='flambda') else: - raise ValueError("Do not recognize Zeropoint string {0}.".format(zeropoint)) + raise GalSimValueError("Unrecognized Zeropoint string.", zeropoint, + ('AB', 'ST', 'VEGA')) zeropoint = sed # Convert `zeropoint` from galsim.SED to float @@ -462,27 +466,30 @@ def truncate(self, blue_limit=None, red_limit=None, relative_throughput=None, # Enforce the choice of a single mode of truncation. if relative_throughput is not None: if blue_limit is not None or red_limit is not None: - raise ValueError("Truncate using relative_throughput or red/blue_limit, not both!") + raise GalSimIncompatibleValuesError( + "Truncate using relative_throughput or red/blue_limit, not both!", + blue_limit=blue_limit, red_limit=red_limit, + relative_throughput=relative_throughput) if preserve_zp == 'auto': if relative_throughput is not None: preserve_zp = True else: preserve_zp = False # Check for weird input if preserve_zp is not True and preserve_zp is not False: - raise ValueError("Unrecognized input for preserve_zp: %s"%preserve_zp) + raise GalSimValueError("Unrecognized input for preserve_zp.",preserve_zp) if blue_limit is None: blue_limit = self.blue_limit else: if blue_limit < self.blue_limit: - raise ValueError("Supplied blue_limit (%f) is bluer than the original (%f)!"% - (blue_limit, self.blue_limit)) + raise GalSimRangeError("Supplied blue_limit may not be bluer than the original.", + blue_limit, self.blue_limit, self.red_limit) if red_limit is None: red_limit = self.red_limit else: if red_limit > self.red_limit: - raise ValueError("Supplied red_limit (%f) is redder than the original (%f)!"% - (red_limit, self.red_limit)) + raise GalSimRangeError("Supplied red_limit may not be redder than the original.", + red_limit, self.blue_limit, self.red_limit) wave_list = self.wave_list if len(self.wave_list) > 0: @@ -495,9 +502,9 @@ def truncate(self, blue_limit=None, red_limit=None, relative_throughput=None, wave_list = wave_list[np.logical_and(wave_list >= blue_limit, wave_list <= red_limit) ] elif relative_throughput is not None: - raise ValueError( + raise GalSimIncompatibleValuesError( "Can only truncate with relative_throughput argument if throughput is " - + "a LookupTable") + "a LookupTable", relative_throughput=relative_throughput, throughput=self._orig_tp) if preserve_zp: return Bandpass(self._orig_tp, self.wave_type, blue_limit, red_limit, @@ -591,7 +598,7 @@ def __hash__(self): return self._hash def __repr__(self): - return ('galsim.Bandpass(%r, wave_type=%r, blue_limit=%r, red_limit=%r, zeropoint=%r, '+ + return ('galsim.Bandpass(%r, wave_type=%r, blue_limit=%r, red_limit=%r, zeropoint=%r, ' '_wave_list=array(%r))')%( self._orig_tp, self.wave_type, self.blue_limit, self.red_limit, self.zeropoint, self.wave_list.tolist()) diff --git a/galsim/bounds.py b/galsim/bounds.py index 0c3c3e49592..ff06d21c015 100644 --- a/galsim/bounds.py +++ b/galsim/bounds.py @@ -20,8 +20,10 @@ """ import math + from . import _galsim from .position import Position, PositionI, PositionD +from .errors import GalSimUndefinedBoundsError class Bounds(object): """A class for representing image bounds as 2D rectangles. @@ -92,7 +94,7 @@ class Bounds(object): information. """ def __init__(self): - raise NotImplementedError("Cannot instantiate the base class. " + + raise NotImplementedError("Cannot instantiate the base class. " "Use either BoundsD or BoundsI.") def _parse_args(self, *args, **kwargs): @@ -127,14 +129,14 @@ def _parse_args(self, *args, **kwargs): self.ymin = min(args[0].y, args[1].y) self.ymax = max(args[0].y, args[1].y) else: - raise TypeError("Two arguments to %s must be either Positions"%( + raise TypeError("Two arguments to %s must be Positions"%( self.__class__.__name__)) else: raise TypeError("%s takes either 1, 2, or 4 arguments (%d given)"%( self.__class__.__name__,len(args))) elif len(args) != 0: - raise TypeError("Cannot provide both keywork and non-keyword arguments to %s"%( - self.__class__.__name__)) + raise TypeError("Cannot provide both keyword and non-keyword arguments to %s"%( + self.__class__.__name__)) else: try: self._isdefined = True @@ -144,7 +146,7 @@ def _parse_args(self, *args, **kwargs): self.ymax = kwargs.pop('ymax') except KeyError: raise TypeError("Keyword arguments, xmin, xmax, ymin, ymax are required for %s"%( - self.__class__.__name__)) + self.__class__.__name__)) if kwargs: raise TypeError("Got unexpected keyword arguments %s"%kwargs.keys()) @@ -188,7 +190,7 @@ def center(self): For a BoundsD, this is equivalent to true_center. """ if not self.isDefined(): - raise ValueError("center is invalid for an undefined Bounds") + raise GalSimUndefinedBoundsError("center is invalid for an undefined Bounds") return self._center @property @@ -199,7 +201,7 @@ def true_center(self): this may not necessarily be an integer PositionI. """ if not self.isDefined(): - raise ValueError("true_center is invalid for an undefined Bounds") + raise GalSimUndefinedBoundsError("true_center is invalid for an undefined Bounds") return PositionD((self.xmax + self.xmin)/2., (self.ymax + self.ymin)/2.) def includes(self, *args): @@ -330,7 +332,7 @@ def __add__(self, other): return self.__class__(other) else: raise TypeError("other must be either a %s or a %s"%( - self.__class__.__name__,self._pos_class.__name__)) + self.__class__.__name__,self._pos_class.__name__)) def __repr__(self): if self.isDefined(): @@ -371,9 +373,6 @@ class BoundsD(Bounds): def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) - if (self.xmin != float(self.xmin) or self.xmax != float(self.xmax) or - self.ymin != float(self.ymin) or self.ymax != float(self.ymax)): - raise ValueError("BoundsD must be initialized with float values") self.xmin = float(self.xmin) self.xmax = float(self.xmax) self.ymin = float(self.ymin) @@ -391,7 +390,7 @@ def _check_scalar(self, x, name): if x == float(x): return except (TypeError, ValueError): pass - raise ValueError("%s must be a float value"%name) + raise TypeError("%s must be a float value"%name) def _area(self): return (self.xmax - self.xmin) * (self.ymax - self.ymin) @@ -414,7 +413,7 @@ def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) if (self.xmin != int(self.xmin) or self.xmax != int(self.xmax) or self.ymin != int(self.ymin) or self.ymax != int(self.ymax)): - raise ValueError("BoundsI must be initialized with integer values") + raise TypeError("BoundsI must be initialized with integer values") # Now make sure they are all ints self.xmin = int(self.xmin) self.xmax = int(self.xmax) @@ -430,7 +429,7 @@ def _check_scalar(self, x, name): if x == int(x): return except (TypeError, ValueError): pass - raise ValueError("%s must be a integer value"%name) + raise TypeError("%s must be an integer value"%name) def numpyShape(self): "A simple utility function to get the numpy shape that corresponds to this Bounds object." diff --git a/galsim/box.py b/galsim/box.py index 8ec8ec147b5..13c60cf8666 100644 --- a/galsim/box.py +++ b/galsim/box.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import convert_cpp_errors class Box(GSObject): @@ -67,7 +68,8 @@ def __init__(self, width, height, flux=1., gsparams=None): @lazy_property def _sbp(self): - return _galsim.SBBox(self._width, self._height, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBBox(self._width, self._height, self._flux, self.gsparams._gsp) @property def width(self): return self._width @@ -225,7 +227,8 @@ def __init__(self, radius, flux=1., gsparams=None): @lazy_property def _sbp(self): - return _galsim.SBTopHat(self._radius, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBTopHat(self._radius, self._flux, self.gsparams._gsp) @property def radius(self): return self._radius diff --git a/galsim/catalog.py b/galsim/catalog.py index 2ed49538279..fdf7291b29f 100644 --- a/galsim/catalog.py +++ b/galsim/catalog.py @@ -24,6 +24,8 @@ import os import numpy as np +from .errors import GalSimValueError, GalSimKeyError, GalSimIndexError + class Catalog(object): """A class storing the data from an input catalog. @@ -78,54 +80,52 @@ def __init__(self, file_name, dir=None, file_type=None, comments='#', hdu=1, else: file_type = 'ASCII' file_type = file_type.upper() - if file_type not in ['FITS', 'ASCII']: - raise ValueError("file_type must be either FITS or ASCII if specified.") + if file_type not in ('FITS', 'ASCII'): + raise GalSimValueError("file_type must be either FITS or ASCII if specified.", + file_type, ('FITS', 'ASCII')) self.file_type = file_type + if comments == '': comments = None # loadtxt actually wants None, not '' self.comments = comments self.hdu = hdu if file_type == 'FITS': self.readFits(hdu, _nobjects_only) - elif file_type == 'ASCII': + else: # file_type == 'ASCII': self.readAscii(comments, _nobjects_only) - else: - raise ValueError("Invalid file_type %s"%file_type) # When we make a proxy of this class (cf. galsim/config/stamp.py), the attributes # don't get proxied. Only callable methods are. So make method versions of these. def getNObjects(self) : return self.nobjects def isFits(self) : return self.isfits + def __len__(self) : return self.nobjects def readAscii(self, comments, _nobjects_only=False): """Read in an input catalog from an ASCII file. """ + if comments is not None and len(comments) > 1: + raise GalSimValueError('Invalid comments character', comments) + # If all we care about is nobjects, this is quicker: if _nobjects_only: # See the script devel/testlinecounting.py that tests several possibilities. # An even faster version using buffering is possible although it requires some care # around edge cases, so we use this one instead, which is "correct by inspection". with open(self.file_name) as f: - if (len(comments) == 1): + if comments is not None: c = comments[0] self.nobjects = sum(1 for line in f if line[0] != c) - else: - self.nobjects = sum(1 for line in f if not line.startswith(comments)) + else: # comments == None. No comments. + self.nobjects = sum(1 for line in f) return # Read in the data using the numpy convenience function # Note: we leave the data as str, rather than convert to float, so that if # we have any str fields, they don't give an error here. They'll only give an # error if one tries to convert them to float at some point. - self.data = np.loadtxt(self.file_name, comments=comments, dtype=bytes) + self.data = np.loadtxt(self.file_name, comments=comments, dtype=bytes, ndmin=2) # Convert the bytes to str. For Py2, this is a no op. self.data = self.data.astype(str) - # If only one row, then the shape comes in as one-d. - if len(self.data.shape) == 1: - self.data = self.data.reshape(1, -1) - if len(self.data.shape) != 2: - raise IOError('Unable to parse the input catalog as a 2-d array') - self.nobjects = self.data.shape[0] self.ncols = self.data.shape[1] self.isfits = False @@ -133,21 +133,12 @@ def readAscii(self, comments, _nobjects_only=False): def readFits(self, hdu, _nobjects_only=False): """Read in an input catalog from a FITS file. """ - from ._pyfits import pyfits, pyfits_version + from ._pyfits import pyfits with pyfits.open(self.file_name) as fits: - raw_data = fits[hdu].data - if pyfits_version > '3.0': - self.names = raw_data.columns.names - else: - self.names = raw_data.dtype.names - self.nobjects = len(raw_data.field(self.names[0])) + self.data = fits[hdu].data.copy() + self.names = self.data.columns.names + self.nobjects = len(self.data) if (_nobjects_only): return - # The pyfits raw_data is a FITS_rec object, which isn't picklable, so we need to - # copy the fields into a new structure to make sure our Catalog is picklable. - # The simplest is probably a dict keyed by the field names, which we save as self.data. - self.data = {} - for name in self.names: - self.data[name] = raw_data.field(name) self.ncols = len(self.names) self.isfits = True @@ -162,19 +153,23 @@ def get(self, index, col): """ if self.isfits: if col not in self.names: - raise KeyError("Column %s is invalid for catalog %s"%(col,self.file_name)) + raise GalSimKeyError("Column is invalid for catalog %s"%self.file_name, col) + if not isinstance(index, int): + raise GalSimIndexError("Index must be an int for catalog %s"%self.file_name, index) if index < 0 or index >= self.nobjects: - raise IndexError("Object %d is invalid for catalog %s"%(index,self.file_name)) - if index >= len(self.data[col]): - raise IndexError("Object %d is invalid for column %s"%(index,col)) + raise GalSimIndexError("Index is invalid for catalog %s"%self.file_name, index) return self.data[col][index] else: - icol = int(col) - if icol < 0 or icol >= self.ncols: - raise IndexError("Column %d is invalid for catalog %s"%(icol,self.file_name)) + if not isinstance(col, int): + raise GalSimIndexError("Column must an int for ASCII catalog %s"%self.file_name, + col) + if col < 0 or col >= self.ncols: + raise GalSimIndexError("Column is invalid for catalog %s"%self.file_name, col) + if not isinstance(index, int): + raise GalSimIndexError("Index must be an int for catalog %s"%self.file_name, index) if index < 0 or index >= self.nobjects: - raise IndexError("Object %d is invalid for catalog %s"%(index,self.file_name)) - return self.data[index, icol] + raise GalSimIndexError("Index is invalid for catalog %s"%self.file_name, col) + return self.data[index, col] def getFloat(self, index, col): """Return the data for the given `index` and `col` as a float if possible @@ -188,10 +183,8 @@ def getInt(self, index, col): def __repr__(self): s = "galsim.Catalog(file_name=%r, file_type=%r"%(self.file_name, self.file_type) - if self.comments != '#': - s += ', comments=%r'%self.comments - if self.hdu != 1: - s += ', hdu=%r'%self.hdu + if self.comments != '#': s += ', comments=%r'%self.comments + if self.hdu != 1: s += ', hdu=%r'%self.hdu s += ')' return s @@ -263,11 +256,12 @@ def __init__(self, file_name, dir=None, file_type=None, key_split='.'): elif ext.lower().startswith('.j'): file_type = 'JSON' else: - raise ValueError('Unable to determine file_type from file_name ending') + raise GalSimValueError('Unable to determine file_type from file_name ending', + file_name, ('*.p*', '*.y*', '*.j*')) file_type = file_type.upper() - if file_type not in ['PICKLE','YAML','JSON']: - raise ValueError("file_type must be one of Pickle, YAML, or JSON if specified.") + if file_type not in ('PICKLE','YAML','JSON'): + raise GalSimValueError("Invalid file_type", file_type, ('Pickle', 'YAML', 'JSON')) self.file_type = file_type self.key_split = key_split @@ -283,12 +277,10 @@ def __init__(self, file_name, dir=None, file_type=None, key_split='.'): import yaml with open(self.file_name, 'r') as f: self.dict = yaml.load(f) - elif file_type == 'JSON': + else: # JSON import json with open(self.file_name, 'r') as f: self.dict = json.load(f) - else: - raise ValueError("Invalid file_type %s"%file_type) def get(self, key, default=None): # Make a list of keys according to our key_split parameter @@ -306,10 +298,9 @@ def get(self, key, default=None): # Otherwise, return the result. else: if k not in d and default is None: - raise ValueError("key=%s not found in dictionary"%key) + raise GalSimKeyError("key not found in dictionary.",key) return d.get(k,default) - - raise ValueError("Invalid key=%s given to Dict.get()"%key) + raise GalSimKeyError("Invalid key given to Dict.get()",key) # The rest of the functions are typical non-mutating functions for a dict, for which we just # pass the request along to self.dict. @@ -323,7 +314,7 @@ def __contains__(self, key): return key in self.dict def __iter__(self): - return self.dict.__iter__ + return self.dict.__iter__() def keys(self): return self.dict.keys() @@ -403,6 +394,7 @@ def __init__(self, names, types=None, _rows=(), _sort_keys=()): def nobjects(self): return len(self.rows) @property def ncols(self): return len(self.names) + def __len__(self): return self.nobjects # Again, when we use this through a proxy, we need getters for the attributes. def getNames(self): return self.names @@ -423,7 +415,8 @@ def addRow(self, row, sort_key=None): which will be used at the end to re-sort the rows. """ if len(row) != self.ncols: - raise ValueError("Length of row does not match the number of columns") + raise GalSimValueError("Length of row does not match the number of columns = %d"%( + self.ncols), len(row)) self.rows.append(tuple(row)) if sort_key is None: self.sort_keys.append(self.nobjects) @@ -453,15 +446,13 @@ def write(self, file_name, dir=None, file_type=None, prec=8): else: file_type = 'ASCII' file_type = file_type.upper() - if file_type not in ['FITS', 'ASCII']: - raise ValueError("file_type must be either FITS or ASCII if specified.") + if file_type not in ('FITS', 'ASCII'): + raise GalSimValueError("Invalid file_type.", file_type, ('FITS', 'ASCII')) if file_type == 'FITS': self.writeFits(file_name) - elif file_type == 'ASCII': + else: # file_type == 'ASCII': self.writeAscii(file_name, prec) - else: - raise ValueError("Invalid file_type %s"%file_type) def makeData(self): """Returns a numpy array of the data as it should be written to an output file. @@ -578,12 +569,7 @@ def writeFitsHdu(self): cols.append(pyfits.Column(name=name, format='%dA'%dt.itemsize, array=data[name])) cols = pyfits.ColDefs(cols) - - # Depending on the version of pyfits, one of these should work: - try: - tbhdu = pyfits.BinTableHDU.from_columns(cols) - except AttributeError: # pragma: no cover - tbhdu = pyfits.new_table(cols) + tbhdu = pyfits.BinTableHDU.from_columns(cols) return tbhdu def __repr__(self): diff --git a/galsim/cdmodel.py b/galsim/cdmodel.py index 2af43b8f263..13f28bf72f0 100644 --- a/galsim/cdmodel.py +++ b/galsim/cdmodel.py @@ -27,6 +27,7 @@ from .image import Image from . import _galsim +from .errors import GalSimValueError, convert_cpp_errors class BaseCDModel(object): """Base class for the most generic, i.e. no with symmetries or distance scaling relationships @@ -66,16 +67,16 @@ def __init__(self, a_l, a_r, a_b, a_t): """ # Some basic sanity checking if (a_l.shape[0] % 2 != 1): - raise ValueError("Input array must be odd-dimensioned") + raise GalSimValueError("Input array must be odd-dimensioned", a_l.shape) for a in (a_l, a_r, a_b, a_t): if a.shape[0] != a.shape[1]: - raise ValueError("Input array is not square") + raise GalSimValueError("Input array is not square", a.shape) if a.shape[0] != a_l.shape[0]: - raise ValueError("Input arrays not all the same dimensions") + raise GalSimValueError("Input arrays not all the same dimensions", a.shape) # Save the relevant dimension and the matrices storing deflection coefficients self.n = a_l.shape[0] // 2 if (self.n < 1): - raise ValueError("Input arrays must be at least 3x3") + raise GalSimValueError("Input arrays must be at least 3x3", a_l.shape) self.a_l = Image(a_l, dtype=np.float64, make_const=True) self.a_r = Image(a_r, dtype=np.float64, make_const=True) @@ -93,9 +94,10 @@ def applyForward(self, image, gain_ratio=1.): flat and science images have the same gain value """ ret = image.copy() - _galsim._ApplyCD( - ret._image, image._image, self.a_l._image, self.a_r._image, self.a_b._image, - self.a_t._image, int(self.n), float(gain_ratio)) + with convert_cpp_errors(): + _galsim._ApplyCD( + ret._image, image._image, self.a_l._image, self.a_r._image, self.a_b._image, + self.a_t._image, int(self.n), float(gain_ratio)) return ret def applyBackward(self, image, gain_ratio=1.): @@ -205,8 +207,7 @@ def __init__(self, n, r0, t0, rx, tx, r, t, alpha): @param t power-law amplitude for contribution to deflection along y from further away @param alpha power-law exponent for deflection from further away """ - if not isinstance(n, int): - raise ValueError("Input separation n must be an int") + n = int(n) # First define x and y coordinates in a square grid of ints of shape (2n + 1) * (2n + 1) x, y = np.meshgrid(np.arange(2 * n + 1) - n, np.arange(2 * n + 1) - n) diff --git a/galsim/chromatic.py b/galsim/chromatic.py index d11cba5f970..6680bdb66fa 100644 --- a/galsim/chromatic.py +++ b/galsim/chromatic.py @@ -33,8 +33,11 @@ from .bandpass import Bandpass from .position import PositionD, PositionI from .utilities import lazy_property +from .gsparams import GSParams from . import utilities from . import integ +from .errors import GalSimError, GalSimRangeError, GalSimSEDError, GalSimValueError +from .errors import GalSimIncompatibleValuesError, GalSimNotImplementedError, galsim_warn class ChromaticObject(object): """Base class for defining wavelength-dependent objects. @@ -157,11 +160,7 @@ class ChromaticObject(object): # - .dimensionless indicates obj.SED.dimensionless def __init__(self, obj): - self.separable = obj.separable - self.interpolated = obj.interpolated - self.wave_list = obj.wave_list self._obj = obj - self.deinterpolated = obj.deinterpolated if isinstance(obj, GSObject): self.SED = SED(obj.flux, 'nm', '1') elif isinstance(obj, ChromaticObject): @@ -169,6 +168,10 @@ def __init__(self, obj): else: raise TypeError("Can only directly instantiate ChromaticObject with a GSObject " "or ChromaticObject argument.") + self.separable = obj.separable + self.interpolated = obj.interpolated + self.wave_list = obj.wave_list + self.deinterpolated = obj.deinterpolated @staticmethod def _get_multiplier(sed, bandpass, wave_list): @@ -208,11 +211,12 @@ def _fiducial_profile(self, bandpass): # Prioritize wavelengths near the bandpass effective wavelength. candidate_waves = candidate_waves[np.argsort(np.abs(candidate_waves - bpwave))] for w in candidate_waves: - prof0 = self.evaluateAtWavelength(w) - if prof0.flux != 0: - return w, prof0 + if bandpass.blue_limit <= w <= bandpass.red_limit: + prof0 = self.evaluateAtWavelength(w) + if prof0.flux != 0: + return w, prof0 - raise ValueError("Could not locate fiducial wavelength where SED * Bandpass is nonzero.") + raise GalSimError("Could not locate fiducial wavelength where SED * Bandpass is nonzero.") def __eq__(self, other): return (isinstance(other, ChromaticObject) and @@ -330,7 +334,8 @@ def _get_integrator(integrator, wave_list): elif integrator == 'midpoint': rule = integ.midptRule else: - raise TypeError("Unrecognized integration rule: %s"%integrator) + raise GalSimValueError("Unrecognized integration rule", integrator, + ('trapezoidal', 'midpoint')) if len(wave_list) > 0: integrator = integ.SampleIntegrator(rule) else: @@ -394,7 +399,7 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs): # Store the last bandpass used and any extra kwargs. self._last_bp = bandpass if self.SED.dimensionless: - raise ValueError("Can only draw ChromaticObjects with spectral SEDs.") + raise GalSimSEDError("Can only draw ChromaticObjects with spectral SEDs.", self.SED) # setup output image using fiducial profile wave0, prof0 = self._fiducial_profile(bandpass) @@ -415,9 +420,9 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs): # merge self.wave_list into bandpass.wave_list if using a sampling integrator if isinstance(integrator, integ.SampleIntegrator): if len(wave_list) < 2: - raise AttributeError( - "Cannot use SampleIntegrator when Bandpass and SED are both " - "analytic.") + raise GalSimIncompatibleValuesError( + "Cannot use SampleIntegrator when Bandpass and SED are both analytic.", + integrator=integrator, bandpass=bandpass, sed=self.SED) bandpass = Bandpass(LookupTable(wave_list, bandpass(wave_list), interpolant='linear'), 'nm') @@ -469,7 +474,7 @@ def drawKImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs): from .table import LookupTable if self.SED.dimensionless: - raise ValueError("Can only drawK ChromaticObjects with spectral SEDs.") + raise GalSimSEDError("Can only drawK ChromaticObjects with spectral SEDs.", self.SED) # setup output image (semi-arbitrarily using the bandpass effective wavelength) prof0 = self.evaluateAtWavelength(bandpass.effective_wavelength) @@ -523,11 +528,12 @@ def evaluateAtWavelength(self, wave): @returns the monochromatic object at the given wavelength. """ - if self.__class__ != ChromaticObject: + if self.__class__ != ChromaticObject: # pragma: no cover raise NotImplementedError( "Subclasses of ChromaticObject must override evaluateAtWavelength()") return self._obj.evaluateAtWavelength(wave) + # Make op* and op*= work to adjust the flux of the object def __mul__(self, flux_ratio): """Scale the flux of the object by the given flux ratio, which may be an SED, a float, or a univariate callable function (of wavelength in nanometers) that returns a float. @@ -551,6 +557,14 @@ def __mul__(self, flux_ratio): """ return self.withScaledFlux(flux_ratio) + __rmul__ = __mul__ + + # Likewise for op/ and op/= + def __div__(self, other): + return self.__mul__(1./other) + + __truediv__ = __div__ + def withScaledFlux(self, flux_ratio): """Multiply the flux of the object by `flux_ratio` @@ -588,8 +602,8 @@ def withMagnitude(self, target_magnitude, bandpass): @returns the new normalized ChromaticObject. """ if bandpass.zeropoint is None: - raise RuntimeError("Cannot call ChromaticObject.withMagnitude on this bandpass, because" - " it does not have a zeropoint. See Bandpass.withZeropoint()") + raise GalSimError("Cannot call ChromaticObject.withMagnitude on this bandpass, because" + " it does not have a zeropoint. See Bandpass.withZeropoint()") current_magnitude = self.calculateMagnitude(bandpass) norm = 10**(-0.4*(target_magnitude - current_magnitude)) return self * norm @@ -607,7 +621,7 @@ def withFluxDensity(self, target_flux_density, wavelength): _photons = units.astrophys.photon/(units.s * units.cm**2 * units.nm) if self.dimensionless: - raise TypeError("Cannot set flux density of dimensionless ChromaticObject.") + raise GalSimSEDError("Cannot set flux density of dimensionless ChromaticObject.", self) if isinstance(wavelength, units.Quantity): wavelength_nm = wavelength.to(units.nm, units.spectral()) current_flux_density = self.SED(wavelength_nm.value) @@ -670,7 +684,8 @@ def calculateFlux(self, bandpass): @returns the flux through the bandpass. """ if self.SED.dimensionless: - raise ValueError("Cannot calculate flux of dimensionless ChromaticObject.") + raise GalSimSEDError("Cannot calculate flux of dimensionless ChromaticObject.", + self.SED) return self.SED.calculateFlux(bandpass) def calculateMagnitude(self, bandpass): @@ -686,7 +701,8 @@ def calculateMagnitude(self, bandpass): @returns the bandpass magnitude. """ if self.SED.dimensionless: - raise ValueError("Cannot calculate magnitude of dimensionless ChromaticObject.") + raise GalSimSEDError("Cannot calculate magnitude of dimensionless ChromaticObject.", + self.SED) return self.SED.calculateMagnitude(bandpass) # Add together `ChromaticObject`s and/or `GSObject`s @@ -697,17 +713,6 @@ def __add__(self, other): def __sub__(self, other): return ChromaticSum(self, -other) - # Make op* and op*= work to adjust the flux of the object - def __rmul__(self, other): - return self.__mul__(other) - - # Likewise for op/ and op/= - def __div__(self, other): - return self.__mul__(1./other) - - def __truediv__(self, other): - return self.__div__(other) - def __neg__(self): return -1. * self @@ -858,7 +863,7 @@ def lens(self, g1, g2, mu): @returns the lensed object. """ from .shear import Shear - if any(hasattr(g, '__call__') for g in [g1,g2]): + if any(hasattr(g, '__call__') for g in (g1,g2)): _g1 = g1 _g2 = g2 if not hasattr(g1, '__call__'): _g1 = lambda w: g1 @@ -911,7 +916,7 @@ def transform(self, dudx, dudy, dvdx, dvdy): @returns the transformed object. """ from .transform import Transform - if any(hasattr(dd, '__call__') for dd in [dudx, dudy, dvdx, dvdy]): + if any(hasattr(dd, '__call__') for dd in (dudx, dudy, dvdx, dvdy)): _dudx = dudx _dudy = dudy _dvdx = dvdx @@ -949,8 +954,11 @@ def shift(self, *args, **kwargs): if len(args) == 0: # Then dx,dy need to be kwargs # If not, then python will raise an appropriate error. - dx = kwargs.pop('dx') - dy = kwargs.pop('dy') + try: + dx = kwargs.pop('dx') + dy = kwargs.pop('dy') + except KeyError: + raise TypeError('shift() requires exactly 2 arguments (dx, dy)') offset = None elif len(args) == 1: if hasattr(args[0], '__call__'): @@ -970,7 +978,8 @@ def offset_func(w): offset = np.asarray( (args[0].x, args[0].y) ) else: # Let python raise the appropriate exception if this isn't valid. - offset = np.asarray(args[0]) + dx, dy = args[0] + offset = np.asarray( (dx, dy) ) elif len(args) == 2: dx = args[0] dy = args[1] @@ -1095,8 +1104,8 @@ def _imageAtWavelength(self, wave): """ # First, some wavelength-related sanity checks. if wave < np.min(self.waves) or wave > np.max(self.waves): - raise RuntimeError("Requested wavelength %.1f is outside the allowed range:" - " %.1f to %.1f nm"%(wave, np.min(self.waves), np.max(self.waves))) + raise GalSimRangeError("Requested wavelength is outside the allowed range.", + wave, np.min(self.waves), np.max(self.waves)) # Figure out where the supplied wavelength is compared to the list of wavelengths on which # images were originally tabulated. @@ -1134,18 +1143,16 @@ def _get_interp_image(self, bandpass, image=None, integrator='trapezoidal', instead interact with the `drawImage` method. """ from .interpolatedimage import InterpolatedImage - if integrator not in ['trapezoidal', 'midpoint']: + if integrator not in ('trapezoidal', 'midpoint'): if not isinstance(integrator, str): raise TypeError("Integrator should be a string indicating trapezoidal" " or midpoint rule for integration") - raise TypeError("Unknown integrator: %s"%integrator) + raise GalSimValueError("Unknown integrator",integrator, ('trapezoidal', 'midpoint')) if _flux_ratio is None: _flux_ratio = lambda w: 1.0 - if not hasattr(_flux_ratio, '__call__'): - # Can't do _flux_ratio = lambda w: _flux_ratio, need a temporary variable. - tmp = _flux_ratio - _flux_ratio = lambda w: tmp + # Constant flux_ratio is already an SED at this point, so can treat as function. + assert hasattr(_flux_ratio, '__call__') # setup output image (semi-arbitrarily using the bandpass effective wavelength). # Note: we cannot just use self._imageAtWavelength, because that routine returns an image @@ -1162,14 +1169,12 @@ def _get_interp_image(self, bandpass, image=None, integrator='trapezoidal', wave_objs += [_flux_ratio] wave_list, _, _ = utilities.combine_wave_list(wave_objs) - if np.min(wave_list) < np.min(self.waves): - raise RuntimeError("Requested wavelength %.1f is outside the allowed range:" - " %.1f to %.1f nm"%(np.min(wave_list), np.min(self.waves), - np.max(self.waves))) - if np.max(wave_list) > np.max(self.waves): - raise RuntimeError("Requested wavelength %.1f is outside the allowed range:" - " %.1f to %.1f nm"%(np.max(wave_list), np.min(self.waves), - np.max(self.waves))) + if ( np.min(wave_list) < np.min(self.waves) + or np.max(wave_list) > np.max(self.waves) ): # pragma: no cover + # MJ: I'm pretty sure it's impossible to hit this. + # But just in case I'm wrong, I'm leaving it here but with pragma: no cover. + raise GalSimRangeError("Requested wavelength is outside the allowed range.", + wave_list, np.min(self.waves), np.max(self.waves)) # The integration is carried out using the following two basic principles: # (1) We use linear interpolation between the stored images to get an image at a given @@ -1248,7 +1253,7 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs): # Store the last bandpass used. self._last_bp = bandpass if self.SED.dimensionless: - raise ValueError("Can only draw ChromaticObjects with spectral SEDs.") + raise GalSimSEDError("Can only draw ChromaticObjects with spectral SEDs.", self.SED) int_im = self._get_interp_image(bandpass, image=image, integrator=integrator, **kwargs) image = int_im.drawImage(image=image, **kwargs) @@ -1336,7 +1341,7 @@ def __init__(self, base_obj, base_wavelength, scale_unit=None, **kwargs): # Any remaining kwargs will get forwarded to galsim.dcr.get_refraction # Check that they're valid for kw in self.kw: - if kw not in ['temperature', 'pressure', 'H2O_pressure']: + if kw not in ('temperature', 'pressure', 'H2O_pressure'): raise TypeError("Got unexpected keyword: {0}".format(kw)) self.base_refraction = dcr.get_refraction(self.base_wavelength, self.zenith_angle, @@ -1460,10 +1465,9 @@ def detjac(w): self.SED *= detjac if obj.interpolated and self.chromatic: - import warnings - warnings.warn("Cannot render image with chromatic transformation applied to it " - "using interpolation between stored images. Reverting to " - "non-interpolated version.") + galsim_warn("Cannot render image with chromatic transformation applied to it " + "using interpolation between stored images. Reverting to " + "non-interpolated version.") obj = obj.deinterpolated self.interpolated = obj.interpolated @@ -1492,7 +1496,7 @@ def new_offset(jac2, off1, off2): if gsparams is None: self.gsparams = self.original.gsparams if hasattr(self.original, 'gsparams') else None else: - self.gsparams = gsparams + self.gsparams = GSParams.check(gsparams) if self.interpolated: self.deinterpolated = ChromaticTransformation( @@ -1507,12 +1511,11 @@ def new_offset(jac2, off1, off2): def __eq__(self, other): if not (isinstance(other, ChromaticTransformation) and self.original == other.original and - self.gsparams == other.gsparams and - self._flux_ratio == other._flux_ratio): + self.gsparams == other.gsparams): return False # There's really no good way to check that two callables are equal, except if they literally - # point to the same object. So we'll just check for that for _jac and _offset. - for attr in ['_jac', '_offset']: + # point to the same object. So we'll just check for that for _jac, _offset, _flux_ratio. + for attr in ('_jac', '_offset', '_flux_ratio'): selfattr = getattr(self, attr) otherattr = getattr(other, attr) # For this attr, either both need to be chromatic or neither. @@ -1531,10 +1534,9 @@ def __eq__(self, other): def __hash__(self): # This one's a bit complicated, so we'll go ahead and cache the hash. if not hasattr(self, '_hash'): - self._hash = hash(("galsim.ChromaticTransformation", self.original, self._flux_ratio, - self.gsparams)) + self._hash = hash(("galsim.ChromaticTransformation", self.original, self.gsparams)) # achromatic _jac and _offset are ndarrays, so need to be handled separately. - for attr in ['_jac', '_offset']: + for attr in ('_jac', '_offset', '_flux_ratio'): selfattr = getattr(self, attr) if hasattr(selfattr, '__call__'): self._hash ^= hash(selfattr) @@ -1555,37 +1557,12 @@ def __repr__(self): self.original, jac, offset, self._flux_ratio, self.gsparams) def __str__(self): - from .wcs import JacobianWCS + from .transform import Transformation s = str(self.original) if hasattr(self._jac, '__call__'): s += '.transform(%s)'%self._jac else: - dudx, dudy, dvdx, dvdy = self._jac.ravel() - if dudx != 1 or dudy != 0 or dvdx != 0 or dvdy != 1: - # Figure out the shear/rotate/dilate calls that are equivalent. - jac = JacobianWCS(dudx,dudy,dvdx,dvdy) - scale, shear, theta, flip = jac.getDecomposition() - single = None - if flip: - single = 0 # Special value indicating to just use transform. - if abs(theta.rad) > 1.e-12: - if single is None: - single = '.rotate(%s)'%theta - else: - single = 0 - if shear.g > 1.e-12: - if single is None: - single = '.shear(%s)'%shear - else: - single = 0 - if abs(scale-1.0) > 1.e-12: - if single is None: - single = '.expand(%s)'%scale - else: - single = 0 - if single == 0: - single = '.transform(%s,%s,%s,%s)'%(dudx,dudy,dvdx,dvdy) - s += single + s += Transformation._str_from_jac(self._jac) if hasattr(self._offset, '__call__'): s += '.shift(%s)'%self._offset elif np.array_equal(self._offset,(0,0)): @@ -1649,7 +1626,7 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs): # Store the last bandpass used. self._last_bp = bandpass if self.SED.dimensionless: - raise ValueError("Can only draw ChromaticObjects with spectral SEDs.") + raise GalSimSEDError("Can only draw ChromaticObjects with spectral SEDs.", self.SED) if isinstance(self.original, InterpolatedChromaticObject): # Pass self._flux_ratio, which *could* depend on wavelength, to _get_interp_image, # where it will be used to reweight the stored images. @@ -1677,7 +1654,7 @@ def noise(self): if (hasattr(self._jac, '__call__') or hasattr(self._offset, '__call__') or not self._flux_ratio._const): - raise TypeError("Cannot propagate noise through chromatic transformation") + raise GalSimError("Cannot propagate noise through chromatic transformation") noise = self.original.noise jac = self._jac flux_ratio = self._flux_ratio(42.) # const, so use any wavelength @@ -1725,7 +1702,7 @@ def __init__(self, *args, **kwargs): if len(args) == 0: # No arguments. Could initialize with an empty list but draw then segfaults. Raise an # exception instead. - raise ValueError("Must provide at least one GSObject or ChromaticObject.") + raise TypeError("Must provide at least one GSObject or ChromaticObject.") elif len(args) == 1: # 1 argument. Should be either a GSObject, ChromaticObject or a list of these. if isinstance(args[0], (GSObject, ChromaticObject)): @@ -1734,7 +1711,7 @@ def __init__(self, *args, **kwargs): args = args[0] else: raise TypeError("Single input argument must be a GSObject, a ChromaticObject," - +" or list of them.") + " or list of them.") # else args is already the list of objects self.interpolated = any(arg.interpolated for arg in args) @@ -1748,7 +1725,8 @@ def __init__(self, *args, **kwargs): dimensionless = all(a.dimensionless for a in args) spectral = all(a.spectral for a in args) if not (dimensionless or spectral): - raise ValueError("Cannot add dimensionless and spectral ChromaticObjects.") + raise GalSimIncompatibleValuesError( + "Cannot add dimensionless and spectral ChromaticObjects.", args=args) # Sort arguments into inseparable objects and groups of separable objects. Note that # separable groups are only identified if the constituent objects have the *same* SED even @@ -1846,7 +1824,7 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs): # Store the last bandpass used. self._last_bp = bandpass if self.SED.dimensionless: - raise ValueError("Can only draw ChromaticObjects with spectral SEDs.") + raise GalSimSEDError("Can only draw ChromaticObjects with spectral SEDs.", self.SED) add_to_image = kwargs.pop('add_to_image', False) # Use given add_to_image for the first one, then add_to_image=False for the rest. image = self.obj_list[0].drawImage( @@ -1903,23 +1881,22 @@ def __init__(self, *args, **kwargs): if len(args) == 0: # No arguments. Could initialize with an empty list but draw then segfaults. Raise an # exception instead. - raise ValueError("Must provide at least one GSObject or ChromaticObject") + raise TypeError("Must provide at least one GSObject or ChromaticObject") elif len(args) == 1: if isinstance(args[0], (GSObject, ChromaticObject)): args = [args[0]] elif isinstance(args[0], list): args = args[0] else: - raise TypeError( - "Single input argument must be a GSObject, or a ChromaticObject," - +" or list of them.") + raise TypeError("Single input argument must be a GSObject, or a ChromaticObject," + " or list of them.") # else args is already the list of objects # Check kwargs # real space convolution is not implemented for chromatic objects. real_space = kwargs.pop("real_space", None) if real_space: - raise NotImplementedError( + raise GalSimNotImplementedError( "Real space convolution of chromatic objects not implemented.") self.gsparams = kwargs.pop("gsparams", None) @@ -1930,7 +1907,8 @@ def __init__(self, *args, **kwargs): # Accumulate convolutant .SEDs. Check if more than one is spectral. nspectral = sum(arg.spectral for arg in args) if nspectral > 1: - raise ValueError("Cannot convolve more than one spectral ChromaticObject.") + raise GalSimIncompatibleValuesError( + "Cannot convolve more than one spectral ChromaticObject.", args=args) self.SED = args[0].SED for obj in args[1:]: self.SED *= obj.SED @@ -1962,10 +1940,9 @@ def __init__(self, *args, **kwargs): for obj in self.obj_list: if not obj.separable and not isinstance(obj, ChromaticSum): n_nonsep += 1 if obj.interpolated: n_interp += 1 - if n_nonsep>1 and n_interp>0: # pragma: no cover - import warnings - warnings.warn( - "Image rendering for this convolution cannot take advantage of " + + if n_nonsep>1 and n_interp>0: + galsim_warn( + "Image rendering for this convolution cannot take advantage of " "interpolation-related optimization. Will use full profile evaluation.") # Assemble wave_lists @@ -2064,7 +2041,7 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', iimult=None, # Store the last bandpass used. self._last_bp = bandpass if self.SED.dimensionless: - raise ValueError("Can only draw ChromaticObjects with spectral SEDs.") + raise GalSimSEDError("Can only draw ChromaticObjects with spectral SEDs.", self.SED) # `ChromaticObject.drawImage()` can just as efficiently handle separable cases. if self.separable: image = ChromaticObject.drawImage(self, bandpass, image=image, **kwargs) @@ -2149,25 +2126,24 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', iimult=None, # Separate convolutants into a Convolution of inseparable profiles multiplied by the # wavelength-dependent normalization of separable profiles, and the achromatic part of # separable profiles. - insep_obj = Convolve([obj for obj in self.obj_list if not obj.separable], - gsparams=self.gsparams) - # Note that insep_obj should always exist, since purely separable ChromaticConvolutions were + insep_obj = [obj for obj in self.obj_list if not obj.separable] + + # Note that len(insep_obj) > 0, since purely separable ChromaticConvolutions were # already handled above. # Don't wrap in Convolution if not needed. Single item can draw itself better than # Convolution can. - if len(insep_obj.obj_list) == 1: - insep_obj = insep_obj.obj_list[0] + if len(insep_obj) == 1: + insep_obj = insep_obj[0] + else: + insep_obj = Convolve(insep_obj, gsparams=self.gsparams) sep_profs = [] for obj in self.obj_list: if not obj.separable: continue - if isinstance(obj, GSObject): - sep_profs.append(obj) - else: - wave0, prof0 = obj._fiducial_profile(bandpass) - sep_profs.append(prof0 / obj.SED(wave0)) - insep_obj *= obj.SED + wave0, prof0 = obj._fiducial_profile(bandpass) + sep_profs.append(prof0 / obj.SED(wave0)) + insep_obj *= obj.SED # Collapse inseparable profiles and chromatic normalizations into one effective profile # Note that at this point, insep_obj.SED should *not* be None. @@ -2190,11 +2166,11 @@ def noise(self): # Exactly one of the convolutants has a .covspec attribute. covspecs = [ obj.covspec for obj in self.obj_list if hasattr(obj, 'covspec') ] if len(covspecs) != 1: - raise TypeError("Cannot compute noise for ChromaticConvolution for which number " - "of convolutants with covspec attribute is not 1.") + raise GalSimError("Cannot compute noise for ChromaticConvolution for which number " + "of convolutants with covspec attribute is not 1.") if not hasattr(self, '_last_bp'): - raise TypeError("Cannot compute noise for ChromaticConvolution until after drawImage " - "has been called.") + raise GalSimError("Cannot compute noise for ChromaticConvolution until after drawImage " + "has been called.") covspec = covspecs[0] other = Convolve([obj for obj in self.obj_list if not hasattr(obj, 'covspec')]) return covspec.toNoise(self._last_bp, other, self._last_wcs) # rng=? @@ -2223,7 +2199,7 @@ class ChromaticDeconvolution(ChromaticObject): """ def __init__(self, obj, **kwargs): if not obj.SED.dimensionless: - raise ValueError("Cannot deconvolve by spectral ChromaticObject.") + raise GalSimSEDError("Cannot deconvolve by spectral ChromaticObject.", obj.SED) self._obj = obj self.kwargs = kwargs self.separable = obj.separable @@ -2279,7 +2255,7 @@ class ChromaticAutoConvolution(ChromaticObject): """ def __init__(self, obj, **kwargs): if not obj.SED.dimensionless: - raise ValueError("Cannot autoconvolve spectral ChromaticObject.") + raise GalSimSEDError("Cannot autoconvolve spectral ChromaticObject.", obj.SED) self._obj = obj self.kwargs = kwargs self.separable = obj.separable @@ -2336,7 +2312,7 @@ class ChromaticAutoCorrelation(ChromaticObject): """ def __init__(self, obj, **kwargs): if not obj.SED.dimensionless: - raise ValueError("Cannot autocorrelate spectral ChromaticObject.") + raise GalSimSEDError("Cannot autocorrelate spectral ChromaticObject.", obj.SED) self._obj = obj self.kwargs = kwargs self.separable = obj.separable @@ -2402,7 +2378,7 @@ class ChromaticFourierSqrtProfile(ChromaticObject): def __init__(self, obj, **kwargs): import math if not obj.SED.dimensionless: - raise ValueError("Cannot take Fourier sqrt of spectral ChromaticObject.") + raise GalSimSEDError("Cannot take Fourier sqrt of spectral ChromaticObject.", obj.SED) self._obj = obj self.kwargs = kwargs self.separable = obj.separable @@ -2415,6 +2391,15 @@ def __init__(self, obj, **kwargs): self.SED = SED(lambda w:math.sqrt(obj.SED(w)), 'nm', '1') self.wave_list = obj.wave_list + def __eq__(self, other): + return (isinstance(other, ChromaticFourierSqrtProfile) and + self._obj == other._obj and + self.kwargs == other.kwargs) + + def __hash__(self): + return hash(("galsim.ChromaticFourierSqrtProfile", self._obj, + frozenset(self.kwargs.items()))) + def __repr__(self): kwargs_str = ', '.join('%s=%s'%(k,v) for k,v in self.kwargs.items()) return 'galsim.ChromaticFourierSqrtProfile(%r, %s)'%(self._obj, kwargs_str) @@ -2490,9 +2475,11 @@ def __init__(self, lam, diam=None, lam_over_diam=None, aberrations=None, self.scale_unit = scale_unit # We have to require either diam OR lam_over_diam: - if (diam is None and lam_over_diam is None) or \ - (diam is not None and lam_over_diam is not None): - raise TypeError("Need to specify telescope diameter OR wavelength/diam ratio") + if ( (diam is None and lam_over_diam is None) or + (diam is not None and lam_over_diam is not None) ): + raise GalSimIncompatibleValuesError( + "Need to specify telescope diameter OR wavelength/diam ratio", + diam=diam, lam_over_diam=lam_over_diam) if diam is not None: self.lam_over_diam = (1.e-9*lam/diam)*radians/self.scale_unit self.diam = diam @@ -2600,9 +2587,11 @@ def __init__(self, lam, diam=None, lam_over_diam=None, scale_unit=None, **kwargs scale_unit = AngleUnit.from_name(scale_unit) self.scale_unit = scale_unit - if (diam is None and lam_over_diam is None) or \ - (diam is not None and lam_over_diam is not None): - raise TypeError("Need to specify telescope diameter OR wavelength/diam ratio") + if ( (diam is None and lam_over_diam is None) or + (diam is not None and lam_over_diam is not None) ): + raise GalSimIncompatibleValuesError( + "Need to specify telescope diameter OR wavelength/diam ratio", + diam=diam, lam_over_diam=lam_over_diam) if diam is not None: self.lam_over_diam = (1.e-9*lam/diam)*radians/self.scale_unit else: diff --git a/galsim/config/extra.py b/galsim/config/extra.py index c250ec971c5..9b72d29db38 100644 --- a/galsim/config/extra.py +++ b/galsim/config/extra.py @@ -17,7 +17,6 @@ # import os -import galsim import logging import inspect @@ -31,6 +30,8 @@ # builder classes that will perform the different processing functions. valid_extra_outputs = {} +import galsim + def SetupExtraOutput(config, logger=None): """ @@ -93,7 +94,7 @@ class OutputManager(BaseManager): pass # Make the data list the right length now to avoid issues with multiple # processes trying to append at the same time. - nimages = config['nimages'] + nimages = config.get('nimages', 1) for k in range(nimages): data.append(None) @@ -114,7 +115,9 @@ def SetupExtraOutputsForImage(config, logger=None): @param logger If given, a logger object to log progress. [default: None] """ if 'output' in config: - for key in [ k for k in valid_extra_outputs.keys() if k in config['output'] ]: + if 'extra_builder' not in config: + SetupExtraOutput(config, logger) + for key in (k for k in valid_extra_outputs.keys() if k in config['output']): builder = config['extra_builder'][key] field = config['output'][key] builder.setupImage(field, config, logger) @@ -132,7 +135,7 @@ def ProcessExtraOutputsForStamp(config, skip, logger=None): """ if 'output' in config: obj_num = config['obj_num'] - for key in [ k for k in valid_extra_outputs.keys() if k in config['output'] ]: + for key in (k for k in valid_extra_outputs.keys() if k in config['output']): builder = config['extra_builder'][key] field = config['output'][key] if skip: @@ -151,14 +154,14 @@ def ProcessExtraOutputsForImage(config, logger=None): """ if 'output' in config: obj_nums = None - for key in [ k for k in valid_extra_outputs.keys() if k in config['output'] ]: + for key in (k for k in valid_extra_outputs.keys() if k in config['output']): + image_num = config.get('image_num',0) + start_image_num = config.get('start_image_num',0) if obj_nums is None: # Figure out which obj_nums were used for this image. - file_num = config['file_num'] - image_num = config['image_num'] - start_image_num = config['start_image_num'] - start_obj_num = config['start_obj_num'] - nobj = config['nobj'] + file_num = config.get('file_num',0) + start_obj_num = config.get('start_obj_num',0) + nobj = config.get('nobj', [1]) k = image_num - start_image_num for i in range(k): start_obj_num += nobj[i] @@ -168,7 +171,7 @@ def ProcessExtraOutputsForImage(config, logger=None): obj_nums = [ n for n in obj_nums if n not in skipped ] builder = config['extra_builder'][key] field = config['output'][key] - index = config['image_num'] - config['start_image_num'] + index = image_num - start_image_num builder.processImage(index, obj_nums, field, config, logger) @@ -203,12 +206,12 @@ def WriteExtraOutputs(config, main_data, logger=None): if 'extra_last_file' not in config: config['extra_last_file'] = {} - for key in [ k for k in valid_extra_outputs.keys() if k in output ]: + for key in (k for k in valid_extra_outputs.keys() if k in output): field = output[key] if 'file_name' in field: galsim.config.SetDefaultExt(field, '.fits') file_name = galsim.config.ParseValue(field,'file_name',config,str)[0] - else: # pragma: no cover + else: # pragma: no cover (it is covered, but codecov wrongly thinks it isn't.) # If no file_name, then probably writing to hdu continue if 'dir' in field: @@ -219,11 +222,11 @@ def WriteExtraOutputs(config, main_data, logger=None): if dir is not None: file_name = os.path.join(dir,file_name) - galsim.config.EnsureDir(file_name) + galsim.config.ensure_dir(file_name) - if noclobber and os.path.isfile(file_name): # pragma: no cover - logger.warning('Not writing %s file %d = %s because output.noclobber = True' + - ' and file exists',key,config['file_num'],file_name) + if noclobber and os.path.isfile(file_name): + logger.warning('Not writing %s file %d = %s because output.noclobber = True ' + 'and file exists',key,config['file_num'],file_name) continue if config['extra_last_file'].get(key, None) == file_name: @@ -264,15 +267,15 @@ def AddExtraOutputHDUs(config, main_data, logger=None): """ output = config['output'] hdus = {} - for key in [ k for k in valid_extra_outputs.keys() if k in output ]: + for key in (k for k in valid_extra_outputs.keys() if k in output): field = output[key] if 'hdu' in field: hdu = galsim.config.ParseValue(field,'hdu',config,int)[0] - else: # pragma: no cover + else: # pragma: no cover (it is covered, but codecov wrongly thinks it isn't.) # If no hdu, then probably writing to file continue if hdu <= 0 or hdu in hdus: - raise ValueError("%s hdu = %d is invalid or a duplicate."%hdu) + raise galsim.GalSimConfigValueError("hdu is invalid or a duplicate.",hdu) builder = config['extra_builder'][key] @@ -285,7 +288,7 @@ def AddExtraOutputHDUs(config, main_data, logger=None): first = len(main_data) for h in range(first,len(hdus)+first): if h not in hdus: - raise ValueError("Cannot skip hdus. No output found for hdu %d"%h) + raise galsim.GalSimConfigError("Cannot skip hdus. No output found for hdu %d"%h) # Turn hdus into a list (in order) hdulist = [ hdus[k] for k in range(first,len(hdus)+first) ] return main_data + hdulist @@ -297,20 +300,21 @@ def CheckNoExtraOutputHDUs(config, output_type, logger=None): """ logger = galsim.config.LoggerWrapper(logger) output = config['output'] - for key in [ k for k in valid_extra_outputs.keys() if k in output ]: + for key in (k for k in valid_extra_outputs.keys() if k in output): field = output[key] if 'hdu' in field: hdu = galsim.config.ParseValue(field,'hdu',config,int)[0] logger.error("Extra output %s requesting to write to hdu %d", key, hdu) - raise AttributeError("Output type %s cannot add extra images as HDUs"%output_type) + raise galsim.GalSimConfigError( + "Output type %s cannot add extra images as HDUs"%output_type) -def GetFinalExtraOutput(key, config, main_data, logger=None): +def GetFinalExtraOutput(key, config, main_data=[], logger=None): """Get the finalized output object for the given extra output key @param key The name of the output field in config['output'] @param config The configuration dict. - @param main_data The main file data in case it is needed. + @param main_data The main file data in case it is needed. [default: []] @param logger If given, a logger object to log progress. [default: None] @returns the final data to be output. @@ -431,7 +435,7 @@ def ensureFinalized(self, config, base, main_data, logger): @returns the final version of the object. """ - if self.final_data is None: # pragma: no branch + if self.final_data is None: self.final_data = self.finalize(config, base, main_data, logger) return self.final_data @@ -478,12 +482,9 @@ def writeHdu(self, config, base, logger): @returns an HDU with the output data. """ - n = len(self.data) - if n == 0: - raise RuntimeError("No %s images were created."%self._extra_output_key) - elif n > 1: - raise RuntimeError( - "%d %s images were created, but expecting only 1."%(n,self._extra_output_key)) + if len(self.data) != 1: # pragma: no cover (Not sure if this is possible.) + raise galsim.GalSimError( + "%d %s images were created. Expecting 1."%(n,self._extra_output_key)) return self.data[0] diff --git a/galsim/config/extra_psf.py b/galsim/config/extra_psf.py index a4559cd83c5..8de22ea8518 100644 --- a/galsim/config/extra_psf.py +++ b/galsim/config/extra_psf.py @@ -22,6 +22,7 @@ import logging # The psf extra output type builds an Image of the PSF at the same locations as the galaxies. +from .stamp import valid_draw_methods # The code the actually draws the PSF on a postage stamp. def DrawPSFStamp(psf, config, base, bounds, offset, method, logger): @@ -32,24 +33,39 @@ def DrawPSFStamp(psf, config, base, bounds, offset, method, logger): """ if 'draw_method' in config: method = galsim.config.ParseValue(config,'draw_method',base,str)[0] - if method not in ['auto', 'fft', 'phot', 'real_space', 'no_pixel', 'sb']: - raise AttributeError("Invalid draw_method: %s"%method) + if method not in valid_draw_methods: + raise galsim.GalSimConfigValueError("Invalid draw_method.", method, valid_draw_methods) else: method = 'auto' + if 'flux' in config: + flux = galsim.config.ParseValue(config,'flux',base,float)[0] + psf = psf.withFlux(flux) + + if method == 'phot': + rng = galsim.config.GetRNG(config, base) + n_photons = psf.flux + else: + rng = None + n_photons = 0 + wcs = base['wcs'].local(base['image_pos']) im = galsim.ImageF(bounds, wcs=wcs) - im = psf.drawImage(image=im, offset=offset, method=method) + im = psf.drawImage(image=im, offset=offset, method=method, rng=rng, n_photons=n_photons) if 'signal_to_noise' in config: + if 'flux' in config: + raise galsim.GalSimConfigError( + "Cannot specify both flux and signal_to_noise for psf output") if method == 'phot': - raise NotImplementedError( + raise galsim.GalSimConfigError( "signal_to_noise option not implemented for draw_method = phot") if 'image' in base and 'noise' in base['image']: noise_var = galsim.config.CalculateNoiseVariance(base) else: - raise AttributeError("Need to specify noise level when using psf.signal_to_noise") + raise galsim.GalSimConfigError( + "Need to specify noise level when using psf.signal_to_noise") sn_target = galsim.config.ParseValue(config, 'signal_to_noise', base, float)[0] diff --git a/galsim/config/extra_truth.py b/galsim/config/extra_truth.py index 9eb642ab895..365f6119d7f 100644 --- a/galsim/config/extra_truth.py +++ b/galsim/config/extra_truth.py @@ -86,7 +86,7 @@ def processStamp(self, obj_num, config, base, logger): base['obj_num']) logger.error("Types for current object = %s",repr(types)) logger.error("Expecting types = %s",repr(self.scratch['types'])) - raise RuntimeError("Type mismatch found when building truth catalog.") + raise galsim.GalSimConfigError("Type mismatch found when building truth catalog.") self.scratch[obj_num] = row # The function to call at the end of building each file to finalize the truth catalog diff --git a/galsim/config/gsobject.py b/galsim/config/gsobject.py index f3303a79046..a0f82ca11b2 100644 --- a/galsim/config/gsobject.py +++ b/galsim/config/gsobject.py @@ -75,7 +75,7 @@ def BuildGSObject(config, key, base=None, gsparams={}, logger=None): # Get the type to be parsed. if not 'type' in param: - raise AttributeError("type attribute required in config.%s"%key) + raise galsim.GalSimConfigError("type attribute required in config.%s"%key) type_name = param['type'] # If we are repeating, then we get to use the current object for repeat times. @@ -134,10 +134,9 @@ def BuildGSObject(config, key, base=None, gsparams={}, logger=None): # need to get the PSF's half_light_radius. if 'resolution' in param: if 'psf' not in base: - raise AttributeError( - "Cannot use gal.resolution if no psf is set.") + raise galsim.GalSimConfigError("Cannot use gal.resolution if no psf is set.") if 'saved_re' not in base['psf']: - raise AttributeError( + raise galsim.GalSimConfigError( 'Cannot use gal.resolution with psf.type = %s'%base['psf']['type']) psf_re = base['psf']['saved_re'] resolution = galsim.config.ParseValue(param, 'resolution', base, float)[0] @@ -145,7 +144,7 @@ def BuildGSObject(config, key, base=None, gsparams={}, logger=None): if 're_from_res' not in param: # The first time, check that half_light_radius isn't also specified. if 'half_light_radius' in param: - raise AttributeError( + raise galsim.GalSimConfigError( 'Cannot specify both gal.resolution and gal.half_light_radius') param['re_from_res'] = True param['half_light_radius'] = gal_re @@ -163,7 +162,7 @@ def BuildGSObject(config, key, base=None, gsparams={}, logger=None): elif type_name in galsim.__dict__: build_func = eval("galsim."+type_name) else: - raise NotImplementedError("Unrecognised config type = %s"%type_name) + raise galsim.GalSimConfigValueError("Unrecognised gsobject type", type_name) if inspect.isclass(build_func) and issubclass(build_func, galsim.GSObject): gsobject, safe = _BuildSimple(build_func, param, base, ignore, gsparams, logger) @@ -174,7 +173,7 @@ def BuildGSObject(config, key, base=None, gsparams={}, logger=None): if key == 'psf': try: param['saved_re'] = gsobject.half_light_radius - except AttributeError: + except (AttributeError, NotImplementedError, TypeError): pass # Apply any dilation, ellip, shear, etc. modifications. @@ -247,7 +246,7 @@ def _BuildAdd(config, base, ignore, gsparams, logger): gsobjects = [] items = config['items'] if not isinstance(items,list): - raise AttributeError("items entry for type=Add is not a list.") + raise galsim.GalSimConfigError("items entry for type=Add is not a list.") safe = True for i in range(len(items)): @@ -260,7 +259,7 @@ def _BuildAdd(config, base, ignore, gsparams, logger): gsobjects.append(gsobject) if len(gsobjects) == 0: - raise ValueError("No valid items for type=Add") + raise galsim.GalSimConfigError("No valid items for type=Add") elif len(gsobjects) == 1: gsobject = gsobjects[0] else: @@ -301,7 +300,7 @@ def _BuildConvolve(config, base, ignore, gsparams, logger): gsobjects = [] items = config['items'] if not isinstance(items,list): - raise AttributeError("items entry for type=Convolve is not a list.") + raise galsim.GalSimConfigError("items entry for type=Convolve is not a list.") safe = True for i in range(len(items)): gsobject, safe1 = BuildGSObject(items, i, base, gsparams, logger) @@ -309,7 +308,7 @@ def _BuildConvolve(config, base, ignore, gsparams, logger): gsobjects.append(gsobject) if len(gsobjects) == 0: - raise ValueError("No valid items for type=Convolve") + raise galsim.GalSimConfigError("No valid items for type=Convolve") elif len(gsobjects) == 1: gsobject = gsobjects[0] else: @@ -335,13 +334,13 @@ def _BuildList(config, base, ignore, gsparams, logger): items = config['items'] if not isinstance(items,list): - raise AttributeError("items entry for type=List is not a list.") + raise galsim.GalSimConfigError("items entry for type=List is not a list.") # Setup the indexing sequence if it hasn't been specified using the length of items. galsim.config.SetDefaultIndex(config, len(items)) index, safe = galsim.config.ParseValue(config, 'index', base, int) if index < 0 or index >= len(items): - raise AttributeError("index %d out of bounds for List"%index) + raise galsim.GalSimConfigError("index %d out of bounds for List"%index) gsobject, safe1 = BuildGSObject(items, index, base, gsparams, logger) safe = safe and safe1 @@ -368,7 +367,8 @@ def _BuildOpticalPSF(config, base, ignore, gsparams, logger): aber_list = [0.0] * 4 # Initial 4 values are ignored. aberrations = config['aberrations'] if not isinstance(aberrations,list): - raise AttributeError("aberrations entry for config.OpticalPSF entry is not a list.") + raise galsim.GalSimConfigError( + "aberrations entry for config.OpticalPSF entry is not a list.") for i in range(len(aberrations)): value, safe1 = galsim.config.ParseValue(aberrations, i, base, float) aber_list.append(value) diff --git a/galsim/config/image.py b/galsim/config/image.py index 58fb95036b2..f1faeaa009c 100644 --- a/galsim/config/image.py +++ b/galsim/config/image.py @@ -122,7 +122,7 @@ def SetupConfigImageNum(config, image_num, obj_num, logger=None): config['image'] = {} image = config['image'] if not isinstance(image, dict): - raise AttributeError("config.image is not a dict.") + raise galsim.GalSimConfigError("config.image is not a dict.") if 'file_num' not in config: config['file_num'] = 0 @@ -131,7 +131,7 @@ def SetupConfigImageNum(config, image_num, obj_num, logger=None): image['type'] = 'Single' image_type = image['type'] if image_type not in valid_image_types: - raise AttributeError("Invalid image.type=%s."%image_type) + raise galsim.GalSimConfigValueError("Invalid image.type.", image_type, valid_image_types) # In case this hasn't been done yet. galsim.config.SetupInput(config, logger) @@ -168,12 +168,13 @@ def SetupConfigImageSize(config, xsize, ysize, logger=None): origin = 1 # default if 'index_convention' in image: convention = galsim.config.ParseValue(image,'index_convention',config,str)[0] - if convention.lower() in [ '0', 'c', 'python' ]: + if convention.lower() in ('0', 'c', 'python'): origin = 0 - elif convention.lower() in [ '1', 'fortran', 'fits' ]: + elif convention.lower() in ('1', 'fortran', 'fits'): origin = 1 else: - raise AttributeError("Unknown index_convention: %s"%convention) + raise galsim.GalSimConfigValueError("Unknown index_convention", convention, + ('0', 'c', 'python', '1', 'fortran', 'fits')) config['image_origin'] = galsim.PositionI(origin,origin) config['image_center'] = galsim.PositionD( origin + (xsize-1.)/2., origin + (ysize-1.)/2. ) @@ -285,7 +286,7 @@ def GetNObjForImage(config, image_num): image = config.get('image',{}) image_type = image.get('type','Single') if image_type not in valid_image_types: - raise AttributeError("Invalid image.type=%s."%image_type) + raise galsim.GalSimConfigValueError("Invalid image.type.", image_type, valid_image_types) return valid_image_types[image_type].getNObj(image,config,image_num) @@ -355,7 +356,7 @@ def MakeImageTasks(config, jobs, logger): image = config.get('image', {}) image_type = image.get('type', 'Single') if image_type not in valid_image_types: - raise AttributeError("Invalid image.type=%s."%image_type) + raise galsim.GalSimConfigValueError("Invalid image.type.", image_type, valid_image_types) return valid_image_types[image_type].makeTasks(image, config, jobs, logger) @@ -398,8 +399,8 @@ def setup(self, config, base, image_num, obj_num, ignore, logger): xsize = params.get('xsize',size) ysize = params.get('ysize',size) if (xsize == 0) != (ysize == 0): - raise AttributeError( - "Both (or neither) of image.xsize and image.ysize need to be defined and != 0.") + raise galsim.GalSimConfigError( + "Both (or neither) of image.xsize and image.ysize need to be defined and != 0.") # We allow world_pos to be in config[image], but we don't want it to lead to a final_shift # in BuildStamp. To mark this, we set image_pos to (0,0) diff --git a/galsim/config/image_scattered.py b/galsim/config/image_scattered.py index 9aaabe3e8f8..9d42d75d133 100644 --- a/galsim/config/image_scattered.py +++ b/galsim/config/image_scattered.py @@ -52,28 +52,19 @@ def setup(self, config, base, image_num, obj_num, ignore, logger): opt = { 'size' : int , 'xsize' : int , 'ysize' : int } params = galsim.config.GetAllParams(config, base, opt=opt, ignore=ignore+extra_ignore)[0] - # Special check for the size. Either size or both xsize and ysize is required. - if 'size' not in params: - if 'xsize' not in params or 'ysize' not in params: - raise AttributeError( - "Either size or both xsize and ysize is required for image.type=Scattered") - full_xsize = params['xsize'] - full_ysize = params['ysize'] - else: - if 'xsize' in params: - raise AttributeError( - "Attributes xsize is invalid if size is set for image.type=Scattered") - if 'ysize' in params: - raise AttributeError( - "Attributes ysize is invalid if size is set for image.type=Scattered") - full_xsize = params['size'] - full_ysize = params['size'] + size = params.get('size',0) + full_xsize = params.get('xsize',size) + full_ysize = params.get('ysize',size) + + if (full_xsize <= 0) or (full_ysize <= 0): + raise galsim.GalSimConfigError( + "Both image.xsize and image.ysize need to be defined and > 0.") # If image_force_xsize and image_force_ysize were set in config, make sure it matches. if ( ('image_force_xsize' in base and full_xsize != base['image_force_xsize']) or ('image_force_ysize' in base and full_ysize != base['image_force_ysize']) ): - raise ValueError( - "Unable to reconcile required image xsize and ysize with provided "+ + raise galsim.GalSimConfigError( + "Unable to reconcile required image xsize and ysize with provided " "xsize=%d, ysize=%d, "%(full_xsize,full_ysize)) return full_xsize, full_ysize @@ -101,7 +92,9 @@ def buildImage(self, config, base, image_num, obj_num, logger): base['current_image'] = full_image if 'image_pos' in config and 'world_pos' in config: - raise AttributeError("Both image_pos and world_pos specified for Scattered image.") + raise galsim.GalSimConfigValueError( + "Both image_pos and world_pos specified for Scattered image.", + (config['image_pos'], config['world_pos'])) if 'image_pos' not in config and 'world_pos' not in config: xmin = base['image_origin'].x @@ -130,9 +123,9 @@ def buildImage(self, config, base, image_num, obj_num, logger): full_image[bounds] += stamps[k][bounds] else: logger.info( - "Object centered at (%d,%d) is entirely off the main image,\n"%( - stamps[k].center.x, stamps[k].center.y) + + "Object centered at (%d,%d) is entirely off the main image, " "whose bounds are (%d,%d,%d,%d)."%( + stamps[k].center.x, stamps[k].center.y, full_image.bounds.xmin, full_image.bounds.xmax, full_image.bounds.ymin, full_image.bounds.ymax)) @@ -190,7 +183,8 @@ def getNObj(self, config, base, image_num): if 'nobjects' not in config: nobj = galsim.config.ProcessInputNObjects(base) if nobj is None: - raise AttributeError("Attribute nobjects is required for image.type = Scattered") + raise galsim.GalSimConfigError( + "Attribute nobjects is required for image.type = Scattered") else: nobj = galsim.config.ParseValue(config,'nobjects',base,int)[0] base['index_key'] = orig_index_key diff --git a/galsim/config/image_tiled.py b/galsim/config/image_tiled.py index 86a5b0627e2..16c3d2b8f04 100644 --- a/galsim/config/image_tiled.py +++ b/galsim/config/image_tiled.py @@ -57,9 +57,9 @@ def setup(self, config, base, image_num, obj_num, ignore, logger): self.stamp_xsize = params.get('stamp_xsize',stamp_size) self.stamp_ysize = params.get('stamp_ysize',stamp_size) - if (self.stamp_xsize == 0) or (self.stamp_ysize == 0): - raise AttributeError( - "Both image.stamp_xsize and image.stamp_ysize need to be defined and != 0.") + if (self.stamp_xsize <= 0) or (self.stamp_ysize <= 0): + raise galsim.GalSimConfigError( + "Both image.stamp_xsize and image.stamp_ysize need to be defined and > 0.") border = params.get("border",0) self.xborder = params.get("xborder",border) @@ -82,12 +82,12 @@ def setup(self, config, base, image_num, obj_num, ignore, logger): # If image_force_xsize and image_force_ysize were set in config, make sure it matches. if ( ('image_force_xsize' in base and full_xsize != base['image_force_xsize']) or ('image_force_ysize' in base and full_ysize != base['image_force_ysize']) ): - raise ValueError( - "Unable to reconcile required image xsize and ysize with provided "+ - "nx_tiles=%d, ny_tiles=%d, "%(self.nx_tiles,self.ny_tiles) + - "xborder=%d, yborder=%d\n"%(self.xborder,self.yborder) + - "Calculated full_size = (%d,%d) "%(full_xsize,full_ysize)+ - "!= required (%d,%d)."%(base['image_force_xsize'],base['image_force_ysize'])) + raise galsim.GalSimConfigError( + "Unable to reconcile required image xsize and ysize with provided " + "nx_tiles=%d, ny_tiles=%d, xborder=%d, yborder=%d\n" + "Calculated full_size = (%d,%d) != required (%d,%d)."%( + self.nx_tiles, self.ny_tiles, self.xborder, self.yborder, + full_xsize, full_ysize, base['image_force_xsize'],base['image_force_ysize'])) return full_xsize, full_ysize @@ -133,7 +133,7 @@ def buildImage(self, config, base, image_num, obj_num, logger): rng = galsim.config.GetRNG(config, base, logger, 'TiledImage, order = '+order) galsim.random.permute(rng, ix_list, iy_list) else: - raise ValueError("Invalid order. Must be row, column, or random") + raise galsim.GalSimConfigValueError("Invalid order.", order, ('row', 'col', 'random')) # Define a 'image_pos' field so the stamps can set their position appropriately in case # we need it for PowerSpectum or NFWHalo. @@ -219,7 +219,7 @@ def getNObj(self, config, base, image_num): base['image_num'] = image_num if 'nx_tiles' not in config or 'ny_tiles' not in config: - raise AttributeError( + raise galsim.GalSimConfigError( "Attributes nx_tiles and ny_tiles are required for image.type = Tiled") nx = galsim.config.ParseValue(config,'nx_tiles',base,int)[0] ny = galsim.config.ParseValue(config,'ny_tiles',base,int)[0] diff --git a/galsim/config/input.py b/galsim/config/input.py index b1115a97acb..55ef564e9c5 100644 --- a/galsim/config/input.py +++ b/galsim/config/input.py @@ -46,12 +46,12 @@ def ProcessInput(config, logger=None, file_scope_only=False, safe_only=False): any objects that need to be initialized. Each item registered as a valid input type will be built and available at the top level - of config in config['input_objs']. Since there is allowed to be more than one of each type + of config in config['_input_objs']. Since there is allowed to be more than one of each type of input object (e.g. multilpe catalogs or multiple dicts), these are actually lists. If there is only one e.g. catalog entry in config['input'], then this list will have one element. - e.g. config['input_objs']['catalog'][0] holds the first catalog item defined in + e.g. config['_input_objs']['catalog'][0] holds the first catalog item defined in config['input']['catalog'] (if any). @param config The configuration dict to process @@ -97,7 +97,7 @@ def ProcessInput(config, logger=None, file_scope_only=False, safe_only=False): ('output' in config and 'nproc' in config['output'] and galsim.config.ParseValue(config['output'], 'nproc', config, int)[0] != 1) ) ) - if use_manager and 'input_manager' not in config: + if use_manager and '_input_manager' not in config: from multiprocessing.managers import BaseManager class InputManager(BaseManager): pass @@ -109,16 +109,16 @@ class InputManager(BaseManager): pass tag = key + str(i) InputManager.register(tag, valid_input_types[key].init_func) # Start up the input_manager - config['input_manager'] = InputManager() - config['input_manager'].start() + config['_input_manager'] = InputManager() + config['_input_manager'].start() - if 'input_objs' not in config: - config['input_objs'] = {} + if '_input_objs' not in config: + config['_input_objs'] = {} for key in all_keys: fields = config['input'][key] nfields = len(fields) if isinstance(fields, list) else 1 - config['input_objs'][key] = [ None for i in range(nfields) ] - config['input_objs'][key+'_safe'] = [ None for i in range(nfields) ] + config['_input_objs'][key] = [ None for i in range(nfields) ] + config['_input_objs'][key+'_safe'] = [ None for i in range(nfields) ] # Read all input fields provided and create the corresponding object # with the parameters given in the config file. @@ -136,8 +136,8 @@ class InputManager(BaseManager): pass for i in range(len(fields)): field = fields[i] - input_objs = config['input_objs'][key] - input_objs_safe = config['input_objs'][key+'_safe'] + input_objs = config['_input_objs'][key] + input_objs_safe = config['_input_objs'][key+'_safe'] logger.debug('file %d: Current values for %s are %s, safe = %s', file_num, key, str(input_objs[i]), input_objs_safe[i]) if input_objs[i] is not None and input_objs_safe[i]: @@ -172,7 +172,7 @@ class InputManager(BaseManager): pass logger.debug('file %d: %s kwargs = %s',file_num,key,kwargs) if use_manager: tag = key + str(i) - input_obj = getattr(config['input_manager'],tag)(**kwargs) + input_obj = getattr(config['_input_manager'],tag)(**kwargs) else: input_obj = loader.init_func(**kwargs) @@ -208,7 +208,7 @@ def SetupInput(config, logger=None): at BuildImage say. This will make sure the input objects are set up in the way that they normally would have been by the first level of processing in a `galsim config_file` run. """ - if 'input_objs' not in config: + if '_input_objs' not in config: orig_index_key = config.get('index_key',None) config['index_key'] = 'file_num' ProcessInput(config, logger=logger) @@ -246,8 +246,8 @@ def ProcessInputNObjects(config, logger=None): # If it's a list, just use the first one. if isinstance(field, list): field = field[0] - if key in config['input_objs'] and config['input_objs'][key+'_safe'][0]: - input_obj = config['input_objs'][key][0] + if key in config['_input_objs'] and config['_input_objs'][key+'_safe'][0]: + input_obj = config['_input_objs'][key][0] else: kwargs, safe = loader.getKwargs(field, config, logger) kwargs['_nobjects_only'] = True @@ -271,7 +271,7 @@ def SetupInputsForImage(config, logger=None): loader = valid_input_types[key] if key in config['input']: fields = config['input'][key] - input_objs = config['input_objs'][key] + input_objs = config['_input_objs'][key] # Make fields a list if necessary. if not isinstance(fields, list): fields = [ fields ] @@ -292,8 +292,9 @@ def GetInputObj(input_type, config, base, param_name): @param param_name The type of value that we are trying to construct (only used for error messages). """ - if 'input_objs' not in base or input_type not in base['input_objs']: - raise ValueError("No input %s available for type = %s"%(input_type,param_name)) + if '_input_objs' not in base or input_type not in base['_input_objs']: + raise galsim.GalSimConfigError( + "No input %s available for type = %s"%(input_type,param_name)) if 'num' in config: num = galsim.config.ParseValue(config, 'num', base, int)[0] @@ -301,11 +302,12 @@ def GetInputObj(input_type, config, base, param_name): num = 0 if num < 0: - raise ValueError("Invalid num < 0 supplied for %s: num = %d"%(param_name,num)) - if num >= len(base['input_objs'][input_type]): - raise ValueError("Invalid num supplied for %s (too large): num = %d"%(param_name,num)) + raise galsim.GalSimConfigValueError("Invalid num < 0 supplied for %s."%param_name, num) + if num >= len(base['_input_objs'][input_type]): + raise galsim.GalSimConfigValueError("Invalid num supplied for %s (too large)"%param_name, + num) - return base['input_objs'][input_type][num] + return base['_input_objs'][input_type][num] class InputLoader(object): diff --git a/galsim/config/input_cosmos.py b/galsim/config/input_cosmos.py index 2eabd816cff..ee8896bda93 100644 --- a/galsim/config/input_cosmos.py +++ b/galsim/config/input_cosmos.py @@ -71,24 +71,20 @@ def _BuildCOSMOSGalaxy(config, base, ignore, gsparams, logger): if 'index' in config: galsim.config.SetDefaultIndex(config, cosmos_cat.getNObjects()) - kwargs, safe = galsim.config.GetAllParams(config, base, - req = galsim.COSMOSCatalog.makeGalaxy._req_params, - opt = galsim.COSMOSCatalog.makeGalaxy._opt_params, - single = galsim.COSMOSCatalog.makeGalaxy._single_params, - ignore = ignore) + opt = { "index" : int, + "gal_type" : str, + "noise_pad_size" : float, + "deep" : bool, + "sersic_prec": float, + "n_random": int + } + + kwargs, safe = galsim.config.GetAllParams(config, base, opt=opt, ignore=ignore) if gsparams: kwargs['gsparams'] = galsim.GSParams(**gsparams) - # Deal with defaults for gal_type, if it wasn't specified: - # If COSMOSCatalog was constructed with 'use_real'=True, then default is 'real'. Otherwise, the - # default is 'parametric'. This code is in makeGalaxy, but since config has to use - # _makeSingleGalaxy, we have to include this here too. - if 'gal_type' not in kwargs: - if cosmos_cat.use_real: kwargs['gal_type'] = 'real' - else: kwargs['gal_type'] = 'parametric' + rng = galsim.config.GetRNG(config, base, logger, 'COSMOSGalaxy') - rng = None if 'index' not in kwargs: - rng = galsim.config.GetRNG(config, base, logger, 'COSMOSGalaxy') kwargs['index'], n_rng_calls = cosmos_cat.selectRandomIndex(1, rng=rng, _n_rng_calls=True) # Make sure this process gives consistent results regardless of the number of processes @@ -98,29 +94,24 @@ def _BuildCOSMOSGalaxy(config, base, ignore, gsparams, logger): # discard the same number of random calls from the one in the config dict. rng.discard(int(n_rng_calls)) - # Even though gal_type is optional, it will have been set in the code above. So we can at this - # point assume that kwargs['gal_type'] exists. - if kwargs['gal_type'] == 'real': - if rng is None: - rng = galsim.config.GetRNG(config, base, logger, 'COSMOSGalaxy') - kwargs['rng'] = rng + kwargs['rng'] = rng # NB. Even though index is officially optional, it will always be present, either because it was # set by a call to selectRandomIndex, explicitly by the user, or due to the call to # SetDefaultIndex. index = kwargs['index'] if index >= cosmos_cat.getNObjects(): - raise IndexError( - "%s index has gone past the number of entries in the catalog"%index) + raise galsim.GalSimConfigError( + "index=%s has gone past the number of entries in the COSMOSCatalog"%index) logger.debug('obj %d: COSMOSGalaxy kwargs = %s',base.get('obj_num',0),kwargs) - kwargs['cosmos_catalog'] = cosmos_cat + kwargs['self'] = cosmos_cat # Use a staticmethod of COSMOSCatalog to avoid pickling the result of makeGalaxy() # The RealGalaxy in particular has a large serialization, so it is more efficient to # make it in this process, which is what happens here. - gal = galsim.COSMOSCatalog._makeSingleGalaxy(**kwargs) + gal = galsim.COSMOSCatalog._makeGalaxy(**kwargs) return gal, safe diff --git a/galsim/config/input_nfw.py b/galsim/config/input_nfw.py index 19fdf13bca7..5846514ef09 100644 --- a/galsim/config/input_nfw.py +++ b/galsim/config/input_nfw.py @@ -19,7 +19,6 @@ from __future__ import print_function import galsim -import warnings # This file adds input type nfw_halo and value types NFWHaloShear and NFWHaloMagnification. @@ -44,11 +43,11 @@ def _GenerateFromNFWHaloShear(config, base, value_type): logger = nfw_halo.logger if 'world_pos' not in base: - raise ValueError("NFWHaloShear requested, but no position defined.") + raise galsim.GalSimConfigError("NFWHaloShear requested, but no position defined.") pos = base['world_pos'] if 'gal' not in base or 'redshift' not in base['gal']: - raise ValueError("NFWHaloShear requested, but no gal.redshift defined.") + raise galsim.GalSimConfigError("NFWHaloShear requested, but no gal.redshift defined.") redshift = galsim.config.GetCurrentValue('redshift', base['gal'], float, base) # There aren't any parameters for this, so just make sure num is the only (optional) @@ -77,11 +76,12 @@ def _GenerateFromNFWHaloMagnification(config, base, value_type): logger = nfw_halo.logger if 'world_pos' not in base: - raise ValueError("NFWHaloMagnification requested, but no position defined.") + raise galsim.GalSimConfigError("NFWHaloMagnification requested, but no position defined.") pos = base['world_pos'] if 'gal' not in base or 'redshift' not in base['gal']: - raise ValueError("NFWHaloMagnification requested, but no gal.redshift defined.") + raise galsim.GalSimConfigError( + "NFWHaloMagnification requested, but no gal.redshift defined.") redshift = galsim.config.GetCurrentValue('redshift', base['gal'], float, base) opt = { 'max_mu' : float, 'num' : int } @@ -89,8 +89,8 @@ def _GenerateFromNFWHaloMagnification(config, base, value_type): max_mu = kwargs.get('max_mu', 25.) if not max_mu > 0.: - raise ValueError( - "Invalid max_mu=%f (must be > 0) for type = NFWHaloMagnification"%max_mu) + raise galsim.GalSimConfigValueError( + "Invalid max_mu for type = NFWHaloMagnification (must be > 0)", max_mu) mu = nfw_halo.getMagnification(pos,redshift) if mu < 0 or mu > max_mu: diff --git a/galsim/config/input_powerspectrum.py b/galsim/config/input_powerspectrum.py index 0cca4ad5d71..526d72b1c17 100644 --- a/galsim/config/input_powerspectrum.py +++ b/galsim/config/input_powerspectrum.py @@ -109,7 +109,8 @@ def setupImage(self, input_obj, config, base, logger=None): scale = base['wcs'].maxLinearScale(base['image_center']) grid_spacing = grid_size * scale else: - raise AttributeError("power_spectrum.grid_spacing required for non-tiled images") + raise galsim.GalSimConfigError( + "power_spectrum.grid_spacing required for non-tiled images") if 'ngrid' in config: ngrid = galsim.config.ParseValue(config, 'ngrid', base, float)[0] @@ -155,7 +156,7 @@ def setupImage(self, input_obj, config, base, logger=None): # We don't care about the output here. This just builds the grid, which we'll # access for each object using its position. - logger.debug('image %d: PowerSpectrum buildGrid(grid_spacing=%s, ngrid=%s, center=%s, ' + + logger.debug('image %d: PowerSpectrum buildGrid(grid_spacing=%s, ngrid=%s, center=%s, ' 'interpolant=%s, variance=%s)', base.get('image_num',0), grid_spacing, ngrid, center, interpolant, variance) input_obj.buildGrid(grid_spacing=grid_spacing, ngrid=ngrid, center=center, @@ -183,7 +184,7 @@ def _GenerateFromPowerSpectrumShear(config, base, value_type): logger = power_spectrum.logger if 'world_pos' not in base: - raise ValueError("PowerSpectrumShear requested, but no position defined.") + raise galsim.GalSimConfigError("PowerSpectrumShear requested, but no position defined.") pos = base['world_pos'] # There aren't any parameters for this, so just make sure num is the only (optional) @@ -223,7 +224,8 @@ def _GenerateFromPowerSpectrumMagnification(config, base, value_type): logger = power_spectrum.logger if 'world_pos' not in base: - raise ValueError("PowerSpectrumMagnification requested, but no position defined.") + raise galsim.GalSimConfigError( + "PowerSpectrumMagnification requested, but no position defined.") pos = base['world_pos'] opt = { 'max_mu' : float, 'num' : int } @@ -240,8 +242,8 @@ def _GenerateFromPowerSpectrumMagnification(config, base, value_type): max_mu = kwargs.get('max_mu', 25.) if not max_mu > 0.: - raise ValueError( - "Invalid max_mu=%f (must be > 0) for type = PowerSpectrumMagnification"%max_mu) + raise galsim.GalSimConfigValueError( + "Invalid max_mu for type = PowerSpectrumMagnification (must be > 0)", max_mu) if mu < 0 or mu > max_mu: logger.warning('obj %d: Warning: PowerSpectrum mu = %f means strong lensing. '%( diff --git a/galsim/config/input_real.py b/galsim/config/input_real.py index 46f94ac84e8..542a85f476d 100644 --- a/galsim/config/input_real.py +++ b/galsim/config/input_real.py @@ -54,9 +54,9 @@ def _BuildRealGalaxy(config, base, ignore, gsparams, logger, param_name='RealGal if 'index' in kwargs: index = kwargs['index'] - if index >= real_cat.getNObjects(): - raise IndexError( - "%s index has gone past the number of entries in the catalog"%index) + if index >= real_cat.getNObjects() or index < 0: + raise galsim.GalSimConfigError( + "index=%s has gone past the number of entries in the RealGalaxyCatalog"%index) kwargs['real_galaxy_catalog'] = real_cat logger.debug('obj %d: %s kwargs = %s',base.get('obj_num',0),param_name,kwargs) diff --git a/galsim/config/noise.py b/galsim/config/noise.py index 8976f3c34a1..a6025091813 100644 --- a/galsim/config/noise.py +++ b/galsim/config/noise.py @@ -56,16 +56,15 @@ def AddNoise(config, im, current_var=0., logger=None): logger = galsim.config.LoggerWrapper(logger) if 'noise' in config['image']: noise = config['image']['noise'] - else: - # No noise. + else: # No noise. return + if not isinstance(noise, dict): + raise galsim.GalSimConfigError("image.noise is not a dict.") - if 'type' in noise: - noise_type = noise['type'] - else: - noise_type = 'Poisson' # Default is Poisson + # Default is Poisson + noise_type = noise.get('type', 'Poisson') if noise_type not in valid_noise_types: - raise AttributeError("Invalid type %s for noise"%noise_type) + raise galsim.GalSimConfigValueError("Invalid noise.type.", noise_type, valid_noise_types) # We need to use image_num for the index_key, but if we are running this from the stamp # building phase, then we want to use obj_num_rng for the noise rng. So get the rng now @@ -94,14 +93,11 @@ def CalculateNoiseVariance(config): """ noise = config['image']['noise'] if not isinstance(noise, dict): - raise AttributeError("image.noise is not a dict.") + raise galsim.GalSimConfigError("image.noise is not a dict.") - if 'type' in noise: - noise_type = noise['type'] - else: - noise_type = 'Poisson' # Default is Poisson + noise_type = noise.get('type', 'Poisson') if noise_type not in valid_noise_types: - raise AttributeError("Invalid type %s for noise"%noise_type) + raise galsim.GalSimConfigValueError("Invalid noise.type.", noise_type, valid_noise_types) index, orig_index_key = galsim.config.GetIndex(noise, config) config['index_key'] = 'image_num' @@ -130,16 +126,14 @@ def AddNoiseVariance(config, im, include_obj_var=False, logger=None): logger = galsim.config.LoggerWrapper(logger) if 'noise' in config['image']: noise = config['image']['noise'] - else: - # No noise. + else: # No noise. return + if not isinstance(noise, dict): + raise galsim.GalSimConfigError("image.noise is not a dict.") - if 'type' in noise: - noise_type = noise['type'] - else: - noise_type = 'Poisson' # Default is Poisson + noise_type = noise.get('type', 'Poisson') if noise_type not in valid_noise_types: - raise AttributeError("Invalid type %s for noise"%noise_type) + raise galsim.GalSimConfigValueError("Invalid noise.type.", noise_type, valid_noise_types) index, orig_index_key = galsim.config.GetIndex(noise, config) config['index_key'] = 'image_num' @@ -159,7 +153,9 @@ def GetSky(config, base, logger=None): logger = galsim.config.LoggerWrapper(logger) if 'sky_level' in config: if 'sky_level_pixel' in config: - raise AttributeError("Cannot specify both sky_level and sky_level_pixel") + raise galsim.GalSimConfigValueError( + "Cannot specify both sky_level and sky_level_pixel", + (config['sky_level'], config['sky_level_pixel'])) sky_level = galsim.config.ParseValue(config,'sky_level',base,float)[0] logger.debug('image %d, obj %d: sky_level = %f', base.get('image_num',0),base.get('obj_num',0), sky_level) @@ -268,7 +264,7 @@ def addNoise(self, config, base, im, rng, current_var, draw_method, logger): logger.debug('image %d, obj %d: Target variance is %f, current variance is %f', base.get('image_num',0),base.get('obj_num',0),var,current_var) if var < current_var: - raise RuntimeError( + raise galsim.GalSimConfigError( "Whitening already added more noise than the requested Gaussian noise.") var -= current_var @@ -324,7 +320,7 @@ def addNoise(self, config, base, im, rng, current_var, draw_method, logger): else: test = (total_sky < current_var) if test: - raise RuntimeError( + raise galsim.GalSimConfigError( "Whitening already added more noise than the requested Poisson noise.") total_sky -= current_var extra_sky -= current_var @@ -428,7 +424,7 @@ def addNoise(self, config, base, im, rng, current_var, draw_method, logger): target_var, current_var) test = target_var < current_var if test: - raise RuntimeError( + raise galsim.GalSimConfigError( "Whitening already added more noise than the requested CCD noise.") if read_noise_var_adu >= current_var: # First try to take away from the read_noise, since this one is actually Gaussian. @@ -540,7 +536,7 @@ def addNoise(self, config, base, im, rng, current_var, draw_method, logger): logger.debug('image %d, obj %d: Target variance is %f, current variance is %f', base.get('image_num',0),base.get('obj_num',0), var, current_var) if var < current_var: - raise RuntimeError( + raise galsim.GalSimConfigError( "Whitening already added more noise than the requested COSMOS noise.") cn -= galsim.UncorrelatedNoise(current_var, rng=rng, wcs=cn.wcs) diff --git a/galsim/config/output.py b/galsim/config/output.py index e2fad2963f3..ce3f07ddd3e 100644 --- a/galsim/config/output.py +++ b/galsim/config/output.py @@ -20,6 +20,8 @@ import galsim import logging +from ..utilities import ensure_dir + # This file handles building the output files according to the specifications in config['output']. # This file includes the basic functionality, but it calls out to helper functions for the # different types of output files. It includes the implementation of the default output type, @@ -116,8 +118,9 @@ def BuildFiles(nfiles, config, file_num=0, logger=None, except_abort=False): def done_func(logger, proc, k, result, t2): file_num, file_name = info[k] file_name2, t = result # This is the t for which 0 means the file was skipped. - if file_name2 != file_name: - raise RuntimeError("Files seem to be out of sync. %s != %s",file_name, file_name2) + if file_name2 != file_name: # pragma: no cover (I think this should never happen.) + raise galsim.GalSimError("Files seem to be out of sync. %s != %s", + file_name, file_name2) if t != 0 and logger: if proc is None: s0 = '' else: s0 = '%s: '%proc @@ -208,7 +211,7 @@ def BuildFile(config, file_num=0, image_num=0, obj_num=0, logger=None): if ('noclobber' in output and galsim.config.ParseValue(output, 'noclobber', config, bool)[0] and os.path.isfile(file_name)): - logger.warning('Skipping file %d = %s because output.noclobber = True' + + logger.warning('Skipping file %d = %s because output.noclobber = True' ' and file exists',file_num,file_name) return file_name, 0 @@ -260,7 +263,8 @@ def GetNFiles(config): output = config.get('output',{}) output_type = output.get('type','Fits') if output_type not in valid_output_types: - raise AttributeError("Invalid output.type=%s."%output_type) + raise galsim.GalSimConfigValueError("Invalid output.type.", output_type, + valid_output_types) return valid_output_types[output_type].getNFiles(output, config) @@ -277,7 +281,8 @@ def GetNImagesForFile(config, file_num): output = config.get('output',{}) output_type = output.get('type','Fits') if output_type not in valid_output_types: - raise AttributeError("Invalid output.type=%s."%output_type) + raise galsim.GalSimConfigValueError("Invalid output.type.", output_type, + valid_output_types) return valid_output_types[output_type].getNImages(output, config, file_num) @@ -295,7 +300,8 @@ def GetNObjForFile(config, file_num, image_num): output = config.get('output',{}) output_type = output.get('type','Fits') if output_type not in valid_output_types: - raise AttributeError("Invalid output.type=%s."%output_type) + raise galsim.GalSimConfigValueError("Invalid output.type.", output_type, + valid_output_types) return valid_output_types[output_type].getNObjPerImage(output, config, file_num, image_num) @@ -336,7 +342,8 @@ def SetupConfigFileNum(config, file_num, image_num, obj_num, logger=None): # Check that the type is valid output_type = config['output']['type'] if output_type not in valid_output_types: - raise AttributeError("Invalid output.type=%s."%output_type) + raise galsim.GalSimConfigValueError("Invalid output.type.", output_type, + valid_output_types) def SetDefaultExt(config, default_ext): @@ -359,12 +366,12 @@ def RetryIO(func, args, ntries, file_name, logger): itry += 1 try: ret = func(*args) - except IOError as e: + except (IOError, OSError) as e: if itry == ntries: # Then this was the last try. Just re-raise the exception. raise else: - logger.warning('File %s: Caught IOError: %s',file_name,str(e)) + logger.warning('File %s: Caught OSError: %s',file_name,str(e)) logger.warning('This is try %d/%d, so sleep for %d sec and try again.', itry,ntries,itry) import time @@ -375,35 +382,6 @@ def RetryIO(func, args, ntries, file_name, logger): return ret -def EnsureDir(target): - """ - Make sure the directory for the target location exists, watching for a race condition - - In particular check if the OS reported that the directory already exists when running - makedirs, which can happen if another process creates it before this one can - """ - - _ERR_FILE_EXISTS=17 - dir = os.path.dirname(target) - if dir == '': return - - exists = os.path.exists(dir) - if not exists: - try: - os.makedirs(dir) - except OSError as err: - # check if the file now exists, which can happen if some other - # process created the directory between the os.path.exists call - # above and the time of the makedirs attempt. This is OK - if err.errno != _ERR_FILE_EXISTS: - raise err - - elif exists and not os.path.isdir(dir): - raise IOError("tried to make directory '%s' " - "but a non-directory file of that " - "name already exists" % dir) - - class OutputBuilder(object): """A base class for building and writing the output objects. @@ -447,14 +425,15 @@ def getFilename(self, config, base, logger): # If a file_name isn't specified, we use the name of the config file + '.fits' file_name = base['root'] + self.default_ext else: - raise AttributeError("No file_name specified and unable to generate it automatically.") + raise galsim.GalSimConfigError( + "No file_name specified and unable to generate it automatically.") # Prepend a dir to the beginning of the filename if requested. if 'dir' in config: dir = galsim.config.ParseValue(config, 'dir', base, str)[0] file_name = os.path.join(dir,file_name) - EnsureDir(file_name) + ensure_dir(file_name) return file_name diff --git a/galsim/config/output_datacube.py b/galsim/config/output_datacube.py index ad77c0db018..4dff3baa37f 100644 --- a/galsim/config/output_datacube.py +++ b/galsim/config/output_datacube.py @@ -21,6 +21,7 @@ import logging from .output import OutputBuilder + class DataCubeBuilder(OutputBuilder): """Builder class for constructing and writing DataCube output types. """ @@ -95,7 +96,8 @@ def getNImages(self, config, base, file_num): if nimages: config['nimages'] = nimages if 'nimages' not in config: - raise AttributeError("Attribute output.nimages is required for output.type = MultiFits") + raise galsim.GalSimConfigError( + "Attribute output.nimages is required for output.type = MultiFits") return galsim.config.ParseValue(config,'nimages',base,int)[0] def writeFile(self, data, file_name, config, base, logger): diff --git a/galsim/config/output_multifits.py b/galsim/config/output_multifits.py index 082b231ee10..10f61b5f38a 100644 --- a/galsim/config/output_multifits.py +++ b/galsim/config/output_multifits.py @@ -67,7 +67,8 @@ def getNImages(self, config, base, file_num): if nimages: config['nimages'] = nimages if 'nimages' not in config: - raise AttributeError("Attribute output.nimages is required for output.type = MultiFits") + raise galsim.GalSimConfigError( + "Attribute output.nimages is required for output.type = MultiFits") return galsim.config.ParseValue(config,'nimages',base,int)[0] diff --git a/galsim/config/process.py b/galsim/config/process.py index a7c6ab35d39..0836617c663 100644 --- a/galsim/config/process.py +++ b/galsim/config/process.py @@ -20,20 +20,7 @@ import galsim import logging import copy - - -# Python 2.6 doesn't include OrderedDict natively. There is a package ordereddict that you -# can pip install. But if the user hasn't done that, we'll just read into a regular dict. -# The only feature that requires the OrderedDict is the truth catalog output. With a regular -# dict the columns will appear in arbitrary order. -try: - from collections import OrderedDict -except ImportError: - try: - from ordereddict import OrderedDict - except ImportError: - OrderedDict = dict - +from collections import OrderedDict def MergeConfig(config1, config2, logger=None): """ @@ -123,20 +110,8 @@ def ReadJson(config_file): import json with open(config_file) as f: - try: - # cf. http://stackoverflow.com/questions/6921699/can-i-get-json-to-load-into-an-ordereddict-in-python - config = json.load(f, object_pairs_hook=OrderedDict) - except TypeError: # pragma: no cover - # for python2.6, json doesn't come with the object_pairs_hook, so - # try using simplejson, and if that doesn't work, just use a regular dict. - # Also, it seems that if the above line raises an exception, the file handle - # is not left at the beginning, so seek back to 0. - f.seek(0) - try: - import simplejson - config = simplejson.load(f, object_pairs_hook=OrderedDict) - except ImportError: - config = json.load(f) + # cf. http://stackoverflow.com/questions/6921699/can-i-get-json-to-load-into-an-ordereddict-in-python + config = json.load(f, object_pairs_hook=OrderedDict) # JSON files only ever define a single job, but we need to return a list with this one item. return [config] @@ -283,7 +258,7 @@ def CopyConfig(config): config1 = copy.copy(config) # Make sure the input_manager isn't in the copy - config1.pop('input_manager',None) + config1.pop('_input_manager',None) # Now deepcopy all the regular config fields to make sure things like current don't # get clobbered by two processes writing to the same dict. Also the rngs. @@ -604,7 +579,8 @@ def ParseExtendedKey(config, key): except (TypeError, KeyError): # TypeError for the case where d is a float or Position2D, so d[k] is invalid. # KeyError for the case where d is a dict, but k is not a valid key. - raise ValueError("Unable to parse extended key %s. Field %s is invalid."%(key,k)) + raise galsim.GalSimConfigError( + "Unable to parse extended key %s. Field %s is invalid."%(key,k)) return d, k def GetFromConfig(config, key): @@ -623,7 +599,8 @@ def GetFromConfig(config, key): try: value = d[k] except Exception as e: - raise ValueError("Unable to parse extended key %s. Field %s is invalid."%(key,k)) + raise galsim.GalSimConfigError( + "Unable to parse extended key %s. Field %s is invalid."%(key,k)) return value def SetInConfig(config, key, value): @@ -646,7 +623,8 @@ def SetInConfig(config, key, value): try: d[k] = value except Exception as e: - raise ValueError("Unable to parse extended key %s. Field %s is invalid."%(key,k)) + raise galsim.GalSimConfigError( + "Unable to parse extended key %s. Field %s is invalid."%(key,k)) def UpdateConfig(config, new_params): @@ -742,11 +720,11 @@ def Process(config, logger=None, njobs=1, job=1, new_params=None, except_abort=F logger = LoggerWrapper(logger) import pprint if njobs < 1: - raise ValueError("Invalid number of jobs %d"%njobs) + raise galsim.GalSimValueError("Invalid number of jobs",njobs) if job < 1: - raise ValueError("Invalid job number %d. Must be >= 1."%job) + raise galsim.GalSimValueError("Invalid job number. Must be >= 1.",job) if job > njobs: - raise ValueError("Invalid job number %d. Must be <= njobs (%d)"%(job,njobs)) + raise galsim.GalSimValueError("Invalid job number. Must be <= njobs (%d)"%(njobs),job) # First thing to do is deep copy the input config to make sure we don't modify the original. config = CopyConfig(config) @@ -1029,7 +1007,7 @@ def GetIndex(config, base, is_sequence=False): if 'index_key' in config: index_key = config['index_key'] if index_key not in valid_index_keys: - raise AttributeError("Invalid index_key=%s."%index_key) + raise galsim.GalSimConfigValueError("Invalid index_key.", index_key, valid_index_keys) else: index_key = base.get('index_key','obj_num') if index_key == 'obj_num' and is_sequence: @@ -1061,13 +1039,18 @@ def GetRNG(config, base, logger=None, tag=''): index, index_key = GetIndex(config, base) logger.debug("GetRNG for %s: %s",index_key,index) - if 'rng_num' in config: - rng_num = config['rng_num'] + rng_num = config.get('rng_num', 0) + if rng_num != 0: if int(rng_num) != rng_num: - raise ValueError("rng_num must be an integer") - if not (index_key + '_rngs') in base: - raise AttributeError("rng_num is only allowed when image.random_seed is a list") - rng = base.get(index_key + '_rngs', None)[int(rng_num)] + raise galsim.GalSimConfigValueError("rng_num must be an integer", rng_num) + rngs = base.get(index_key + '_rngs', None) + if rngs is None: + raise galsim.GalSimConfigError( + "rng_num is only allowed when image.random_seed is a list") + if rng_num < 0 or rng_num > len(rngs): + raise galsim.GalSimConfigError( + "rng_num is invalid. Must be in [0,%d]"%(len(rngs))) + rng = rngs[int(rng_num)] else: rng = base.get(index_key + '_rng', None) @@ -1084,7 +1067,7 @@ def GetRNG(config, base, logger=None, tag=''): return rng -def CleanConfig(config): # pragma: no cover +def CleanConfig(config, keep_current=False): """Return a "clean" config dict without any leading-underscore values GalSim config dicts store a lot of ancillary information internally to help improve @@ -1097,8 +1080,9 @@ def CleanConfig(config): # pragma: no cover >>> print(galsim.config.CleanConfig(config_dict)) """ if isinstance(config, dict): - return { k : CleanConfig(config[k]) for k in config if k[0] != '_' } + return { k : CleanConfig(config[k], keep_current) for k in config + if k[0] != '_' and (keep_current or k != 'current') } elif isinstance(config, list): - return [ CleanConfig(item) for item in config ] + return [ CleanConfig(item, keep_current) for item in config ] else: return config diff --git a/galsim/config/stamp.py b/galsim/config/stamp.py index 5f3725eb2b0..c3a89cab66c 100644 --- a/galsim/config/stamp.py +++ b/galsim/config/stamp.py @@ -116,7 +116,7 @@ def except_func(logger, proc, k, e, tr): # A list of keys that really belong in stamp, but are allowed in image both for convenience # and backwards-compatibility reasons. Any of these present will be copied over to # config['stamp'] if they exist in config['image']. -stamp_image_keys = ['offset', 'retry_failures', 'gsparams', 'draw_method', 'wmult', +stamp_image_keys = ['offset', 'retry_failures', 'gsparams', 'draw_method', 'n_photons', 'max_extra_noise', 'poisson_flux'] def SetupConfigObjNum(config, obj_num, logger=None): @@ -144,7 +144,7 @@ def SetupConfigObjNum(config, obj_num, logger=None): config['stamp'] = {} stamp = config['stamp'] if not isinstance(stamp, dict): - raise AttributeError("config.stamp is not a dict.") + raise galsim.GalSimConfigError("config.stamp is not a dict.") if 'type' not in stamp: stamp['type'] = 'Basic' @@ -261,9 +261,11 @@ def SetupConfigStampSize(config, xsize, ysize, image_pos, world_pos, logger=None # Ignore these when parsing the parameters for specific stamp types: stamp_ignore = ['xsize', 'ysize', 'size', 'image_pos', 'world_pos', 'offset', 'retry_failures', 'gsparams', 'draw_method', - 'wmult', 'n_photons', 'max_extra_noise', 'poisson_flux', + 'n_photons', 'max_extra_noise', 'poisson_flux', 'skip', 'reject', 'min_flux_frac', 'min_snr', 'max_snr'] +valid_draw_methods = ('auto', 'fft', 'phot', 'real_space', 'no_pixel', 'sb') + def BuildStamp(config, obj_num=0, xsize=0, ysize=0, do_noise=True, logger=None): """ Build a single stamp image using the given config file @@ -284,7 +286,7 @@ def BuildStamp(config, obj_num=0, xsize=0, ysize=0, do_noise=True, logger=None): stamp = config['stamp'] stamp_type = stamp['type'] if stamp_type not in valid_stamp_types: - raise AttributeError("Invalid stamp.type=%s."%stamp_type) + raise galsim.GalSimConfigValueError("Invalid stamp.type.", stamp_type, valid_stamp_types) builder = valid_stamp_types[stamp_type] # Add 1 to the seed here so the first object has a different rng than the file or image. @@ -363,8 +365,9 @@ def BuildStamp(config, obj_num=0, xsize=0, ysize=0, do_noise=True, logger=None): if not skip: method = galsim.config.ParseValue(stamp,'draw_method',config,str)[0] - if method not in ['auto', 'fft', 'phot', 'real_space', 'no_pixel', 'sb']: - raise AttributeError("Invalid draw_method: %s"%method) + if method not in valid_draw_methods: + raise galsim.GalSimConfigValueError("Invalid draw_method.", method, + valid_draw_methods) offset = config['stamp_offset'] if 'offset' in stamp: @@ -403,9 +406,9 @@ def BuildStamp(config, obj_num=0, xsize=0, ysize=0, do_noise=True, logger=None): builder.reset(config, logger) continue else: - raise RuntimeError( - "Rejected an object %d times. If this is expected, "%ntries+ - "you should specify a larger stamp.retry_failures.") + raise galsim.GalSimConfigError( + "Rejected an object %d times. If this is expected, " + "you should specify a larger stamp.retry_failures."%(ntries)) galsim.config.ProcessExtraOutputsForStamp(config, skip, logger) @@ -498,8 +501,6 @@ def DrawBasic(prof, image, method, offset, config, base, logger, **kwargs): kwargs['image'] = image kwargs['offset'] = offset kwargs['method'] = method - if 'wmult' in config and 'wmult' not in kwargs: # pragma: no cover - kwargs['wmult'] = galsim.config.ParseValue(config, 'wmult', base, float)[0] if 'wcs' not in kwargs and 'scale' not in kwargs: kwargs['wcs'] = base['wcs'].local(image_pos = base['image_pos']) if method == 'phot' and 'rng' not in kwargs: @@ -509,31 +510,31 @@ def DrawBasic(prof, image, method, offset, config, base, logger, **kwargs): max_extra_noise = None if 'n_photons' in config and 'n_photons' not in kwargs: if method != 'phot': - raise AttributeError('n_photons is invalid with method != phot') + raise galsim.GalSimConfigError('n_photons is invalid with method != phot') if 'max_extra_noise' in config: logger.warning( - "Both 'max_extra_noise' and 'n_photons' are set in config dict, "+ + "Both 'max_extra_noise' and 'n_photons' are set in config dict, " "ignoring 'max_extra_noise'.") kwargs['n_photons'] = galsim.config.ParseValue(config, 'n_photons', base, int)[0] elif 'max_extra_noise' in config: max_extra_noise = galsim.config.ParseValue(config, 'max_extra_noise', base, float)[0] if method != 'phot' and max_extra_noise is not None: - raise AttributeError('max_extra_noise is invalid with method != phot') + raise galsim.GalSimConfigError('max_extra_noise is invalid with method != phot') if 'poisson_flux' in config and 'poisson_flux' not in kwargs: if method != 'phot': - raise AttributeError('poisson_flux is invalid with method != phot') + raise galsim.GalSimConfigError('poisson_flux is invalid with method != phot') kwargs['poisson_flux'] = galsim.config.ParseValue(config, 'poisson_flux', base, bool)[0] if max_extra_noise is not None and 'max_extra_noise' not in kwargs: if max_extra_noise < 0.: - raise ValueError("image.max_extra_noise cannot be negative") + raise galsim.GalSimConfigError("image.max_extra_noise cannot be negative") if 'image' in base and 'noise' in base['image']: noise_var = galsim.config.CalculateNoiseVariance(base) else: - raise AttributeError("Need to specify noise level when using max_extra_noise") + raise galsim.GalSimConfigError("Need to specify noise level when using max_extra_noise") if noise_var < 0.: - raise ValueError("noise_var calculated to be < 0.") + raise galsim.GalSimConfigError("noise_var calculated to be < 0.") max_extra_noise *= noise_var kwargs['max_extra_noise'] = max_extra_noise @@ -584,15 +585,11 @@ def ParseWorldPos(config, param_name, base, logger): @returns either a CelestialCoord or a PositionD instance. """ param = config[param_name] - if isinstance(param, dict): - value_type = galsim.config.value.valid_value_types[param.get('type','XY')][1][0] - if value_type not in [galsim.PositionD, galsim.CelestialCoord]: - raise AttributeError('Invalid type %s for %s',param.get('type',None),param_name) - return galsim.config.ParseValue(config, param_name, base, value_type)[0] + wcs = base.get('wcs', galsim.PixelScale(1.0)) # should be here, but just in case... + if wcs.isCelestial(): + return galsim.config.ParseValue(config, param_name, base, galsim.CelestialCoord)[0] else: - value_type = (galsim.CelestialCoord if type(param) == galsim.CelestialCoord - else galsim.PositionD) - return galsim.config.ParseValue(config, param_name, base, value_type)[0] + return galsim.config.ParseValue(config, param_name, base, galsim.PositionD)[0] class StampBuilder(object): """A base class for building stamp images of individual objects. @@ -693,8 +690,9 @@ def buildProfile(self, config, base, psf, gsparams, logger): elif 'gal' in base or 'psf' in base: return None else: - raise AttributeError("At least one of gal or psf must be specified in config. " + - "If you really don't want any object, use gal type = None.") + raise galsim.GalSimConfigError( + "At least one of gal or psf must be specified in config. " + "If you really don't want any object, use gal type = None.") def makeStamp(self, config, base, xsize, ysize, logger): """Make the initial empty postage stamp image, if possible. @@ -734,7 +732,7 @@ def updateSkip(self, prof, image, method, offset, config, base, logger): @returns whether to skip drawing this object. """ - if prof is not None and base.get('current_image',None) is not None: + if isinstance(prof,galsim.GSObject) and base.get('current_image',None) is not None: if image is None: prof = base['wcs'].toImage(prof, image_pos=base['image_pos']) N = prof.getGoodImageSize(1.) @@ -791,19 +789,23 @@ def whiten(self, prof, image, config, base, logger): """ # If the object has a noise attribute, then check if we need to do anything with it. current_var = 0. # Default if not overwritten - if prof is not None and prof.noise is not None: + if isinstance(prof,galsim.GSObject) and prof.noise is not None: if 'image' in base and 'noise' in base['image']: noise = base['image']['noise'] + whiten = symmetrize = False if 'whiten' in noise: - if 'symmetrize' in noise: - raise AttributeError('Only one of whiten or symmetrize is allowed') - whiten, safe = galsim.config.ParseValue(noise, 'whiten', base, bool) + whiten = galsim.config.ParseValue(noise, 'whiten', base, bool)[0] + if 'symmetrize' in noise: + symmetrize = galsim.config.ParseValue(noise, 'symmetrize', base, int)[0] + if whiten and symmetrize: + raise galsim.GalSimConfigError('Only one of whiten or symmetrize is allowed') + if whiten or symmetrize: # In case the galaxy was cached, update the rng rng = galsim.config.GetRNG(noise, base, logger, "whiten") prof.noise.rng.reset(rng) + if whiten: current_var = prof.noise.whitenImage(image) - elif 'symmetrize' in noise: - symmetrize, safe = galsim.config.ParseValue(noise, 'symmetrize', base, int) + if symmetrize: current_var = prof.noise.symmetrizeImage(image, symmetrize) return current_var @@ -829,15 +831,20 @@ def getSNRScale(self, image, config, base, logger): return 1. if 'flux' in base[key]: - raise AttributeError( + raise galsim.GalSimConfigError( 'Only one of signal_to_noise or flux may be specified for %s'%key) if 'image' in base and 'noise' in base['image']: noise_var = galsim.config.CalculateNoiseVariance(base) else: - raise AttributeError( + raise galsim.GalSimConfigError( "Need to specify noise level when using %s.signal_to_noise"%key) sn_target = galsim.config.ParseValue(base[key], 'signal_to_noise', base, float)[0] + try: + # In case noise variance is an image + noise_var = noise_var.array.mean() + except AttributeError: + pass # Now determine what flux we need to get our desired S/N # There are lots of definitions of S/N, but here is the one used by Great08 @@ -899,8 +906,9 @@ def reject(self, config, base, prof, psf, image, logger): return True if 'min_flux_frac' in config: if not isinstance(prof, galsim.GSObject): - raise ValueError("Cannot apply min_flux_frac for stamp types that do not use "+ - "a single GSObject profile.") + raise galsim.GalSimConfigError( + "Cannot apply min_flux_frac for stamp types that do not use " + "a single GSObject profile.") expected_flux = prof.flux measured_flux = np.sum(image.array, dtype=float) min_flux_frac = galsim.config.ParseValue(config, 'min_flux_frac', base, float)[0] @@ -912,8 +920,9 @@ def reject(self, config, base, prof, psf, image, logger): return True if 'min_snr' in config or 'max_snr' in config: if not isinstance(prof, galsim.GSObject): - raise ValueError("Cannot apply min_snr for stamp types that do not use "+ - "a single GSObject profile.") + raise galsim.GalSimConfigError( + "Cannot apply min_snr for stamp types that do not use " + "a single GSObject profile.") var = galsim.config.CalculateNoiseVariance(base) sumsq = np.sum(image.array**2, dtype=float) snr = np.sqrt(sumsq / var) @@ -941,7 +950,7 @@ def reset(self, base, logger): """ # Clear current values out of psf, gal, and stamp if they are not safe to reuse. # This means they are either marked as safe or indexed by something other than obj_num. - for field in ['psf', 'gal', 'stamp']: + for field in ('psf', 'gal', 'stamp'): if field in base: galsim.config.RemoveCurrent(base[field], keep_safe=True, index_key='obj_num') diff --git a/galsim/config/stamp_ring.py b/galsim/config/stamp_ring.py index 121af198aa6..809e140b5fa 100644 --- a/galsim/config/stamp_ring.py +++ b/galsim/config/stamp_ring.py @@ -55,7 +55,8 @@ def setup(self, config, base, xsize, ysize, ignore, logger): num = params['num'] if num <= 0: - raise ValueError("Attribute num for gal.type == Ring must be > 0") + raise galsim.GalSimConfigValueError("Attribute num for gal.type == Ring must be > 0", + num) # Setup the indexing sequence if it hasn't been specified using the number of items. galsim.config.SetDefaultIndex(config, num) @@ -90,20 +91,21 @@ def buildProfile(self, config, base, psf, gsparams, logger): num = galsim.config.ParseValue(config, 'num', base, int)[0] index = galsim.config.ParseValue(config, 'index', base, int)[0] if index < 0 or index >= num: - raise AttributeError("index %d out of bounds for Ring"%index) + raise galsim.GalSimConfigError("index %d out of bounds for Ring"%index) if index % num == 0: # Then we are on the first item in the ring, so make it normally. gal = galsim.config.BuildGSObject(base, 'gal', gsparams=gsparams, logger=logger)[0] if gal is None: - raise AttributeError( + raise galsim.GalSimConfigError( "The gal field must define a valid galaxy for stamp type=Ring.") # Save the galaxy profile for next time. self.first = gal else: # Grab the saved first galaxy. if not hasattr(self, 'first'): - raise RuntimeError("Building Ring after the first item, but no first gal stored.") + raise galsim.GalSimConfigError( + "Building Ring after the first item, but no first gal stored.") gal = self.first full_rot = galsim.config.ParseValue(config, 'full_rotation', base, galsim.Angle)[0] dtheta = full_rot / num @@ -153,7 +155,7 @@ def makeTasks(self, config, base, jobs, logger): @returns a list of tasks """ if 'num' not in config: - raise AttributeError("Attribute num is required for type = Ring") + raise galsim.GalSimConfigError("Attribute num is required for type = Ring") num = galsim.config.ParseValue(config, 'num', base, int)[0] ntot = len(jobs) tasks = [ [ (jobs[j], j) for j in range(k,min(k+num,ntot)) ] for k in range(0, ntot, num) ] diff --git a/galsim/config/value.py b/galsim/config/value.py index 715343c47ad..31b45852ca4 100644 --- a/galsim/config/value.py +++ b/galsim/config/value.py @@ -73,30 +73,30 @@ def ParseValue(config, key, base, value_type): if use_current: if (value_type is not None and cvalue_type is not None and cvalue_type != value_type): - raise ValueError( - "Attempt to parse %s multiple times with different value types:"%key + - " %s and %s"%(value_type, cvalue_type)) + raise galsim.GalSimConfigError( + "Attempt to parse %s multiple times with different value types: " + "%s and %s"%(key, value_type, cvalue_type)) #print(index,'Using current value of ',key,' = ',param['current'][0]) return cval, csafe else: # Only need to check this the first time. if 'type' not in param: - raise AttributeError( - "%s.type attribute required in config for non-constant parameter %s."%(key,key)) + raise galsim.GalSimConfigError( + "%s.type attribute required when providing a dict."%(key)) # Check if the value_type is valid. # (See valid_value_types defined at the top of the file.) if type_name not in valid_value_types: - raise AttributeError( - "Unrecognized type = %s specified for parameter %s"%(type_name,key)) + raise galsim.GalSimConfigValueError("Unrecognized %s.type"%(key), type_name, + valid_value_types) # Get the generating function and the list of valid types for it. generate_func, valid_types = valid_value_types[type_name] if value_type not in valid_types: - raise AttributeError( - "Invalid value_type = %s specified for parameter %s with type = %s."%( - value_type, key, type_name)) + raise galsim.GalSimConfigValueError( + "Invalid value_type specified for parameter %s with type=%s."%(key, type_name), + value_type, valid_types) param['_gen_fn'] = generate_func @@ -163,7 +163,10 @@ def ParseValue(config, key, base, value_type): # This makes sure strings are converted to float (or other type) if necessary. # In particular things like 1.e6 aren't converted to float automatically # by the yaml reader. (Although I think this is a bug.) - val = value_type(param) + try: + val = value_type(param) + except (ValueError, TypeError): + raise galsim.GalSimConfigError("Could not parse %s as a %s"%(param, value_type)) #print(key,' = ',val) # Save the converted type for next time so it will hit the first if statement here @@ -204,7 +207,7 @@ def EvaluateCurrentValue(key, config, base, value_type=None): that the value is the right type.] """ if not isinstance(config[key], dict): - if value_type is not None or (isinstance(config[key],str) and config[key][0] in ['@','$']): + if value_type is not None or (isinstance(config[key],str) and config[key][0] in ('@','$')): # This will work fine to evaluate the current value, but will also # compute it if necessary #print('Not dict. Parse value normally') @@ -287,11 +290,9 @@ def CheckAllParams(config, req={}, opt={}, single=[], ignore=[]): for (key, value_type) in req.items(): if key in config: get[key] = value_type - else: # pragma: no cover - if 'type' in config: - raise AttributeError("Attribute %s is required for type = %s"%(key,config['type'])) - else: - raise AttributeError("Attribute %s is required"%key) + else: + raise galsim.GalSimConfigError( + "Attribute %s is required for type = %s"%(key,config.get('type',None))) # Check optional items: for (key, value_type) in opt.items(): @@ -305,20 +306,15 @@ def CheckAllParams(config, req={}, opt={}, single=[], ignore=[]): for (key, value_type) in s.items(): if key in config: count += 1 - if count > 1: # pragma: no cover - if 'type' in config: - raise AttributeError( - "Only one of the attributes %s is allowed for type = %s"%( - s.keys(),config['type'])) - else: - raise AttributeError("Only one of the attributes %s is allowed"%s.keys()) + if count > 1: + raise galsim.GalSimConfigError( + "Only one of the attributes %s is allowed for type = %s"%( + s.keys(),config.get('type',None))) get[key] = value_type - if count == 0: # pragma: no cover - if 'type' in config: - raise AttributeError( - "One of the attributes %s is required for type = %s"%(s.keys(),config['type'])) - else: - raise AttributeError("One of the attributes %s is required"%s.keys()) + if count == 0: + raise galsim.GalSimConfigError( + "One of the attributes %s is required for type = %s"%( + s.keys(),config.get('type',None))) # Check that there aren't any extra keys in config aside from a few we expect: valid_keys += ignore @@ -326,7 +322,7 @@ def CheckAllParams(config, req={}, opt={}, single=[], ignore=[]): for key in config: # Generators are allowed to use item names that start with _, which we ignore here. if key not in valid_keys and not key.startswith('_'): - raise AttributeError("Unexpected attribute %s found"%(key)) + raise galsim.GalSimConfigError("Unexpected attribute %s found"%(key)) config['_get'] = get return get @@ -344,9 +340,6 @@ def GetAllParams(config, base, req={}, opt={}, single=[], ignore=[]): val, safe1 = ParseValue(config, key, base, value_type) safe = safe and safe1 kwargs[key] = val - # Just in case there are unicode strings. python 2.6 doesn't like them in kwargs. - if sys.version_info < (2,7): # pragma: no cover - kwargs = dict([(k.encode('utf-8'), v) for k,v in kwargs.items()]) return kwargs, safe @@ -365,8 +358,8 @@ def _GetAngleValue(param): value = float(value) unit = galsim.AngleUnit.from_name(unit) return galsim.Angle(value, unit) - except (TypeError, AttributeError) as e: # pragma: no cover - raise AttributeError("Unable to parse %s as an Angle. Caught %s"%(param,e)) + except (ValueError, TypeError, AttributeError) as e: + raise galsim.GalSimConfigError("Unable to parse %s as an Angle. Caught %s"%(param,e)) def _GetPositionValue(param): @@ -380,8 +373,9 @@ def _GetPositionValue(param): x, y = param.split(',') x = float(x.strip()) y = float(y.strip()) - except (TypeError, AttributeError) as e: # pragma: no cover - raise AttributeError("Unable to parse %s as a PositionD. Caught %s"%(param,e)) + except (ValueError, TypeError, AttributeError) as e: + raise galsim.GalSimConfigError( + "Unable to parse %s as a PositionD. Caught %s"%(param,e)) return galsim.PositionD(x,y) @@ -389,23 +383,21 @@ def _GetBoolValue(param): """ @brief Convert a string to a bool """ if isinstance(param,str): - if param.strip().upper() in [ 'TRUE', 'YES' ]: + if param.strip().upper() in ('TRUE', 'YES'): return True - elif param.strip().upper() in [ 'FALSE', 'NO' ]: + elif param.strip().upper() in ('FALSE', 'NO'): return False else: try: val = bool(int(param)) return val - except (TypeError, AttributeError) as e: # pragma: no cover - raise AttributeError("Unable to parse %s as a bool. Caught %s"%(param,e)) + except (ValueError, TypeError, AttributeError) as e: + raise galsim.GalSimConfigError( + "Unable to parse %s as a bool. Caught %s"%(param,e)) else: - try: - val = bool(param) - return val - except (TypeError, AttributeError) as e: # pragma: no cover - raise AttributeError("Unable to parse %s as a bool. Caught %s"%(param,e)) - + # This always works. + # Everything in Python is convertible to bool. + return bool(param) # @@ -526,10 +518,10 @@ def _GenerateFromSequence(config, base, value_type): nitems = kwargs.get('nitems',None) if repeat <= 0: - raise ValueError( - "Invalid repeat=%d (must be > 0) for type = Sequence"%repeat) + raise galsim.GalSimConfigValueError( + "Invalid repeat for type = Sequence (must be > 0)", repeat) if last is not None and nitems is not None: - raise AttributeError( + raise galsim.GalSimConfigError( "At most one of the attributes last and nitems is allowed for type = Sequence") index, index_key = galsim.config.GetIndex(kwargs, base, is_sequence=True) @@ -612,7 +604,7 @@ def _GenerateFromFormattedStr(config, base, value_type): continue token = token.lstrip('0123456789lLh') # ignore field size, and long/short specification if len(token) == 0: - raise ValueError("Unable to parse '%s' as a valid format string"%format) + raise galsim.GalSimConfigError("Unable to parse %r as a valid format string"%format) if token[0].lower() in 'diouxX': val_types.append(int) elif token[0].lower() in 'eEfFgG': @@ -620,12 +612,12 @@ def _GenerateFromFormattedStr(config, base, value_type): elif token[0].lower() in 'rs': val_types.append(str) else: - raise ValueError("Unable to parse '%s' as a valid format string"%format) + raise galsim.GalSimConfigError("Unable to parse %r as a valid format string"%format) if len(val_types) != len(items): - raise ValueError( - "Number of items for FormatStr (%d) does not match number expected from "%len(items)+ - "format string (%d)"%len(val_types)) + raise galsim.GalSimConfigError( + "Number of items for FormatStr (%d) does not match number expected from " + "format string (%d)"%(len(items), len(val_types))) vals = [] for index in range(len(items)): val, safe1 = ParseValue(items, index, base, val_types[index]) @@ -646,14 +638,14 @@ def _GenerateFromList(config, base, value_type): CheckAllParams(config, req=req, opt=opt) items = config['items'] if not isinstance(items,list): - raise AttributeError("items entry for type=List is not a list.") + raise galsim.GalSimConfigError("items entry for type=List is not a list.") # Setup the indexing sequence if it hasn't been specified using the length of items. SetDefaultIndex(config, len(items)) index, safe = ParseValue(config, 'index', base, int) if index < 0 or index >= len(items): - raise AttributeError("index %d out of bounds for type=List"%index) + raise galsim.GalSimConfigError("index %d out of bounds for type=List"%index) val, safe1 = ParseValue(items, index, base, value_type) safe = safe and safe1 #print(base['obj_num'],'List index = %d, val = %s'%(index,val)) @@ -667,7 +659,7 @@ def _GenerateFromSum(config, base, value_type): CheckAllParams(config, req=req) items = config['items'] if not isinstance(items,list): - raise AttributeError("items entry for type=List is not a list.") + raise galsim.GalSimConfigError("items entry for type=List is not a list.") sum, safe = ParseValue(items, 0, base, value_type) @@ -694,8 +686,8 @@ def _GenerateFromCurrent(config, base, value_type): try: return EvaluateCurrentValue(k, d, base, value_type) - except ValueError as e: # pragma: no cover - raise ValueError("%s\nError generating Current value with key = %s"%(e,k)) + except (TypeError, ValueError) as e: + raise galsim.GalSimConfigError("%s\nError generating Current value with key = %s"%(e,k)) def RegisterValueType(type_name, gen_func, valid_types, input_type=None): diff --git a/galsim/config/value_eval.py b/galsim/config/value_eval.py index 0a016b63a40..cabe97fb249 100644 --- a/galsim/config/value_eval.py +++ b/galsim/config/value_eval.py @@ -25,7 +25,7 @@ def _type_by_letter(key): if len(key) < 2: - raise AttributeError("Invalid user-defined variable %r"%key) + raise galsim.GalSimConfigError("Invalid user-defined variable %r"%key) if key[0] == 'f': return float elif key[0] == 'i': @@ -45,7 +45,8 @@ def _type_by_letter(key): elif key[0] == 'x': return None else: - raise AttributeError("Invalid Eval variable: %s (starts with an invalid letter)"%key) + raise galsim.GalSimConfigError( + "Invalid Eval variable: %s (starts with an invalid letter)"%key) eval_base_variables = [ 'image_pos', 'world_pos', 'image_center', 'image_origin', 'image_bounds', 'image_xsize', 'image_ysize', 'stamp_xsize', 'stamp_ysize', 'pixel_scale', @@ -91,7 +92,8 @@ def _GenerateFromEval(config, base, value_type): gdict = base['eval_gdict'] if 'str' not in config: - raise AttributeError("Attribute str is required for type = %s"%(config['type'])) + raise galsim.GalSimConfigError( + "Attribute str is required for type = %s"%(config['type'])) string = config['str'] # Turn any "Current" items indicated with an @ sign into regular variables. @@ -121,7 +123,7 @@ def _GenerateFromEval(config, base, value_type): if 'eval_variables' in base: #print('found eval_variables = ',galsim.config.CleanConfig(base['eval_variables'])) if not isinstance(base['eval_variables'],dict): - raise AttributeError("eval_variables must be a dict") + raise galsim.GalSimConfigError("eval_variables must be a dict") for key in base['eval_variables']: # Only add variables that appear in the string. if _isWordInString(key[1:],string) and key[1:] not in params: @@ -151,8 +153,9 @@ def _GenerateFromEval(config, base, value_type): config['_fn'] = fn except KeyboardInterrupt: raise - except Exception as e: # pragma: no cover - raise ValueError("Unable to evaluate string %r as a %s\n"%(string,value_type) + str(e)) + except Exception as e: + raise galsim.GalSimConfigError( + "Unable to evaluate string %r as a %s\n%r"%(string, value_type, e)) # Always need to evaluate any parameters to pass to the function opt = {} @@ -174,9 +177,9 @@ def _GenerateFromEval(config, base, value_type): return val, safe except KeyboardInterrupt: raise - except Exception as e: # pragma: no cover - raise ValueError("Unable to evaluate string %r as a %s\n"%(config['str'],value_type) + - str(e)) + except Exception as e: + raise galsim.GalSimConfigError( + "Unable to evaluate string %r as a %s\n%r"%(config['str'],value_type, e)) # Register this as a valid value type diff --git a/galsim/config/value_random.py b/galsim/config/value_random.py index 8d9a27e343c..d8356525a2c 100644 --- a/galsim/config/value_random.py +++ b/galsim/config/value_random.py @@ -96,25 +96,25 @@ def _GenerateFromRandomGaussian(config, base, value_type): do_abs = False do_neg = False - if min == mean: + if (min >= mean) and (max > mean): do_abs = True - max -= mean - min = -max - elif max == mean: + lo = min - mean + hi = max - mean + elif (min < mean) and (max <= mean): do_abs = True do_neg = True - min -= mean - max = -min + hi = mean - min + lo = mean - max else: - min -= mean - max -= mean + lo = min - mean + hi = max - mean # Emulate a do-while loop import math while True: val = gd() if do_abs: val = math.fabs(val) - if val >= min and val <= max: break + if val >= lo and val <= hi: break if do_neg: val = -val val += mean else: @@ -158,7 +158,8 @@ def _GenerateFromRandomBinomial(config, base, value_type): N = kwargs.get('N',1) p = kwargs.get('p',0.5) if value_type is bool and N != 1: - raise ValueError("N must = 1 for type = RandomBinomial used in bool context") + raise galsim.GalSimConfigValueError( + "N must = 1 for type = RandomBinomial used in bool context", N) dev = galsim.BinomialDeviate(rng,N=N,p=p) val = dev() @@ -230,9 +231,11 @@ def _GenerateFromRandomDistribution(config, base, value_type): # Allow the user to give x,f instead of function to define a LookupTable. if 'x' in config or 'f' in config: if 'x' not in config or 'f' not in config: - raise AttributeError("Both x and f must be provided for type=RandomDistribution") + raise galsim.GalSimConfigError( + "Both x and f must be provided for type=RandomDistribution") if 'function' in kwargs: - raise AttributeError("Cannot provide function with x,f for type=RandomDistribution") + raise galsim.GalSimConfigError( + "Cannot provide function with x,f for type=RandomDistribution") x = config['x'] f = config['f'] x_log = config.get('x_log', False) @@ -242,9 +245,11 @@ def _GenerateFromRandomDistribution(config, base, value_type): interpolant=interpolant) else: if 'function' not in kwargs: - raise AttributeError("function or x,f must be provided for type=RandomDistribution") + raise galsim.GalSimConfigError( + "function or x,f must be provided for type=RandomDistribution") if 'x_log' in config or 'f_log' in config: - raise AttributeError("x_log, f_log are invalid with function for type=RandomDistribution") + raise galsim.GalSimConfigError( + "x_log, f_log are invalid with function for type=RandomDistribution") if '_distdev' not in config or config['_distdev_kwargs'] != kwargs: # The overhead for making a DistDeviate is large enough that we'd rather not do it every @@ -281,8 +286,9 @@ def _GenerateFromRandomCircle(config, base, value_type): min_rsq = inner_radius**2 if min_rsq >= max_rsq: - raise ValueError("inner_radius (%f) must be less than radius (%f) for type=RandomCircle"%( - inner_radius, radius)) + raise galsim.GalSimConfigValueError( + "inner_radius must be less than radius (%f) for type=RandomCircle"%(radius), + inner_radius) # Emulate a do-while loop while True: diff --git a/galsim/config/wcs.py b/galsim/config/wcs.py index 9209777f64c..cc8706ead74 100644 --- a/galsim/config/wcs.py +++ b/galsim/config/wcs.py @@ -57,14 +57,14 @@ def BuildWCS(config, key, base, logger=None): elif param == str(param) and (param[0] == '$' or param[0] == '@'): return galsim.config.ParseValue(config, key, base, None)[0] elif not isinstance(param, dict): - raise ValueError("wcs must be either a BaseWCS or a dict") + raise galsim.GalSimConfigError("wcs must be either a BaseWCS or a dict") elif 'type' in param: wcs_type = param['type'] else: wcs_type = 'PixelScale' # For these two, just do the usual ParseValue function. - if wcs_type in ['Eval', 'Current']: + if wcs_type in ('Eval', 'Current'): return galsim.config.ParseValue(config, key, base, None)[0] # Check if we can use the current cached object @@ -83,7 +83,7 @@ def BuildWCS(config, key, base, logger=None): param['origin'] = origin if wcs_type not in valid_wcs_types: - raise AttributeError("Invalid image.wcs.type=%s."%wcs_type) + raise galsim.GalSimConfigValueError("Invalid image.wcs.type.", wcs_type, valid_wcs_types) logger.debug('image %d: Building wcs type %s', base.get('image_num',0), wcs_type) builder = valid_wcs_types[wcs_type] wcs = builder.buildWCS(param, base, logger) @@ -247,14 +247,14 @@ def buildWCS(self, config, base, logger): galsim.config.CheckAllParams(config, req=req, opt=opt) items = config['items'] if not isinstance(items,list): - raise AttributeError("items entry for type=List is not a list.") + raise galsim.GalSimConfigError("items entry for type=List is not a list.") # Setup the indexing sequence if it hasn't been specified using the length of items. galsim.config.SetDefaultIndex(config, len(items)) index, safe = galsim.config.ParseValue(config, 'index', base, int) if index < 0 or index >= len(items): - raise AttributeError("index %d out of bounds for wcs type=List"%index) + raise galsim.GalSimConfigError("index %d out of bounds for wcs type=List"%index) return BuildWCS(items, index, base) def RegisterWCSType(wcs_type, builder, input_type=None): diff --git a/galsim/convolve.py b/galsim/convolve.py index 4e8a703ace9..848f8ff96d8 100644 --- a/galsim/convolve.py +++ b/galsim/convolve.py @@ -23,6 +23,7 @@ from .gsobject import GSObject from .chromatic import ChromaticObject, ChromaticConvolution from .utilities import lazy_property, doc_inherit +from .errors import GalSimError, convert_cpp_errors, galsim_warn def Convolve(*args, **kwargs): """A function for convolving 2 or more GSObject or ChromaticObject instances. @@ -160,37 +161,26 @@ def __init__(self, *args, **kwargs): # Warn if doing DFT convolution for objects with hard edges if not real_space and hard_edge: - import warnings if len(args) == 2: - msg = """ - Doing convolution of 2 objects, both with hard edges. - This might be more accurate and/or faster using real_space=True""" + galsim_warn("Doing convolution of 2 objects, both with hard edges. " + "This might be more accurate and/or faster using real_space=True") else: - msg = """ - Doing convolution where all objects have hard edges. - There might be some inaccuracies due to ringing in k-space.""" - warnings.warn(msg) + galsim_warn("Doing convolution where all objects have hard edges. " + "There might be some inaccuracies due to ringing in k-space.") if real_space: # Can't do real space if nobj > 2 if len(args) > 2: - import warnings - msg = """ - Real-space convolution of more than 2 objects is not implemented. - Switching to DFT method.""" - warnings.warn(msg) + galsim_warn("Real-space convolution of more than 2 objects is not implemented. " + "Switching to DFT method.") real_space = False # Also can't do real space if any object is not analytic, so check for that. else: for obj in args: if not obj.is_analytic_x: - import warnings - msg = """ - A component to be convolved is not analytic in real space. - Cannot use real space convolution. - Switching to DFT method.""" - warnings.warn(msg) + galsim_warn("A component to be convolved is not analytic in real space. " + "Cannot use real space convolution. Switching to DFT method.") real_space = False break @@ -209,7 +199,8 @@ def real_space(self): return self._real_space @lazy_property def _sbp(self): SBList = [obj._sbp for obj in self.obj_list] - return _galsim.SBConvolve(SBList, self._real_space, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBConvolve(SBList, self._real_space, self.gsparams._gsp) @lazy_property def _noise(self): @@ -219,9 +210,8 @@ def _noise(self): for i, obj in enumerate(self.obj_list): if obj.noise is not None: if _noise is not None: - import warnings - warnings.warn("Unable to propagate noise in galsim.Convolution when " - "multiple objects have noise attribute") + galsim_warn("Unable to propagate noise in galsim.Convolution when " + "multiple objects have noise attribute") break _noise = obj.noise others = [ obj2 for k, obj2 in enumerate(self.obj_list) if k != i ] @@ -348,14 +338,11 @@ def _xValue(self, pos): elif len(self.obj_list) == 2: try: return self._sbp.xValue(pos._p) - except AttributeError: # pragma: no cover - # TODO: Once we have a GSObject subclass that doesn't implement the _sbp - # attribute, add a test that this branch works properly. - # (Currently it is unreachable, since all profiles have _sbp.) - raise NotImplementedError( + except (AttributeError, RuntimeError): + raise GalSimError( "At least one profile in %s does not implement real-space convolution"%self) else: - raise ValueError("Cannot use real_space convolution for >2 profiles") + raise GalSimError("Cannot use real_space convolution for >2 profiles") @doc_inherit def _kValue(self, pos): @@ -369,11 +356,11 @@ def _drawReal(self, image): elif len(self.obj_list) == 2: try: self._sbp.draw(image._image, image.scale) - except AttributeError: # pragma: no cover - raise NotImplementedError( + except (AttributeError, RuntimeError): + raise GalSimError( "At least one profile in %s does not implement real-space convolution"%self) else: - raise ValueError("Cannot use real_space convolution for >2 profiles") + raise GalSimError("Cannot use real_space convolution for >2 profiles") @doc_inherit def _shoot(self, photons, ud): @@ -474,7 +461,8 @@ def __init__(self, obj, gsparams=None): @lazy_property def _sbp(self): - return _galsim.SBDeconvolve(self.orig_obj._sbp, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBDeconvolve(self.orig_obj._sbp, self.gsparams._gsp) @property def orig_obj(self): return self._orig_obj @@ -482,8 +470,7 @@ def orig_obj(self): return self._orig_obj @property def _noise(self): if self.orig_obj.noise is not None: - import warnings - warnings.warn("Unable to propagate noise in galsim.Deconvolution") + galsim_warn("Unable to propagate noise in galsim.Deconvolution") return None def __eq__(self, other): @@ -644,20 +631,13 @@ def __init__(self, obj, real_space=None, gsparams=None): # Warn if doing DFT convolution for objects with hard edges. if not real_space and hard_edge: - import warnings - msg = """ - Doing auto-convolution of object with hard edges. - This might be more accurate and/or faster using real_space=True""" - warnings.warn(msg) + galsim_warn("Doing auto-convolution of object with hard edges. " + "This might be more accurate and/or faster using real_space=True") # Can't do real space if object is not analytic, so check for that. if real_space and not obj.is_analytic_x: - import warnings - msg = """ - Object to be auto-convolved is not analytic in real space. - Cannot use real space convolution. - Switching to DFT method.""" - warnings.warn(msg) + galsim_warn("Object to be auto-convolved is not analytic in real space. " + "Cannot use real space convolution. Switching to DFT method.") real_space = False # Save the construction parameters (as they are at this point) as attributes so they @@ -671,7 +651,8 @@ def __init__(self, obj, real_space=None, gsparams=None): @lazy_property def _sbp(self): - return _galsim.SBAutoConvolve(self.orig_obj._sbp, self._real_space, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBAutoConvolve(self.orig_obj._sbp, self._real_space, self.gsparams._gsp) @property def orig_obj(self): return self._orig_obj @@ -681,8 +662,7 @@ def real_space(self): return self._real_space @property def _noise(self): if self.orig_obj.noise is not None: - import warnings - warnings.warn("Unable to propagate noise in galsim.AutoConvolution") + galsim_warn("Unable to propagate noise in galsim.AutoConvolution") return None def __eq__(self, other): @@ -787,20 +767,13 @@ def __init__(self, obj, real_space=None, gsparams=None): # Warn if doing DFT convolution for objects with hard edges. if not real_space and hard_edge: - import warnings - msg = """ - Doing auto-correlation of object with hard edges. - This might be more accurate and/or faster using real_space=True""" - warnings.warn(msg) + galsim_warn("Doing auto-correlation of object with hard edges. " + "This might be more accurate and/or faster using real_space=True") # Can't do real space if object is not analytic, so check for that. if real_space and not obj.is_analytic_x: - import warnings - msg = """ - Object to be auto-correlated is not analytic in real space. - Cannot use real space convolution. - Switching to DFT method.""" - warnings.warn(msg) + galsim_warn("Object to be auto-correlated is not analytic in real space. " + "Cannot use real space convolution. Switching to DFT method.") real_space = False # Save the construction parameters (as they are at this point) as attributes so they @@ -814,7 +787,8 @@ def __init__(self, obj, real_space=None, gsparams=None): @lazy_property def _sbp(self): - return _galsim.SBAutoCorrelate(self.orig_obj._sbp, self._real_space, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBAutoCorrelate(self.orig_obj._sbp, self._real_space, self.gsparams._gsp) @property def orig_obj(self): return self._orig_obj @@ -824,8 +798,7 @@ def real_space(self): return self._real_space @property def _noise(self): if self.orig_obj.noise is not None: - import warnings - warnings.warn("Unable to propagate noise in galsim.AutoCorrelation") + galsim_warn("Unable to propagate noise in galsim.AutoCorrelation") return None def __eq__(self, other): diff --git a/galsim/correlatednoise.py b/galsim/correlatednoise.py index 63a900b3adc..3222a58a992 100644 --- a/galsim/correlatednoise.py +++ b/galsim/correlatednoise.py @@ -21,10 +21,14 @@ import numpy as np from future.utils import iteritems + from .image import Image from .random import BaseDeviate from .gsobject import GSObject +from .gsparams import GSParams from . import utilities +from .errors import GalSimError, GalSimValueError, GalSimRangeError, GalSimUndefinedBoundsError +from .errors import GalSimIncompatibleValuesError, galsim_warn def whitenNoise(self, noise): # This will be inserted into the Image class as a method. So self = image. @@ -80,9 +84,6 @@ def __init__(self, rng, gsobject, wcs): if rng is not None and not isinstance(rng, BaseDeviate): raise TypeError( "Supplied rng argument not a galsim.BaseDeviate or derived class instance.") - if not isinstance(gsobject, GSObject): - raise TypeError( - "Supplied gsobject argument not a galsim.GSObject or derived class instance.") if rng is None: rng = BaseDeviate() self._rng = rng @@ -91,18 +92,18 @@ def __init__(self, rng, gsobject, wcs): self.wcs = wcs # When applying normal or whitening noise to an image, we normally do calculations. - # If _profile_for_stored is profile, then it means that we can use the stored values in - # _rootps_store, _rootps_whitening_store, and/or _rootps_symmetrizing_store and avoid having + # If _profile_for_cached is profile, then it means that we can use the stored values in + # _rootps_cache, _rootps_whitening_cache, and/or _rootps_symmetrizing_cache and avoid having # to redo the calculations. - # So for now, we start out with _profile_for_stored = None, and _rootps_store, - # _rootps_whitening_store, _rootps_symmetrizing_store empty. - self._profile_for_stored = None - self._rootps_store = [] - self._rootps_whitening_store = [] - self._rootps_symmetrizing_store = [] + # So for now, we start out with _profile_for_cached = None, and _rootps_cache, + # _rootps_whitening_cache, _rootps_symmetrizing_cache empty. + self._profile_for_cache = None + self._rootps_cache = {} + self._rootps_whitening_cache = {} + self._rootps_symmetrizing_cache = {} # Also set up the cache for a stored value of the variance, needed for efficiency once the # noise field can get convolved with other GSObjects making is_analytic_x False - self._variance_stored = None + self._variance_cached = None @property def rng(self): return self._rng @@ -112,25 +113,22 @@ def rng(self): return self._rng def __add__(self, other): from . import wcs if not wcs.compatible(self.wcs, other.wcs): - import warnings - warnings.warn("Adding two CorrelatedNoise objects with incompatible WCS functions.\n"+ - "The result will have the WCS of the first object.") + galsim_warn("Adding two CorrelatedNoise objects with incompatible WCS. " + "The result will have the WCS of the first object.") return _BaseCorrelatedNoise(self.rng, self._profile + other._profile, self.wcs) def __sub__(self, other): from . import wcs if not wcs.compatible(self.wcs, other.wcs): - import warnings - warnings.warn("Subtracting two CorrelatedNoise objects with incompatible WCS functions.\n"+ - "The result will have the WCS of the first object.") + galsim_warn("Subtracting two CorrelatedNoise objects with incompatible WCS. " + "The result will have the WCS of the first object.") return _BaseCorrelatedNoise(self.rng, self._profile - other._profile, self.wcs) def __mul__(self, variance_ratio): return self.withScaledVariance(variance_ratio) def __div__(self, variance_ratio): return self.withScaledVariance(1./variance_ratio) - def __truediv__(self, variance_ratio): - return self.withScaledVariance(1./variance_ratio) + __truediv__ = __div__ def copy(self, rng=None): """Returns a copy of the correlated noise model. @@ -156,6 +154,17 @@ def __eq__(self, other): return repr(self) == repr(other) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(repr(self)) + def _clear_cache(self): + """Check if the profile has changed and clear caches if appropriate. + """ + if self._profile_for_cache is not self._profile: + self._rootps_cache.clear() + self._rootps_whitening_cache.clear() + self._rootps_symmetrizing_cache.clear() + self._variance_cached = None + # Set profile_for_cache for next time. + self._profile_for_cache = self._profile + def applyTo(self, image): """Apply this correlated Gaussian random noise field to an input Image. @@ -213,16 +222,11 @@ def applyTo(self, image): if not isinstance(image, Image): raise TypeError("Input image argument must be a galsim.Image.") if not image.bounds.isDefined(): - raise ValueError("Input image argument must have defined bounds.") + raise GalSimUndefinedBoundsError("Input image argument must have defined bounds.") # If the profile has changed since last time (or if we have never been here before), # clear out the stored values. - if self._profile_for_stored is not self._profile: - self._rootps_store = [] - self._rootps_whitening_store = [] - self._variance_stored = None - # Set profile_for_stored for next time. - self._profile_for_stored = self._profile + self._clear_cache() if image.wcs is None: wcs = self.wcs @@ -239,10 +243,6 @@ def applyTo(self, image): image.array[:,:] += noise_array return image - def applyToView(self, image_view): - raise RuntimeError( - "CorrelatedNoise can only be applied to a regular Image, not an ImageView") - def whitenImage(self, image): """Apply noise designed to whiten correlated Gaussian random noise in an input Image. @@ -310,16 +310,11 @@ def whitenImage(self, image): if not isinstance(image, Image): raise TypeError("Input image not a galsim.Image object") if not image.bounds.isDefined(): - raise ValueError("Input image argument must have defined bounds.") + raise GalSimUndefinedBoundsError("Input image argument must have defined bounds.") # If the profile has changed since last time (or if we have never been here before), # clear out the stored values. - if self._profile_for_stored is not self._profile: - self._rootps_store = [] - self._rootps_whitening_store = [] - self._variance_stored = None - # Set profile_for_stored for next time. - self._profile_for_stored = self._profile + self._clear_cache() if image.wcs is None: wcs = self.wcs @@ -394,24 +389,20 @@ def symmetrizeImage(self, image, order=4): if not isinstance(image, Image): raise TypeError("Input image not a galsim.Image object") if not image.bounds.isDefined(): - raise ValueError("Input image argument must have defined bounds.") + raise GalSimUndefinedBoundsError("Input image argument must have defined bounds.") # Check that the input is square in shape. if image.array.shape[0] != image.array.shape[1]: - raise ValueError("Input image is not square!") + raise GalSimValueError("Input image must be square.", image.array.shape) # Check that the input order is an allowed value. if order % 2 != 0 or order <= 2: - raise ValueError("Order must be an even number >=4!") + raise GalSimValueError("Order must be an even number >=4.", order) # If the profile has changed since last time (or if we have never been here before), # clear out the stored values. Note that this cache is not the same as the one used for # whitening. - if self._profile_for_stored is not self._profile: - self._rootps_store = [] - self._rootps_symmetrizing_store = [] - # Set profile_for_stored for next time. - self._profile_for_stored = self._profile + self._clear_cache() if image.wcs is None: wcs = self.wcs @@ -482,9 +473,6 @@ def rotate(self, theta): @returns a new CorrelatedNoise object with the specified rotation. """ - from .angle import Angle - if not isinstance(theta, Angle): - raise TypeError("Input theta should be an Angle") return _BaseCorrelatedNoise(self.rng, self._profile.rotate(theta), self.wcs) def shear(self, *args, **kwargs): @@ -528,21 +516,17 @@ def getVariance(self): else: # If the profile has changed since last time (or if we have never been here before), # clear out the stored values. - if self._profile_for_stored is not self._profile: - self._rootps_store = [] - self._rootps_whitening_store = [] - self._variance_stored = None - # Set profile_for_stored for next time. - self._profile_for_stored = self._profile + self._clear_cache() + # Then use cached version or rebuild if necessary - if self._variance_stored is not None: - variance = self._variance_stored + if self._variance_cached is not None: + variance = self._variance_cached else: imtmp = Image(1, 1, dtype=float) # GalSim internals handle this correctly w/out folding self.drawImage(imtmp, scale=1.) variance = imtmp(1, 1) - self._variance_stored = variance # Store variance for next time + self._variance_cached = variance # Store variance for next time return variance def withVariance(self, variance): @@ -554,6 +538,8 @@ def withVariance(self, variance): @returns a CorrelatedNoise object with the new variance. """ + if variance <= 0.: + raise GalSimValueError("variance must be > 0 in withVariance", variance) variance_ratio = variance / self.getVariance() return self * variance_ratio @@ -699,19 +685,16 @@ def _get_update_rootps(self, shape, wcs): """Internal utility function for querying the `rootps` cache, used by applyTo(), whitenImage(), and symmetrizeImage() methods. """ - # First check whether we can just use a stored power spectrum (no drawing necessary if so) - use_stored = False # Query using the rfft2/irfft2 half-sized shape (shape[0], shape[1] // 2 + 1) half_shape = (shape[0], shape[1] // 2 + 1) - for rootps_array, saved_wcs in self._rootps_store: - if rootps_array.shape == half_shape and wcs == saved_wcs: - use_stored = True - rootps = rootps_array - break + key = (half_shape, wcs) + + # Use the cached value if possible. + rootps = self._rootps_cache.get(key, None) # If not, draw the correlation function to the desired size and resolution, then DFT to # generate the required array of the square root of the power spectrum - if use_stored is False: + if rootps is None: # Draw this correlation function into an array. If this is not done at the same wcs as # the original image from which the CF derives, even if the image is rotated, then this # step requires interpolation and the newcf (used to generate the PS below) is thus @@ -721,10 +704,10 @@ def _get_update_rootps(self, shape, wcs): # Since we just drew it, save the variance value for posterity. var = newcf(newcf.bounds.center) - self._variance_stored = var + self._variance_cached = var - if var <= 0.: - raise RuntimeError("CorrelatedNoise found to have negative variance.") + if var <= 0.: # pragma: no cover This should be impossible... + raise GalSimError("CorrelatedNoise found to have negative variance.") # Then calculate the sqrt(PS) that will be used to generate the actual noise. First do # the power spectrum (PS) @@ -748,8 +731,8 @@ def _get_update_rootps(self, shape, wcs): # For now we just take the sqrt(abs(PS)): rootps = np.sqrt(np.abs(ps)) - # Then add this and the relevant wcs to the _rootps_store for later use - self._rootps_store.append((rootps, newcf.wcs)) + # Save this in the cache + self._rootps_cache[key] = rootps return rootps @@ -759,23 +742,20 @@ def _get_update_rootps_whitening(self, shape, wcs, headroom=1.05): @returns rootps_whitening, variance """ - # First check whether we can just use a stored whitening power spectrum - use_stored = False # Query using the rfft2/irfft2 half-sized shape (shape[0], shape[1] // 2 + 1) half_shape = (shape[0], shape[1] // 2 + 1) - for rootps_whitening_array, saved_wcs, var in self._rootps_whitening_store: - if rootps_whitening_array.shape == half_shape and wcs == saved_wcs: - use_stored = True - rootps_whitening = rootps_whitening_array - variance = var - break + + key = (half_shape, wcs) + + # Use the cached values if possible. + rootps_whitening, variance = self._rootps_whitening_cache.get(key, (None,None)) # If not, calculate the whitening power spectrum as (almost) the smallest power spectrum # that when added to rootps**2 gives a flat resultant power that is nowhere negative. # Note that rootps = sqrt(power spectrum), and this procedure therefore works since power # spectra add (rather like variances). The resulting power spectrum will be all positive # (and thus physical). - if use_stored is False: + if rootps_whitening is None: rootps = self._get_update_rootps(shape, wcs) ps_whitening = -rootps * rootps @@ -787,8 +767,8 @@ def _get_update_rootps_whitening(self, shape, wcs, headroom=1.05): # element we could use any as the PS should be flat. variance = rootps[0, 0]**2 + ps_whitening[0, 0] - # Then add all this and the relevant wcs to the _rootps_whitening_store - self._rootps_whitening_store.append((rootps_whitening, wcs, variance)) + # Then add all this and the relevant wcs to the _rootps_whitening_cache + self._rootps_whitening_cache[key] = (rootps_whitening, variance) return rootps_whitening, variance @@ -798,28 +778,20 @@ def _get_update_rootps_symmetrizing(self, shape, wcs, order, headroom=1.02): @returns rootps_symmetrizing, variance """ - # First check whether we can just use a stored symmetrizing power spectrum. In addition for - # the considerations for use of cached observations for noise whitening, we need the - # requested order of the symmetry to be the same as the stored one. - use_stored = False # Query using the rfft2/irfft2 half-sized shape (shape[0], shape[1] // 2 + 1) half_shape = (shape[0], shape[1] // 2 + 1) - for ( - rootps_symmetrizing_array, saved_wcs, var, saved_order - ) in self._rootps_symmetrizing_store: - if (rootps_symmetrizing_array.shape == half_shape and saved_order == order and - wcs == saved_wcs): - use_stored = True - rootps_symmetrizing = rootps_symmetrizing_array - variance = var - break + + key = (half_shape, wcs, order) + + # Use the cached values if possible. + rootps_symmetrizing, variance = self._rootps_symmetrizing_cache.get(key, (None,None)) # If not, calculate the symmetrizing power spectrum as (almost) the smallest power spectrum # that when added to rootps**2 gives a power that has N-fold symmetry, where `N=order`. # Note that rootps = sqrt(power spectrum), and this procedure therefore works since power # spectra add (rather like variances). The resulting power spectrum will be all positive # (and thus physical). - if use_stored is False: + if rootps_symmetrizing is None: rootps = self._get_update_rootps(shape, wcs) ps_actual = rootps * rootps @@ -833,10 +805,12 @@ def _get_update_rootps_symmetrizing(self, shape, wcs, order, headroom=1.02): # be generated with the rootps_symmetrizing. # Here, unlike in _get_update_rootps_whitening, the final power spectrum is not flat, so # we have to take the mean power instead of just using the [0, 0] element. + # Note that the mean of the power spectrum (fourier space) is the zero lag value in + # real space, which is the desired variance. variance = np.mean(rootps**2 + ps_symmetrizing) - # Then add all this and the relevant wcs to the _rootps_symmetrizing_store - self._rootps_symmetrizing_store.append((rootps_symmetrizing, wcs, variance, order)) + # Then add all this and the relevant wcs to the _rootps_symmetrizing_cache + self._rootps_symmetrizing_cache[key] = (rootps_symmetrizing, variance) return rootps_symmetrizing, variance @@ -925,15 +899,11 @@ def _generate_noise_from_rootps(rng, shape, rootps): @returns a NumPy array (contiguous) of the requested shape, filled with the noise field. """ from .random import GaussianDeviate - # Sanity check on requested shape versus that of rootps - if len(shape) != 2 or (shape[0], shape[1]//2+1) != rootps.shape: - raise ValueError("Requested shape does not match that of the supplied rootps") - # Quickest to create Gaussian rng each time needed, so do that here... - gd = GaussianDeviate( - rng, sigma=np.sqrt(.5 * shape[0] * shape[1])) # Note sigma scaling: 1/sqrt(2) needed so - # <|gaussvec|**2> = product(shape); shape - # needed because of the asymmetry in the - # 1/N^2 division in the NumPy FFT/iFFT + # Quickest to create Gaussian rng each time needed, so do that here... + # Note sigma scaling: 1/sqrt(2) needed so <|gaussvec|**2> = product(shape) + # shape needed because of the asymmetry in the 1/N^2 division in the NumPy FFT/iFFT + gd = GaussianDeviate(rng, sigma=np.sqrt(.5 * shape[0] * shape[1])) + # Fill a couple of arrays with this noise gvec_real = utilities.rand_arr((shape[0], shape[1]//2+1), gd) gvec_imag = utilities.rand_arr((shape[0], shape[1]//2+1), gd) @@ -1213,11 +1183,12 @@ def __init__(self, image, rng=None, scale=None, wcs=None, x_interpolant=None, # Set the wcs if necessary if wcs is not None: if scale is not None: - raise ValueError("Cannot provide both wcs and scale") - if not wcs.isUniform(): - raise ValueError("Cannot provide non-uniform wcs") + raise GalSimIncompatibleValuesError("Cannot provide both wcs and scale", + scale=scale, wcs=scale) if not isinstance(wcs, BaseWCS): raise TypeError("wcs must be a BaseWCS instance") + if not wcs.isUniform(): + raise GalSimValueError("Cannot provide non-uniform wcs", wcs) cf_image.wcs = wcs elif scale is not None: cf_image.scale = scale @@ -1243,10 +1214,12 @@ def __init__(self, image, rng=None, scale=None, wcs=None, x_interpolant=None, _BaseCorrelatedNoise.__init__(self, rng, cf_object, cf_image.wcs) if store_rootps: - # If it corresponds to the CF above, store useful data as a (rootps, wcs) tuple for - # efficient later use: - self._profile_for_stored = self._profile - self._rootps_store.append((np.sqrt(ps_array), cf_image.wcs)) + # If it corresponds to the CF above, store in the cache + self._profile_for_cached = self._profile + shape = ps_array.shape + half_shape = (shape[0], shape[1] // 2 + 1) + key = (half_shape, cf_image.wcs) + self._rootps_cache[key] = np.sqrt(ps_array) self._image = image @@ -1393,22 +1366,16 @@ def getCOSMOSNoise(file_name=None, rng=None, cosmos_scale=0.03, variance=0., x_i if file_name is None: file_name = os.path.join(meta_data.share_dir,'acs_I_unrot_sci_20_cf.fits') if not os.path.isfile(file_name): - raise IOError("The file '"+str(file_name)+"' does not exist.") + raise OSError("The file %r does not exist."%(file_name)) try: cfimage = fits.read(file_name) - except KeyboardInterrupt: - raise - except: # pragma: no cover - # Give a vaguely helpful warning, then raise the original exception for extra diagnostics - import warnings - warnings.warn( - "Function getCOSMOSNoise() unable to read FITS image from "+str(file_name)+", "+ - "more information on the error in the following Exception...") - raise + except (IOError, OSError, AttributeError, TypeError) as e: + raise OSError("Unable to read COSMOSNoise file %s.\n%r"%(file_name,e)) # Then check for negative variance before doing anything time consuming if variance < 0: - raise ValueError("Input keyword variance must be zero or positive.") + raise GalSimRangeError("Specified variance must be zero or positive.", + variance, 0, None) # If x_interpolant not specified on input, use bilinear if x_interpolant is None: @@ -1471,15 +1438,17 @@ def __init__(self, variance, rng=None, scale=None, wcs=None, gsparams=None): from .box import Pixel from .convolve import AutoConvolve if variance < 0: - raise ValueError("Input keyword variance must be zero or positive.") + raise GalSimRangeError("Specified variance must be zero or positive.", + variance, 0, None) if wcs is not None: if scale is not None: - raise ValueError("Cannot provide both wcs and scale") + raise GalSimIncompatibleValuesError("Cannot provide both wcs and scale", + scale=scale, wcs=wcs) if not isinstance(wcs, BaseWCS): raise TypeError("wcs must be a BaseWCS instance") if not wcs.isUniform(): - raise ValueError("Cannot provide non-uniform wcs") + raise GalSimValueError("Cannot provide non-uniform wcs", wcs) elif scale is not None: wcs = PixelScale(scale) else: @@ -1487,7 +1456,7 @@ def __init__(self, variance, rng=None, scale=None, wcs=None, gsparams=None): # Save the things that won't get saved by the base class, for use in repr. self.variance = variance - self._gsparams = gsparams + self._gsparams = GSParams.check(gsparams) # Need variance == xvalue(0,0) after autoconvolution # So the Pixel needs to have an amplitude of sigma at (0,0) diff --git a/galsim/dcr.py b/galsim/dcr.py index 7d324764b2f..ed37c781484 100644 --- a/galsim/dcr.py +++ b/galsim/dcr.py @@ -22,6 +22,7 @@ apparent zenith angles of an object), as a function of zenith angle, wavelength, temperature, pressure, and water vapor content. """ +from .errors import GalSimIncompatibleValuesError def air_refractive_index_minus_one(wave, pressure=69.328, temperature=293.15, H2O_pressure=1.067): """Return the refractive index of air as function of wavelength. @@ -94,9 +95,15 @@ def zenith_parallactic_angles(obj_coord, zenith_coord=None, HA=None, latitude=No from .angle import degrees if zenith_coord is None: if HA is None or latitude is None: - raise ValueError("Need to provide either zenith_coord or (HA, latitude) to" - +"zenith_parallactic_angles()") + raise GalSimIncompatibleValuesError( + "Must provide either zenith_coord or (HA, latitude).", + HA=HA, latitude=latitude, zenith_coord=zenith_coord) zenith_coord = CelestialCoord(HA + obj_coord.ra, latitude) + else: + if HA is not None or latitude is not None: + raise GalSimIncompatibleValuesError( + "Cannot provide both zenith_coord and (HA, latitude).", + HA=HA, latitude=latitude, zenith_coord=zenith_coord) zenith_angle = obj_coord.distanceTo(zenith_coord) NCP = CelestialCoord(0.0*degrees, 90*degrees) parallactic_angle = obj_coord.angleBetween(NCP, zenith_coord) @@ -125,23 +132,17 @@ def parse_dcr_angles(**kwargs): if 'zenith_angle' in kwargs: zenith_angle = kwargs.pop('zenith_angle') parallactic_angle = kwargs.pop('parallactic_angle', 0.0*degrees) - if not isinstance(zenith_angle, Angle) or \ - not isinstance(parallactic_angle, Angle): - raise TypeError("zenith_angle and parallactic_angle must be galsim.Angles.") + if not isinstance(zenith_angle, Angle): + raise TypeError("zenith_angle must be a galsim.Angle.") + if not isinstance(parallactic_angle, Angle): + raise TypeError("parallactic_angle must be a galsim.Angles.") elif 'obj_coord' in kwargs: obj_coord = kwargs.pop('obj_coord') - if 'zenith_coord' in kwargs: - zenith_coord = kwargs.pop('zenith_coord') - zenith_angle, parallactic_angle = zenith_parallactic_angles( - obj_coord=obj_coord, zenith_coord=zenith_coord) - else: - if 'HA' not in kwargs or 'latitude' not in kwargs: - raise TypeError("Either zenith_coord or (HA, latitude) is required " + - "when obj_coord is specified.") - HA = kwargs.pop('HA') - latitude = kwargs.pop('latitude') - zenith_angle, parallactic_angle = zenith_parallactic_angles( - obj_coord=obj_coord, HA=HA, latitude=latitude) + zenith_coord = kwargs.pop('zenith_coord', None) + HA = kwargs.pop('HA', None) + latitude = kwargs.pop('latitude', None) + zenith_angle, parallactic_angle = zenith_parallactic_angles( + obj_coord=obj_coord, zenith_coord=zenith_coord, HA=HA, latitude=latitude) else: raise TypeError("Need to specify zenith_angle and parallactic_angle.") return zenith_angle, parallactic_angle, kwargs diff --git a/galsim/deltafunction.py b/galsim/deltafunction.py index 0e747fc57d0..c7ad6ae3c5f 100644 --- a/galsim/deltafunction.py +++ b/galsim/deltafunction.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .position import PositionD from .utilities import doc_inherit +from .errors import convert_cpp_errors class DeltaFunction(GSObject): @@ -71,7 +72,8 @@ def __init__(self, flux=1., gsparams=None): @property def _sbp(self): # NB. I only need this until compound and transform are reimplemented in Python... - return _galsim.SBDeltaFunction(self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBDeltaFunction(self._flux, self.gsparams._gsp) def __eq__(self, other): return (isinstance(other, DeltaFunction) and diff --git a/galsim/deprecated/__init__.py b/galsim/deprecated/__init__.py index f202c26658f..2e8ca2f7011 100644 --- a/galsim/deprecated/__init__.py +++ b/galsim/deprecated/__init__.py @@ -16,12 +16,7 @@ # and/or other materials provided with the distribution. # -# Note: By default python2.7 ignores DeprecationWarnings. Apparently they are really -# for python system deprecations. I think GalSim deprecations are more appropriately -# considered UserWarnings, which are not ignored by default. -class GalSimDeprecationWarning(UserWarning): - def __init__(self, s): - super(GalSimDeprecationWarning, self).__init__(self, s) +from ..errors import GalSimDeprecationWarning def depr(f, v, s1, s2=None): """A helper function for emitting a GalSimDeprecationWarning. @@ -35,13 +30,13 @@ def depr(f, v, s1, s2=None): 2. You can add extra information if you want to point out something about the new syntax: depr('draw', 1.1, "drawImage(..., method='no_pixel')", - 'Note: drawImage has different args than draw did. ' + + 'Note: drawImage has different args than draw did. ' 'Read the docs for the method keywords carefully.') 3. If the deprecated function has no replacement, you can use '' for the first string. depr('calculateCovarianceMatrix', 1.3, '', - 'This functionality has been removed. If you have a need for it, please open '+ + 'This functionality has been removed. If you have a need for it, please open ' 'an issue requesting the functionality.') """ import warnings diff --git a/galsim/des/des_meds.py b/galsim/des/des_meds.py index 2dec557fe1a..77b43994f4a 100644 --- a/galsim/des/des_meds.py +++ b/galsim/des/des_meds.py @@ -78,51 +78,55 @@ def __init__(self, images, weight=None, badpix=None, seg=None, psf=None, if not isinstance(images,list): raise TypeError('images should be a list') if len(images) == 0: - raise ValueError('no cutouts in this object') + raise galsim.GalSimValueError('no cutouts in this object', images) # Check that the box sizes are valid for i in range(len(images)): s = images[i].array.shape if s[0] != s[1]: - raise ValueError('Array shape %s is invalid. Must be square'%(str(s))) + raise galsim.GalSimValueError('Array shape %s is invalid. Must be square'%(str(s)), + images[i]) if s[0] not in BOX_SIZES: - raise ValueError('Array shape %s is invalid. Size must be in %s'%( - str(s),str(BOX_SIZES))) + raise galsim.GalSimValueError('Array shape %s is invalid. Size must be in %s'%( + str(s),str(BOX_SIZES)), images[i]) if i > 0 and s != images[0].array.shape: - raise ValueError('Images must all be the same shape') + raise galsim.GalSimValueError('Images must all be the same shape', images) # The others are optional, but if given, make sure they are ok. - for lst, name, isim in [ (weight, 'weight', True), (badpix, 'badpix', True), - (seg, 'seg', True), (psf, 'psf', False), (wcs, 'wcs', False) ]: + for lst, name, isim in ( (weight, 'weight', True), (badpix, 'badpix', True), + (seg, 'seg', True), (psf, 'psf', False), (wcs, 'wcs', False) ): if lst is not None: if not isinstance(lst,list): raise TypeError('%s should be a list'%name) if len(lst) != len(images): - raise ValueError('%s is the wrong length'%name) + raise galsim.GalSimValueError('%s is the wrong length'%name, lst) if isim: for i in range(len(images)): im1 = lst[i] im2 = images[i] if (im1.array.shape != im2.array.shape): - raise ValueError("%s[%d] has the wrong shape."%(name, i)) + raise galsim.GalSimValueError( + "%s[%d] has the wrong shape."%(name, i), im1) # The PSF images don't have to be the same shape as the main images. # But make sure all psf images are square and the same shape if psf is not None: s = psf[i].array.shape if s[0] != s[1]: - raise ValueError('PSF array shape %s is invalid. Must be square'%(str(s))) + raise galsim.GalSimValueError( + 'PSF array shape %s is invalid. Must be square'%(str(s)), psf[i]) if s[0] not in BOX_SIZES: - raise ValueError('PSF array shape %s is invalid. Size must be in %s'%( - str(s),str(BOX_SIZES))) + raise galsim.GalSimValueError( + 'PSF array shape %s is invalid. Size must be in %s'%( + str(s),str(BOX_SIZES)), psf[i]) if i > 0 and s != psf[0].array.shape: - raise ValueError('PSF images must all be the same shape') + raise galsim.GalSimValueError('PSF images must all be the same shape', psf[i]) # Check that wcs are Uniform and convert them to AffineTransforms in case they aren't. if wcs is not None: for i in range(len(wcs)): if not isinstance(wcs[i], galsim.wcs.UniformWCS): - raise TypeError('wcs list should contain UniformWCS objects') + raise galsim.GalSimValueError('wcs list should contain UniformWCS objects', wcs) elif not isinstance(wcs[i], galsim.AffineTransform): wcs[i] = wcs[i].affine() @@ -265,7 +269,8 @@ def WriteMEDS(obj_list, file_name, clobber=True): vec['image'].append(obj.images[i].array.flatten()) vec['seg'].append(obj.seg[i].array.flatten()) vec['weight'].append(obj.weight[i].array.flatten()) - vec['psf'].append(obj.psf[i].array.flatten()) + if obj.psf is not None: + vec['psf'].append(obj.psf[i].array.flatten()) # append cutout_row/col cutout_row[i] = obj.cutout_row[i] @@ -281,9 +286,9 @@ def WriteMEDS(obj_list, file_name, clobber=True): # check if we are running out of memory if sys.getsizeof(vec) > MAX_MEMORY: # pragma: no cover - raise MemoryError( - 'Running out of memory > %1.0fGB '%MAX_MEMORY/1.e9 + - '- you can increase the limit by changing MAX_MEMORY') + raise galsim.GalSimError( + "Running out of memory > %1.0fGB - you can increase the limit by changing " + "galsim.des_meds.MAX_MEMORY"%(MAX_MEMORY/1.e9)) # update the start rows fields in the catalog cat['start_row'].append(start_rows) @@ -303,7 +308,8 @@ def WriteMEDS(obj_list, file_name, clobber=True): vec['image'] = np.concatenate(vec['image']) vec['seg'] = np.concatenate(vec['seg']) vec['weight'] = np.concatenate(vec['weight']) - vec['psf'] = np.concatenate(vec['psf']) + if obj.psf is not None: + vec['psf'] = np.concatenate(vec['psf']) # get the primary HDU primary = pyfits.PrimaryHDU() @@ -412,13 +418,11 @@ def WriteMEDS(obj_list, file_name, clobber=True): metadata.update_ext_name('metadata') # rest of HDUs are image vectors - image_cutouts = pyfits.ImageHDU( vec['image'] , name='image_cutouts' ) - weight_cutouts = pyfits.ImageHDU( vec['weight'], name='weight_cutouts' ) - seg_cutouts = pyfits.ImageHDU( vec['seg'] , name='seg_cutouts' ) - psf_cutouts = pyfits.ImageHDU( vec['psf'] , name='psf' ) + image_cutouts = pyfits.ImageHDU( vec['image'] , name='image_cutouts') + weight_cutouts = pyfits.ImageHDU( vec['weight'], name='weight_cutouts') + seg_cutouts = pyfits.ImageHDU( vec['seg'] , name='seg_cutouts') - # write all - hdu_list = pyfits.HDUList([ + hdu_list = [ primary, object_data, image_info, @@ -426,9 +430,13 @@ def WriteMEDS(obj_list, file_name, clobber=True): image_cutouts, weight_cutouts, seg_cutouts, - psf_cutouts - ]) - galsim.fits.writeFile(file_name, hdu_list) + ] + + if obj.psf is not None: + psf_cutouts = pyfits.ImageHDU( vec['psf'], name='psf') + hdu_list.append(psf_cutouts) + + galsim.fits.writeFile(file_name, pyfits.HDUList(hdu_list)) # Make the class that will @@ -452,10 +460,9 @@ def buildImages(self, config, base, file_num, image_num, obj_num, ignore, logger import time t1 = time.time() - if 'image' in base and 'type' in base['image']: - image_type = base['image']['type'] - if image_type != 'Single': - raise AttibuteError("MEDS files are not compatible with image type %s."%image_type) + if base.get('image',{}).get('type', 'Single') != 'Single': + raise galsim.GalSimConfigError( + "MEDS files are not compatible with image type %s."%base['image']['type']) req = { 'nobjects' : int , 'nstamps_per_object' : int } ignore += [ 'file_name', 'dir', 'nfiles' ] diff --git a/galsim/des/des_psfex.py b/galsim/des/des_psfex.py index b149cda1580..9014fad4762 100644 --- a/galsim/des/des_psfex.py +++ b/galsim/des/des_psfex.py @@ -27,11 +27,11 @@ https://www.astromatic.net/pubsvn/software/psfex/trunk/doc/psfex.pdf """ -from past.builtins import basestring +import os +import numpy as np import galsim import galsim.config -import numpy as np class DES_PSFEx(object): """Class that handles DES files describing interpolated principal component images @@ -106,16 +106,17 @@ class DES_PSFEx(object): def __init__(self, file_name, image_file_name=None, wcs=None, dir=None): if dir: - if not isinstance(file_name, basestring): - raise ValueError("Cannot provide dir and an HDU instance") - import os + if not isinstance(file_name, str): + raise TypeError("file_name must be a string") file_name = os.path.join(dir,file_name) if image_file_name is not None: image_file_name = os.path.join(dir,image_file_name) self.file_name = file_name if image_file_name: if wcs is not None: - raise AttributeError("Cannot provide both image_file_name and wcs") + raise galsim.GalSimIncompatibleValuesError( + "Cannot provide both image_file_name and wcs", + image_file_name=image_file_name, wcs=wcs) header = galsim.FitsHeader(file_name=image_file_name) wcs, origin = galsim.wcs.readFromFitsHeader(header) self.wcs = wcs @@ -127,7 +128,7 @@ def __init__(self, file_name, image_file_name=None, wcs=None, dir=None): def read(self): from galsim._pyfits import pyfits - if isinstance(self.file_name, basestring): + if isinstance(self.file_name, str): hdu_list = pyfits.open(self.file_name) hdu = hdu_list[1] else: @@ -198,37 +199,30 @@ def read(self): psf_samp = hdu.header['PSF_SAMP'] # The basis object is a data cube (assuming PSFNAXIS==3) - # Note: older pyfits versions don't get the shape right. - # For newer pyfits versions the reshape command should be a no op. - basis = hdu.data.field('PSF_MASK')[0].reshape(psf_axis3,psf_axis2,psf_axis1) + basis = hdu.data.field('PSF_MASK')[0] # Make sure to close the hdu before we might raise exceptions. if hdu_list: hdu_list.close() # Check for valid values of all these things. - if pol_naxis != 2: - raise IOError("PSFEx: Expected POLNAXIS == 2, got %d"%pol_naxis) - if not (pol_name1.startswith('X') and pol_name1.endswith('IMAGE')): - raise IOError("PSFEx: Expected POLNAME1 == X*_IMAGE, got %s"%pol_name1) - if not (pol_name2.startswith('Y') and pol_name2.endswith('IMAGE')): - raise IOError("PSFEx: Expected POLNAME2 == Y*_IMAGE, got %s"%pol_name2) - if pol_ngrp != 1: - raise IOError("PSFEx: Current implementation requires POLNGRP == 1, got %d"%pol_ngrp) - if pol_group1 != 1: - raise IOError("PSFEx: Expected POLGRP1 == 1, got %s"%pol_group1) - if pol_group2 != 1: - raise IOError("PSFEx: Expected POLGRP2 == 1, got %s"%pol_group2) - if psf_naxis != 3: - raise IOError("PSFEx: Expected PSFNAXIS == 3, got %d"%psf_naxis) - if psf_axis3 != ((pol_deg+1)*(pol_deg+2))//2: - raise IOError("PSFEx: POLDEG and PSFAXIS3 disagree") - if basis.shape[0] != psf_axis3: - raise IOError("PSFEx: PSFAXIS3 disagrees with actual basis size") - if basis.shape[1] != psf_axis2: - raise IOError("PSFEx: PSFAXIS2 disagrees with actual basis size") - if basis.shape[2] != psf_axis1: - raise IOError("PSFEx: PSFAXIS1 disagrees with actual basis size") + # Not sure which of these are actually required in PSFEx files, but this implementation + # assumes these things are true, so if this fails, we probably need to rework some aspect + # of this code. + try: + assert pol_naxis == 2 + assert pol_name1.startswith('X') and pol_name1.endswith('IMAGE') + assert pol_name2.startswith('Y') and pol_name2.endswith('IMAGE') + assert pol_ngrp == 1 + assert pol_group1 == 1 + assert pol_group2 == 1 + assert psf_naxis == 3 + assert psf_axis3 == ((pol_deg+1)*(pol_deg+2))//2 + assert basis.shape[0] == psf_axis3 + assert basis.shape[1] == psf_axis2 + assert basis.shape[2] == psf_axis1 + except AssertionError as e: + raise OSError("PSFEx file %s is not as expected.\n%r"%(self.file_name, e)) # Save some of these values for use in building the interpolated images self.basis = basis @@ -338,7 +332,7 @@ def BuildDES_PSFEx(config, base, ignore, gsparams, logger): elif 'image_pos' in base: image_pos = base['image_pos'] else: - raise ValueError("DES_PSFEx requested, but no image_pos defined in base.") + raise galsim.GalSimConfigError("DES_PSFEx requested, but no image_pos defined in base.") # Convert gsparams from a dict to an actual GSParams object if gsparams: gsparams = galsim.GSParams(**gsparams) diff --git a/galsim/des/des_shapelet.py b/galsim/des/des_shapelet.py index 0965e883ad3..44a00bdbf3e 100644 --- a/galsim/des/des_shapelet.py +++ b/galsim/des/des_shapelet.py @@ -29,12 +29,11 @@ class DES_Shapelet(object): """Class that handles DES files describing interpolated polar shapelet decompositions. - These are usually stored as *_fitpsf.fits files, although there is also an ASCII - version stored as *_fitpsf.dat. + These are stored as *_fitpsf.fits files. They are not used in DES anymore, so this + class is at best of historical interest - The shapelet PSFs are built as part of the WL portion of the DES pipeline. They measure - a shapelet decomposition of each star and interpolate the shapelet coefficients over the - image positions. + The shapelet PSFs measure a shapelet decomposition of each star and interpolate the shapelet + coefficients over the image positions. Unlike PSFEx, these PSF models are built directly in world coordinates. The shapelets know about the WCS, so they are able to fit the shapelet model directly in terms of arcsec. @@ -65,76 +64,21 @@ class DES_Shapelet(object): @param file_name The name of the file to be read in. @param dir Optionally a directory name can be provided if the file names do not already include it. [default: None] - @param file_type Either 'ASCII' or 'FITS' or None. If None, infer from the file name - ending. [default: None] """ _req_params = { 'file_name' : str } - _opt_params = { 'dir' : str, 'file_type' : str } + _opt_params = { 'dir' : str } _single_params = [] _takes_rng = False - def __init__(self, file_name, dir=None, file_type=None): + def __init__(self, file_name, dir=None): if dir: import os file_name = os.path.join(dir,file_name) self.file_name = file_name - - if not file_type: # pragma: no branch - if self.file_name.lower().endswith('.fits'): - file_type = 'FITS' - else: # pragma: no cover - file_type = 'ASCII' - file_type = file_type.upper() - if file_type not in ['FITS', 'ASCII']: - raise ValueError("file_type must be either FITS or ASCII if specified.") - - if file_type == 'FITS': - self.read_fits() - else: # pragma: no cover - self.read_ascii() - - # We haven't used these for a long time, so this is at best of historical interest... - def read_ascii(self): # pragma: no cover - """Read in a DES_Shapelet stored using the the ASCII-file version. - """ - fin = open(self.file_name, 'r') - lines = fin.readlines() - temp = lines[0].split() - self.psf_order = int(temp[0]) - self.psf_size = (self.psf_order+1) * (self.psf_order+2) // 2 - self.sigma = float(temp[1]) - self.fit_order = int(temp[2]) - self.fit_size = (self.fit_order+1) * (self.fit_order+2) // 2 - self.npca = int(temp[3]) - - temp = lines[1].split() - self.bounds = galsim.BoundsD( - float(temp[0]), float(temp[1]), - float(temp[2]), float(temp[3])) - - temp = lines[2].split() - assert int(temp[0]) == self.psf_size - self.ave_psf = np.array(temp[2:self.psf_size+2]).astype(float) - assert self.ave_psf.shape == (self.psf_size,) - - temp = lines[3].split() - assert int(temp[0]) == self.npca - assert int(temp[1]) == self.psf_size - self.rot_matrix = np.array( - [ lines[4+k].split()[1:self.psf_size+1] for k in range(self.npca) ] - ).astype(float) - assert self.rot_matrix.shape == (self.npca, self.psf_size) - - temp = lines[5+self.npca].split() - assert int(temp[0]) == self.fit_size - assert int(temp[1]) == self.npca - self.interp_matrix = np.array( - [ lines[6+self.npca+k].split()[1:self.npca+1] for k in range(self.fit_size) ] - ).astype(float) - assert self.interp_matrix.shape == (self.fit_size, self.npca) + self.read_fits() def read_fits(self): - """Read in a DES_Shapelet stored using the the FITS-file version. + """Read in a DES_Shapelet stored in FITS file. """ from galsim._pyfits import pyfits with pyfits.open(self.file_name) as fits: @@ -192,7 +136,8 @@ def getB(self, pos): """Get the B vector as a numpy array at position pos """ if not self.bounds.includes(pos): - raise IndexError("position in DES_Shapelet.getPSF is out of bounds") + raise galsim.GalSimBoundsError("position in DES_Shapelet.getPSF is out of bounds", + pos, self.bounds) Px = self._definePxy(pos.x,self.bounds.xmin,self.bounds.xmax) Py = self._definePxy(pos.y,self.bounds.ymin,self.bounds.ymax) @@ -246,7 +191,7 @@ def BuildDES_Shapelet(config, base, ignore, gsparams, logger): elif 'image_pos' in base: image_pos = base['image_pos'] else: - raise ValueError("DES_Shapelet requested, but no image_pos defined in base.") + raise galsim.GalSimConfigError("DES_Shapelet requested, but no image_pos defined in base.") # Convert gsparams from a dict to an actual GSParams object if gsparams: gsparams = galsim.GSParams(**gsparams) diff --git a/galsim/detectors.py b/galsim/detectors.py index 3d64ca07c47..bbfbf9ed6ac 100644 --- a/galsim/detectors.py +++ b/galsim/detectors.py @@ -22,7 +22,9 @@ import numpy as np import sys + from .image import Image +from .errors import GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError, galsim_warn def applyNonlinearity(self, NLfunc, *args): """ @@ -72,9 +74,10 @@ def applyNonlinearity(self, NLfunc, *args): # Extract out the array from Image since not all functions can act directly on Images result = NLfunc(self.array,*args) if not isinstance(result, np.ndarray): - raise ValueError("NLfunc does not return a NumPy array.") + raise GalSimValueError("NLfunc does not return a NumPy array.", NLfunc) if self.array.shape != result.shape: - raise ValueError("NLfunc does not return a NumPy array of the same shape as input!") + raise GalSimValueError("NLfunc does not return a NumPy array of the same shape as input.", + NLfunc) self.array[:,:] = result @@ -131,16 +134,18 @@ def addReciprocityFailure(self, exp_time, alpha, base_flux): value. """ - if not isinstance(alpha, float) or alpha < 0.: - raise ValueError("Invalid value of alpha, must be float >= 0") - if not (isinstance(exp_time, float) or isinstance(exp_time, int)) or exp_time < 0.: - raise ValueError("Invalid value of exp_time, must be float or int >= 0") - if not (isinstance(base_flux, float) or isinstance(base_flux,int)) or base_flux < 0.: - raise ValueError("Invalid value of base_flux, must be float or int >= 0") + if alpha < 0.: + raise GalSimRangeError("Invalid value of alpha, must be >= 0", + alpha, 0, None) + if exp_time < 0.: + raise GalSimRangeError("Invalid value of exp_time, must be >= 0", + exp_time, 0, None) + if base_flux < 0.: + raise GalSimRangeError("Invalid value of base_flux, must be >= 0", + base_flux, 0, None) if np.any(self.array<0): - import warnings - warnings.warn("One or more pixel values are negative and will be set as 'nan'.") + galsim_warn("One or more pixel values are negative and will be set as 'nan'.") p0 = exp_time*base_flux a = alpha/np.log(10) @@ -200,25 +205,20 @@ def applyIPC(self, IPC_kernel, edge_treatment='extend', fill_value=None, kernel_ @returns None """ - # IPC kernel has to be a 3x3 Image instance - if not isinstance(IPC_kernel, Image): - raise ValueError("IPC_kernel must be an Image instance .") + # IPC kernel has to be a 3x3 Image ipc_kernel = IPC_kernel.array if not ipc_kernel.shape==(3,3): - raise ValueError("IPC kernel must be an Image instance of size 3x3.") + raise GalSimValueError("IPC kernel must be an Image instance of size 3x3.", IPC_kernel) # Check for non-negativity of the kernel - if kernel_nonnegativity is True: - if (ipc_kernel<0).any() is True: - raise ValueError("IPC kernel must not contain negative entries") + if kernel_nonnegativity and (ipc_kernel<0).any(): + raise GalSimValueError("IPC kernel must not contain negative entries", IPC_kernel) # Check and enforce correct normalization for the kernel - if kernel_normalization is True: - if abs(ipc_kernel.sum() - 1.0) > 10.*np.finfo(ipc_kernel.dtype.type).eps: - import warnings - warnings.warn("The entries in the IPC kernel did not sum to 1. Scaling the kernel to "\ - +"ensure correct normalization.") - IPC_kernel = IPC_kernel/ipc_kernel.sum() + if kernel_normalization and abs(ipc_kernel.sum()-1) > 10.*np.finfo(ipc_kernel.dtype.type).eps: + galsim_warn("The entries in the IPC kernel did not sum to 1. Scaling the kernel to " + "ensure correct normalization.") + IPC_kernel = IPC_kernel/ipc_kernel.sum() # edge_treatment can be 'extend', 'wrap' or 'crop' if edge_treatment=='crop': @@ -238,7 +238,8 @@ def applyIPC(self, IPC_kernel, edge_treatment='extend', fill_value=None, kernel_ pad_array[:,0] = pad_array[:,-2] pad_array[:,-1] = pad_array[:,1] else: - raise ValueError("edge_treatment has to be one of 'extend', 'wrap' or 'crop'. ") + raise GalSimValueError("Invalid edge_treatment.", edge_treatment, + ('extend', 'wrap', 'crop')) # Generating different segments of the padded array center = pad_array[1:-1,1:-1] @@ -265,13 +266,10 @@ def applyIPC(self, IPC_kernel, edge_treatment='extend', fill_value=None, kernel_ self.array[1:-1,1:-1] = out_array #Explicit edge effects handling with filling the edges with the value given in fill_value if fill_value is not None: - if isinstance(fill_value, float) or isinstance(fill_value, int): - self.array[0,:] = fill_value - self.array[-1,:] = fill_value - self.array[:,0] = fill_value - self.array[:,-1] = fill_value - else: - raise ValueError("'fill_value' must be either a float or an int") + self.array[0,:] = fill_value + self.array[-1,:] = fill_value + self.array[:,0] = fill_value + self.array[:,-1] = fill_value else: self.array[:,:] = out_array @@ -299,33 +297,16 @@ def applyPersistence(self,imgs,coeffs): >>> img.applyPersistence(imgs=img_list, coeffs=coeffs_list) - @ param imgs A list of previous Image instances that still persist. - @ param coeffs A list of floats that specifies the retention factors for the corresponding + @param imgs A list of previous Image instances that still persist. + @param coeffs A list of floats that specifies the retention factors for the corresponding Image instances listed in 'imgs'. - @ returns None + @returns None """ - - if not hasattr(imgs,'__iter__') or not hasattr(coeffs,'__iter__'): - raise TypeError("Type mismatch between 'imgs' and 'coeffs' in 'applyPersistence' routine. " - "'imgs' must be a list of Image instances and 'coeffs' must be a list of " - "floats of the same length.") - if not len(imgs)==len(coeffs): - raise TypeError("The length of 'imgs' and 'coeffs' must be the same, if passed as a " - "list") - # If this error is not raised, then the images are added as long as one of the list is - # exhausted. - + raise GalSimIncompatibleValuesError("The length of 'imgs' and 'coeffs' must be the same", + imgs=imgs, coeffs=coeffs) for img,coeff in zip(imgs,coeffs): - if not isinstance(img, Image): - raise ValueError("In 'applyPersistence', the objects in 'imgs' must be " - "galsim.Image instances") - - if not isinstance(coeff,float): - raise ValueError("In 'applyPersistence', the objects in 'coeffs' must be " - "of type float") - self += coeff*img def quantize(self): diff --git a/galsim/download_cosmos.py b/galsim/download_cosmos.py index 8499a913ae1..4f51713917a 100644 --- a/galsim/download_cosmos.py +++ b/galsim/download_cosmos.py @@ -21,13 +21,14 @@ from __future__ import print_function from builtins import input - import os, sys, tarfile, subprocess, shutil, json try: from urllib2 import urlopen except: from urllib.request import urlopen +from .utilities import ensure_dir + script_name = 'galsim_download_cosmos' def parse_args(): @@ -153,11 +154,6 @@ def query_yes_no(question, default="yes"): sys.stdout.write("Please respond with 'yes' or 'no' "\ "(or 'y' or 'n').\n") -def ensure_dir(target): - d = os.path.dirname(target) - if not os.path.exists(d): - os.makedirs(d) - def download(url, target, unpack_dir, args, logger): logger.warning('Downloading from url:\n %s',url) logger.warning('Target location is:\n %s',target) @@ -232,7 +228,7 @@ def download(url, target, unpack_dir, args, logger): if obsolete: if args.quiet or args.force: - logger.warning("The version currently on disk is obsolete. "+ + logger.warning("The version currently on disk is obsolete. " "Downloading new version.") else: q = "The version currently on disk is obsolete. Download new version?" @@ -240,10 +236,10 @@ def download(url, target, unpack_dir, args, logger): if yn == 'no': do_download = False elif args.force: - logger.info("Target file has already been downloaded and unpacked. "+ + logger.info("Target file has already been downloaded and unpacked. " "Forced re-download.") elif args.quiet: - logger.info("Target file has already been downloaded and unpacked. "+ + logger.info("Target file has already been downloaded and unpacked. " "Not re-downloading.") do_download = False args.save = True # Don't delete it! @@ -285,9 +281,9 @@ def download(url, target, unpack_dir, args, logger): sys.stdout.flush() next_dot += file_size/100. logger.info("Download complete.") - except IOError as e: + except (IOError, OSError) as e: # Try to give a reasonable suggestion for some common IOErrors. - logger.error("\n\nIOError: %s",str(e)) + logger.error("\n\nOSError: %s",str(e)) if 'Permission denied' in str(e): logger.error("Rerun using sudo %s",script_name) logger.error("If this is not possible, you can download to an alternate location:") @@ -301,12 +297,7 @@ def download(url, target, unpack_dir, args, logger): def unpack(target, target_dir, unpack_dir, meta, args, logger): logger.info("Unpacking the tarball...") - #with tarfile.open(target) as tar: - # The above line works on python 2.7+. But to make sure we work for 2.6, we use the - # following workaround. - # cf. http://stackoverflow.com/questions/6086603/statement-with-and-tarfile - from contextlib import closing - with closing(tarfile.open(target)) as tar: + with tarfile.open(target) as tar: if args.verbosity >= 3: tar.list(verbose=True) elif args.verbosity >= 2: diff --git a/galsim/errors.py b/galsim/errors.py new file mode 100644 index 00000000000..2c85021290e --- /dev/null +++ b/galsim/errors.py @@ -0,0 +1,424 @@ +# Copyright (c) 2012-2018 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. + +import warnings +from builtins import super +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. +# Note: In Python 2, there was a distinction between IOError and OSError, but +# there was never much difference in reality, and in Python 3, they made both +# OSError. We should just use OSError for all such kinds of errors. +# +# 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. + +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 repr(self) == repr(other) + def __hash__(self): return hash(repr(self)) + + +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().__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) + + +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().__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) + + +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().__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) + + +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().__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) + + +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().__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) + + +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)) + + +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().__init__(message) + + def __repr__(self): + return 'galsim.GalSimImmutableError(%r,%r)'%(self.message, self.image) + def __reduce__(self): + return GalSimImmutableError, (self.message, self.image) + + +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().__init__(message) + + def __repr__(self): + return 'galsim.GalSimIncompatibleValuesError(%r,%r)'%(self.message, self.values) + def __reduce__(self): + return GalSimIncompatibleValuesError, (self.message, self.values) + + +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().__init__(message) + + def __repr__(self): + return 'galsim.GalSimSEDError(%r,%r)'%(self.message, self.sed) + def __reduce__(self): + return GalSimSEDError, (self.message, self.sed) + + +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)) + + +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().__init__(message) + + def __repr__(self): + return 'galsim.GalSimFFTSizeError(%r,%r)'%(self.message, self.size) + def __reduce__(self): + return GalSimFFTSizeError, (self.message, self.size) + + +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)) + + +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) + + +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. +class GalSimWarning(UserWarning): + """The base class for GalSim-emitted warnings. + """ + def __repr__(self): return 'galsim.GalSimWarning(%r)'%(str(self)) + def __eq__(self, other): return 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. +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)) + +def galsim_warn(message): + """A helper function for emitting a GalSimWarning with the given message + """ + warnings.warn(message, GalSimWarning) diff --git a/galsim/exponential.py b/galsim/exponential.py index dc143e74118..54827c14b7c 100644 --- a/galsim/exponential.py +++ b/galsim/exponential.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import GalSimIncompatibleValuesError, convert_cpp_errors class Exponential(GSObject): @@ -75,15 +76,15 @@ class Exponential(GSObject): def __init__(self, half_light_radius=None, scale_radius=None, flux=1., gsparams=None): if half_light_radius is not None: if scale_radius is not None: - raise TypeError( - "Only one of scale_radius and half_light_radius may be " + - "specified for Exponential") + raise GalSimIncompatibleValuesError( + "Only one of scale_radius and half_light_radius may be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius) else: scale_radius = half_light_radius / Exponential._hlr_factor elif scale_radius is None: - raise TypeError( - "Either scale_radius or half_light_radius must be " + - "specified for Exponential") + raise GalSimIncompatibleValuesError( + "Either scale_radius or half_light_radius must be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius) self._r0 = float(scale_radius) self._flux = float(flux) self._gsparams = GSParams.check(gsparams) @@ -92,7 +93,8 @@ def __init__(self, half_light_radius=None, scale_radius=None, flux=1., gsparams= @lazy_property def _sbp(self): - return _galsim.SBExponential(self._r0, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBExponential(self._r0, self._flux, self.gsparams._gsp) @property def scale_radius(self): return self._r0 diff --git a/galsim/fds_test.py b/galsim/fds_test.py index dde0355f9b5..cb6b5095dd6 100644 --- a/galsim/fds_test.py +++ b/galsim/fds_test.py @@ -40,8 +40,8 @@ """ from __future__ import print_function - import builtins + openfiles = set() oldfile = builtins.file class newfile(oldfile): diff --git a/galsim/fft.py b/galsim/fft.py index 93e76223a84..586262ec4e4 100644 --- a/galsim/fft.py +++ b/galsim/fft.py @@ -35,9 +35,11 @@ """ import numpy as np + from . import _galsim from .image import Image, ImageD, ImageCD from .bounds import BoundsI +from .errors import GalSimValueError, convert_cpp_errors def fft2(a, shift_in=False, shift_out=False): """Compute the 2-dimensional discrete Fourier Transform. @@ -77,19 +79,20 @@ def fft2(a, shift_in=False, shift_out=False): """ s = a.shape if len(s) != 2: - raise ValueError("Input array must be 2D. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must be 2D.",s) M, N = s Mo2 = M // 2 No2 = N // 2 if M != Mo2*2 or N != No2*2: - raise ValueError("Input array must have even sizes. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must have even sizes.",s) if a.dtype.kind == 'c': a = a.astype(np.complex128, copy=False) xim = ImageCD(a, xmin = -No2, ymin = -Mo2) kim = ImageCD(BoundsI(-No2,No2-1,-Mo2,Mo2-1)) - _galsim.cfft(xim._image, kim._image, False, shift_in, shift_out) + with convert_cpp_errors(): + _galsim.cfft(xim._image, kim._image, False, shift_in, shift_out) kar = kim.array else: a = a.astype(np.float64, copy=False) @@ -102,7 +105,8 @@ def fft2(a, shift_in=False, shift_out=False): # Faster to start with rfft2 version rkim = ImageCD(BoundsI(0,No2,-Mo2,Mo2-1)) - _galsim.rfft(xim._image, rkim._image, shift_in, shift_out) + with convert_cpp_errors(): + _galsim.rfft(xim._image, rkim._image, shift_in, shift_out) # This only returns kx >= 0. Fill out the full image. kar = np.empty( (M,N), dtype=np.complex128) rkar = rkim.array @@ -162,13 +166,13 @@ def ifft2(a, shift_in=False, shift_out=False): """ s = a.shape if len(s) != 2: - raise ValueError("Input array must be 2D. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must be 2D.",s) M,N = s Mo2 = M // 2 No2 = N // 2 if M != Mo2*2 or N != No2*2: - raise ValueError("Input array must have even sizes. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must have even sizes.",s) if a.dtype.kind == 'c': a = a.astype(np.complex128, copy=False) @@ -177,7 +181,8 @@ def ifft2(a, shift_in=False, shift_out=False): a = a.astype(np.float64, copy=False) kim = ImageD(a, xmin = -No2, ymin = -Mo2) xim = ImageCD(BoundsI(-No2,No2-1,-Mo2,Mo2-1)) - _galsim.cfft(kim._image, xim._image, True, shift_in, shift_out) + with convert_cpp_errors(): + _galsim.cfft(kim._image, xim._image, True, shift_in, shift_out) return xim.array @@ -218,18 +223,19 @@ def rfft2(a, shift_in=False, shift_out=False): """ s = a.shape if len(s) != 2: - raise ValueError("Input array must be 2D. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must be 2D.",s) M,N = s Mo2 = M // 2 No2 = N // 2 if M != Mo2*2 or N != No2*2: - raise ValueError("Input array must have even sizes. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must have even sizes.",s) a = a.astype(np.float64, copy=False) xim = ImageD(a, xmin = -No2, ymin = -Mo2) kim = ImageCD(BoundsI(0,No2,-Mo2,Mo2-1)) - _galsim.rfft(xim._image, kim._image, shift_in, shift_out) + with convert_cpp_errors(): + _galsim.rfft(xim._image, kim._image, shift_in, shift_out) return kim.array @@ -270,18 +276,19 @@ def irfft2(a, shift_in=False, shift_out=False): """ s = a.shape if len(s) != 2: - raise ValueError("Input array must be 2D. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must be 2D.",s) M,No2 = s No2 -= 1 # s is (M,No2+1) Mo2 = M // 2 if M != Mo2*2: - raise ValueError("Input array must have even sizes. Got shape=%s"%str(s)) + raise GalSimValueError("Input array must have even sizes.",s) a = a.astype(np.complex128, copy=False) kim = ImageCD(a, xmin = 0, ymin = -Mo2) xim = ImageD(BoundsI(-No2,No2+1,-Mo2,Mo2-1)) - _galsim.irfft(kim._image, xim._image, shift_in, shift_out) + with convert_cpp_errors(): + _galsim.irfft(kim._image, xim._image, shift_in, shift_out) xim = xim.subImage(BoundsI(-No2,No2-1,-Mo2,Mo2-1)) return xim.array diff --git a/galsim/fits.py b/galsim/fits.py index 44a6333fed9..c5d142e09c9 100644 --- a/galsim/fits.py +++ b/galsim/fits.py @@ -26,7 +26,9 @@ from past.builtins import basestring import os import numpy as np + from .image import Image +from .errors import GalSimError, GalSimValueError, GalSimIncompatibleValuesError, galsim_warn ############################################################################################## @@ -37,7 +39,6 @@ ############################################################################################## def _parse_compression(compression, file_name): - from ._pyfits import pyfits, pyfits_version file_compress = None pyfits_compress = None if compression == 'rice' or compression == 'RICE_1': pyfits_compress = 'RICE_1' @@ -52,15 +53,12 @@ def _parse_compression(compression, file_name): if file_name.lower().endswith('.fz'): pyfits_compress = 'RICE_1' elif file_name.lower().endswith('.gz'): file_compress = 'gzip' elif file_name.lower().endswith('.bz2'): file_compress = 'bzip2' - else: pass + else: # pragma: no cover (Not sure why Travis thinks this isn't covered.) + pass else: - raise ValueError("Invalid compression %s"%compression) - if pyfits_compress: - if 'CompImageHDU' not in pyfits.__dict__: - raise NotImplementedError( - 'Compressed Images not supported before pyfits version 2.0. You have version %s.'%( - pyfits_version)) - + raise GalSimValueError("Invalid compression", compression, + ('rice', 'gzip_tile', 'hcompress', 'plio', 'gzip', 'bzip2', + 'none', 'auto')) return file_compress, pyfits_compress # This is a class rather than a def, since we want to store some variable, and that's easier @@ -78,12 +76,25 @@ def gunzip_call(self, file): # (with zcat being a symlink to uncompress instead). # Also, I'd rather all these use `with subprocess.Popen(...) as p:`, but that's not # supported in 2.7. So need to keep things this way for now. - p = subprocess.Popen(["gunzip", "-c", file], stdout=subprocess.PIPE, close_fds=True) - fin = BytesIO(p.communicate()[0]) - if p.returncode != 0: - raise IOError("Error running gunzip. Return code = %s"%p.returncode) + try: + p = subprocess.Popen(["gunzip", "-c", file], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + except OSError: + # This OSError should mean that the gunzip call itself was invalid on this system. + # Convert to a NotImplementedError, so we can try a different method. + raise NotImplementedError() + ret = p.communicate() + if ret == '': # pragma: no cover + raise OSError("Error running gunzip. stderr output = %s"%ret[1]) + if p.returncode != 0: # pragma: no cover + raise OSError("Error running gunzip. Return code = %s"%p.returncode) + fin = BytesIO(ret[0]) p.wait() - hdu_list = pyfits.open(fin, 'readonly') + try: + hdu_list = pyfits.open(fin, 'readonly') + except (OSError, AttributeError, TypeError, ValueError): # pragma: no cover + # In case astropy fails. + raise NotImplementedError() return hdu_list, fin # Note: the above gzip_call function succeeds on travis, so the rest don't get run. @@ -93,115 +104,65 @@ def gzip_in_mem(self, file): # pragma: no cover from ._pyfits import pyfits fin = gzip.open(file, 'rb') hdu_list = pyfits.open(fin, 'readonly') - # Sometimes this doesn't work. The symptoms may be that this raises an - # exception, or possibly the hdu_list comes back empty, in which case the - # next line will raise an exception. - hdu = hdu_list[0] # pyfits doesn't actually read the file yet, so we can't close fin here. # Need to pass it back to the caller and let them close it when they are # done with hdu_list. return hdu_list, fin - def pyfits_open(self, file): # pragma: no cover - from ._pyfits import pyfits - # This usually works, although pyfits internally may (depending on the version) - # use a temporary file, which is why we prefer the above in-memory code if it works. - # For some versions of pyfits, this is actually the same as the in_mem version. - hdu_list = pyfits.open(file, 'readonly') - return hdu_list, None - - def gzip_tmp(self, file): # pragma: no cover - import gzip - from ._pyfits import pyfits - # Finally, just in case, if everything else failed, here is an implementation that - # should always work. - fin = gzip.open(file, 'rb') - data = fin.read() - tmp = file + '.tmp' - # It would be pretty odd for this filename to already exist, but just in case... - while os.path.isfile(tmp): - tmp = tmp + '.tmp' - with open(tmp,"w") as tmpout: - tmpout.write(data) - hdu_list = pyfits.open(tmp) - return hdu_list, tmp - def bunzip2_call(self, file): import subprocess from io import BytesIO from ._pyfits import pyfits - p = subprocess.Popen(["bunzip2", "-c", file], stdout=subprocess.PIPE, close_fds=True) - fin = BytesIO(p.communicate()[0]) - if p.returncode != 0: - raise IOError("Error running bunzip2. Return code = %s"%p.returncode) + try: + p = subprocess.Popen(["bunzip2", "-c", file], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + except OSError: + # This OSError should mean that the gunzip call itself was invalid on this system. + # Convert to a NotImplementedError, so we can try a different method. + raise NotImplementedError() + ret = p.communicate() + if ret == '': # pragma: no cover + raise OSError("Error running bunzip2. stderr output = %s"%ret[1]) + if p.returncode != 0: # pragma: no cover + raise OSError("Error running bunzip2. Return code = %s"%p.returncode) + fin = BytesIO(ret[0]) p.wait() - hdu_list = pyfits.open(fin, 'readonly') + try: + hdu_list = pyfits.open(fin, 'readonly') + except (OSError, AttributeError, TypeError, ValueError): # pragma: no cover + # In case astropy fails. + raise NotImplementedError() return hdu_list, fin def bz2_in_mem(self, file): # pragma: no cover import bz2 from ._pyfits import pyfits - # This normally works. But it might not on old versions of pyfits. fin = bz2.BZ2File(file, 'rb') hdu_list = pyfits.open(fin, 'readonly') - # Sometimes this doesn't work. The symptoms may be that this raises an - # exception, or possibly the hdu_list comes back empty, in which case the - # next line will raise an exception. - hdu = hdu_list[0] return hdu_list, fin - def bz2_tmp(self, file): # pragma: no cover - import bz2 - from ._pyfits import pyfits - fin = bz2.BZ2File(file, 'rb') - data = fin.read() - tmp = file + '.tmp' - # It would be pretty odd for this filename to already exist, but just in case... - while os.path.isfile(tmp): - tmp = tmp + '.tmp' - with open(tmp,"w") as tmpout: - tmpout.write(data) - hdu_list = pyfits.open(tmp) - return hdu_list, tmp - def __init__(self): - # For each compression type, we try them in rough order of efficiency and keep track of - # which method worked for next time. Whenever one doesn't work, we increment the - # method number and try the next one. The *_call methods are usually the fastest, - # sometimes much, much faster than the *_in_mem version. At least for largish files, - # which are precisely the ones that people would most likely want to compress. - # However, we can't require the user to have the system executables installed. So if - # that fails, we move on to the other options. It varies which of the other options - # is fastest, but they all usually succeed, which is the most important thing for a - # backup method, so it probably doesn't matter much what order we do the rest. + # We used to have multiple options for gzip and bzip2. However, with recent versions of + # astropy for the fits I/O, the in memory version should always work. So we first + # try the command line method, which is usually faster. Then if that fails, we let + # astropy do the compression. self.gz_index = 0 self.bz2_index = 0 - self.gz_methods = [self.gunzip_call, self.gzip_in_mem, self.pyfits_open, self.gzip_tmp] - self.bz2_methods = [self.bunzip2_call, self.bz2_in_mem, self.bz2_tmp] + self.gz_methods = [self.gunzip_call, self.gzip_in_mem] + self.bz2_methods = [self.bunzip2_call, self.bz2_in_mem] self.gz = self.gz_methods[0] self.bz2 = self.bz2_methods[0] def __call__(self, file, dir, file_compress): - from ._pyfits import pyfits, pyfits_version + from ._pyfits import pyfits if dir: - import os file = os.path.join(dir,file) + if not os.path.isfile(file): + raise OSError("File %s not found"%file) + if not file_compress: - if pyfits_version < '3.1': # pragma: no cover - # Sometimes early versions of pyfits do weird things with the final hdu when - # writing fits files with rice compression. It seems to add a bunch of '\0' - # characters after the end of what should be the last hdu. When reading this - # back in, it gets interpreted as the start of another hdu, which is then found - # to be missing its END card in the header. The easiest workaround is to just - # tell it to ignore any missing END problems on the read command. Also ignore - # the warnings it emits along the way. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - hdu_list = pyfits.open(file, 'readonly', ignore_missing_end=True) - else: - hdu_list = pyfits.open(file, 'readonly') + hdu_list = pyfits.open(file, 'readonly') return hdu_list, None elif file_compress == 'gzip': # Before trying all the gzip options, first make sure the file exists and is readable. @@ -211,62 +172,50 @@ def __call__(self, file, dir, file_compress): while self.gz_index < len(self.gz_methods): try: return self.gz(file) - except KeyboardInterrupt: - raise - except: # pragma: no cover - self.gz_index += 1 - self.gz = self.gz_methods[self.gz_index] - raise RuntimeError("None of the options for gunzipping were successful.") + except (ImportError, NotImplementedError): # pragma: no cover + if self.gz_index == len(self.gz_methods-1): + raise + else: + self.gz_index += 1 + self.gz = self.gz_methods[self.gz_index] + else: # pragma: no cover + raise GalSimError("None of the options for gunzipping were successful.") elif file_compress == 'bzip2': with open(file) as fid: pass while self.bz2_index < len(self.bz2_methods): try: return self.bz2(file) - except KeyboardInterrupt: - raise - except: # pragma: no cover - self.bz2_index += 1 - self.bz2 = self.bz2_methods[self.bz2_index] - raise RuntimeError("None of the options for bunzipping were successful.") - else: - raise ValueError("Unknown file_compression") + except (ImportError, NotImplementedError): # pragma: no cover + if self.bz2_index == len(self.bz2_methods-1): + raise + else: + self.bz2_index += 1 + self.bz2 = self.bz2_methods[self.bz2_index] + else: # pragma: no cover + raise GalSimError("None of the options for bunzipping were successful.") + else: # pragma: no cover (can't get here from public API) + raise GalSimValueError("Unknown file_compression", file_compress, ('gzip', 'bzip2')) _read_file = _ReadFile() # Do the same trick for _write_file(file,hdu_list,clobber,file_compress,pyfits_compress): class _WriteFile: # There are several methods available for each of gzip and bzip2. Each is its own function. - def gzip_call2(self, hdu_list, file): # pragma: no cover - root, ext = os.path.splitext(file) - import subprocess - if os.path.isfile(root): - tmp = root + '.tmp' - # It would be pretty odd for this filename to already exist, but just in case... - while os.path.isfile(tmp): - tmp = tmp + '.tmp' - hdu_list.writeto(tmp) - p = subprocess.Popen(["gzip", tmp], close_fds=True) - p.communicate() - if p.returncode != 0: - raise IOError("Error running gzip. Return code = %s"%p.returncode) - p.wait() - os.rename(tmp+".gz",file) - else: - hdu_list.writeto(root) - p = subprocess.Popen(["gzip", "-S", ext, "-f", root], close_fds=True) - p.communicate() - if p.returncode != 0: - raise IOError("Error running gzip. Return code = %s"%p.returncode) - p.wait() - def gzip_call(self, hdu_list, file): import subprocess with open(file, 'wb') as fout: - p = subprocess.Popen(["gzip", "-"], stdin=subprocess.PIPE, stdout=fout, close_fds=True) - hdu_list.writeto(p.stdin) + try: + p = subprocess.Popen(["gzip", "-"], stdin=subprocess.PIPE, stdout=fout, + close_fds=True) + hdu_list.writeto(p.stdin) + except (OSError, AttributeError, TypeError, ValueError): # pragma: no cover + # This OSError should mean that the gunzip call itself was invalid on this system. + # Convert to a NotImplementedError, so we can try a different method. + # The others are in case astropy fails. + raise NotImplementedError() p.communicate() - if p.returncode != 0: - raise IOError("Error running gzip. Return code = %s"%p.returncode) + if p.returncode != 0: # pragma: no cover + raise OSError("Error running gzip. Return code = %s"%p.returncode) p.wait() def gzip_in_mem(self, hdu_list, file): # pragma: no cover @@ -282,52 +231,20 @@ def gzip_in_mem(self, hdu_list, file): # pragma: no cover with gzip.open(file, 'wb') as fout: fout.write(data) - def gzip_tmp(self, hdu_list, file): # pragma: no cover - import gzip - # However, pyfits versions before 2.3 do not support writing to a buffer, so the - # above code will fail. We need to use a temporary in that case. - tmp = file + '.tmp' - # It would be pretty odd for this filename to already exist, but just in case... - while os.path.isfile(tmp): - tmp = tmp + '.tmp' - hdu_list.writeto(tmp) - with open(tmp,"r") as buf: - data = buf.read() - os.remove(tmp) - with gzip.open(file, 'wb') as fout: - fout.write(data) - - def bzip2_call2(self, hdu_list, file): # pragma: no cover - root, ext = os.path.splitext(file) - import subprocess - if os.path.isfile(root) or ext != '.bz2': - tmp = root + '.tmp' - # It would be pretty odd for this filename to already exist, but just in case... - while os.path.isfile(tmp): - tmp = tmp + '.tmp' - hdu_list.writeto(tmp) - p = subprocess.Popen(["bzip2", tmp], close_fds=True) - p.communicate() - if p.returncode != 0: - raise IOError("Error running bzip2. Return code = %s"%p.returncode) - p.wait() - os.rename(tmp+".bz2",file) - else: - hdu_list.writeto(root) - p = subprocess.Popen(["bzip2", root], close_fds=True) - p.communicate() - if p.returncode != 0: - raise IOError("Error running bzip2. Return code = %s"%p.returncode) - p.wait() - def bzip2_call(self, hdu_list, file): import subprocess with open(file, 'wb') as fout: - p = subprocess.Popen(["bzip2"], stdin=subprocess.PIPE, stdout=fout, close_fds=True) - hdu_list.writeto(p.stdin) + try: + p = subprocess.Popen(["bzip2"], stdin=subprocess.PIPE, stdout=fout, close_fds=True) + hdu_list.writeto(p.stdin) + except (OSError, AttributeError, TypeError, ValueError): # pragma: no cover + # This OSError should mean that the gunzip call itself was invalid on this system. + # Convert to a NotImplementedError, so we can try a different method. + # The others are in case astropy fails. + raise NotImplementedError() p.communicate() - if p.returncode != 0: - raise IOError("Error running bzip2. Return code = %s"%p.returncode) + if p.returncode != 0: # pragma: no cover + raise OSError("Error running bzip2. Return code = %s"%p.returncode) p.wait() def bz2_in_mem(self, hdu_list, file): # pragma: no cover @@ -339,40 +256,19 @@ def bz2_in_mem(self, hdu_list, file): # pragma: no cover with bz2.BZ2File(file, 'wb') as fout: fout.write(data) - def bz2_tmp(self, hdu_list, file): # pragma: no cover - import bz2 - tmp = file + '.tmp' - while os.path.isfile(tmp): - tmp = tmp + '.tmp' - hdu_list.writeto(tmp) - with open(tmp,"r") as buf: - data = buf.read() - os.remove(tmp) - with bz2.BZ2File(file, 'wb') as fout: - fout.write(data) - def __init__(self): - # For each compression type, we try them in rough order of efficiency and keep track of - # which method worked for next time. Whenever one doesn't work, we increment the - # method number and try the next one. The *_call methods seem to be usually the fastest, - # and we expect that they will usually work. However, we can't require the user - # to have the system executables. Also, some versions of pyfits can't handle writing - # to the stdin pipe of a subprocess. So if that fails, the next one, *_call2 is often - # fastest if the failure was due to pyfits. If the user does not have gzip or bzip2 (then - # why are they requesting this compression?), we switch to *_in_mem, which is often - # almost as good. (Sometimes it is faster than the call2 option, but when it is slower it - # can be much slower.) And finally, if this fails, which I think may happen for very old - # versions of pyfits, *_tmp is the fallback option. + # Again, we used to have a number of methods here for gzip and bzip2, but now only two. + # We first try using a command-line call to either gzip or bzip2. But if that doesn't + # work, we use either the gzip or bz2 module in memory, which is usually not quite as + # fast, but should always work. self.gz_index = 0 self.bz2_index = 0 - self.gz_methods = [self.gzip_call, self.gzip_call2, self.gzip_in_mem, self.gzip_tmp] - self.bz2_methods = [self.bzip2_call, self.bzip2_call2, self.bz2_in_mem, self.bz2_tmp] + self.gz_methods = [self.gzip_call, self.gzip_in_mem] + self.bz2_methods = [self.bzip2_call, self.bz2_in_mem] self.gz = self.gz_methods[0] self.bz2 = self.bz2_methods[0] def __call__(self, file, dir, hdu_list, clobber, file_compress, pyfits_compress): - import os - from ._pyfits import pyfits, pyfits_version if dir: file = os.path.join(dir,file) @@ -380,7 +276,7 @@ def __call__(self, file, dir, hdu_list, clobber, file_compress, pyfits_compress) if clobber: os.remove(file) else: - raise IOError('File %r already exists'%file) + raise OSError('File %r already exists'%file) if not file_compress: hdu_list.writeto(file) @@ -388,61 +284,37 @@ def __call__(self, file, dir, hdu_list, clobber, file_compress, pyfits_compress) while self.gz_index < len(self.gz_methods): try: return self.gz(hdu_list, file) - except KeyboardInterrupt: - raise - except: # pragma: no cover - self.gz_index += 1 - self.gz = self.gz_methods[self.gz_index] - raise RuntimeError("None of the options for gunzipping were successful.") + except (ImportError, NotImplementedError): # pragma: no cover + if self.gz_index == len(self.gz_methods)-1: + raise + else: + self.gz_index += 1 + self.gz = self.gz_methods[self.gz_index] + else: # pragma: no cover + raise GalSimError("None of the options for gzipping were successful.") elif file_compress == 'bzip2': while self.bz2_index < len(self.bz2_methods): try: return self.bz2(hdu_list, file) - except KeyboardInterrupt: - raise - except: # pragma: no cover - self.bz2_index += 1 - self.bz2 = self.bz2_methods[self.bz2_index] - raise RuntimeError("None of the options for bunzipping were successful.") - else: - raise ValueError("Unknown file_compression") - - # There is a bug in pyfits where they don't add the size of the variable length array - # to the TFORMx header keywords. They should have size at the end of them. - # This bug has been fixed in version 3.1.2. - # (See http://trac.assembla.com/pyfits/ticket/199) - if pyfits_compress and pyfits_version < '3.1.2': - with pyfits.open(file,'update',disable_image_compression=True) as hdu_list: - for hdu in hdu_list[1:]: # Skip PrimaryHDU - # Find the maximum variable array length - max_ar_len = max([ len(ar[0]) for ar in hdu.data ]) - # Add '(N)' to the TFORMx keywords for the variable array items - s = '(%d)'%max_ar_len - for key in hdu.header.keys(): - if key.startswith('TFORM'): - tform = hdu.header[key] - # Only update if the form is a P (= variable length data) - # and the (*) is not there already. - if 'P' in tform and '(' not in tform: - hdu.header[key] = tform + s - - # Workaround for a bug in some pyfits 3.0.x versions - # It was fixed in 3.0.8. I'm not sure when the bug was - # introduced, but I believe it was 3.0.3. - if (pyfits_version > '3.0' and pyfits_version < '3.0.8' and - 'COMPRESSION_ENABLED' in pyfits.hdu.compressed.__dict__): - pyfits.hdu.compressed.COMPRESSION_ENABLED = True + except (ImportError, NotImplementedError): # pragma: no cover + if self.bz2_index == len(self.bz2_methods)-1: + raise + else: + self.bz2_index += 1 + self.bz2 = self.bz2_methods[self.bz2_index] + else: # pragma: no cover + raise GalSimError("None of the options for bzipping were successful.") + else: # pragma: no cover (can't get here from public API) + raise GalSimValueError("Unknown file_compression", file_compress, ('gzip', 'bzip2')) + _write_file = _WriteFile() def _add_hdu(hdu_list, data, pyfits_compress): - from ._pyfits import pyfits, pyfits_version + from ._pyfits import pyfits if pyfits_compress: if len(hdu_list) == 0: hdu_list.append(pyfits.PrimaryHDU()) # Need a blank PrimaryHDU - if pyfits_version < '4.3': - hdu = pyfits.CompImageHDU(data, compressionType=pyfits_compress) - else: - hdu = pyfits.CompImageHDU(data, compression_type=pyfits_compress) + hdu = pyfits.CompImageHDU(data, compression_type=pyfits_compress) else: if len(hdu_list) == 0: hdu = pyfits.PrimaryHDU(data) @@ -465,19 +337,11 @@ def _check_hdu(hdu, pyfits_compress): # Check that the specified compression is right for the given hdu type. if pyfits_compress: - if not isinstance(hdu, pyfits.CompImageHDU): # pragma: no cover - if isinstance(hdu, pyfits.BinTableHDU): - raise IOError('Expecting a CompImageHDU, but got a BinTableHDU\n' + - 'Probably your pyfits installation does not have the pyfitsComp module '+ - 'installed.') - elif isinstance(hdu, pyfits.ImageHDU): - import warnings - warnings.warn("Expecting a CompImageHDU, but found an uncompressed ImageHDU") - else: - raise IOError('Found invalid HDU reading FITS file (expected an ImageHDU)') + if not isinstance(hdu, pyfits.CompImageHDU): + raise OSError('Found invalid HDU type reading FITS file (expected a CompImageHDU)') else: if not isinstance(hdu, pyfits.ImageHDU) and not isinstance(hdu, pyfits.PrimaryHDU): - raise IOError('Found invalid HDU reading FITS file (expected an ImageHDU)') + raise OSError('Found invalid HDU type reading FITS file (expected an ImageHDU)') def _get_hdu(hdu_list, hdu, pyfits_compress): @@ -489,15 +353,17 @@ def _get_hdu(hdu_list, hdu, pyfits_compress): if hdu is None: if pyfits_compress: if len(hdu_list) <= 1: - raise IOError('Expecting at least one extension HDU in galsim.read') + raise OSError('Expecting at least one extension HDU in galsim.read') hdu = 1 else: hdu = 0 if len(hdu_list) <= hdu: - raise IOError('Expecting at least %d HDUs in galsim.read'%(hdu+1)) + raise OSError('Expecting at least %d HDUs in galsim.read'%(hdu+1)) hdu = hdu_list[hdu] - else: + elif isinstance(hdu_list, (pyfits.ImageHDU, pyfits.PrimaryHDU, pyfits.CompImageHDU)): hdu = hdu_list + else: + raise TypeError("Invalid hdu_list: %s",hdu_list) _check_hdu(hdu, pyfits_compress) return hdu @@ -508,14 +374,7 @@ def closeHDUList(hdu_list, fin): """If necessary, close the file handle that was opened to read in the `hdu_list`""" hdu_list.close() if fin: - if isinstance(fin, basestring): # pragma: no cover - # In this case, it is a file name that we need to delete. - # Note: This is relevant for the _tmp versions that are not run on Travis, so - # don't include this bit in the coverage report. - import os - os.remove(fin) - else: - fin.close() + fin.close() ############################################################################################## # @@ -570,15 +429,17 @@ def write(image, file_name=None, dir=None, hdu_list=None, clobber=True, compress from ._pyfits import pyfits if image.iscomplex: - raise ValueError("Cannot write complex Images to a fits file. " - "Write image.real and image.imag separately.") + raise GalSimValueError("Cannot write complex Images to a fits file. " + "Write image.real and image.imag separately.", image) file_compress, pyfits_compress = _parse_compression(compression,file_name) if file_name and hdu_list is not None: - raise TypeError("Cannot provide both file_name and hdu_list") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and hdu_list", file_name=file_name, hdu_list=hdu_list) if not (file_name or hdu_list is not None): - raise TypeError("Must provide either file_name or hdu_list") + raise GalSimIncompatibleValuesError( + "Must provide either file_name or hdu_list", file_name=file_name, hdu_list=hdu_list) if hdu_list is None: hdu_list = pyfits.HDUList() @@ -619,15 +480,17 @@ def writeMulti(image_list, file_name=None, dir=None, hdu_list=None, clobber=True from ._pyfits import pyfits if any(image.iscomplex for image in image_list if isinstance(image, Image)): - raise ValueError("Cannot write complex Images to a fits file. " - "Write image.real and image.imag separately.") + raise GalSimValueError("Cannot write complex Images to a fits file. " + "Write image.real and image.imag separately.", image_list) file_compress, pyfits_compress = _parse_compression(compression,file_name) if file_name and hdu_list is not None: - raise TypeError("Cannot provide both file_name and hdu_list") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and hdu_list", file_name=file_name, hdu_list=hdu_list) if not (file_name or hdu_list is not None): - raise TypeError("Must provide either file_name or hdu_list") + raise GalSimIncompatibleValuesError( + "Must provide either file_name or hdu_list", file_name=file_name, hdu_list=hdu_list) if hdu_list is None: hdu_list = pyfits.HDUList() @@ -686,25 +549,29 @@ def writeCube(image_list, file_name=None, dir=None, hdu_list=None, clobber=True, if isinstance(image_list, np.ndarray): is_all_numpy = True if image_list.dtype.kind == 'c': - raise ValueError("Cannot write complex numpy arrays to a fits file. " - "Write array.real and array.imag separately.") + raise GalSimValueError("Cannot write complex numpy arrays to a fits file. " + "Write array.real and array.imag separately.", image_list) + elif len(image_list) == 0: + raise GalSimValueError("In writeCube: image_list has no images", image_list) elif all(isinstance(item, np.ndarray) for item in image_list): is_all_numpy = True if any(a.dtype.kind == 'c' for a in image_list): - raise ValueError("Cannot write complex numpy arrays to a fits file. " - "Write array.real and array.imag separately.") + raise GalSimValueError("Cannot write complex numpy arrays to a fits file. " + "Write array.real and array.imag separately.", image_list) else: is_all_numpy = False if any(im.iscomplex for im in image_list): - raise ValueError("Cannot write complex images to a fits file. " - "Write image.real and image.imag separately.") + raise GalSimValueError("Cannot write complex images to a fits file. " + "Write image.real and image.imag separately.", image_list) file_compress, pyfits_compress = _parse_compression(compression,file_name) if file_name and hdu_list is not None: - raise TypeError("Cannot provide both file_name and hdu_list") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and hdu_list", file_name=file_name, hdu_list=hdu_list) if not (file_name or hdu_list is not None): - raise TypeError("Must provide either file_name or hdu_list") + raise GalSimIncompatibleValuesError( + "Must provide either file_name or hdu_list", file_name=file_name, hdu_list=hdu_list) if hdu_list is None: hdu_list = pyfits.HDUList() @@ -719,8 +586,6 @@ def writeCube(image_list, file_name=None, dir=None, hdu_list=None, clobber=True, wcs = None else: nimages = len(image_list) - if (nimages == 0): - raise IndexError("In writeCube: image_list has no images") im = image_list[0] dtype = im.array.dtype nx = im.xmax - im.xmin + 1 @@ -736,8 +601,9 @@ def writeCube(image_list, file_name=None, dir=None, hdu_list=None, clobber=True, nx_k = im.xmax-im.xmin+1 ny_k = im.ymax-im.ymin+1 if nx_k != nx or ny_k != ny: - raise IndexError("In writeCube: image %d has the wrong shape"%k + - "Shape is (%d,%d). Should be (%d,%d)"%(nx_k,ny_k,nx,ny)) + raise GalSimValueError("In writeCube: image %d has the wrong shape. " + "Shape is (%d,%d) should be (%d,%d)"%(k,nx_k,ny_k,nx,ny), + im) cube[k,:,:] = image_list[k].array @@ -782,7 +648,8 @@ def writeFile(file_name, hdu_list, dir=None, clobber=True, compression='auto'): if pyfits_compress and compression != 'auto': # If compression is auto and it determined that it should use rice, then we # should presume that the hdus were already rice compressed, so we can ignore it here. - raise ValueError("Compression %s is invalid for writeFile"%compression) + # Otherwise, any pyfits_compression options are invalid. + raise GalSimValueError("Compression %s is invalid for writeFile",compression) _write_file(file_name, dir, hdu_list, clobber, file_compress, pyfits_compress) @@ -846,9 +713,11 @@ def read(file_name=None, dir=None, hdu_list=None, hdu=None, compression='auto'): file_compress, pyfits_compress = _parse_compression(compression,file_name) if file_name and hdu_list is not None: - raise TypeError("Cannot provide both file_name and hdu_list to read()") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and hdu_list", file_name=file_name, hdu_list=hdu_list) if not (file_name or hdu_list is not None): - raise TypeError("Must provide either file_name or hdu_list to read()") + raise GalSimIncompatibleValuesError( + "Must provide either file_name or hdu_list", file_name=file_name, hdu_list=hdu_list) if file_name: hdu_list, fin = _read_file(file_name, dir, file_compress) @@ -856,14 +725,16 @@ def read(file_name=None, dir=None, hdu_list=None, hdu=None, compression='auto'): try: hdu = _get_hdu(hdu_list, hdu, pyfits_compress) + if hdu.data is None: + raise OSError("HDU is empty. (data is None)") + wcs, origin = wcs.readFromFitsHeader(hdu.header) dt = hdu.data.dtype.type if dt in Image.valid_dtypes: data = hdu.data else: - import warnings - warnings.warn("No C++ Image template instantiation for data type %s" % dt) - warnings.warn(" Using numpy.float64 instead.") + galsim_warn("No C++ Image template instantiation for data type %s. " + "Using numpy.float64 instead."%(dt)) data = hdu.data.astype(np.float64) image = Image(array=data) @@ -921,9 +792,11 @@ def readMulti(file_name=None, dir=None, hdu_list=None, compression='auto'): file_compress, pyfits_compress = _parse_compression(compression,file_name) if file_name and hdu_list is not None: - raise TypeError("Cannot provide both file_name and hdu_list to readMulti()") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and hdu_list", file_name=file_name, hdu_list=hdu_list) if not (file_name or hdu_list is not None): - raise TypeError("Must provide either file_name or hdu_list to readMulti()") + raise GalSimIncompatibleValuesError( + "Must provide either file_name or hdu_list", file_name=file_name, hdu_list=hdu_list) if file_name: hdu_list, fin = _read_file(file_name, dir, file_compress) @@ -935,11 +808,11 @@ def readMulti(file_name=None, dir=None, hdu_list=None, compression='auto'): if pyfits_compress: first = 1 if len(hdu_list) <= 1: - raise IOError('Expecting at least one extension HDU in galsim.read') + raise OSError('Expecting at least one extension HDU in galsim.read') else: first = 0 if len(hdu_list) < 1: - raise IOError('Expecting at least one HDU in galsim.readMulti') + raise OSError('Expecting at least one HDU in galsim.readMulti') for hdu in range(first,len(hdu_list)): image_list.append(read(hdu_list=hdu_list, hdu=hdu, compression=pyfits_compress)) @@ -994,24 +867,28 @@ def readCube(file_name=None, dir=None, hdu_list=None, hdu=None, compression='aut file_compress, pyfits_compress = _parse_compression(compression,file_name) if file_name and hdu_list is not None: - raise TypeError("Cannot provide both file_name and hdu_list to read()") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and hdu_list", file_name=file_name, hdu_list=hdu_list) if not (file_name or hdu_list is not None): - raise TypeError("Must provide either file_name or hdu_list to read()") + raise GalSimIncompatibleValuesError( + "Must provide either file_name or hdu_list", file_name=file_name, hdu_list=hdu_list) if file_name: hdu_list, fin = _read_file(file_name, dir, file_compress) - hdu = _get_hdu(hdu_list, hdu, pyfits_compress) - try: + hdu = _get_hdu(hdu_list, hdu, pyfits_compress) + + if hdu.data is None: + raise OSError("HDU is empty. (data is None)") + wcs, origin = wcs.readFromFitsHeader(hdu.header) dt = hdu.data.dtype.type if dt in Image.valid_dtypes: data = hdu.data else: - import warnings - warnings.warn("No C++ Image template instantiation for data type %s" % dt) - warnings.warn(" Using numpy.float64 instead.") + galsim_warn("No C++ Image template instantiation for data type %s. " + "Using numpy.float64 instead."%(dt)) data = hdu.data.astype(np.float64) nimages = data.shape[0] @@ -1177,11 +1054,14 @@ def __init__(self, header=None, file_name=None, dir=None, hdu_list=None, hdu=Non from ._pyfits import pyfits if header and file_name: - raise TypeError("Cannot provide both file_name and header to FitsHeader") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and header", file_name=file_name, header=header) if header and hdu_list: - raise TypeError("Cannot provide both hdu_list and header to FitsHeader") + raise GalSimIncompatibleValuesError( + "Cannot provide both hdu_list and header", hdu_list=hdu_list, header=header) if file_name and hdu_list: - raise TypeError("Cannot provide both file_name and hdu_list to FitsHeader") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and hdu_list", file_name=file_name, hdu_list=hdu_list) # Interpret a string header as though it were passed as file_name. if isinstance(header, basestring): @@ -1193,7 +1073,6 @@ def __init__(self, header=None, file_name=None, dir=None, hdu_list=None, hdu=Non if file_name is not None: if dir is not None: - import os self._tag = 'file_name='+repr(os.path.join(dir,file_name)) else: self._tag = 'file_name='+repr(file_name) @@ -1205,7 +1084,6 @@ def __init__(self, header=None, file_name=None, dir=None, hdu_list=None, hdu=Non if text_file: self._tag += ', text_file=True' if dir is not None: - import os file_name = os.path.join(dir,file_name) with open(file_name,"r") as fin: lines = [ line.strip() for line in fin ] @@ -1243,30 +1121,20 @@ def __init__(self, header=None, file_name=None, dir=None, hdu_list=None, hdu=Non # update() should handle anything that acts like a dict. self.update(header) else: - # for a list, just add each item one at a time. - for k,v in header: - self.append(k,v,useblanks=False) + for card in header: + self.header.append(card, end=True) # The rest of the functions are typical non-mutating functions for a dict, for which we # generally just pass the request along to self.header. def __len__(self): - from ._pyfits import pyfits_version - if pyfits_version < '3.1': - return len(self.header.ascard) - else: - return len(self.header) + return len(self.header) def __contains__(self, key): return key in self.header def __delitem__(self, key): self._tag = None - # This is equivalent to the newer pyfits implementation, but older versions silently - # did nothing if the key was not in the header. - if key in self.header: - del self.header[key] - else: - raise KeyError("key "+key+" not in FitsHeader") + del self.header[key] def __getitem__(self, key): return self.header[key] @@ -1275,44 +1143,12 @@ def __iter__(self): return self.header.__iter__() def __setitem__(self, key, value): - # pyfits doesn't like getting bytes in python 3, so decode if appropriate - try: - key = str(key.decode()) - except AttributeError: - pass - try: - value = str(value.decode()) - except AttributeError: - pass - from ._pyfits import pyfits_version self._tag = None - if pyfits_version < '3.1': - if isinstance(value, tuple): - # header[key] = (value, comment) syntax - if not (0 < len(value) <= 2): - raise ValueError( - 'A Header item may be set with either a scalar value, ' - 'a 1-tuple containing a scalar value, or a 2-tuple ' - 'containing a scalar value and comment string.') - elif len(value) == 1: - self.header.update(key, value[0]) - else: - self.header.update(key, value[0], value[1]) - else: - # header[key] = value syntax - self.header.update(key, value) - else: - # Recent versions implement the above logic with the regular setitem method. - self.header[key] = value + self.header[key] = value def clear(self): - from ._pyfits import pyfits_version self._tag = None - if pyfits_version < '3.1': - # Not sure when clear() was added, but not present in 2.4, and present in 3.1. - del self.header.ascardlist()[:] - else: - self.header.clear() + self.header.clear() def get(self, key, default=None): return self.header.get(key, default) @@ -1321,44 +1157,24 @@ def items(self): return self.header.items() def iteritems(self): - from ._pyfits import pyfits_version - if pyfits_version < '3.1': - return self.header.items() - else: - return iteritems(self.header) + return iteritems(self.header) def iterkeys(self): - from ._pyfits import pyfits_version - if pyfits_version < '3.1': - return self.header.keys() - else: - return iterkeys(self.header) + return iterkeys(self.header) def itervalues(self): - from ._pyfits import pyfits_version - if pyfits_version < '3.1': - return self.header.ascard.values() - else: - return itervalues(self.header) + return itervalues(self.header) def keys(self): return self.header.keys() def update(self, dict2): - from ._pyfits import pyfits_version self._tag = None - # dict2 may be a dict or another FitsHeader (or anything that acts like a dict). - # Note: Don't use self.header.update, since that sometimes has problems (in astropy) - # with COMMENT lines. The __setitem__ syntax seems to work properly though. - for k, v in iteritems(dict2): - self[k] = v + for key, item in dict2.items(): + self.header[key] = item def values(self): - from ._pyfits import pyfits_version - if pyfits_version < '3.1': - return self.header.ascard.values() - else: - return self.header.values() + return self.header.values() def append(self, key, value='', useblanks=True): """Append an item to the end of the header. @@ -1371,19 +1187,10 @@ def append(self, key, value='', useblanks=True): @param useblanks If there are blank entries currently at the end, should they be overwritten with the new entry? [default: True] """ - from ._pyfits import pyfits, pyfits_version self._tag = None - if pyfits_version < '3.1': - # NB. append doesn't quite do what it claims when useblanks=False. - # If there are blanks, it doesn't put the new item after the blanks. - # Inserting before the end does do what we want. - self.header.ascardlist().insert(len(self), pyfits.Card(key, value), - useblanks=useblanks) - else: - self.header.insert(len(self), (key, value), useblanks=useblanks) + self.header.insert(len(self), (key, value), useblanks=useblanks) def __repr__(self): - from ._pyfits import pyfits_str if self._tag is None: return "galsim.FitsHeader(header=%r)"%list(self.items()) else: @@ -1404,26 +1211,5 @@ def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(tuple(sorted(self.items()))) - def __deepcopy__(self, memo): - # Need this because pyfits.Header deepcopy was broken before 3.0.6. - # cf. https://aeon.stsci.edu/ssb/trac/pyfits/ticket/115 - from ._pyfits import pyfits, pyfits_version - import copy - # Boilerplate deepcopy implementation. - # cf. http://stackoverflow.com/questions/1500718/what-is-the-right-way-to-override-the-copy-deepcopy-operations-on-an-object-in-p - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - d1 = self.__dict__ - # This is the special bit for this case. - if pyfits_version < '3.0.6': - # Not technically a deepcopy apparently, but good enough in most cases. - result.header = self.header.copy() - d1 = d1.copy() - del d1['header'] - for k, v in d1.items(): - setattr(result, k, copy.deepcopy(v, memo)) - return result - # inject write as method of Image class Image.write = write diff --git a/galsim/fitswcs.py b/galsim/fitswcs.py index 9beb584b93d..81f018e6942 100644 --- a/galsim/fitswcs.py +++ b/galsim/fitswcs.py @@ -23,11 +23,14 @@ import warnings import numpy as np -from .wcs import CelestialWCS, JacobianWCS, AffineTransform + +from .wcs import CelestialWCS from .position import PositionD, PositionI from .angle import radians, arcsec, degrees, AngleUnit from . import _galsim from . import fits +from .errors import GalSimError, GalSimValueError, GalSimIncompatibleValuesError +from .errors import GalSimNotImplementedError, convert_cpp_errors, galsim_warn ######################################################################################### # @@ -123,65 +126,58 @@ def __init__(self, file_name=None, dir=None, hdu=None, header=None, compression= if compression is not 'auto': self._tag += ', compression=%r'%compression if header is not None: - raise TypeError("Cannot provide both file_name and pyfits header") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and pyfits header", + file_name=file_name, header=header) if wcs is not None: - raise TypeError("Cannot provide both file_name and wcs") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and wcs", file_name=file_name, wcs=wcs) hdu, hdu_list, fin = fits.readFile(file_name, dir, hdu, compression) - header = hdu.header - - # At least as late as version 1.1.2, astropy thinks it knows how to parse ZPX files, - # but can at least sometimes seg fault when it tries to parse the header. Check for - # that explicitly here and raise an exception before getting to _load_from_header - # I think this is fixed in 1.2, but I'm not 100% sure. - # Update: Nope. Still broken. cf. Issue #783. - # TODO: If they ever fix this bug, use the correct version here. - if (astropy.__version__ < '999' and header is not None and - 'CTYPE1' in header and 'ZPX' in header['CTYPE1'].upper()): - raise RuntimeError("AstropyWCS cannot (always) parse ZPX files") - - # Load the wcs from the header. - if header is not None: - if self._tag is None: - self.header = header - if wcs is not None: - raise TypeError("Cannot provide both pyfits header and wcs") - wcs = self._load_from_header(header, hdu) - if wcs is None: - raise TypeError("Must provide one of file_name, header, or wcs") + try: + if file_name is not None: + header = hdu.header - if file_name is not None: - fits.closeHDUList(hdu_list, fin) + # Load the wcs from the header. + if header is not None: + if wcs is not None: + raise GalSimIncompatibleValuesError( + "Cannot provide both pyfits header and wcs", header=header, wcs=wcs) + self.header = fits.FitsHeader(header) + try: + wcs = self._load_from_header(self.header) + except (TypeError, AttributeError, ValueError) as e: + # When parsing ZPX files, astropy raises a very unhelpful error message. + # Ignore that (ValueError in that case, but ignore any similarly mundane error) + # and turn it into a more appropriate OSError. + raise OSError("Astropy failed to read WCS from %s. Original error: %s"%( + file_name, e)) + else: + self.header = None - # If astropy.wcs cannot parse the header, it won't notice from just doing the - # WCS(header) command. It will silently move on, thinking things are fine until - # later when if will fail (with `RuntimeError: NULL error object in wcslib`). - # We'd rather get that to happen now rather than later. - try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - ra, dec = wcs.all_pix2world( [ [0, 0] ], 1)[0] - except Exception as err: # pragma: no cover - raise RuntimeError("AstropyWCS was unable to read the WCS specification in the header.") + if wcs is None: + raise GalSimIncompatibleValuesError( + "Must provide one of file_name, header, or wcs", + file_name=file_name, header=header, wcs=wcs) + + finally: + if file_name is not None: + fits.closeHDUList(hdu_list, fin) + if not wcs.is_celestial: + raise GalSimError("The WCS read in does not define a pair of celestial axes" ) self._wcs = wcs - def _load_from_header(self, header, hdu): + def _load_from_header(self, header): import astropy.wcs from . import fits - self._fix_header(header) with warnings.catch_warnings(): # The constructor might emit warnings if it wants to fix the header # information (e.g. RADECSYS -> RADESYSa). We'd rather ignore these # warnings, since we don't much care if the input file is non-standard # so long as we can make it work. warnings.simplefilter("ignore") - # Some versions of astropy don't like to accept a galsim.FitsHeader object - # as the header attribute here, even though they claim that dict-like objects - # are ok. So pull out the astropy.io.header object in this case. - if isinstance(header,fits.FitsHeader): - header = header.header - wcs = astropy.wcs.WCS(header) + wcs = astropy.wcs.WCS(header.header) return wcs @property @@ -190,41 +186,11 @@ def wcs(self): return self._wcs @property def origin(self): return self._origin - def _fix_header(self, header): - # We allow for the option to fix up the header information when a modification can - # make it readable by astropy.wcs. - - # Older versions of astropy had trouble with files where the axes were swapped. - # So fix them if necessary. I know >= 1.0.1 works. 0.2.4 and 0.3.1 both fail. - import astropy - if astropy.__version__ < '1.0.1': # pragma: no cover - ctype1 = header.get('CTYPE1', 'RA---') - ctype2 = header.get('CTYPE2', 'DEC--') - if ctype1.startswith('DEC--') and ctype2.startswith('RA---'): - for key1, key2 in [ ('CTYPE1', 'CTYPE2'), - ('CRVAL1', 'CRVAL2'), - ('CDELT1', 'CDELT2'), - ('CD1_1', 'CD2_1'), - ('CD1_2', 'CD2_2'), - ('PC1_1', 'PC2_1'), - ('PC1_2', 'PC2_2'), - ('CUNIT1', 'CUNIT2') ]: - if key1 in header and key2 in header: - header[key1], header[key2] = header[key2], header[key1] - def _radec(self, x, y, color=None): x1 = np.atleast_1d(x) y1 = np.atleast_1d(y) - try: - # Old versions fail with an AttributeError about astropy.wcs.Wcsprm.lattype - # cf. https://github.com/astropy/astropy/pull/1463 - # This has been fixed for a while now, but leave in this workaround for old versions. - ra, dec = self._wcs.all_pix2world(x1, y1, 1, ra_dec_order=True) - except AttributeError: # pragma: no cover - # If that failed, then we should be on version < 1.0.1, and the header should have - # been fixed above by _fix_header. So this should work correctly. - ra, dec = self._wcs.all_pix2world(x1, y1, 1) + ra, dec = self.wcs.all_pix2world(x1, y1, 1, ra_dec_order=True) # astropy outputs ra, dec in degrees. Need to convert to radians. factor = degrees / radians @@ -246,60 +212,9 @@ def _xy(self, ra, dec, color=None): import astropy factor = radians / degrees rd = np.atleast_2d([ra, dec]) * factor - # Here we have to work around another astropy.wcs bug. The way they use scipy's - # Broyden's method doesn't work. So I implement a fix here. - if astropy.__version__ >= '1.0.1': - # This works now on recent vesions of astropy. At least >= 1.0.1, but possibly - # 1.0 also included the fix. - # cf. https://github.com/astropy/astropy/issues/1977 - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - xy = self._wcs.all_world2pix(rd, 1, ra_dec_order=True)[0] - else: # pragma: no cover - # This section is basically a copy of astropy.wcs's _all_world2pix function, but - # simplified a bit to remove some features we don't need, and with corrections - # to make it work correctly. - import astropy.wcs - import scipy.optimize - - origin = 1 - tolerance = 1.e-6 - - # This call emits a RuntimeWarning about: - # [...]/site-packages/scipy/optimize/nonlin.py:943: RuntimeWarning: invalid value encountered in divide - # d = v / vdot(df, v) - # It seems to be harmless, so we explicitly ignore it here: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - xy0 = self._wcs.wcs_world2pix(rd, origin) - - # Note that the fmod bit accounts for the possibility that ra and the ra returned - # from all_pix2world have a different wrapping around 360. We fmod dec too even - # though it won't do anything, since that's how the numpy array fmod2 has to work. - func = lambda pix: ( - (np.fmod(self._wcs.all_pix2world(np.atleast_2d(pix),origin) - - rd + 180,360) - 180).ravel() ) - - # This is the main bit that the astropy function is missing. - # The scipy.optimize.broyden1 function can't handle starting at exactly the right - # solution. It iterates to its limit and then ends with - # Traceback (most recent call last): - # [... snip ...] - # File "[...]/site-packages/scipy/optimize/nonlin.py", line 331, in nonlin_solve - # raise NoConvergence(_array_like(x, x0)) - # scipy.optimize.nonlin.NoConvergence: [ 113.74961526 179.99982209] - # - # Providing a good estimate of the scale size gets rid of this. And even if we aren't - # starting at exactly the right value, it is hugely more efficient to give it an - # estimate of alpha, since it is not typically near unity in this case, so it is much - # faster to start with something closer to the right value. - alpha = np.mean(np.abs(self._wcs.wcs.get_cdelt())) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - xy = [ scipy.optimize.broyden1(func, xy_init, x_tol=tolerance, alpha=alpha) - for xy_init in xy0 ] + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + xy = self.wcs.all_world2pix(rd, 1, ra_dec_order=True)[0] x, y = xy return x, y @@ -312,55 +227,51 @@ def _writeHeader(self, header, bounds): # Make a new header with the contents of this WCS. # Note: relax = True means to write out non-standard FITS types. # Weirdly, this is the default when reading the header, but not when writing. - header.update(self._wcs.to_header(relax=True)) + header.update(self.wcs.to_header(relax=True)) # And write the name as a special GalSim key header["GS_WCS"] = ("AstropyWCS", "GalSim WCS name") - # Finally, update the CRPIX items if necessary. - if self.origin.x != 0: - header["CRPIX1"] = header["CRPIX1"] + self.origin.x - if self.origin.y != 0: - header["CRPIX2"] = header["CRPIX2"] + self.origin.y + # And the image origin. + header["GS_X0"] = (self.origin.x, "GalSim image origin x") + header["GS_Y0"] = (self.origin.y, "GalSim image origin y") return header @staticmethod def _readHeader(header): - return AstropyWCS(header=header) + x0 = header.get("GS_X0",0.) + y0 = header.get("GS_Y0",0.) + return AstropyWCS(header=header, origin=PositionD(x0,y0)) def copy(self): - # The copy module version of copying the dict works fine here. - import copy - return copy.copy(self) + ret = AstropyWCS.__new__(AstropyWCS) + ret.__dict__.update(self.__dict__) + return ret def __eq__(self, other): return ( isinstance(other, AstropyWCS) and - self._wcs.to_header(relax=True) == other.wcs.to_header(relax=True) and + self.wcs.to_header(relax=True) == other.wcs.to_header(relax=True) and self.origin == other.origin ) def __repr__(self): - if self._tag is None: - if hasattr(self,'header'): - self._tag = 'header=%r'%fits.FitsHeader(self.header) - else: - self._tag = 'wcs=%r'%self.wcs - return "galsim.AstropyWCS(%s, origin=%r)"%(self._tag, self.origin) + if self._tag is not None: + tag = self._tag + elif self.header is not None: + tag = 'header=%r'%self.header + else: + tag = 'wcs=%r'%self.wcs + return "galsim.AstropyWCS(%s, origin=%r)"%(tag, self.origin) def __hash__(self): return hash(repr(self)) def __getstate__(self): d = self.__dict__.copy() - # If header or wcs is in the tag, then it might still be picklable, so let pickle - # try and raise the normal exception if it can't. - if self._tag is not None and 'wcs' not in self._tag and 'header' not in self._tag: # pragma: no branch - del d['_wcs'] + del d['_wcs'] return d def __setstate__(self, d): import galsim self.__dict__ = d - hdu, hdu_list, fin = eval('galsim.fits.readFile('+d['_tag']+')') - self._wcs = self._load_from_header(hdu.header, hdu) - fits.closeHDUList(hdu_list, fin) + self._wcs = self._load_from_header(self.header) class PyAstWCS(CelestialWCS): @@ -375,9 +286,6 @@ class PyAstWCS(CelestialWCS): https://pypi.python.org/pypi/starlink-pyast/ - Note: There were bugs in starlink.Ast prior to version 2.6, so if you have an earlier version, - you should upgrade to at least 2.6. - Initialization -------------- A PyAstWCS is initialized with one of the following commands: @@ -403,7 +311,7 @@ class PyAstWCS(CelestialWCS): a FitsHeader object. [default: None] @param compression Which decompression scheme to use (if any). See galsim.fits.read() for the available options. [default:'auto'] - @param wcsinfo An existing starlink.Ast.WcsMap [default: None] + @param wcsinfo An existing starlink.Ast.FrameSet [default: None] @param origin Optional origin position for the image coordinate system. If provided, it should be a PositionD or PositionI. [default: None] @@ -432,44 +340,60 @@ def __init__(self, file_name=None, dir=None, hdu=None, header=None, compression= if compression is not 'auto': self._tag += ', compression=%r'%compression if header is not None: - raise TypeError("Cannot provide both file_name and pyfits header") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and pyfits header", + file_name=file_name, header=header) if wcsinfo is not None: - raise TypeError("Cannot provide both file_name and wcsinfo") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and wcsinfo", + file_name=file_name, wcsinfo=wcsinfo) hdu, hdu_list, fin = fits.readFile(file_name, dir, hdu, compression) - header = hdu.header - # Load the wcs from the header. - if header is not None: - if self._tag is None: - self.header = header - if wcsinfo is not None: - raise TypeError("Cannot provide both pyfits header and wcsinfo") - wcsinfo = self._load_from_header(header, hdu) + try: + if file_name is not None: + header = hdu.header - if wcsinfo is None: - raise TypeError("Must provide one of file_name, header, or wcsinfo") + # Load the wcs from the header. + if header is not None: + if wcsinfo is not None: + raise GalSimIncompatibleValuesError( + "Cannot provide both pyfits header and wcsinfo", + header=header, wcsinfo=wcsinfo) + self.header = fits.FitsHeader(header) + wcsinfo = self._load_from_header(self.header) + else: + self.header = None - # We can only handle WCS with 2 pixel axes (given by Nin) and 2 WCS axes - # (given by Nout). - if wcsinfo.Nin != 2 or wcsinfo.Nout != 2: - raise RuntimeError("The world coordinate system is not 2-dimensional") + if wcsinfo is None: + raise GalSimIncompatibleValuesError( + "Must provide one of file_name, header, or wcsinfo", + file_name=file_name, header=header, wcsinfo=wcsinfo) - if file_name is not None: - fits.closeHDUList(hdu_list, fin) + # We can only handle WCS with 2 pixel axes (given by Nin) and 2 WCS axes + # (given by Nout). + if wcsinfo.Nin != 2 or wcsinfo.Nout != 2: # pragma: no cover + raise GalSimError("The world coordinate system is not 2-dimensional") + + finally: + if file_name is not None: + fits.closeHDUList(hdu_list, fin) self._wcsinfo = wcsinfo - def _load_from_header(self, header, hdu): + def _load_from_header(self, header): import starlink.Atl # Note: For much of this class implementation, I've followed the example provided here: # http://dsberry.github.io/starlink/node4.html self._fix_header(header) - # PyFITSAdapter requires an hdu, not a header, so if we were given a header directly, - # then we need to mock it up. - if hdu is None: - from ._pyfits import pyfits - hdu = pyfits.PrimaryHDU() - fits.FitsHeader(hdu_list=hdu).update(header) + + # PyFITSAdapter requires an hdu, not a header, so just put it in a pyfits header object. + # It turns out there are subtle differences between this and using the original FITS + # file hdu that we read in above. So there is a slight inefficiency here in creating + # a new blank PrimaryHDU for this. But in return we gain more reliable serializability. + from ._pyfits import pyfits + hdu = pyfits.PrimaryHDU() + fits.FitsHeader(hdu_list=hdu).update(header) + with warnings.catch_warnings(): warnings.simplefilter("ignore") # They aren't so good at keeping up with the latest pyfits and numpy syntax, so @@ -480,7 +404,7 @@ def _load_from_header(self, header, hdu): wcsinfo = fc.read() if wcsinfo is None: - raise RuntimeError("Failed to read WCS information from fits file") + raise OSError("Failed to read WCS information from fits file") # The PyAst WCS might not have (RA,Dec) axes, which we want. It might for instance have # (Dec, RA) instead. If it's possible to convert to an (RA,Dec) system, this next line @@ -488,7 +412,7 @@ def _load_from_header(self, header, hdu): # cf. https://github.com/timj/starlink-pyast/issues/8 wcsinfo = wcsinfo.findframe(starlink.Ast.SkyFrame()) if wcsinfo is None: - raise RuntimeError("The WCS read in does not define a pair of celestial axes" ) + raise GalSimError("The WCS read in does not define a pair of celestial axes" ) return wcsinfo @@ -517,7 +441,7 @@ def _radec(self, x, y, color=None): # if input is either scalar x,y or two arrays. xy = np.array([np.atleast_1d(x), np.atleast_1d(y)]) - ra, dec = self._wcsinfo.tran( xy ) + ra, dec = self.wcsinfo.tran( xy ) # PyAst returns ra, dec in radians, so we're good. try: @@ -532,7 +456,7 @@ def _radec(self, x, y, color=None): def _xy(self, ra, dec, color=None): rd = np.array([np.atleast_1d(ra), np.atleast_1d(dec)]) - x, y = self._wcsinfo.tran( rd, False ) + x, y = self.wcsinfo.tran( rd, False ) return x[0], y[0] def _newOrigin(self, origin): @@ -553,24 +477,24 @@ def _writeHeader(self, header, bounds): warnings.simplefilter("ignore") fc = starlink.Ast.FitsChan(None, starlink.Atl.PyFITSAdapter(hdu) , "Encoding=FITS-WCS") # Let Ast know how big the image is that we'll be writing. - for key in ['NAXIS', 'NAXIS1', 'NAXIS2']: - if key in header: + for key in ('NAXIS', 'NAXIS1', 'NAXIS2'): + if key in header: # pragma: no branch fc[key] = header[key] - success = fc.write(self._wcsinfo) + success = fc.write(self.wcsinfo) # PyAst doesn't write out TPV or ZPX correctly. It writes them as TAN and ZPN # respectively. However, if the maximum error is less than 0.1 pixel, it claims # success nonetheless. This doesn't seem accurate enough for many purposes, # so we need to countermand that. # The easiest way I found to check for them is that the string TPN is in the string # version of wcsinfo. So check for that and set success = False in that case. - if 'TPN' in str(self._wcsinfo): success = False + if 'TPN' in str(self.wcsinfo): success = False # Likewise for SIP. MPF seems to be an appropriate string to look for. - if 'MPF' in str(self._wcsinfo): success = False + if 'MPF' in str(self.wcsinfo): success = False if not success: # This should always work, since it uses starlinks own proprietary encoding, but # it won't necessarily be readable by ds9. fc = starlink.Ast.FitsChan(None, starlink.Atl.PyFITSAdapter(hdu)) - fc.write(self._wcsinfo) + fc.write(self.wcsinfo) fc.writefits() header.update(hdu.header) @@ -588,44 +512,42 @@ def _readHeader(header): return PyAstWCS(header=header, origin=PositionD(x0,y0)) def copy(self): - # The copy module version of copying the dict works fine here. - import copy - return copy.copy(self) + ret = PyAstWCS.__new__(PyAstWCS) + ret.__dict__.update(self.__dict__) + return ret def __eq__(self, other): return ( isinstance(other, PyAstWCS) and - str(self._wcsinfo) == str(other.wcsinfo) and + repr(self.wcsinfo) == repr(other.wcsinfo) and self.origin == other.origin) def __repr__(self): - if self._tag is None: - if hasattr(self, 'header'): - self._tag = 'header=%r'%fits.FitsHeader(self.header) - else: - # Ast doesn't have a good repr for a FrameSet, so do it ourselves. - self._tag = 'wcsinfo='%id(self.wcsinfo) - return "galsim.PyAstWCS(%s, origin=%r)"%(self._tag, self.origin) + if self._tag is not None: + tag = self._tag + elif self.header is not None: + tag = 'header=%r'%self.header + else: + # Ast doesn't have a good repr for a FrameSet, so do it ourselves. + tag = 'wcsinfo='%id(self.wcsinfo) + return "galsim.PyAstWCS(%s, origin=%r)"%(tag, self.origin) def __hash__(self): return hash(repr(self)) def __getstate__(self): d = self.__dict__.copy() - # If header or wcsinfo is in the tag, then we can't pickle. Just leave it alone - # and let pickle raise the normal exception. - if self._tag is not None and 'wcsinfo' not in self._tag and 'header' not in self._tag: # pragma: no branch - del d['_wcsinfo'] + del d['_wcsinfo'] return d def __setstate__(self, d): import galsim self.__dict__ = d - hdu, hdu_list, fin = eval('galsim.fits.readFile('+d['_tag']+')') - self._wcsinfo = self._load_from_header(hdu.header, hdu) - fits.closeHDUList(hdu_list, fin) + self._wcsinfo = self._load_from_header(self.header) # I can't figure out how to get wcstools installed in the travis environment (cf. .travis.yml). # So until that gets resolved, we omit this class from the coverage report. +# This class was mostly useful as a refernce implementation anyway. It's much too slow for most +# users to ever want to use it. class WcsToolsWCS(CelestialWCS): # pragma: no cover """This WCS uses wcstools executables to perform the appropriate WCS transformations for a given FITS file. It requires wcstools command line functions to be installed. @@ -662,7 +584,7 @@ def __init__(self, file_name, dir=None, origin=None): if dir: file_name = os.path.join(dir, file_name) if not os.path.isfile(file_name): - raise IOError('Cannot find file '+file_name) + raise OSError('Cannot find file '+file_name) self._file_name = file_name # Check wcstools is installed and that it can read the file. @@ -672,8 +594,14 @@ def __init__(self, file_name, dir=None, origin=None): stdout=subprocess.PIPE) results = p.communicate()[0] p.stdout.close() - if len(results) == 0: - raise IOError('wcstools (specifically xy2sky) was unable to read '+file_name) + if len(results) == 0 or 'cannot' in results: + raise OSError('wcstools (specifically xy2sky) was unable to read '+file_name) + + # wcstools supports LINEAR WCS's, but we don't want to allow them, since then + # the CelestialWCS base class is inappropriate. The clue to detect this is that + # the results only have 4 values, rather than use usual 5 (missing epoch). + if len(results.split()) == 4: + raise GalSimError("The WCS read in does not define a pair of celestial axes" ) @property def file_name(self): return self._file_name @@ -731,10 +659,10 @@ def _radec(self, x, y, color=None): results = p.communicate()[0] p.stdout.close() if len(results) == 0: - raise IOError('wcstools command xy2sky was unable to read '+ self._file_name) + raise OSError('wcstools command xy2sky was unable to read '+ self._file_name) if results[0] != '*': break if results[0] == '*': - raise IOError('wcstools command xy2sky was unable to read '+self._file_name) + raise OSError('wcstools command xy2sky was unable to read '+self._file_name) lines = results.splitlines() # Each line of output should looke like: @@ -744,7 +672,7 @@ def _radec(self, x, y, color=None): for line in lines: vals = line.split() if len(vals) != 5: - raise RuntimeError('wcstools xy2sky returned invalid result near %s'%(xy1)) + raise GalSimError('wcstools xy2sky returned invalid result near %s'%(xy1)) ra.append(float(vals[0])) dec.append(float(vals[1])) @@ -772,20 +700,20 @@ def _xy(self, ra, dec, color=None): results = p.communicate()[0] p.stdout.close() if len(results) == 0: - raise IOError('wcstools (specifically sky2xy) was unable to read '+self._file_name) + raise OSError('wcstools (specifically sky2xy) was unable to read '+self._file_name) if results[0] != '*': break if results[0] == '*': - raise IOError('wcstools (specifically sky2xy) was unable to read '+self._file_name) + raise OSError('wcstools (specifically sky2xy) was unable to read '+self._file_name) # The output should looke like: # ra dec J2000 -> x y # However, if there was an error, the J200 might be missing. vals = results.split() if len(vals) < 6: - raise RuntimeError('wcstools sky2xy returned invalid result for %f,%f'%(ra,dec)) + raise GalSimError('wcstools sky2xy returned invalid result for %f,%f'%(ra,dec)) if len(vals) > 6: - warnings.warn('wcstools sky2xy indicates that %f,%f is off the image\n'%(ra,dec) + - 'output is %r'%results) + galsim_warn("wcstools sky2xy indicates that %f,%f is off the image. " + "output is %r"%(ra,dec,results)) x = float(vals[4]) y = float(vals[5]) @@ -908,7 +836,7 @@ def __init__(self, file_name=None, dir=None, hdu=None, header=None, compression= self.pv = _data[4] self.ab = _data[5] self.abp = _data[6] - if self.wcs_type in [ 'TAN', 'TPV' ]: + if self.wcs_type in ('TAN', 'TPV'): self.projection = 'gnomonic' elif self.wcs_type == 'STG': self.projection = 'stereographic' @@ -930,18 +858,25 @@ def __init__(self, file_name=None, dir=None, hdu=None, header=None, compression= if compression is not 'auto': self._tag += ', compression=%r'%compression if header is not None: - raise TypeError("Cannot provide both file_name and pyfits header") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and pyfits header", + file_name=file_name, header=header) hdu, hdu_list, fin = fits.readFile(file_name, dir, hdu, compression) - header = hdu.header - if header is None: - raise TypeError("Must provide either file_name or header") + try: + if file_name is not None: + header = hdu.header + + if header is None: + raise GalSimIncompatibleValuesError( + "Must provide either file_name or header", file_name=file_name, header=header) - # Read the wcs information from the header. - self._read_header(header) + # Read the wcs information from the header. + self._read_header(header) - if file_name is not None: - fits.closeHDUList(hdu_list, fin) + finally: + if file_name is not None: + fits.closeHDUList(hdu_list, fin) if origin is not None: self.crpix += [ origin.x, origin.y ] @@ -956,20 +891,20 @@ def _read_header(self, header): from .angle import AngleUnit from .celestial import CelestialCoord # Start by reading the basic WCS stuff that most types have. - ctype1 = header['CTYPE1'] - ctype2 = header['CTYPE2'] + ctype1 = header.get('CTYPE1','') + ctype2 = header.get('CTYPE2','') if ctype1.startswith('DEC--') and ctype2.startswith('RA---'): flip = True elif ctype1.startswith('RA---') and ctype2.startswith('DEC--'): flip = False else: - raise RuntimeError("The WCS read in does not define a pair of celestial axes. " - "Expecting CTYPE1,2 to start with RA--- and DEC--. Got %s, %s"%( - ctype1, ctype2)) - if ctype1[5:] != ctype2[5:]: - raise RuntimeError("ctype1, ctype2 do not seem to agree on the WCS type") + raise GalSimError( + "GSFitsWCS only supports celestial coordinate systems." + "Expecting CTYPE1,2 to start with RA--- and DEC--. Got %s, %s"%(ctype1, ctype2)) + if ctype1[5:] != ctype2[5:]: # pragma: no cover + raise OSError("ctype1, ctype2 do not seem to agree on the WCS type") self.wcs_type = ctype1[5:] - if self.wcs_type in [ 'TAN', 'TPV', 'TNX', 'TAN-SIP' ]: + if self.wcs_type in ('TAN', 'TPV', 'TNX', 'TAN-SIP'): self.projection = 'gnomonic' elif self.wcs_type == 'STG': self.projection = 'stereographic' @@ -978,7 +913,9 @@ def _read_header(self, header): elif self.wcs_type == 'ARC': self.projection = 'postel' else: - raise RuntimeError("GSFitsWCS cannot read files using WCS type "+self.wcs_type) + raise GalSimValueError("GSFitsWCS cannot read files using given wcs_type.", + self.wcs_type, + ('TAN', 'TPV', 'TNX', 'TAN-SIP', 'STG', 'ZEA', 'ARC')) crval1 = float(header['CRVAL1']) crval2 = float(header['CRVAL2']) crpix1 = float(header['CRPIX1']) @@ -1071,10 +1008,10 @@ def _read_tpv(self, header): if ( 'PV1_3' in header and header['PV1_3'] != 0.0 or 'PV1_11' in header and header['PV1_11'] != 0.0 or 'PV2_3' in header and header['PV1_3'] != 0.0 or - 'PV2_11' in header and header['PV1_11'] != 0.0 ): - raise NotImplementedError("We don't implement odd powers of r for TPV") - if 'PV1_12' in header: - raise NotImplementedError("We don't implement past 3rd order terms for TPV") + 'PV2_11' in header and header['PV1_11'] != 0.0 ): # pragma: no cover + raise GalSimNotImplementedError("TPV not implemented for odd powers of r") + if 'PV1_12' in header: # pragma: no cover + raise GalSimNotImplementedError("TPV not implemented past 3rd order terms") # Another strange thing is that the two matrices are defined in the opposite order # with respect to their element ordering. And remember that we skipped k=3 in the @@ -1144,16 +1081,16 @@ def _read_tnx(self, header): wat1[2] != 'lngcor' or wat1[3] != '=' or not wat1[4].startswith('"') or - not wat1[-1].endswith('"') ): - raise RuntimeError("TNX WAT1 was not as expected") + not wat1[-1].endswith('"') ): # pragma: no cover + raise GalSimError("TNX WAT1 was not as expected") if ( len(wat2) < 12 or wat2[0] != 'wtype=tnx' or wat2[1] != 'axtype=dec' or wat2[2] != 'latcor' or wat2[3] != '=' or not wat2[4].startswith('"') or - not wat2[-1].endswith('"') ): - raise RuntimeError("TNX WAT2 was not as expected") + not wat2[-1].endswith('"') ): # pragma: no cover + raise GalSimError("TNX WAT2 was not as expected") # Break the next bit out into another function, since it is the same for x and y. pv1 = self._parse_tnx_data(wat1[4:]) @@ -1182,19 +1119,19 @@ def _parse_tnx_data(self, data): data = data[1:] else: data[0] = data[0][1:] - if data[-1] == '"': # pragma: no branch + if data[-1] == '"': data = data[:-1] - else: + else: # pragma: no cover data[-1] = data[-1][:-1] code = int(data[0].strip('.')) # Weirdly, these integers are given with decimal points. xorder = int(data[1].strip('.')) yorder = int(data[2].strip('.')) cross = int(data[3].strip('.')) - if cross != 2: - raise NotImplementedError("TNX only implemented for half-cross option.") - if xorder != 4 or yorder != 4: - raise NotImplementedError("TNX only implemented for order = 4") + if cross != 2: # pragma: no cover + raise GalSimNotImplementedError("TNX only implemented for half-cross option.") + if xorder != 4 or yorder != 4: # pragma: no cover + raise GalSimNotImplementedError("TNX only implemented for order = 4") # Note: order = 4 really means cubic. order is how large the pv matrix is, i.e. 4x4. xmin = float(data[4]) @@ -1203,8 +1140,8 @@ def _parse_tnx_data(self, data): ymax = float(data[7]) pv1 = [ float(x) for x in data[8:] ] - if len(pv1) != 10: - raise RuntimeError("Wrong number of items found in WAT data") + if len(pv1) != 10: # pragma: no cover + raise GalSimError("Wrong number of items found in WAT data") # Put these into our matrix formulation. pv = np.array( [ [ pv1[0], pv1[4], pv1[7], pv1[9] ], @@ -1272,20 +1209,23 @@ def _parse_tnx_data(self, data): def _apply_pv(self, u, v): # Do this in C++ layer for speed. - _galsim.ApplyPV(len(u), 4, u.ctypes.data, v.ctypes.data, self.pv.ctypes.data) + with convert_cpp_errors(): + _galsim.ApplyPV(len(u), 4, u.ctypes.data, v.ctypes.data, self.pv.ctypes.data) return u, v def _apply_ab(self, x, y): # Do this in C++ layer for speed. dx = x.copy() dy = y.copy() - _galsim.ApplyPV(len(x), len(self.ab[0]), dx.ctypes.data, dy.ctypes.data, - self.ab.ctypes.data) + with convert_cpp_errors(): + _galsim.ApplyPV(len(x), len(self.ab[0]), dx.ctypes.data, dy.ctypes.data, + self.ab.ctypes.data) return x+dx, y+dy def _apply_cd(self, x, y): # Do this in C++ layer for speed. - _galsim.ApplyCD(len(x), x.ctypes.data, y.ctypes.data, self.cd.ctypes.data) + with convert_cpp_errors(): + _galsim.ApplyCD(len(x), x.ctypes.data, y.ctypes.data, self.cd.ctypes.data) return x, y def _uv(self, x, y): @@ -1333,12 +1273,14 @@ def _radec(self, x, y, color=None): def _invert_pv(self, u, v): # Do this in C++ layer for speed. - return _galsim.InvertPV(u, v, self.pv.ctypes.data) + with convert_cpp_errors(): + return _galsim.InvertPV(u, v, self.pv.ctypes.data) def _invert_ab(self, x, y): # Do this in C++ layer for speed. abp_data = 0 if self.abp is None else self.abp.ctypes.data - return _galsim.InvertAB(len(self.ab[0]), x, y, self.ab.ctypes.data, abp_data) + with convert_cpp_errors(): + return _galsim.InvertAB(len(self.ab[0]), x, y, self.ab.ctypes.data, abp_data) def _xy(self, ra, dec, color=None): u, v = self.center.project_rad(ra, dec, projection=self.projection) @@ -1366,11 +1308,11 @@ def _xy(self, ra, dec, color=None): return x, y # Override the version in CelestialWCS, since we can do this more efficiently. - def _local(self, image_pos, world_pos, color=None): + def _local(self, image_pos, color=None): + from .wcs import JacobianWCS + if image_pos is None: - if world_pos is None: - raise TypeError("Either image_pos or world_pos must be provided") - image_pos = self._posToImage(world_pos, color=color) + raise TypeError("origin must be a PositionD or PositionI argument") # The key lemma here is that chain rule for jacobians is just matrix multiplication. # i.e. if s = s(u,v), t = t(u,v) and u = u(x,y), v = v(x,y), then @@ -1453,8 +1395,7 @@ def _local(self, image_pos, world_pos, color=None): def _newOrigin(self, origin): ret = self.copy() - if origin is not None: - ret.crpix = ret.crpix + [ origin.x, origin.y ] + ret.crpix = ret.crpix + [ origin.x, origin.y ] return ret def _writeHeader(self, header, bounds): @@ -1611,16 +1552,11 @@ def TanWCS(affine, world_origin, units=arcsec): PyAstWCS, # This requires `import starlink.Ast` to succeed. This handles the largest # number of WCS types of any of these. In fact, it worked for every one - # we tried in our unit tests (which was not exhaustive). This is a bit - # slower than Astropy, but I think mostly due to their initial reading of - # the fits header -- that seems to take a lot of time for some reason. - # Once it is loaded, the actual usage seems to be quite fast. + # we tried in our unit tests (which was not exhaustive). - AstropyWCS, # This requires `import astropy.wcs` to succeed. So far, they only handle - # the standard official WCS types. So not TPV, for instance. Also, it is - # a little faster than PyAst, so we prefer PyAst when it is available. - # (But only because of our fix in the _xy function to not use the astropy - # version of all_world2pix function!) + AstropyWCS, # This requires `import astropy.wcs` to succeed. It doesn't support quite as + # many WCS types as PyAst. It's also usually a little slower, so we prefer + # PyAstWCS when it is available. WcsToolsWCS, # This requires the wcstool command line functions to be installed. # It is very slow, so it should only be used as a last resort. @@ -1664,16 +1600,33 @@ def FitsWCS(file_name=None, dir=None, hdu=None, header=None, compression='auto', (Note: this is set to True when this function is implicitly called from one of the galsim.fits.read* functions.) """ + from .wcs import AffineTransform, PixelScale, OffsetWCS + if file_name is not None: if header is not None: - raise TypeError("Cannot provide both file_name and pyfits header") + raise GalSimIncompatibleValuesError( + "Cannot provide both file_name and pyfits header", + file_name=file_name, header=header) header = fits.FitsHeader(file_name=file_name, dir=dir, hdu=hdu, compression=compression, text_file=text_file) else: file_name = 'header' # For sensible error messages below. if header is None: - raise TypeError("Must provide either file_name or header") + raise GalSimIncompatibleValuesError( + "Must provide either file_name or header", file_name=file_name, header=header) + + # For linear WCS specifications, AffineTransformation should work. + if header.get('CTYPE1', 'LINEAR') == 'LINEAR': + wcs = AffineTransform._readHeader(header) + # Convert to PixelScale if possible. + if (wcs.dudx == wcs.dvdy and wcs.dudy == wcs.dvdx == 0): + if wcs.x0 == wcs.y0 == wcs.u0 == wcs.v0 == 0: + wcs = PixelScale(wcs.dudx) + else: + wcs = OffsetWCS(wcs.dudx, wcs.origin, wcs.world_origin) + return wcs + # Otherwise (and typically), try the various wcs types that can read celestial coordinates. for wcs_type in fits_wcs_types: try: wcs = wcs_type._readHeader(header) @@ -1689,16 +1642,17 @@ def FitsWCS(file_name=None, dir=None, hdu=None, header=None, compression='auto', if compression is not 'auto': wcs._tag += ', compression=%r'%compression return wcs + except KeyboardInterrupt: + raise except Exception as err: pass else: # pragma: no cover # Finally, this one is really the last resort, since it only reads in the linear part of the # WCS. It defaults to the equivalent of a pixel scale of 1.0 if even these are not present. if not suppress_warning: - warnings.warn("All the fits WCS types failed to read "+file_name+". " + - "Using AffineTransform instead, which will not really be correct.") - wcs = AffineTransform._readHeader(header) - return wcs + galsim_warn("All the fits WCS types failed to read %r. Using AffineTransform " + "instead, which will not really be correct."%(file_name)) + return AffineTransform._readHeader(header) # Let this function work like a class in config. FitsWCS._req_params = { "file_name" : str } diff --git a/galsim/fouriersqrt.py b/galsim/fouriersqrt.py index 05e6a12a401..f31265f4778 100644 --- a/galsim/fouriersqrt.py +++ b/galsim/fouriersqrt.py @@ -23,6 +23,7 @@ from .gsobject import GSObject from .chromatic import ChromaticObject from .utilities import lazy_property, doc_inherit +from .errors import convert_cpp_errors, galsim_warn def FourierSqrt(obj, gsparams=None): @@ -100,13 +101,13 @@ def orig_obj(self): return self._orig_obj @property def _sbp(self): - return _galsim.SBFourierSqrt(self.orig_obj._sbp, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBFourierSqrt(self.orig_obj._sbp, self.gsparams._gsp) @property def _noise(self): if self.orig_obj.noise is not None: - import warnings - warnings.warn("Unable to propagate noise in galsim.FourierSqrtProfile") + galsim_warn("Unable to propagate noise in galsim.FourierSqrtProfile") return None def __eq__(self, other): diff --git a/galsim/gaussian.py b/galsim/gaussian.py index 45281f20af4..4e3357b457d 100644 --- a/galsim/gaussian.py +++ b/galsim/gaussian.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import GalSimIncompatibleValuesError, convert_cpp_errors class Gaussian(GSObject): @@ -84,22 +85,22 @@ class Gaussian(GSObject): def __init__(self, half_light_radius=None, sigma=None, fwhm=None, flux=1., gsparams=None): if fwhm is not None : if sigma is not None or half_light_radius is not None: - raise TypeError( - "Only one of sigma, fwhm, and half_light_radius may be " + - "specified for Gaussian") + raise GalSimIncompatibleValuesError( + "Only one of sigma, fwhm, and half_light_radius may be specified", + fwhm=fwhm, sigma=sigma, half_light_radius=half_light_radius) else: sigma = fwhm / Gaussian._fwhm_factor elif half_light_radius is not None: if sigma is not None: - raise TypeError( - "Only one of sigma, fwhm, and half_light_radius may be " + - "specified for Gaussian") + raise GalSimIncompatibleValuesError( + "Only one of sigma, fwhm, and half_light_radius may be specified", + fwhm=fwhm, sigma=sigma, half_light_radius=half_light_radius) else: sigma = half_light_radius / Gaussian._hlr_factor elif sigma is None: - raise TypeError( - "One of sigma, fwhm, or half_light_radius must be " + - "specified for Gaussian") + raise GalSimIncompatibleValuesError( + "One of sigma, fwhm, and half_light_radius must be specified", + fwhm=fwhm, sigma=sigma, half_light_radius=half_light_radius) self._sigma = float(sigma) self._flux = float(flux) @@ -110,7 +111,8 @@ def __init__(self, half_light_radius=None, sigma=None, fwhm=None, flux=1., gspar @lazy_property def _sbp(self): - return _galsim.SBGaussian(self._sigma, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBGaussian(self._sigma, self._flux, self.gsparams._gsp) @property def sigma(self): return self._sigma diff --git a/galsim/gsobject.py b/galsim/gsobject.py index f15b6d9af5a..0d5fcfefb84 100644 --- a/galsim/gsobject.py +++ b/galsim/gsobject.py @@ -58,6 +58,8 @@ from . import _galsim from .position import PositionD, PositionI from .utilities import lazy_property, parse_pos_args +from .errors import GalSimError, GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError +from .errors import GalSimFFTSizeError, GalSimNotImplementedError, convert_cpp_errors, galsim_warn class GSObject(object): @@ -188,19 +190,20 @@ class GSObject(object): >>> gal = galsim.Sersic(n=4, half_light_radius=4.3) >>> psf = galsim.Moffat(beta=3, fwhm=2.85) >>> conv = galsim.Convolve([gal,psf]) - >>> im = galsim.Image(1000,1000, scale=0.05) # Note the very small pixel scale! + >>> im = galsim.Image(1000,1000, scale=0.02) # Note the very small pixel scale! >>> im = conv.drawImage(image=im) # This uses the default GSParams. Traceback (most recent call last): File "", line 1, in - File "galsim/gsobject.py", line 1615, in drawImage + File "galsim/gsobject.py", line 1666, in drawImage added_photons = prof.drawFFT(draw_image, add) - File "galsim/gsobject.py", line 1827, in drawFFT + File "galsim/gsobject.py", line 1877, in drawFFT kimage, wrap_size = self.drawFFT_makeKImage(image) - File "galsim/gsobject.py", line 1753, in drawFFT_makeKImage - "If you can handle the large FFT, you may update gsparams.maximum_fft_size.") - RuntimeError: drawFFT requires an FFT that is too large: 6144. + File "galsim/gsobject.py", line 1802, in drawFFT_makeKImage + raise GalSimFFTSizeError("drawFFT requires an FFT that is too large.", Nk) + galsim.errors.GalSimFFTSizeError: drawFFT requires an FFT that is too large. + The required FFT size would be 12288 x 12288, which requires 3.38 GB of memory. If you can handle the large FFT, you may update gsparams.maximum_fft_size. - >>> big_fft_params = galsim.GSParams(maximum_fft_size=10240) + >>> big_fft_params = galsim.GSParams(maximum_fft_size=12300) >>> conv = galsim.Convolve([gal,psf],gsparams=big_fft_params) >>> im = conv.drawImage(image=im) # Now it works (but is slow!) >>> im.write('high_res_sersic.fits') @@ -602,8 +605,8 @@ def calculateMomentRadius(self, size=None, scale=None, centroid=None, rtype='det @returns an estimate of the radius in physical units (or both estimates if rtype == 'both') """ - if rtype not in ['trace', 'det', 'both']: - raise ValueError("rtype must be one of 'trace', 'det', or 'both'") + if rtype not in ('trace', 'det', 'both'): + raise GalSimValueError("Invalid rtype.", rtype, ('trace', 'det', 'both')) if hasattr(self, 'sigma'): if rtype == 'both': @@ -683,7 +686,7 @@ def xValue(self, *args, **kwargs): Not all GSObject classes can use this method. Classes like Convolution that require a Discrete Fourier Transform to determine the real space values will not do so for a single - position. Instead a RuntimeError will be raised. The xValue() method is available if and + position. Instead a GalSimError will be raised. The xValue() method is available if and only if `obj.is_analytic_x == True`. Users who wish to use the xValue() method for an object that is the convolution of other @@ -725,7 +728,7 @@ def kValue(self, *args, **kwargs): kpos = parse_pos_args(args,kwargs,'kx','ky') return self._kValue(kpos) - def _kValue(self, kpos): + def _kValue(self, kpos): # pragma: no cover (all our classes override this) """Equivalent to kValue(kpos), but kpos must be a galsim.PositionD instance. """ raise NotImplementedError("%s does not implement kValue"%self.__class__.__name__) @@ -871,6 +874,8 @@ def shear(self, *args, **kwargs): shear = args[0] elif len(args) > 1: raise TypeError("Error, too many unnamed arguments to GSObject.shear!") + elif len(kwargs) == 0: + raise TypeError("Error, shear argument is required") else: shear = Shear(**kwargs) return Transform(self, jac=shear.getMatrix().ravel().tolist()) @@ -1013,25 +1018,34 @@ def _setup_image(self, image, nx, ny, bounds, add_to_image, dtype, odd=False): # Check validity of nx,ny,bounds: if image is not None: if bounds is not None: - raise ValueError("Cannot provide bounds if image is provided") + raise GalSimIncompatibleValuesError( + "Cannot provide bounds if image is provided", bounds=bounds, image=image) if nx is not None or ny is not None: - raise ValueError("Cannot provide nx,ny if image is provided") + raise GalSimIncompatibleValuesError( + "Cannot provide nx,ny if image is provided", nx=nx, ny=ny, image=image) if dtype is not None and image.array.dtype != dtype: - raise ValueError("Cannot specify dtype != image.array.dtype if image is provided") + raise GalSimIncompatibleValuesError( + "Cannot specify dtype != image.array.dtype if image is provided", + dtype=dtype, image=image) # Make image if necessary if image is None: # Can't add to image if none is provided. if add_to_image: - raise ValueError("Cannot add_to_image if image is None") + raise GalSimIncompatibleValuesError( + "Cannot add_to_image if image is None", add_to_image=add_to_image, image=image) # Use bounds or nx,ny if provided if bounds is not None: if nx is not None or ny is not None: - raise ValueError("Cannot set both bounds and (nx, ny)") + raise GalSimIncompatibleValuesError( + "Cannot set both bounds and (nx, ny)", nx=nx, ny=ny, bounds=bounds) + if not bounds.isDefined(): + raise GalSimValueError("Cannot use undefined bounds", bounds) image = Image(bounds=bounds, dtype=dtype) elif nx is not None or ny is not None: if nx is None or ny is None: - raise ValueError("Must set either both or neither of nx, ny") + raise GalSimIncompatibleValuesError( + "Must set either both or neither of nx, ny", nx=nx, ny=ny) image = Image(nx, ny, dtype=dtype) else: N = self.getGoodImageSize(1.0) @@ -1042,7 +1056,9 @@ def _setup_image(self, image, nx, ny, bounds, add_to_image, dtype, odd=False): elif not image.bounds.isDefined(): # Can't add to image if need to resize if add_to_image: - raise ValueError("Cannot add_to_image if image bounds are not defined") + raise GalSimIncompatibleValuesError( + "Cannot add_to_image if image bounds are not defined", + add_to_image=add_to_image, image=image) N = self.getGoodImageSize(1.0) if odd: N += 1 bounds = _BoundsI(1,N,1,N) @@ -1062,7 +1078,9 @@ def _local_wcs(self, wcs, image, offset, use_true_center, new_bounds): else: bounds = image.bounds if not bounds.isDefined(): - raise ValueError("Cannot provide non-local wcs with automatically sized image") + raise GalSimIncompatibleValuesError( + "Cannot provide non-local wcs with automatically sized image", + wcs=wcs, image=image, bounds=new_bounds) elif use_true_center: obj_cen = bounds.true_center else: @@ -1114,7 +1132,8 @@ def _determine_wcs(self, scale, wcs, image, default_wcs=None): # Determine the correct wcs given the input scale, wcs and image. if wcs is not None: if scale is not None: - raise ValueError("Cannot provide both wcs and scale") + raise GalSimIncompatibleValuesError( + "Cannot provide both wcs and scale", wcs=wcs, scale=scale) if not isinstance(wcs, BaseWCS): raise TypeError("wcs must be a BaseWCS instance") if image is not None: image.wcs = None @@ -1485,24 +1504,24 @@ def drawImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, wcs=N # Check that image is sane if image is not None and not isinstance(image, Image): - raise ValueError("image is not an Image instance") + raise TypeError("image is not an Image instance", image) # Make sure (gain, area, exptime) have valid values: if gain <= 0.: - raise ValueError("Invalid gain <= 0.") + raise GalSimRangeError("Invalid gain <= 0.", gain, 0., None) if area <= 0.: - raise ValueError("Invalid area <= 0.") + raise GalSimRangeError("Invalid area <= 0.", area, 0., None) if exptime <= 0.: - raise ValueError("Invalid exptime <= 0.") + raise GalSimRangeError("Invalid exptime <= 0.", exptime, 0., None) - if method not in ['auto', 'fft', 'real_space', 'phot', 'no_pixel', 'sb']: - raise ValueError("Invalid method name = %s"%method) + if method not in ('auto', 'fft', 'real_space', 'phot', 'no_pixel', 'sb'): + raise GalSimValueError("Invalid method name", method, + ('auto', 'fft', 'real_space', 'phot', 'no_pixel', 'sb')) # Check that the user isn't convolving by a Pixel already. This is almost always an error. if method == 'auto' and isinstance(self, Convolution): if any([ isinstance(obj, Pixel) for obj in self.obj_list ]): - import warnings - warnings.warn( + galsim_warn( "You called drawImage with `method='auto'` " "for an object that includes convolution by a Pixel. " "This is probably an error. Normally, you should let GalSim " @@ -1514,21 +1533,34 @@ def drawImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, wcs=N # Some parameters are only relevant for method == 'phot' if method != 'phot' and sensor is None: if n_photons != 0.: - raise ValueError("n_photons is only relevant for method='phot'") + raise GalSimIncompatibleValuesError( + "n_photons is only relevant for method='phot'", + method=method, sensor=sensor, n_photons=n_photons) if rng is not None: - raise ValueError("rng is only relevant for method='phot'") + raise GalSimIncompatibleValuesError( + "rng is only relevant for method='phot'", + method=method, sensor=sensor, rng=rng) if max_extra_noise != 0.: - raise ValueError("max_extra_noise is only relevant for method='phot'") + raise GalSimIncompatibleValuesError( + "max_extra_noise is only relevant for method='phot'", + method=method, sensor=sensor, max_extra_noise=max_extra_noise) if poisson_flux is not None: - raise ValueError("poisson_flux is only relevant for method='phot'") + raise GalSimIncompatibleValuesError( + "poisson_flux is only relevant for method='phot'", + method=method, sensor=sensor, poisson_flux=poisson_flux) if surface_ops != (): - raise ValueError("surface_ops are only relevant for method='phot'") + raise GalSimIncompatibleValuesError( + "surface_ops are only relevant for method='phot'", + method=method, sensor=sensor, surface_ops=surface_ops) if save_photons: - raise ValueError("save_photons is only valid for method='phot'") + raise GalSimIncompatibleValuesError( + "save_photons is only valid for method='phot'", + method=method, sensor=sensor, save_photons=save_photons) else: # If we want to save photons, it doesn't make sense to limit the number per shoot call. if save_photons and maxN is not None: - raise ValueError("Setting maxN is incompatible with save_photons=True") + raise GalSimIncompatibleValuesError( + "Setting maxN is incompatible with save_photons=True") # Do any delayed computation needed by fft or real_space drawing. if method != 'phot': @@ -1568,7 +1600,7 @@ def drawImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, wcs=N prof *= flux_scale # If necessary, convolve by the pixel - if method in ['auto', 'fft', 'real_space']: + if method in ('auto', 'fft', 'real_space'): if method == 'auto': real_space = None elif method == 'fft': @@ -1600,13 +1632,13 @@ def drawImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, wcs=N else: # If not using phot, but doing sensor, then make a copy. if sensor is not None: - if imview.dtype in [np.float32, np.float64]: + if imview.dtype in (np.float32, np.float64): dtype = None else: dtype = np.float64 draw_image = imview.real.subsample(n_subsample, n_subsample, dtype=dtype) draw_image.setCenter(0,0) - if method in ['auto', 'fft', 'real_space']: + if method in ('auto', 'fft', 'real_space'): # Need to reconvolve by the new smaller pixel instead prof = Convolve( prof_no_pixel, @@ -1636,7 +1668,7 @@ def drawImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, wcs=N photons = PhotonArray.makeFromImage(draw_image, rng=ud) for op in surface_ops: op.applyTo(photons, local_wcs) - if imview.dtype in [np.float32, np.float64]: + if imview.dtype in (np.float32, np.float64): added_photons = sensor.accumulate(photons, imview, orig_center) else: # Need a temporary @@ -1676,14 +1708,14 @@ def drawReal(self, image, add_to_image=False): """ from .image import ImageD, ImageF if image.wcs is None or not image.wcs.isPixelScale(): - raise ValueError("drawReal requires an image with a PixelScale wcs") + raise GalSimValueError("drawReal requires an image with a PixelScale wcs", image) - if image.dtype in [ np.float64, np.float32 ] and not add_to_image and image.iscontiguous: + if image.dtype in (np.float64, np.float32) and not add_to_image and image.iscontiguous: self._drawReal(image) return image.array.sum(dtype=float) else: # Need a temporary - if image.dtype in [ np.complex128, np.int32, np.uint32 ]: + if image.dtype in (np.complex128, np.int32, np.uint32): im1 = ImageD(bounds=image.bounds, scale=image.scale) else: im1 = ImageF(bounds=image.bounds, scale=image.scale) @@ -1765,12 +1797,10 @@ def drawFFT_makeKImage(self, image): Nk = int(np.ceil(maxk/dk)) * 2 if Nk > self.gsparams.maximum_fft_size: - raise RuntimeError( - "drawFFT requires an FFT that is too large: %s. "%Nk + - "If you can handle the large FFT, you may update gsparams.maximum_fft_size.") + raise GalSimFFTSizeError("drawFFT requires an FFT that is too large.", Nk) bounds = _BoundsI(0,Nk//2,-Nk//2,Nk//2) - if image.dtype in [ np.complex128, np.float64, np.int32, np.uint32 ]: + if image.dtype in (np.complex128, np.float64, np.int32, np.uint32): kimage = ImageCD(bounds=bounds, scale=dk) else: kimage = ImageCF(bounds=bounds, scale=dk) @@ -1803,7 +1833,8 @@ def drawFFT_finish(self, image, kimage, wrap_size, add_to_image): # Perform the fourier transform. breal = _BoundsI(-wrap_size//2, wrap_size//2+1, -wrap_size//2, wrap_size//2-1) real_image = Image(breal, dtype=float) - _galsim.irfft(kimage_wrap._image, real_image._image, True, True) + with convert_cpp_errors(): + _galsim.irfft(kimage_wrap._image, real_image._image, True, True) # Add (a portion of) this to the original image. temp = real_image.subImage(image.bounds) @@ -1839,7 +1870,7 @@ def drawFFT(self, image, add_to_image=False): @returns The total flux drawn inside the image bounds. """ if image.wcs is None or not image.wcs.isPixelScale(): - raise ValueError("drawPhot requires an image with a PixelScale wcs") + raise GalSimValueError("drawPhot requires an image with a PixelScale wcs", image) kimage, wrap_size = self.drawFFT_makeKImage(image) self._drawKImage(kimage) @@ -1952,15 +1983,12 @@ def _calculate_nphotons(self, n_photons, poisson_flux, max_extra_noise, rng): # Make n_photons an integer. iN = int(n_photons + 0.5) - if iN <= 0: # pragma: no cover - import warnings - warnings.warn("Automatic n_photons calculation did not end up with positive N. " + - "(n_photons = %s) No photons will be shot. "%n_photons + - "prof = %s "%self + - "flux = %s "%self.flux + - "poisson_flux = %s "%poisson_flux + - "max_extra_noise = %s "%max_extra_noise + - "g = %s "%g) + if iN <= 0: + galsim_warn("Automatic n_photons calculation did not end up with positive N. " + "(n_photons = {0}) No photons will be shot.\n" + " prof = {1}\n flux = {2}\n poisson_flux = {3}\n" + " max_extra_noise = {4}\n g = {5}".format( + n_photons, self, self.flux, poisson_flux, max_extra_noise, g)) return 0, 1. return iN, g @@ -2040,7 +2068,7 @@ def drawPhot(self, image, gain=1., add_to_image=False, from .image import ImageD # Make sure the type of n_photons is correct and has a valid value: if n_photons < 0.: - raise ValueError("Invalid n_photons < 0.") + raise GalSimRangeError("Invalid n_photons < 0.", n_photons, 0., None) if poisson_flux is None: if n_photons == 0.: poisson_flux = True @@ -2048,8 +2076,7 @@ def drawPhot(self, image, gain=1., add_to_image=False, # Check that either n_photons is set to something or flux is set to something if n_photons == 0. and self.flux == 1.: - import warnings - warnings.warn( + galsim_warn( "Warning: drawImage for object with flux == 1, area == 1, and " "exptime == 1, but n_photons == 0. This will only shoot a single photon.") @@ -2057,7 +2084,7 @@ def drawPhot(self, image, gain=1., add_to_image=False, # Make sure the image is set up to have unit pixel scale and centered at 0,0. if image.wcs is None or not image.wcs.isPixelScale(): - raise ValueError("drawPhot requires an image with a PixelScale wcs") + raise GalSimValueError("drawPhot requires an image with a PixelScale wcs", image) if sensor is None: sensor = Sensor() @@ -2087,14 +2114,11 @@ def drawPhot(self, image, gain=1., add_to_image=False, try: photons = self.shoot(thisN, ud) - except RuntimeError: # pragma: no cover - # Give some extra explanation as a warning, then raise the original exception - # so the traceback shows as much detail as possible. - import warnings - warnings.warn( - "Unable to draw this GSObject with photon shooting. Perhaps it is a "+ - "Deconvolve or is a compound including one or more Deconvolve objects.") - raise + except (GalSimError, NotImplementedError) as e: + raise GalSimNotImplementedError( + "Unable to draw this GSObject with photon shooting. Perhaps it " + "is a Deconvolve or is a compound including one or more " + "Deconvolve objects.\nOriginal error: %r"%(e)) if g != 1. or thisN != Ntot: photons.scaleFlux(g * thisN / Ntot) @@ -2105,7 +2129,7 @@ def drawPhot(self, image, gain=1., add_to_image=False, for op in surface_ops: op.applyTo(photons, local_wcs) - if image.dtype in [np.float32, np.float64]: + if image.dtype in (np.float32, np.float64): added_flux += sensor.accumulate(photons, image, orig_center, resume=resume) resume = True # Resume from this point if there are any further iterations. else: @@ -2194,8 +2218,12 @@ def drawKImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, from .wcs import PixelScale from .image import Image # Make sure provided image is complex - if image is not None and not image.iscomplex: - raise ValueError("Provided image must be complex") + if image is not None: + if not isinstance(image, Image): + raise TypeError("Provided image must be galsim.Image", image) + + if not image.iscomplex: + raise GalSimValueError("Provided image must be complex", image) # Possibly get the scale from image. if image is not None and scale is None: @@ -2225,10 +2253,20 @@ def drawKImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, real_prof = PixelScale(dx).toImage(self) dtype = np.complex128 if image is None else image.dtype image = real_prof._setup_image(image, nx, ny, bounds, add_to_image, dtype, odd=True) + else: + # Do some checks that setup_image would have done for us + if bounds is not None: + raise GalSimIncompatibleValuesError( + "Cannot provide bounds if image is provided", bounds=bounds, image=image) + if nx is not None or ny is not None: + raise GalSimIncompatibleValuesError( + "Cannot provide nx,ny if image is provided", nx=nx, ny=ny, image=image) # Can't both recenter a provided image and add to it. if recenter and image.center != PositionI(0,0) and add_to_image: - raise ValueError("Cannot recenter a non-centered image when add_to_image=True") + raise GalSimIncompatibleValuesError( + "Cannot use add_to_image=True unless image is centered at (0,0) or recenter=False", + recenter=recenter, image=image, add_to_image=add_to_image) # Set the center to 0,0 if appropriate if recenter and image.center != PositionI(0,0): @@ -2248,7 +2286,7 @@ def drawKImage(self, image=None, nx=None, ny=None, bounds=None, scale=None, image += im2 return image - def _drawKImage(self, image): + def _drawKImage(self, image): # pragma: no cover (all our classes override this) """Equivalent to drawKImage(image, add_to_image, recenter=False, add_to_image=False), but without the normal sanity checks or the option to create the image automatically. diff --git a/galsim/gsparams.py b/galsim/gsparams.py index 195225dae9a..57be8f3f3ac 100644 --- a/galsim/gsparams.py +++ b/galsim/gsparams.py @@ -20,6 +20,7 @@ """ from . import _galsim +from .errors import convert_cpp_errors class GSParams(object): """GSParams stores a set of numbers that govern how GSObjects make various speed/accuracy @@ -51,7 +52,7 @@ class GSParams(object): will raise an exception indicating that the image is too large, which is often a sign of an error in the user's code. However, if you have the memory to handle it, you can raise this limit to - allow the calculation to happen. [default: 4096] + allow the calculation to happen. [default: 8192] @param folding_threshold This sets a maximum amount of real space folding that is allowed, an effect caused by the periodic nature of FFTs. FFTs implicitly use periodic boundary conditions, and a profile specified on a @@ -123,7 +124,7 @@ class GSParams(object): of probability are considered ok to use with the dominant-sampling algorithm. [default: 1.e-4] """ - def __init__(self, minimum_fft_size=128, maximum_fft_size=4096, + def __init__(self, minimum_fft_size=128, maximum_fft_size=8192, folding_threshold=5.e-3, stepk_minimum_hlr=5, maxk_threshold=1.e-3, kvalue_accuracy=1.e-5, xvalue_accuracy=1.e-5, table_spacing=1, realspace_relerr=1.e-4, realspace_abserr=1.e-6, @@ -148,7 +149,8 @@ def __init__(self, minimum_fft_size=128, maximum_fft_size=4096, self._small_fraction_of_flux = small_fraction_of_flux # This is the thing that is needed for any c++ calls. - self._gsp = _galsim.GSParams(*self._getinitargs()) + with convert_cpp_errors(): + self._gsp = _galsim.GSParams(*self._getinitargs()) # Make all the attributes read-only @property diff --git a/galsim/hsm.py b/galsim/hsm.py index 5f7d0cb3154..b418fb08526 100644 --- a/galsim/hsm.py +++ b/galsim/hsm.py @@ -57,11 +57,14 @@ import numpy as np + from . import _galsim from .position import PositionD from .bounds import BoundsI from .shear import Shear from .image import Image, ImageI, ImageF, ImageD +from .errors import GalSimError, GalSimValueError, GalSimHSMError, GalSimIncompatibleValuesError +from .errors import convert_cpp_errors class ShapeData(object): @@ -149,14 +152,15 @@ def __init__(self, image_bounds=BoundsI(), moments_status=-1, raise TypeError("image_bounds must be a BoundsI instance") # The others will raise an appropriate TypeError from the call to _galsim.ShapeData. - self._data = _galsim.ShapeData( - image_bounds._b, int(moments_status), observed_shape.e1, observed_shape.e2, - float(moments_sigma), float(moments_amp), moments_centroid._p, float(moments_rho4), - int(moments_n_iter), int(correction_status), float(corrected_e1), float(corrected_e2), - float(corrected_g1), float(corrected_g2), str(meas_type), float(corrected_shape_err), - str(correction_method), float(resolution_factor), float(psf_sigma), - psf_shape.e1, psf_shape.e2, str(error_message)) - + with convert_cpp_errors(GalSimHSMError): + self._data = _galsim.ShapeData( + image_bounds._b, int(moments_status), observed_shape.e1, observed_shape.e2, + float(moments_sigma), float(moments_amp), moments_centroid._p, + float(moments_rho4), int(moments_n_iter), int(correction_status), + float(corrected_e1), float(corrected_e2), float(corrected_g1), float(corrected_g2), + str(meas_type), float(corrected_shape_err), str(correction_method), + float(resolution_factor), float(psf_sigma), psf_shape.e1, psf_shape.e2, + str(error_message)) @property def image_bounds(self): return BoundsI(self._data.image_bounds) @@ -365,7 +369,8 @@ def __init__(self, nsig_rg=3.0, nsig_rg2=3.6, max_moment_nsig2=25.0, regauss_too self._make_hsmp() def _make_hsmp(self): - self._hsmp = _galsim.HSMParams(*self._getinitargs()) + with convert_cpp_errors(GalSimHSMError): + self._hsmp = _galsim.HSMParams(*self._getinitargs()) def _getinitargs(self): return (self.nsig_rg, self.nsig_rg2, self.max_moment_nsig2, self.regauss_too_small, @@ -456,11 +461,13 @@ def _convertMask(image, weight=None, badpix=None): else: # if weight image was supplied, check if it has the right bounds and is non-negative if weight.bounds != image.bounds: - raise ValueError("Weight image does not have same bounds as the input Image!") + raise GalSimIncompatibleValuesError( + "Weight image does not have same bounds as the input Image.", + weight=weight, image=image) # also make sure there are no negative values if np.any(weight.array < 0) == True: - raise ValueError("Weight image cannot contain negative values!") + raise GalSimValueError("Weight image cannot contain negative values.", weight) # if weight is an ImageI, then we can use it as the mask image: if weight.dtype == np.int32: @@ -479,12 +486,14 @@ def _convertMask(image, weight=None, badpix=None): # image; also check bounds if badpix is not None: if badpix.bounds != image.bounds: - raise ValueError("Badpix image does not have the same bounds as the input Image!") + raise GalSimIncompatibleValuesError( + "Badpix image does not have the same bounds as the input Image.", + badpix=badpix, image=image) mask.array[badpix.array != 0] = 0 # if no pixels are used, raise an exception if mask.array.sum() == 0: - raise RuntimeError("No pixels are being used!") + raise GalSimHSMError("No pixels are being used!") # finally, return the Image for the weight map return mask @@ -595,9 +604,9 @@ def EstimateShear(gal_image, PSF_image, weight=None, badpix=None, sky_var=0.0, the lower-left pixel is (image.xmin, image.ymin). [default: gal_image.true_center] @param strict Whether to require success. If `strict=True`, then there will be a - `RuntimeError` exception if shear estimation fails. If set to `False`, - then information about failures will be silently stored in the output - ShapeData object. [default: True] + `GalSimHSMError` exception if shear estimation fails. If set to + `False`, then information about failures will be silently stored in the + output ShapeData object. [default: True] @param hsmparams The hsmparams keyword can be used to change the settings used by EstimateShear() when estimating shears; see HSMParams documentation using help(galsim.hsm.HSMParams) for more information. [default: None] @@ -622,7 +631,7 @@ def EstimateShear(gal_image, PSF_image, weight=None, badpix=None, sky_var=0.0, return result except RuntimeError as err: if (strict == True): - raise + raise GalSimHSMError(str(err)) else: return ShapeData(error_message = str(err)) @@ -674,7 +683,7 @@ def FindAdaptiveMom(object_image, weight=None, badpix=None, guess_sig=5.0, preci >>> my_moments = my_gaussian_image.FindAdaptiveMom() - then the result will be a RuntimeError due to moment measurement failing because the object is + then the result will be a GalSimHSMError due to moment measurement failing because the object is so large. While the list of all possible settings that can be changed is accessible in the docstring of the HSMParams class, in this case we need to modify `max_amoment` which is the maximum value of the moments in units of pixel^2. The following measurement, using the @@ -703,9 +712,9 @@ def FindAdaptiveMom(object_image, weight=None, badpix=None, guess_sig=5.0, preci is (image.xmin, image.ymin). [default: object_image.true_center] @param strict Whether to require success. If `strict=True`, then there will be a - `RuntimeError` exception if shear estimation fails. If set to `False`, - then information about failures will be silently stored in the output - ShapeData object. [default: True] + `GalSimHSMError` exception if shear estimation fails. If set to + `False`, then information about failures will be silently stored in the + output ShapeData object. [default: True] @param round_moments Use a circular weight function instead of elliptical. [default: False] @param hsmparams The hsmparams keyword can be used to change the settings used by @@ -731,7 +740,7 @@ def FindAdaptiveMom(object_image, weight=None, badpix=None, guess_sig=5.0, preci return result except RuntimeError as err: if (strict == True): - raise + raise GalSimHSMError(str(err)) else: return ShapeData(error_message = str(err)) diff --git a/galsim/image.py b/galsim/image.py index 67a8a3e93f3..d66f6e44d2e 100644 --- a/galsim/image.py +++ b/galsim/image.py @@ -21,11 +21,14 @@ from __future__ import division import numpy as np + from . import _galsim from .position import PositionI, PositionD from .bounds import BoundsI, BoundsD from .wcs import BaseWCS, PixelScale, JacobianWCS from . import utilities +from .errors import GalSimError, GalSimBoundsError, GalSimValueError, GalSimImmutableError +from .errors import GalSimUndefinedBoundsError, GalSimIncompatibleValuesError, convert_cpp_errors # Sometimes (on 32-bit systems) there are two numpy.int32 types. This can lead to some confusion # when doing arithmetic with images. So just make sure both of them point to ImageViewI in the @@ -35,8 +38,7 @@ # the following (closed, marked "wontfix") ticket on the numpy issue tracker: # http://projects.scipy.org/numpy/ticket/1246 -alt_int32 = ( np.array([0]).astype(np.int16) + - np.array([0]).astype(np.int32) ).dtype.type +alt_int32 = (np.array([0], dtype=np.int16) + np.array([0], dtype=np.int32)).dtype.type class Image(object): @@ -289,11 +291,8 @@ def __init__(self, *args, **kwargs): # Figure out what dtype we want: dtype = Image._alias_dtypes.get(dtype,dtype) if dtype is not None and dtype not in Image.valid_dtypes: - raise ValueError("dtype must be one of "+str(Image.valid_dtypes)+ - ". Instead got "+str(dtype)) + raise GalSimValueError("Invlid dtype.", dtype, Image.valid_dtypes) if array is not None: - if not isinstance(array, np.ndarray): - raise TypeError("array must be a numpy.ndarray instance") if copy is None: copy = False if dtype is None: dtype = array.dtype.type @@ -301,10 +300,8 @@ def __init__(self, *args, **kwargs): dtype = Image._alias_dtypes[dtype] array = array.astype(dtype, copy=copy) elif dtype not in Image._cpp_valid_dtypes: - raise ValueError( - "array's dtype.type must be one of "+str(Image._cpp_valid_dtypes)+ - ". Instead got "+str(array.dtype.type)+". Or can set "+ - "dtype explicitly.") + raise GalSimValueError("Invalid dtype of provided array.", array.dtype, + Image._cpp_valid_dtypes) elif copy: array = np.array(array) else: @@ -324,7 +321,8 @@ def __init__(self, *args, **kwargs): # Construct the image attribute if (ncol is not None or nrow is not None): if ncol is None or nrow is None: - raise TypeError("Both nrow and ncol must be provided") + raise GalSimIncompatibleValuesError( + "Both nrow and ncol must be provided", ncol=ncol, nrow=nrow) if ncol != int(ncol) or nrow != int(nrow): raise TypeError("nrow, ncol must be integers") ncol = int(ncol) @@ -347,12 +345,14 @@ def __init__(self, *args, **kwargs): if make_const or not array.flags.writeable: self._array.flags.writeable = False if init_value is not None: - raise TypeError("Cannot specify init_value with array") + raise GalSimIncompatibleValuesError( + "Cannot specify init_value with array", init_value=init_value, array=array) elif image is not None: if not isinstance(image, Image): raise TypeError("image must be an Image") if init_value is not None: - raise TypeError("Cannot specify init_value with image") + raise GalSimIncompatibleValuesError( + "Cannot specify init_value with image", init_value=init_value, image=image) if wcs is None and scale is None: wcs = image.wcs self._bounds = image.bounds @@ -372,22 +372,21 @@ def __init__(self, *args, **kwargs): self._array = np.zeros(shape=(1,1), dtype=self._dtype) self._bounds = BoundsI() if init_value is not None: - raise TypeError("Cannot specify init_value without setting an initial size") + raise GalSimIncompatibleValuesError( + "Cannot specify init_value without setting an initial size", + init_value=init_value, ncol=ncol, nrow=nrow, bounds=bounds) # Construct the wcs attribute if scale is not None: if wcs is not None: - raise TypeError("Cannot provide both scale and wcs to Image constructor") + raise GalSimIncompatibleValuesError( + "Cannot provide both scale and wcs to Image constructor", wcs=wcs, scale=scale) self.wcs = PixelScale(float(scale)) else: if wcs is not None and not isinstance(wcs,BaseWCS): raise TypeError("wcs parameters must be a galsim.BaseWCS instance") self.wcs = wcs - # dtype is read-only - @property - def dtype(self): return self._dtype - @staticmethod def _get_xmin_ymin(array, kwargs): """A helper function for parsing xmin, ymin, bounds options with a given array @@ -401,9 +400,11 @@ def _get_xmin_ymin(array, kwargs): if not isinstance(b, BoundsI): raise TypeError("bounds must be a galsim.BoundsI instance") if b.xmax-b.xmin+1 != array.shape[1]: - raise ValueError("Shape of array is inconsistent with provided bounds") + raise GalSimIncompatibleValuesError( + "Shape of array is inconsistent with provided bounds", array=array, bounds=b) if b.ymax-b.ymin+1 != array.shape[0]: - raise ValueError("Shape of array is inconsistent with provided bounds") + raise GalSimIncompatibleValuesError( + "Shape of array is inconsistent with provided bounds", array=array, bounds=b) if b.isDefined(): xmin = b.xmin ymin = b.ymin @@ -464,7 +465,7 @@ def isconst(self): return self._array.flags.writeable == False @property def iscomplex(self): return self._array.dtype.kind == 'c' @property - def isinteger(self): return self._array.dtype.kind in ['i','u'] + def isinteger(self): return self._array.dtype.kind in ('i','u') @property def iscontiguous(self): @@ -490,14 +491,14 @@ def scale(self): if self.wcs.isPixelScale(): return self.wcs.scale else: - raise TypeError("image.wcs is not a simple PixelScale; scale is undefined.") + raise GalSimError("image.wcs is not a simple PixelScale; scale is undefined.") else: return None @scale.setter def scale(self, value): if self.wcs is not None and not self.wcs.isPixelScale(): - raise TypeError("image.wcs is not a simple PixelScale; scale is undefined.") + raise GalSimError("image.wcs is not a simple PixelScale; scale is undefined.") else: self.wcs = PixelScale(value) @@ -583,7 +584,7 @@ def resize(self, bounds, wcs=None): which means keep the existing wcs] """ if self.isconst: - raise ValueError("Cannot modify an immutable Image") + raise GalSimImmutableError("Cannot modify an immutable Image", self) if not isinstance(bounds, BoundsI): raise TypeError("bounds must be a galsim.BoundsI instance") self._array = self._make_empty(shape=bounds.numpyShape(), dtype=self.dtype) @@ -614,7 +615,7 @@ def setSubImage(self, bounds, rhs): This is equivalent to self[bounds] = rhs """ if self.isconst: - raise ValueError("Cannot modify the values of an immutable Image") + raise GalSimImmutableError("Cannot modify the values of an immutable Image", self) self.subImage(bounds).copyFrom(rhs) def __getitem__(self, *args): @@ -710,21 +711,32 @@ def wrap(self, bounds, hermitian=False): # possibly writing data past the edge of the image. ret = self.subImage(bounds) if not hermitian: - _galsim.wrapImage(self._image, bounds._b, False, False) + with convert_cpp_errors(): + _galsim.wrapImage(self._image, bounds._b, False, False) elif hermitian == 'x': if self.bounds.xmin != 0: - raise ValueError("hermitian == 'x' requires self.bounds.xmin == 0") + raise GalSimIncompatibleValuesError( + "hermitian == 'x' requires self.bounds.xmin == 0", + hermitian=hermitian, bounds=self.bounds) if bounds.xmin != 0: - raise ValueError("hermitian == 'x' requires bounds.xmin == 0") - _galsim.wrapImage(self._image, bounds._b, True, False) + raise GalSimIncompatibleValuesError( + "hermitian == 'x' requires bounds.xmin == 0", + hermitian=hermitian, bounds=bounds) + with convert_cpp_errors(): + _galsim.wrapImage(self._image, bounds._b, True, False) elif hermitian == 'y': if self.bounds.ymin != 0: - raise ValueError("hermitian == 'y' requires self.bounds.ymin == 0") + raise GalSimIncompatibleValuesError( + "hermitian == 'y' requires self.bounds.ymin == 0", + hermitian=hermitian, bounds=self.bounds) if bounds.ymin != 0: - raise ValueError("hermitian == 'y' requires bounds.ymin == 0") - _galsim.wrapImage(self._image, bounds._b, False, True) + raise GalSimIncompatibleValuesError( + "hermitian == 'y' requires bounds.ymin == 0", + hermitian=hermitian, bounds=bounds) + with convert_cpp_errors(): + _galsim.wrapImage(self._image, bounds._b, False, True) else: - raise ValueError("Invalid value for hermitian: %s"%hermitian) + raise GalSimValueError("Invalid value for hermitian", hermitian, (False, 'x', 'y')) return ret; def _wrap(self, bounds, hermx, hermy): @@ -732,7 +744,8 @@ def _wrap(self, bounds, hermx, hermy): without some of the sanity checks that the regular function does. """ ret = self.subImage(bounds) - _galsim.wrapImage(self._image, bounds._b, hermx, hermy) + with convert_cpp_errors(): + _galsim.wrapImage(self._image, bounds._b, hermx, hermy) return ret def bin(self, nx, ny): @@ -862,11 +875,12 @@ def calculate_fft(self): @returns an Image instance with the k-space image. """ if self.wcs is None: - raise ValueError("calculate_fft requires that the scale be set.") + raise GalSimError("calculate_fft requires that the scale be set.") if not self.wcs.isPixelScale(): - raise ValueError("calculate_fft requires that the image has a PixelScale wcs.") + raise GalSimError("calculate_fft requires that the image has a PixelScale wcs.") if not self.bounds.isDefined(): - raise ValueError("calculate_fft requires that the image have defined bounds.") + raise GalSimUndefinedBoundsError( + "calculate_fft requires that the image have defined bounds.") No2 = max(-self.bounds.xmin, self.bounds.xmax+1, -self.bounds.ymin, self.bounds.ymax+1) @@ -884,7 +898,8 @@ def calculate_fft(self): dk = np.pi / (No2 * dx) out = Image(BoundsI(0,No2,-No2,No2-1), dtype=np.complex128, scale=dk) - _galsim.rfft(ximage._image, out._image, True, True) + with convert_cpp_errors(): + _galsim.rfft(ximage._image, out._image, True, True) out *= dx*dx out.setOrigin(0,-No2) return out @@ -909,13 +924,15 @@ def calculate_inverse_fft(self): @returns an ImageD instance with the real-space image. """ if self.wcs is None: - raise ValueError("calculate_inverse_fft requires that the scale be set.") + raise GalSimError("calculate_inverse_fft requires that the scale be set.") if not self.wcs.isPixelScale(): - raise ValueError("calculate_inverse_fft requires that the image has a PixelScale wcs.") + raise GalSimError("calculate_inverse_fft requires that the image has a PixelScale wcs.") if not self.bounds.isDefined(): - raise ValueError("calculate_inverse_fft requires that the image have defined bounds.") + raise GalSimUndefinedBoundsError("calculate_inverse_fft requires that the image have " + "defined bounds.") if not self.bounds.includes(0,0): - raise ValueError("calculate_inverse_fft requires that the image includes point (0,0)") + raise GalSimBoundsError("calculate_inverse_fft requires that the image includes (0,0)", + PositionI(0,0), self.bounds) No2 = max(self.bounds.xmax, -self.bounds.ymin, self.bounds.ymax) @@ -937,7 +954,8 @@ def calculate_inverse_fft(self): # For the inverse, we need a bit of extra space for the fft. out_extra = Image(BoundsI(-No2,No2+1,-No2,No2-1), dtype=float, scale=dx) - _galsim.irfft(kimage._image, out_extra._image, True, True) + with convert_cpp_errors(): + _galsim.irfft(kimage._image, out_extra._image, True, True) # Now cut off the bit we don't need. out = out_extra.subImage(BoundsI(-No2,No2-1,-No2,No2-1)) out *= (dk * No2 / np.pi)**2 @@ -952,16 +970,19 @@ def good_fft_size(cls, input_size): going to be performing FFTs on an image, these will tend to be faster at performing the FFT. """ - return _galsim.goodFFTSize(int(input_size)) + with convert_cpp_errors(): + return _galsim.goodFFTSize(int(input_size)) def copyFrom(self, rhs): """Copy the contents of another image """ if self.isconst: - raise ValueError("Cannot modify the values of an immutable Image") + raise GalSimImmutableError("Cannot modify the values of an immutable Image", self) + if not isinstance(rhs, Image): + raise TypeError("Trying to copyFrom a non-image") if self.bounds.numpyShape() != rhs.bounds.numpyShape(): - raise ValueError("Trying to copy images that are not the same shape (%s -> %s)"%( - rhs.bounds, self.bounds)) + raise GalSimIncompatibleValuesError( + "Trying to copy images that are not the same shape", self_image=self, rhs=rhs) self._array[:,:] = rhs.array[:,:] def view(self, scale=None, wcs=None, origin=None, center=None, make_const=False): @@ -980,11 +1001,13 @@ def view(self, scale=None, wcs=None, origin=None, center=None, make_const=False) @param make_const Make the view's data array immutable. [default: False] """ if origin is not None and center is not None: - raise TypeError("Cannot provide both center and origin") + raise GalSimIncompatibleValuesError( + "Cannot provide both center and origin", center=center, origin=origin) if scale is not None: if wcs is not None: - raise TypeError("Cannot provide both scale and wcs") + raise GalSimIncompatibleValuesError( + "Cannot provide both scale and wcs", scale=scale, wcs=wcs) wcs = PixelScale(scale) elif wcs is not None: if not isinstance(wcs,BaseWCS): @@ -1202,9 +1225,10 @@ def getValue(self, x, y): im(pos) or im(x=x,y=y)) """ if not self.bounds.isDefined(): - raise RuntimeError("Attempt to access values of an undefined image") + raise GalSimUndefinedBoundsError("Attempt to access values of an undefined image") if not self.bounds.includes(x,y): - raise RuntimeError("Attempt to access position %s,%s, not in bounds %s"%(x,y,self.bounds)) + raise GalSimBoundsError("Attempt to access position not in bounds of image.", + PositionI(x,y), self.bounds) return self._getValue(x,y) def _getValue(self, x, y): @@ -1222,13 +1246,14 @@ def setValue(self, *args, **kwargs): This is equivalent to self[x,y] = rhs """ if self.isconst: - raise ValueError("Cannot modify the values of an immutable Image") + raise GalSimImmutableError("Cannot modify the values of an immutable Image", self) if not self.bounds.isDefined(): - raise RuntimeError("Attempt to set value of an undefined image") + raise GalSimUndefinedBoundsError("Attempt to set value of an undefined image") pos, value = utilities.parse_pos_args(args, kwargs, 'x', 'y', integer=True, others=['value']) if not self.bounds.includes(pos): - raise RuntimeError("Attempt to set position %s, not in bounds %s"%(pos,self.bounds)) + raise GalSimBoundsError("Attempt to set position not in bounds of image", + pos, self.bounds) self._setValue(pos.x,pos.y,value) def _setValue(self, x, y, value): @@ -1246,13 +1271,14 @@ def addValue(self, *args, **kwargs): This is equivalent to self[x,y] += rhs """ if self.isconst: - raise ValueError("Cannot modify the values of an immutable Image") + raise GalSimImmutableError("Cannot modify the values of an immutable Image", self) if not self.bounds.isDefined(): - raise RuntimeError("Attempt to set value of an undefined image") + raise GalSimUndefinedBoundsError("Attempt to set value of an undefined image") pos, value = utilities.parse_pos_args(args, kwargs, 'x', 'y', integer=True, others=['value']) if not self.bounds.includes(pos): - raise RuntimeError("Attempt to set position %s, not in bounds %s"%(pos,self.bounds)) + raise GalSimBoundsError("Attempt to set position not in bounds of image", + pos,self.bounds) self._addValue(pos.x,pos.y,value) def _addValue(self, x, y, value): @@ -1265,9 +1291,9 @@ def fill(self, value): """Set all pixel values to the given `value` """ if self.isconst: - raise ValueError("Cannot modify the values of an immutable Image") + raise GalSimImmutableError("Cannot modify the values of an immutable Image", self) if not self.bounds.isDefined(): - raise RuntimeError("Attempt to set values of an undefined image") + raise GalSimUndefinedBoundsError("Attempt to set values of an undefined image") self._fill(value) def _fill(self, value): @@ -1280,7 +1306,7 @@ def setZero(self): """Set all pixel values to zero. """ if self.isconst: - raise ValueError("Cannot modify the values of an immutable Image") + raise GalSimImmutableError("Cannot modify the values of an immutable Image", self) self._fill(0) # This might be made faster with a C++ call to use memset def invertSelf(self): @@ -1290,9 +1316,9 @@ def invertSelf(self): on the output, rather than turning into inf. """ if self.isconst: - raise ValueError("Cannot modify the values of an immutable Image") + raise GalSimImmutableError("Cannot modify the values of an immutable Image", self) if not self.bounds.isDefined(): - raise RuntimeError("Attempt to set values of an undefined image") + raise GalSimUndefinedBoundsError("Attempt to set values of an undefined image") self._invertSelf() def _invertSelf(self): @@ -1300,7 +1326,8 @@ def _invertSelf(self): are defined. """ # C++ version skips 0's to 1/0 -> 0 instead of inf. - _galsim.invertImage(self._image) + with convert_cpp_errors(): + _galsim.invertImage(self._image) def replaceNegative(self, replace_value=0): """Replace any negative values currently in the image with 0 (or some other value). @@ -1388,8 +1415,8 @@ def calculateMomentRadius(self, center=None, flux=None, rtype='det'): @returns an estimate of the radius in physical units defined by the pixel scale (or both estimates if rtype == 'both'). """ - if rtype not in ['trace', 'det', 'both']: - raise ValueError("rtype must be one of 'trace', 'det', or 'both'") + if rtype not in ('trace', 'det', 'both'): + raise GalSimValueError("Invalid rtype.", rtype, ('trace', 'det', 'both')) if center is None: center = self.true_center @@ -1402,7 +1429,7 @@ def calculateMomentRadius(self, center=None, flux=None, rtype='det'): x = x - center.x + self.bounds.xmin y = y - center.y + self.bounds.ymin - if rtype in ['trace', 'both']: + if rtype in ('trace', 'both'): # Calculate trace measure: rsq = x*x + y*y Irr = np.sum(rsq * self.array, dtype=float) / flux @@ -1410,7 +1437,7 @@ def calculateMomentRadius(self, center=None, flux=None, rtype='det'): # This has all been done in pixels. So normalize according to the pixel scale. sigma_trace = (Irr/2.)**0.5 * self.scale - if rtype in ['det', 'both']: + if rtype in ('det', 'both'): # Calculate det measure: Ixx = np.sum(x*x * self.array, dtype=float) / flux Iyy = np.sum(y*y * self.array, dtype=float) / flux @@ -1573,12 +1600,12 @@ def ImageCD(*args, **kwargs): # Define a utility function to be used by the arithmetic functions below def check_image_consistency(im1, im2, integer=False): if integer and not im1.isinteger: - raise ValueError("Image must have integer values, not %s"%im1.dtype) + raise GalSimValueError("Image must have integer values.",im1) if isinstance(im2, Image): if im1.array.shape != im2.array.shape: - raise ValueError("Image shapes are inconsistent") + raise GalSimIncompatibleValuesError( "Image shapes are inconsistent", im1=im1, im2=im2) if integer and not im2.isinteger: - raise ValueError("Image must have integer values, not %s"%im2.dtype) + raise GalSimValueError("Image must have integer values.",im2) def Image_add(self, other): check_image_consistency(self, other) @@ -1746,7 +1773,7 @@ def Image_neg(self): # Define &, ^ and | only for integer-type images def Image_and(self, other): - check_image_consistency(self, other) + check_image_consistency(self, other, integer=True) try: a = other.array except AttributeError: diff --git a/galsim/inclined.py b/galsim/inclined.py index 22b7b186d53..fdf217c1c6a 100644 --- a/galsim/inclined.py +++ b/galsim/inclined.py @@ -25,6 +25,7 @@ from .exponential import Exponential from .angle import Angle from .position import PositionD +from .errors import GalSimRangeError, GalSimIncompatibleValuesError, convert_cpp_errors class InclinedExponential(GSObject): @@ -100,37 +101,37 @@ def __init__(self, inclination, half_light_radius=None, scale_radius=None, scale # Check that the scale/half-light radius is valid if scale_radius is not None: if not scale_radius > 0.: - raise ValueError("scale_radius must be > zero.") + raise GalSimRangeError("scale_radius must be > 0.", scale_radius, 0.) if half_light_radius is not None: - raise TypeError( - "Only one of scale_radius and half_light_radius may be " + - "specified for InclinedExponential") + raise GalSimIncompatibleValuesError( + "Only one of scale_radius and half_light_radius may be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius) self._r0 = float(scale_radius) elif half_light_radius is not None: if not half_light_radius > 0.: - raise ValueError("half_light_radius must be > zero.") + raise GalSimRangeError("half_light_radius must be > 0.", half_light_radius, 0.) # Use the factor from the Exponential class self._r0 = float(half_light_radius) / Exponential._hlr_factor else: - raise TypeError( - "Either scale_radius or half_light_radius must be " + - "specified for InclinedExponential") + raise GalSimIncompatibleValuesError( + "Either scale_radius or half_light_radius must be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius) # Check that the height specification is valid if scale_height is not None: if not scale_height > 0.: - raise ValueError("scale_height must be > zero.") + raise GalSimRangeError("scale_height must be > 0.", scale_height, 0.) if scale_h_over_r is not None: - raise TypeError( - "Only one of scale_height and scale_h_over_r may be " + - "specified for InclinedExponential") + raise GalSimIncompatibleValuesError( + "Only one of scale_height and scale_h_over_r may be specified.", + scale_height=scale_height, scale_h_over_r=scale_h_over_r) self._h0 = float(scale_height) else: if scale_h_over_r is None: # Use the default scale_h_over_r scale_h_over_r = 0.1 elif not scale_h_over_r > 0.: - raise ValueError("half_light_radius must be > zero.") + raise GalSimRangeError("half_light_radius must be > 0.", scale_h_over_r, 0.) self._h0 = float(self._r0) * float(scale_h_over_r) # Explicitly check for angle type, so we can give more informative error if eg. a float is @@ -144,8 +145,9 @@ def __init__(self, inclination, half_light_radius=None, scale_radius=None, scale @lazy_property def _sbp(self): - return _galsim.SBInclinedExponential(self._inclination.rad, self._r0, - self._h0, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBInclinedExponential(self._inclination.rad, self._r0, + self._h0, self._flux, self.gsparams._gsp) @property def inclination(self): return self._inclination @@ -172,9 +174,9 @@ def __hash__(self): self.scale_height, self.flux, self.gsparams)) def __repr__(self): - return ('galsim.InclinedExponential(inclination=%r, scale_radius=%r, scale_height=%r, ' + - 'flux=%r, gsparams=%r)') % ( - self.inclination, self.scale_radius, self.scale_height, self.flux, self.gsparams) + return ('galsim.InclinedExponential(inclination=%r, scale_radius=%r, scale_height=%r, ' + 'flux=%r, gsparams=%r)')%( + self.inclination, self.scale_radius, self.scale_height, self.flux, self.gsparams) def __str__(self): s = 'galsim.InclinedExponential(inclination=%s, scale_radius=%s, scale_height=%s' % ( @@ -307,47 +309,50 @@ def __init__(self, n, inclination, half_light_radius=None, scale_radius=None, sc # Check that trunc is valid if trunc < 0.: - raise ValueError("trunc must be >= zero (zero implying no truncation).") + raise GalSimRangeError("trunc must be >= 0. (zero implying no truncation).", trunc, 0.) # Parse the radius options if scale_radius is not None: if not scale_radius > 0.: - raise ValueError("scale_radius must be > zero.") + raise GalSimRangeError("scale_radius must be > 0.", scale_radius, 0.) if half_light_radius is not None: - raise TypeError( - "Only one of scale_radius and half_light_radius may be " + - "specified for InclinedSersic") + raise GalSimIncompatibleValuesError( + "Only one of scale_radius and half_light_radius may be specified.", + half_light_radius=half_light_radius, scale_radius=scale_radius) self._r0 = float(scale_radius) self._hlr = 0. elif half_light_radius is not None: if not half_light_radius > 0.: - raise ValueError("half_light_radius must be > zero.") + raise GalSimRangeError("half_light_radius must be > 0.", half_light_radius, 0.) self._hlr = float(half_light_radius) if self._trunc == 0. or flux_untruncated: - self._r0 = self._hlr / _galsim.SersicHLR(self._n, 1.) + with convert_cpp_errors(): + self._r0 = self._hlr / _galsim.SersicHLR(self._n, 1.) else: if self._trunc <= math.sqrt(2.) * self._hlr: - raise ValueError("Sersic trunc must be > sqrt(2) * half_light_radius") - self._r0 = _galsim.SersicTruncatedScale(self._n, self._hlr, self._trunc) + raise GalSimRangeError("Sersic trunc must be > sqrt(2) * half_light_radius", + self._trunc, math.sqrt(2.) * self._hlr) + with convert_cpp_errors(): + self._r0 = _galsim.SersicTruncatedScale(self._n, self._hlr, self._trunc) else: - raise TypeError( - "Either scale_radius or half_light_radius must be " + - "specified for InclinedSersic") + raise GalSimIncompatibleValuesError( + "Either scale_radius or half_light_radius must be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius) # Parse the height options if scale_height is not None: if not scale_height > 0.: - raise ValueError("scale_height must be > zero.") + raise GalSimRangeError("scale_height must be > 0.", scale_height, 0.) if scale_h_over_r is not None: - raise TypeError( - "Only one of scale_height and scale_h_over_r may be " + - "specified for InclinedExponential") + raise GalSimIncompatibleValuesError( + "Only one of scale_height and scale_h_over_r may be specified", + scale_height=scale_height, scale_h_over_r=scale_h_over_r) self._h0 = float(scale_height) else: if scale_h_over_r is None: scale_h_over_r = 0.1 elif not scale_h_over_r > 0.: - raise ValueError("half_light_radius must be > zero.") + raise GalSimRangeError("half_light_radius must be > 0.", scale_h_over_r, 0.) self._h0 = float(scale_h_over_r) * self._r0 # Explicitly check for angle type, so we can give more informative error if eg. a float is @@ -358,7 +363,8 @@ def __init__(self, n, inclination, half_light_radius=None, scale_radius=None, sc # If flux_untrunctated, then the above picked the right radius, but the flux needs # to be updated. if self._trunc > 0.: - self._flux_fraction = _galsim.SersicIntegratedFlux(self._n, self._trunc/self._r0) + with convert_cpp_errors(): + self._flux_fraction = _galsim.SersicIntegratedFlux(self._n, self._trunc/self._r0) if flux_untruncated: self._flux *= self._flux_fraction self._hlr = 0. # This will be updated by getHalfLightRadius if necessary. @@ -367,8 +373,9 @@ def __init__(self, n, inclination, half_light_radius=None, scale_radius=None, sc @lazy_property def _sbp(self): - return _galsim.SBInclinedSersic(self._n, self._inclination.rad, self._r0, self._h0, - self._flux, self._trunc, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBInclinedSersic(self._n, self._inclination.rad, self._r0, self._h0, + self._flux, self._trunc, self.gsparams._gsp) @property def n(self): return self._n @@ -387,7 +394,8 @@ def scale_h_over_r(self): return self._h0 / self._r0 @property def disk_half_light_radius(self): if self._hlr == 0.: - self._hlr = self._r0 * _galsim.SersicHLR(self._n, self._flux_fraction) + with convert_cpp_errors(): + self._hlr = self._r0 * _galsim.SersicHLR(self._n, self._flux_fraction) return self._hlr def __eq__(self, other): @@ -404,10 +412,10 @@ def __hash__(self): return hash(("galsim.InclinedSersic", self.n, self.inclination, self.scale_radius, self.scale_height, self.flux, self.trunc, self.gsparams)) def __repr__(self): - return ('galsim.InclinedSersic(n=%r, inclination=%r, scale_radius=%r, scale_height=%r, ' + - 'flux=%r, trunc=%r, gsparams=%r)') % (self.n, - self.inclination, self.scale_radius, self.scale_height, self.flux, self.trunc, - self.gsparams) + return ('galsim.InclinedSersic(n=%r, inclination=%r, scale_radius=%r, scale_height=%r, ' + 'flux=%r, trunc=%r, gsparams=%r)')%( + self.n, self.inclination, self.scale_radius, self.scale_height, self.flux, + self.trunc, self.gsparams) def __str__(self): s = 'galsim.InclinedSersic(n=%s, inclination=%s, scale_radius=%s, scale_height=%s' % ( diff --git a/galsim/integ.py b/galsim/integ.py index 9ff9a95bed0..1dc4b6842ff 100644 --- a/galsim/integ.py +++ b/galsim/integ.py @@ -20,10 +20,12 @@ and python image integrators for use in galsim.chromatic """ -from . import _galsim import numpy as np from functools import reduce +from . import _galsim +from .errors import GalSimError, GalSimRangeError, GalSimValueError, convert_cpp_errors + def int1d(func, min, max, rel_err=1.e-6, abs_err=1.e-12): """Integrate a 1-dimensional function from min to max. @@ -51,11 +53,12 @@ def int1d(func, min, max, rel_err=1.e-6, abs_err=1.e-12): max = float(max) rel_err = float(rel_err) abs_err = float(abs_err) - success, result = _galsim.PyInt1d(func,min,max,rel_err,abs_err) + with convert_cpp_errors(): + success, result = _galsim.PyInt1d(func,min,max,rel_err,abs_err) if success: return result else: - raise RuntimeError(result) + raise GalSimError(result) def midpt(fvals, x): """Midpoint rule for integration. @@ -93,9 +96,9 @@ def trapz(func, min, max, points=10000): """ if not np.isscalar(points): if (np.max(points) > max) or (np.min(points) < min): - raise ValueError("Points outside of range: %s -- %s"%(min,max)) + raise GalSimRangeError("Points outside of specified range", points, min, max) elif int(points) != points: - raise TypeError("'npoints' must be integer type or array") + raise TypeError("npoints must be integer type or array") else: points = np.linspace(min, max, points) @@ -111,7 +114,7 @@ def midptRule(f, xs): @returns Midpoint rule approximation to the integral. """ if len(xs) < 2: - raise ValueError("Not enough points for midptRule integration") + raise GalSimValueError("Not enough points for midptRule integration", xs) x, xp = xs[:2] result = f(x)*(xp-x) for x, xp, xpp in zip(xs[0:-2], xs[1:-1], xs[2:]): @@ -129,7 +132,7 @@ def trapzRule(f, xs): @returns Trapezoidal rule approximation to the integral. """ if len(xs) < 2: - raise ValueError("Not enough points for trapzRule integration") + raise GalSimValueError("Not enough points for trapzRule integration", xs) x, xp = xs[:2] result = 0.5*f(x)*(xp-x) for x, xp, xpp in zip(xs[0:-2], xs[1:-1], xs[2:]): @@ -144,7 +147,7 @@ def __init__(self): # subclasses must define # 1) a method `.calculateWaves(bandpass)` which will return the wavelengths at which to # evaluate the integrand - # 2) an function attribute `.rule` which takes a integrand function as its first + # 2) an function attribute `.rule` which takes an integrand function as its first # argument, and a list of evaluation wavelengths as its second argument, and returns # an approximation to the integral. (E.g., the function midptRule above) @@ -190,9 +193,6 @@ def __init__(self, rule): self.rule = rule def calculateWaves(self, bandpass): - if len(bandpass.wave_list) < 0: - raise AttributeError("Bandpass does not have attribute `wave_list` needed by " + - "SampleIntegrator.") return bandpass.wave_list diff --git a/galsim/interpolant.py b/galsim/interpolant.py index 5cf4167c7e8..46f67d1e507 100644 --- a/galsim/interpolant.py +++ b/galsim/interpolant.py @@ -22,9 +22,11 @@ import math from past.builtins import basestring + from . import _galsim from .gsparams import GSParams from .utilities import lazy_property +from .errors import GalSimValueError, convert_cpp_errors class Interpolant(object): """A base class that defines how interpolation should be done. @@ -34,7 +36,7 @@ class Interpolant(object): """ def __init__(self): raise NotImplementedError( - "The Interpolant bas class should not be instantiated directly. "+ + "The Interpolant base class should not be instantiated directly. " "Use one of the subclasses instead, or use the `from_name` factory function.") @staticmethod @@ -70,14 +72,14 @@ def from_name(name, tol=1.e-4, gsparams=None): return Quintic(tol, gsparams) if name.lower().startswith('lanczos'): conserve_dc = True - if name[-1].upper() in ['T', 'F']: + if name[-1].upper() in ('T', 'F'): conserve_dc = (name[-1].upper() == 'T') name = name[:-1] try: n = int(name[7:]) except: - raise ValueError("Invalid Lanczos specification %s. "%name + - "Should look like lanczosN, where N is an integer") + raise GalSimValueError("Invalid Lanczos specification. Should look like " + "lanczosN, where N is an integer", name) return Lanczos(n, conserve_dc, tol, gsparams) elif name.lower() == 'linear': return Linear(tol, gsparams) @@ -90,7 +92,9 @@ def from_name(name, tol=1.e-4, gsparams=None): elif name.lower() == 'sinc': return SincInterpolant(tol, gsparams) else: - raise ValueError("Invalid Interpolant name %s."%name) + raise GalSimValueError("Invalid Interpolant name %s.",name, + ('linear', 'cubic', 'quintic', 'lanczosN', 'nearest', 'delta', + 'sinc')) def __getstate__(self): d = self.__dict__.copy() @@ -130,7 +134,8 @@ def __init__(self, tol=1.e-4, gsparams=None): @lazy_property def _i(self): - return _galsim.Delta(self._tol, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.Delta(self._tol, self._gsparams._gsp) def __repr__(self): return "galsim.Delta(%r, %r)"%(self._tol, self._gsparams) @@ -166,7 +171,8 @@ def __init__(self, tol=1.e-4, gsparams=None): @lazy_property def _i(self): - return _galsim.Nearest(self._tol, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.Nearest(self._tol, self._gsparams._gsp) def __repr__(self): return "galsim.Nearest(%r, %r)"%(self._tol, self._gsparams) @@ -203,7 +209,8 @@ def __init__(self, tol=1.e-4, gsparams=None): @lazy_property def _i(self): - return _galsim.SincInterpolant(self._tol, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SincInterpolant(self._tol, self._gsparams._gsp) def __repr__(self): return "galsim.SincInterpolant(%r, %r)"%(self._tol, self._gsparams) @@ -239,7 +246,8 @@ def __init__(self, tol=1.e-4, gsparams=None): @lazy_property def _i(self): - return _galsim.Linear(self._tol, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.Linear(self._tol, self._gsparams._gsp) def __repr__(self): return "galsim.Linear(%r, %r)"%(self._tol, self._gsparams) @@ -273,7 +281,8 @@ def __init__(self, tol=1.e-4, gsparams=None): @lazy_property def _i(self): - return _galsim.Cubic(self._tol, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.Cubic(self._tol, self._gsparams._gsp) def __repr__(self): return "galsim.Cubic(%r, %r)"%(self._tol, self._gsparams) @@ -307,7 +316,8 @@ def __init__(self, tol=1.e-4, gsparams=None): @lazy_property def _i(self): - return _galsim.Quintic(self._tol, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.Quintic(self._tol, self._gsparams._gsp) def __repr__(self): return "galsim.Quintic(%r, %r)"%(self._tol, self._gsparams) @@ -351,7 +361,8 @@ def __init__(self, n, conserve_dc=True, tol=1.e-4, gsparams=None): @lazy_property def _i(self): - return _galsim.Lanczos(self._n, self._conserve_dc, self._tol, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.Lanczos(self._n, self._conserve_dc, self._tol, self._gsparams._gsp) def __repr__(self): return "galsim.Lanczos(%r, %r, %r, %r)"%(self._n, self._conserve_dc, self._tol, diff --git a/galsim/interpolatedimage.py b/galsim/interpolatedimage.py index b3c030c316d..92036a28538 100644 --- a/galsim/interpolatedimage.py +++ b/galsim/interpolatedimage.py @@ -29,11 +29,13 @@ from .image import Image from .bounds import _BoundsI from .position import PositionD -from .interpolant import Quintic, Interpolant +from .interpolant import Quintic, Interpolant, SincInterpolant from .utilities import convert_interpolant, lazy_property, doc_inherit from .random import BaseDeviate from . import _galsim from . import fits +from .errors import GalSimError, GalSimRangeError, GalSimValueError, GalSimUndefinedBoundsError +from .errors import GalSimIncompatibleValuesError, convert_cpp_errors, galsim_warn class InterpolatedImage(GSObject): """A class describing non-parametric profiles specified using an Image, which can be @@ -271,28 +273,29 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization= use_cache=True, use_true_center=True, offset=None, gsparams=None, _force_stepk=0., _force_maxk=0., hdu=None): - from .wcs import BaseWCS, PixelScale - from .wcs import BaseWCS, PixelScale from .random import BaseDeviate # If the "image" is not actually an image, try to read the image as a file. - if not isinstance(image, Image): + if isinstance(image, str): image = fits.read(image, hdu=hdu) + elif not isinstance(image, Image): + raise TypeError("Supplied image must be an Image or file name") # make sure image is really an image and has a float type if image.dtype != np.float32 and image.dtype != np.float64: - raise ValueError("Supplied image does not have dtype of float32 or float64!") + raise GalSimValueError("Supplied image must have dtype = float32 or float64.", + image.dtype) # it must have well-defined bounds, otherwise seg fault in SBInterpolatedImage constructor if not image.bounds.isDefined(): - raise ValueError("Supplied image does not have bounds defined!") + raise GalSimUndefinedBoundsError("Supplied image does not have bounds defined.") # check what normalization was specified for the image: is it an image of surface # brightness, or flux? if not normalization.lower() in ("flux", "f", "surface brightness", "sb"): - raise ValueError(("Invalid normalization requested: '%s'. Expecting one of 'flux', "+ - "'f', 'surface brightness', or 'sb'.") % normalization) + raise GalSimValueError("Invalid normalization requested.", normalization, + ('flux', 'f', 'surface brightness', 'sb')) # set up the interpolants if none was provided by user, or check that the user-provided ones # are of a valid type @@ -314,14 +317,17 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization= # Set the wcs if necessary if scale is not None: if wcs is not None: - raise TypeError("Cannot provide both scale and wcs to InterpolatedImage") + raise GalSimIncompatibleValuesError( + "Cannot provide both scale and wcs to InterpolatedImage", scale=scale, wcs=wcs) self._image.wcs = PixelScale(scale) elif wcs is not None: if not isinstance(wcs, BaseWCS): raise TypeError("wcs parameter is not a galsim.BaseWCS instance") self._image.wcs = wcs elif self._image.wcs is None: - raise ValueError("No information given with Image or keywords about pixel scale!") + raise GalSimIncompatibleValuesError( + "No information given with Image or keywords about pixel scale!", + scale=scale, wcs=wcs, image=image) # Figure out the offset to apply based on the original image (not the padded one). # We will apply this below in _sbp. @@ -338,8 +344,8 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization= # I think the only things that will mess up if flux == 0 are the # calculateStepK and calculateMaxK functions, and rescaling the flux to some value. if (calculate_stepk or calculate_maxk or flux is not None) and self._image_flux == 0.: - raise RuntimeError("This input image has zero total flux. " - "It does not define a valid surface brightness profile.") + raise GalSimValueError("This input image has zero total flux. It does not define a " + "valid surface brightness profile.", image) # Process the different options for flux, stepk, maxk self._flux = self._getFlux(flux, normalization) @@ -351,12 +357,13 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization= def _sbp(self): min_scale = self._wcs._minScale() max_scale = self._wcs._maxScale() - self._sbii = _galsim.SBInterpolatedImage( - self._xim._image, self._image.bounds._b, self._pad_image.bounds._b, - self._x_interpolant._i, self._k_interpolant._i, - self._stepk*min_scale, - self._maxk*max_scale, - self.gsparams._gsp) + with convert_cpp_errors(): + self._sbii = _galsim.SBInterpolatedImage( + self._xim._image, self._image.bounds._b, self._pad_image.bounds._b, + self._x_interpolant._i, self._k_interpolant._i, + self._stepk*min_scale, + self._maxk*max_scale, + self.gsparams._gsp) self._sbp = self._sbii # Temporary. Will overwrite this with the return value. @@ -388,25 +395,37 @@ def image(self): def _buildRealImage(self, pad_factor, pad_image, noise_pad_size, noise_pad, rng, use_cache): # Check that given pad_image is valid: - if pad_image: + if pad_image is not None: if isinstance(pad_image, basestring): pad_image = fits.read(pad_image) - else: + elif isinstance(pad_image, Image): pad_image = pad_image._view() - if not isinstance(pad_image, Image): - raise ValueError("Supplied pad_image is not an Image!") + else: + raise TypeError("Supplied pad_image must be an Image.", pad_image) if pad_image.dtype != np.float32 and pad_image.dtype != np.float64: - raise ValueError("Supplied pad_image is not one of the allowed types!") + raise GalSimValueError("Invalid dtype for Supplied pad_image.", pad_image.dtype, + (np.float32, np.float64)) if pad_factor <= 0.: - raise ValueError("Invalid pad_factor <= 0 in InterpolatedImage") + raise GalSimRangeError("Invalid pad_factor <= 0 in InterpolatedImage", pad_factor, 0.) # Convert noise_pad_size from arcsec to pixels according to the local wcs. # Use the minimum scale, since we want to make sure noise_pad_size is # as large as we need in any direction. if noise_pad_size: + if noise_pad_size < 0: + raise GalSimValueError("noise_pad_size may not be negative", noise_pad_size) + if not noise_pad: + raise GalSimIncompatibleValuesError( + "Must provide noise_pad if noise_pad_size > 0", + noise_pad=noise_pad, noise_pad_size=noise_pad_size) noise_pad_size = int(math.ceil(noise_pad_size / self._wcs._minScale())) noise_pad_size = Image.good_fft_size(noise_pad_size) + else: + if noise_pad: + raise GalSimIncompatibleValuesError( + "Must provide noise_pad_size if noise_pad != 0", + noise_pad=noise_pad, noise_pad_size=noise_pad_size) # The size of the final padded image is the largest of the various size specifications pad_size = max(self._image.array.shape) @@ -425,7 +444,7 @@ def _buildRealImage(self, pad_factor, pad_image, noise_pad_size, noise_pad, rng, # If requested, fill (some of) this image with noise padding. nz_bounds = self._image.bounds - if noise_pad_size > 0 and noise_pad is not None: + if noise_pad: # This is a bit involved, so pass this off to another helper function. b = self._buildNoisePadImage(noise_pad_size, noise_pad, rng, use_cache) nz_bounds += b @@ -464,10 +483,6 @@ def _buildNoisePadImage(self, noise_pad_size, noise_pad, rng, use_cache): # Figure out what kind of noise to apply to the image try: noise_pad = float(noise_pad) - if noise_pad < 0.: - raise ValueError("Noise variance cannot be negative!") - noise = GaussianNoise(rng1, sigma = np.sqrt(noise_pad)) - except (TypeError, ValueError): if isinstance(noise_pad, _BaseCorrelatedNoise): noise = noise_pad.copy(rng=rng1) @@ -484,9 +499,14 @@ def _buildNoisePadImage(self, noise_pad_size, noise_pad, rng, use_cache): if use_cache: InterpolatedImage._cache_noise_pad[noise_pad] = noise else: - raise ValueError( - "Input noise_pad must be a float/int, a CorrelatedNoise, Image, or filename "+ - "containing an image to use to make a CorrelatedNoise!") + raise GalSimValueError( + "Input noise_pad must be a float/int, a CorrelatedNoise, Image, or filename " + "containing an image to use to make a CorrelatedNoise.", noise_pad) + + else: + if noise_pad < 0.: + raise GalSimRangeError("Noise variance may not be negative.", noise_pad, 0.) + noise = GaussianNoise(rng1, sigma = np.sqrt(noise_pad)) # Find the portion of xim to fill with noise. # It's allowed for the noise padding to not cover the whole pad image @@ -504,7 +524,7 @@ def _getFlux(self, flux, normalization): # need to rescale flux by the pixel area to get proper normalization. if flux is None: flux = self._image_flux - if normalization.lower() in ['surface brightness','sb']: + if normalization.lower() in ('surface brightness','sb'): flux *= self._wcs.pixelArea() return flux @@ -531,7 +551,8 @@ def _getStepK(self, calculate_stepk, _force_stepk): b = self._image.bounds & b im = self._image[b] thresh = (1.-self.gsparams.folding_threshold) * self._image_flux - R = _galsim.CalculateSizeContainingFlux(self._image._image, thresh) + with convert_cpp_errors(): + R = _galsim.CalculateSizeContainingFlux(self._image._image, thresh) else: R = np.max(self._image.array.shape) / 2. - 0.5 return self._getSimpleStepK(R) @@ -653,7 +674,8 @@ def _kValue(self, kpos): @doc_inherit def _shoot(self, photons, ud): - self._sbp.shoot(photons._pa, ud._rng) + with convert_cpp_errors(): + self._sbp.shoot(photons._pa, ud._rng) @doc_inherit def _drawReal(self, image): @@ -821,35 +843,47 @@ def __init__(self, kimage=None, k_interpolant=None, stepk=None, gsparams=None, real_kimage=None, imag_kimage=None, real_hdu=None, imag_hdu=None): if kimage is None: if real_kimage is None or imag_kimage is None: - raise ValueError("Must provide either kimage or real_kimage/imag_kimage") + raise GalSimIncompatibleValuesError( + "Must provide either kimage or real_kimage/imag_kimage", + kimage=kimage, real_kimage=real_kimage, imag_kimage=imag_kimage) # If the "image" is not actually an image, try to read the image as a file. - if not isinstance(real_kimage, Image): + if isinstance(real_kimage, str): real_kimage = fits.read(real_kimage, hdu=real_hdu) - if not isinstance(imag_kimage, Image): + elif not isinstance(real_kimage, Image): + raise TypeError("real_kimage must be either an Image or a file name") + if isinstance(imag_kimage, str): imag_kimage = fits.read(imag_kimage, hdu=imag_hdu) + elif not isinstance(imag_kimage, Image): + raise TypeError("imag_kimage must be either an Image or a file name") - # make sure real_kimage, imag_kimage are really `Image`s, are floats, and are - # congruent. - if not isinstance(real_kimage, Image): - raise ValueError("Supplied real_kimage is not an Image instance") - if not isinstance(imag_kimage, Image): - raise ValueError("Supplied imag_kimage is not an Image instance") + # make sure real_kimage, imag_kimage are congruent. if real_kimage.bounds != imag_kimage.bounds: - raise ValueError("Real and Imag kimages must have same bounds.") + raise GalSimIncompatibleValuesError( + "Real and Imag kimages must have same bounds.", + real_kimage=real_kimage, imag_kimage=imag_kimage) if real_kimage.wcs != imag_kimage.wcs: - raise ValueError("Real and Imag kimages must have same scale/wcs.") + raise GalSimIncompatibleValuesError( + "Real and Imag kimages must have same scale/wcs.", + real_kimage=real_kimage, imag_kimage=imag_kimage) kimage = real_kimage + 1j*imag_kimage else: if real_kimage is not None or imag_kimage is not None: - raise ValueError("Cannot provide both kimage and real_kimage/imag_kimage") + raise GalSimIncompatibleValuesError( + "Cannot provide both kimage and real_kimage/imag_kimage", + kimage=kimage, real_kimage=real_kimage, imag_kimage=imag_kimage) + if not isinstance(kimage, Image): + raise TypeError("kimage must be a galsim.Image isntance") if not kimage.iscomplex: - raise ValueError("Supplied kimage is not complex") + raise GalSimValueError("Supplied kimage is not complex", kimage) # Make sure wcs is a PixelScale. if kimage.wcs is not None and not kimage.wcs.isPixelScale(): - raise ValueError("kimage wcs must be PixelScale or None.") + raise GalSimValueError("kimage wcs must be PixelScale or None.", kimage.wcs) + + if not kimage.bounds.isDefined(): + raise GalSimUndefinedBoundsError("Supplied image does not have bounds defined.") self._kimage = kimage.copy() self._gsparams = GSParams.check(gsparams) @@ -866,7 +900,8 @@ def __init__(self, kimage=None, k_interpolant=None, stepk=None, gsparams=None, kimage[bd].real.array[::-1,::-1]) and np.allclose(kimage[bd].imag.array, -kimage[bd].imag.array[::-1,::-1])): - raise ValueError("Real and Imag kimages must form a Hermitian complex matrix.") + raise GalSimIncompatibleValuesError( + "Real and Imag kimages must form a Hermitian complex matrix.", kimage=kimage) if stepk is None: if self._kimage.scale is None: @@ -874,8 +909,7 @@ def __init__(self, kimage=None, k_interpolant=None, stepk=None, gsparams=None, self._kimage.scale = 1. self._stepk = self._kimage.scale elif stepk < kimage.scale: - import warnings - warnings.warn( + galsim_warn( "Provided stepk is smaller than kimage.scale; overriding with kimage.scale.") self._stepk = kimage.scale else: @@ -899,14 +933,16 @@ def k_interpolant(self): @lazy_property def _sbp(self): stepk_image = self.stepk / self.kimage.scale # usually 1, but could be larger - self._sbiki = _galsim.SBInterpolatedKImage( - self.kimage._image, stepk_image, self.k_interpolant._i, self.gsparams._gsp) + with convert_cpp_errors(): + self._sbiki = _galsim.SBInterpolatedKImage( + self.kimage._image, stepk_image, self.k_interpolant._i, self.gsparams._gsp) scale = self.kimage.scale if scale != 1.: - return _galsim.SBTransform(self._sbiki, 1./scale, 0., 0., 1./scale, - _galsim.PositionD(0.,0.), scale**2, - self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBTransform(self._sbiki, 1./scale, 0., 0., 1./scale, + _galsim.PositionD(0.,0.), scale**2, + self.gsparams._gsp) else: return self._sbiki diff --git a/galsim/kolmogorov.py b/galsim/kolmogorov.py index 5afa8ba99f9..ffe62c46964 100644 --- a/galsim/kolmogorov.py +++ b/galsim/kolmogorov.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import GalSimIncompatibleValuesError, convert_cpp_errors class Kolmogorov(GSObject): @@ -155,25 +156,33 @@ def __init__(self, lam_over_r0=None, fwhm=None, half_light_radius=None, lam=None if fwhm is not None : if any(item is not None for item in (lam_over_r0, lam, r0, r0_500, half_light_radius)): - raise TypeError( - "Only one of lam_over_r0, fwhm, half_light_radius, or lam (with r0 or "+ - "r0_500) may be specified for Kolmogorov") + raise GalSimIncompatibleValuesError( + "Only one of lam_over_r0, fwhm, half_light_radius, or lam (with r0 or r0_500) " + "may be specified", + fwhm=fwhm, lam_over_r0=lam_over_r0, lam=lam, r0=r0, r0_500=r0_500, + half_light_radius=half_light_radius) self._lor0 = float(fwhm) / Kolmogorov._fwhm_factor elif half_light_radius is not None: if any(item is not None for item in (lam_over_r0, lam, r0, r0_500)): - raise TypeError( - "Only one of lam_over_r0, fwhm, half_light_radius, or lam (with r0 or "+ - "r0_500) may be specified for Kolmogorov") + raise GalSimIncompatibleValuesError( + "Only one of lam_over_r0, fwhm, half_light_radius, or lam (with r0 or r0_500) " + "may be specified", + fwhm=fwhm, lam_over_r0=lam_over_r0, lam=lam, r0=r0, r0_500=r0_500, + half_light_radius=half_light_radius) self._lor0 = float(half_light_radius) / Kolmogorov._hlr_factor elif lam_over_r0 is not None: if any(item is not None for item in (lam, r0, r0_500)): - raise TypeError("Cannot specify lam, r0 or r0_500 in conjunction with lam_over_r0.") + raise GalSimIncompatibleValuesError( + "Cannot specify lam, r0 or r0_500 in conjunction with lam_over_r0.", + lam_over_r0=lam_over_r0, lam=lam, r0=r0, r0_500=r0_500) self._lor0 = float(lam_over_r0) else: if lam is None or (r0 is None and r0_500 is None): - raise TypeError( - "One of lam_over_r0, fwhm, half_light_radius, or lam (with r0 or "+ - "r0_500) must be specified for Kolmogorov") + raise GalSimIncompatibleValuesError( + "One of lam_over_r0, fwhm, half_light_radius, or lam (with r0 or r0_500) " + "must be specified", + fwhm=fwhm, lam_over_r0=lam_over_r0, lam=lam, r0=r0, r0_500=r0_500, + half_light_radius=half_light_radius) # In this case we're going to use scale_unit, so parse it in case of string input: if scale_unit is None: scale_unit = arcsec @@ -187,7 +196,8 @@ def __init__(self, lam_over_r0=None, fwhm=None, half_light_radius=None, lam=None @lazy_property def _sbp(self): - return _galsim.SBKolmogorov(self._lor0, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBKolmogorov(self._lor0, self._flux, self.gsparams._gsp) @property def lam_over_r0(self): return self._lor0 diff --git a/galsim/lensing_ps.py b/galsim/lensing_ps.py index 00015ab3740..1d1082f7db5 100644 --- a/galsim/lensing_ps.py +++ b/galsim/lensing_ps.py @@ -20,6 +20,7 @@ """ import numpy as np + from .angle import arcsec, AngleUnit from .position import PositionD, PositionI from .bounds import BoundsD, BoundsI @@ -30,7 +31,8 @@ from .table import LookupTable from . import utilities from . import integ -from . import _galsim +from .errors import GalSimError, GalSimValueError, GalSimIncompatibleValuesError +from .errors import GalSimNotImplementedError, galsim_warn def theoryToObserved(gamma1, gamma2, kappa): """Helper function to convert theoretical lensing quantities to observed ones. @@ -168,8 +170,9 @@ class PowerSpectrum(object): def __init__(self, e_power_function=None, b_power_function=None, delta2=False, units=arcsec): # Check that at least one power function is not None if e_power_function is None and b_power_function is None: - raise AttributeError( - "At least one of e_power_function or b_power_function must be provided.") + raise GalSimIncompatibleValuesError( + "At least one of e_power_function or b_power_function must be provided.", + e_power_function=e_power_function, b_power_function=b_power_function) self.e_power_function = e_power_function self.b_power_function = b_power_function @@ -189,7 +192,8 @@ def __init__(self, e_power_function=None, b_power_function=None, delta2=False, u # if the string is invalid, this raises a reasonable error message. units = AngleUnit.from_name(units) if not isinstance(units, AngleUnit): - raise ValueError("units must be either an AngleUnit or a string") + raise GalSimValueError("units must be either an AngleUnit or a string", units, + ('arcsec', 'arcmin', 'degree', 'hour', 'radian')) if units == arcsec: self.scale = 1 @@ -229,7 +233,8 @@ def _get_scale_fac(self, units): # if the string is invalid, this raises a reasonable error message. units = AngleUnit.from_name(units) if not isinstance(units, AngleUnit): - raise ValueError("units must be either an AngleUnit or a string") + raise GalSimValueError("units must be either an AngleUnit or a string", units, + ('arcsec', 'arcmin', 'degree', 'hour', 'radian')) return units / arcsec def _get_bandlimit_func(self, bandlimit): @@ -240,7 +245,8 @@ def _get_bandlimit_func(self, bandlimit): elif bandlimit is None: return lambda k, kmax: 1.0 else: - raise ValueError("Unrecognized option for band limit!") + raise GalSimValueError("Unrecognized option for band limit!", bandlimit, + (None, 'soft', 'hard')) def _get_pk(self, power_function, k_max, bandlimit_func): if power_function is None: @@ -263,7 +269,7 @@ def _get_pk(self, power_function, k_max, bandlimit_func): else: return lambda k : power_function(k) * bandlimit_func(k, k_max) - def buildGrid(self, grid_spacing=None, ngrid=None, rng=None, interpolant=None, + def buildGrid(self, grid_spacing, ngrid, rng=None, interpolant=None, center=PositionD(0,0), units=arcsec, get_convergence=False, kmax_factor=1, kmin_factor=1, bandlimit="hard", variance=None): """Generate a realization of the current power spectrum on the specified grid. @@ -459,26 +465,24 @@ def buildGrid(self, grid_spacing=None, ngrid=None, rng=None, interpolant=None, @returns the tuple (g1,g2[,kappa]), where each is a 2-d NumPy array and kappa is included iff `get_convergence` is set to True. """ - # Check problem cases for regular grid of points - if grid_spacing is None or ngrid is None: - raise ValueError("Both a spacing and a size are required for buildGrid.") # Check for validity of integer values if not isinstance(ngrid, int): if ngrid != int(ngrid): - raise ValueError("ngrid must be an integer") + raise GalSimValueError("ngrid must be an integer", ngrid) ngrid = int(ngrid) if not isinstance(kmin_factor, int): if kmin_factor != int(kmin_factor): - raise ValueError("kmin_factor must be an integer") + raise GalSimValueError("kmin_factor must be an integer", kmin_factor) kmin_factor = int(kmin_factor) if not isinstance(kmax_factor, int): if kmax_factor != int(kmax_factor): - raise ValueError("kmax_factor must be an integer") + raise GalSimValueError("kmax_factor must be an integer", kmax_factor) kmax_factor = int(kmax_factor) # Check if center is a PositionD if not isinstance(center, PositionD): - raise ValueError("center argument for buildGrid must be a PositionD instance") + raise GalSimValueError("center argument for buildGrid must be a PositionD instance", + center) # Automatically convert units to arcsec at the outset, then forget about it. This is # because PowerSpectrum by default wants to work in arsec, and all power functions are @@ -560,7 +564,7 @@ def nRandCallsForBuildGrid(self): (e.g. when calling the function through a Proxy object). """ if not hasattr(self,'ngrid_tot'): - raise RuntimeError("BuildGrid has not been called yet.") + raise GalSimError("BuildGrid has not been called yet.") ntot = 0 # cf. PowerSpectrumRealizer._generate_power_array temp = 2 * np.product( (self.ngrid_tot, self.ngrid_tot//2 +1 ) ) @@ -586,11 +590,10 @@ def _convert_power_function(self, pf, pf_str): pf = utilities.math_eval('lambda k : ' + pf) pf(1.0) except Exception as e: - raise ValueError( - "String %s must either be a valid filename or something that "%pf_str+ - "can eval to a function of k.\n"+ - "Input provided: {0}\n".format(origpf)+ - "Caught error: {0}".format(e)) + raise GalSimValueError( + "String {0} must either be a valid filename or something that " + "can eval to a function of k.\n" + "Caught error: {1}".format(pf_str, e), origpf) # Check that the function is sane. # Note: Only try tests below if it's not a LookupTable. @@ -764,13 +767,13 @@ def _wrap_image(self, im, border=7): Utility function to wrap an input image with some number of border pixels. By default, the number of border pixels is 7, but this function works as long as it's less than the size of the input image itself. This function is used for periodic interpolation by the - getShear() and other methods, but eventually if we make a 2d LookupTable-type class, this - should become a method of that class. + getShear() and other methods, but eventually if we upgrade LookupTable2D to allow + Lanczos interpolation, we should ust that. cf. Issue #751 """ # We should throw an exception if the image is smaller than 'border', since at this point # this process doesn't make sense. if im.bounds.xmax - im.bounds.xmin < border: - raise RuntimeError("Periodic wrapping does not work with images this small!") + raise GalSimError("Periodic wrapping does not work with images this small!") expanded_bounds = im.bounds.withBorder(border) # Make new image with those bounds. im_new = ImageD(expanded_bounds, scale=self.grid_spacing) @@ -907,7 +910,7 @@ def getShear(self, pos, units=arcsec, reduced=True, periodic=False): If the input `pos` is given a list/array of positions, they are NumPy arrays. """ if not hasattr(self, 'im_g1'): - raise RuntimeError("PowerSpectrum.buildGrid must be called before getShear") + raise GalSimError("PowerSpectrum.buildGrid must be called before getShear") # Convert to numpy arrays for internal usage: pos_x, pos_y = utilities._convertPositions(pos, units, 'getShear') @@ -969,11 +972,9 @@ def _getSingleShear(self, x, y, ii_g1, ii_g2, periodic): if not periodic: # We're not treating this as a periodic box, so issue a warning and set the # shear to zero for positions that are outside the original grid. - import warnings - warnings.warn( - "Warning: position (%f,%f) not within the bounds "%(x,y) + - "of the gridded shear values: " + str(self.bounds) + - ". Returning a shear of (0,0) for this point.") + galsim_warn( + "Warning: position (%f,%f) not within the bounds (%s) of the gridded shear " + "values. Returning a shear of (0,0) for this point."%(x,y,self.bounds)) return 0., 0. else: # Treat this as a periodic box. @@ -1021,7 +1022,7 @@ def getConvergence(self, pos, units=arcsec, periodic=False): If the input `pos` is given a list/array of positions, kappa is a NumPy array. """ if not hasattr(self, 'im_kappa'): - raise RuntimeError("PowerSpectrum.buildGrid must be called before getConvergence") + raise GalSimError("PowerSpectrum.buildGrid must be called before getConvergence") # Convert to numpy arrays for internal usage: pos_x, pos_y = utilities._convertPositions(pos, units, 'getConvergence') @@ -1067,11 +1068,10 @@ def _getSingleConvergence(self, x, y, ii_kappa, periodic): # Check that the position is in the bounds of the interpolated image if not self.bounds.includes(pos): if not periodic: - import warnings - warnings.warn( - "Warning: position (%f,%f) not within the bounds "%(x,y) + - "of the gridded convergence values: " + str(self.bounds) + - ". Returning a convergence of 0 for this point.") + galsim_warn( + "Warning: position (%f,%f) not within the bounds (%s) of the gridded " + "convergence values. Returning a convergence of 0 for this point."%( + x,y,self.bounds)) return 0. else: # Treat this as a periodic box. @@ -1118,7 +1118,7 @@ def getMagnification(self, pos, units=arcsec, periodic=False): If the input `pos` is given a list/array of positions, mu is a NumPy array. """ if not hasattr(self, 'im_kappa'): - raise RuntimeError("PowerSpectrum.buildGrid must be called before getMagnification") + raise GalSimError("PowerSpectrum.buildGrid must be called before getMagnification") # Convert to numpy arrays for internal usage: pos_x, pos_y = utilities._convertPositions(pos, units, 'getMagnification') @@ -1166,11 +1166,10 @@ def _getSingleMagnification(self, x, y, ii_mu, periodic): # Check that the position is in the bounds of the interpolated image if not self.bounds.includes(pos): if not periodic: - import warnings - warnings.warn( - "Warning: position (%f,%f) not within the bounds "%(x,y) + - "of the gridded convergence values: " + str(self.bounds) + - ". Returning a magnification of 1 for this point.") + galsim_warn( + "Warning: position (%f,%f) not within the bounds (%s) of the gridded " + "convergence values. Returning a magnification of 1 for this point."%( + x,y,self.bounds)) return 1. else: # Treat this as a periodic box. @@ -1219,7 +1218,7 @@ def getLensing(self, pos, units=arcsec, periodic=False): If the input `pos` is given a list/array of positions, they are NumPy arrays. """ if not hasattr(self, 'im_kappa'): - raise RuntimeError("PowerSpectrum.buildGrid must be called before getLensing") + raise GalSimError("PowerSpectrum.buildGrid must be called before getLensing") # Convert to numpy arrays for internal usage: pos_x, pos_y = utilities._convertPositions(pos, units, 'getLensing') @@ -1274,11 +1273,9 @@ def _getSingleLensing(self, x, y, ii_g1, ii_g2, ii_mu, periodic): # Check that the position is in the bounds of the interpolated image if not self.bounds.includes(pos): if not periodic: - import warnings - warnings.warn( - "Warning: position (%f,%f) not within the bounds "%(x,y) + - "of the gridded values: " + str(self.bounds) + - ". Returning 0 for lensing observables at this point.") + galsim_warn( + "Warning: position (%f,%f) not within the bounds (%s) of the gridded " + "values. Returning 0 for lensing observables at this point."%(x,y,self.bounds)) return 0., 0., 1. else: # Treat this as a periodic box. @@ -1475,20 +1472,13 @@ def _generate_power_array(self, power_function): # Fudge the value at k=0, so we don't have to evaluate power there k[0,0] = k[1,0] - # Raise a clear exception for LookupTable that are not defined on the full k range! - if isinstance(power_function, LookupTable): - mink = np.min(k) - maxk = np.max(k) - if mink < power_function.x_min or maxk > power_function.x_max: - raise ValueError( - "LookupTable P(k) is not defined for full k range on grid, %f= 1"%args.job) + raise GalSimRangeError("Invalid job number. Must be >= 1", args.job, 1, args.njobs) if args.job > args.njobs: - raise ValueError("Invalid job number %d. Must be <= njobs (%d)"%(args.job,args.njobs)) + raise GalSimRangeError("Invalid job number. Must be <= njobs",args.job, 1, args.njobs) # Parse the integer verbosity level from the command line args into a logging_level string logging_levels = { 0: logging.CRITICAL, @@ -219,7 +220,7 @@ def main(): logging.basicConfig(format="%(message)s", level=logging_level, filename=args.log_file) logger = logging.getLogger('galsim') - logger.warn('Using config file %s', args.config_file) + logger.warning('Using config file %s', args.config_file) all_config = ReadConfig(args.config_file, args.file_type, logger) logger.debug('Successfully read in config file.') diff --git a/galsim/moffat.py b/galsim/moffat.py index 394dd5e80ad..f27dba6f404 100644 --- a/galsim/moffat.py +++ b/galsim/moffat.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import GalSimRangeError, GalSimIncompatibleValuesError, convert_cpp_errors class Moffat(GSObject): @@ -86,20 +87,30 @@ def __init__(self, beta, scale_radius=None, half_light_radius=None, fwhm=None, t self._flux = float(flux) self._gsparams = GSParams.check(gsparams) + if self._trunc == 0. and self._beta <= 1.1: + raise GalSimRangeError("Moffat profiles with beta <= 1.1 must be truncated", + beta, 1.1) + if self._trunc < 0.: + raise GalSimRangeError("Moffat trunc must be >= 0", self._trunc, 0.) + # Parse the radius options if half_light_radius is not None: if scale_radius is not None or fwhm is not None: - raise TypeError( - "Only one of scale_radius, half_light_radius, or fwhm may be " + - "specified for Moffat") + raise GalSimIncompatibleValuesError( + "Only one of scale_radius, half_light_radius, or fwhm may be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius, fwhm=fwhm) self._hlr = float(half_light_radius) - self._r0 = _galsim.MoffatCalculateSRFromHLR(self._hlr, self._trunc, self._beta) + if self._trunc > 0. and self._trunc <= math.sqrt(2.) * self._hlr: + raise GalSimRangeError("Moffat trunc must be > sqrt(2) * half_light_radius.", + self._trunc, math.sqrt(2.) * self._hlr) + with convert_cpp_errors(): + self._r0 = _galsim.MoffatCalculateSRFromHLR(self._hlr, self._trunc, self._beta) self._fwhm = 0. elif fwhm is not None: if scale_radius is not None: - raise TypeError( - "Only one of scale_radius, half_light_radius, or fwhm may be " + - "specified for Moffat") + raise GalSimIncompatibleValuesError( + "Only one of scale_radius, half_light_radius, or fwhm may be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius, fwhm=fwhm) self._fwhm = float(fwhm) self._r0 = self._fwhm / (2. * math.sqrt(2.**(1./self._beta) - 1.)) self._hlr = 0. @@ -108,13 +119,15 @@ def __init__(self, beta, scale_radius=None, half_light_radius=None, fwhm=None, t self._hlr = 0. self._fwhm = 0. else: - raise TypeError( - "One of scale_radius, half_light_radius, or fwhm must be " + - "specified for Moffat") + raise GalSimIncompatibleValuesError( + "One of scale_radius, half_light_radius, or fwhm must be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius, fwhm=fwhm) @lazy_property def _sbp(self): - return _galsim.SBMoffat(self._beta, self._r0, self._trunc, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBMoffat(self._beta, self._r0, self._trunc, self._flux, + self.gsparams._gsp) def getFWHM(self): """Return the FWHM for this Moffat profile. diff --git a/galsim/nfw_halo.py b/galsim/nfw_halo.py index b503ba39877..cfdfaa445ec 100644 --- a/galsim/nfw_halo.py +++ b/galsim/nfw_halo.py @@ -19,10 +19,12 @@ """ import numpy as np + from .position import PositionD from .angle import arcsec from . import integ from . import utilities +from .errors import GalSimRangeError, GalSimIncompatibleValuesError class Cosmology(object): """Basic cosmology calculations. @@ -89,9 +91,9 @@ def Da(self, z, z_ref=0): return da else: if z < 0: - raise ValueError("Redshift z must not be negative") + raise GalSimRangeError("Redshift z must be >= 0", z, 0.) if z < z_ref: - raise ValueError("Redshift z must not be smaller than the reference redshift") + raise GalSimRangeError("Redshift z must be >= the reference redshift", z, z_ref) d = integ.int1d(self.__angKernel, z_ref+1, z+1) # check for curvature @@ -134,7 +136,9 @@ def __init__(self, mass, conc, redshift, halo_pos=PositionD(0,0), omega_m=None, omega_lam=None, cosmo=None): if omega_m is not None or omega_lam is not None: if cosmo is not None: - raise TypeError("NFWHalo constructor received both cosmo and omega parameters") + raise GalSimIncompatibleValuesError( + "NFWHalo constructor received both cosmo and omega parameters", + cosmo=cosmo, omega_m=omega_m, omega_lam=omega_lam) if omega_m is None: omega_m = 1.-omega_lam if omega_lam is None: omega_lam = 1.-omega_m cosmo = Cosmology(omega_m=omega_m, omega_lam=omega_lam) diff --git a/galsim/noise.py b/galsim/noise.py index 8f63442f22e..c7ee6537fa4 100644 --- a/galsim/noise.py +++ b/galsim/noise.py @@ -23,7 +23,10 @@ import numpy as np import math + from .image import Image, ImageD +from .utilities import doc_inherit +from .errors import GalSimError, GalSimIncompatibleValuesError def addNoise(self, noise): @@ -547,13 +550,13 @@ def _applyTo(self, image): image.array[:,:] += noise_array.reshape(image.array.shape).astype(image.dtype) def _getVariance(self): - raise RuntimeError("No single variance value for DeviateNoise") + raise GalSimError("No single variance value for DeviateNoise") def _withVariance(self, variance): - raise RuntimeError("Changing the variance is not allowed for DeviateNoise") + raise GalSimError("Changing the variance is not allowed for DeviateNoise") def _withScaledVariance(self, variance): - raise RuntimeError("Changing the variance is not allowed for DeviateNoise") + raise GalSimError("Changing the variance is not allowed for DeviateNoise") def copy(self, rng=None): """Returns a copy of the Deviate noise model. @@ -618,13 +621,15 @@ def var_image(self): # Repeat this here, since we want to add an extra sanity check, which should go in the # non-underscore version. + @doc_inherit def applyTo(self, image): if not isinstance(image, Image): raise TypeError("Provided image must be a galsim.Image") if image.array.shape != self.var_image.array.shape: - raise ValueError("Provided image shape does not match the shape of var_image") + raise GalSimIncompatibleValuesError( + "Provided image shape does not match the shape of var_image", + image=image, var_image=self.var_image) return self._applyTo(image) - applyTo.__doc__ = BaseNoise.applyTo.__doc__ def _applyTo(self, image): noise_array = self.var_image.array.flatten() # NB. Makes a copy! (which is what we want) @@ -643,15 +648,15 @@ def copy(self, rng=None): return VariableGaussianNoise(rng, self.var_image) def _getVariance(self): - raise RuntimeError("No single variance value for VariableGaussianNoise") + raise GalSimError("No single variance value for VariableGaussianNoise") def _withVariance(self, variance): - raise RuntimeError("Changing the variance is not allowed for VariableGaussianNoise") + raise GalSimError("Changing the variance is not allowed for VariableGaussianNoise") def _withScaledVariance(self, variance): # This one isn't undefined like withVariance, but it's inefficient. Better to # scale the values in the image before constructing VariableGaussianNoise. - raise RuntimeError("Changing the variance is not allowed for VariableGaussianNoise") + raise GalSimError("Changing the variance is not allowed for VariableGaussianNoise") def __repr__(self): return 'galsim.VariableGaussianNoise(rng=%r, var_image%r)'%(self.rng, self.var_image) diff --git a/galsim/phase_psf.py b/galsim/phase_psf.py index 045d712048c..1a1983b392e 100644 --- a/galsim/phase_psf.py +++ b/galsim/phase_psf.py @@ -71,7 +71,6 @@ from heapq import heappush, heappop import numpy as np -from . import _galsim from .gsobject import GSObject from .gsparams import GSParams from .angle import radians, degrees, arcsec, Angle, AngleUnit @@ -80,6 +79,8 @@ from .wcs import PixelScale from .interpolatedimage import InterpolatedImage from .utilities import doc_inherit, OrderedWeakRef, rotate_xy, lazy_property +from .errors import GalSimError, GalSimValueError, GalSimRangeError, GalSimIncompatibleValuesError +from .errors import GalSimFFTSizeError, galsim_warn class Aperture(object): """ Class representing a telescope aperture embedded in a larger pupil plane array -- for use @@ -217,11 +218,12 @@ def __init__(self, diam, lam=None, circular_pupil=True, obscuration=0.0, self.diam = diam # Always need to explicitly specify an aperture diameter. self._obscuration = obscuration # We store this, even though it's not always used. - self._gsparams = gsparams + self._gsparams = GSParams.check(gsparams) - if obscuration >= 1.: - raise ValueError("Pupil fully obscured! obscuration = {:0} (>= 1)" - .format(obscuration)) + if diam <= 0.: + raise GalSimRangeError("Invalid diam.", diam, 0.) + if obscuration < 0. or obscuration >= 1.: + raise GalSimRangeError("Invalid obscuration.", obscuration, 0., 1.) # You can either set geometric properties, or use a pupil image, but not both, so check for # that here. One caveat is that we allow sanity checking the sampling of a pupil_image by @@ -233,10 +235,15 @@ def __init__(self, diam, lam=None, circular_pupil=True, obscuration=0.0, strut_thick == 0.05 and strut_angle == 0.0*radians) if not is_default_geom and pupil_plane_im is not None: - raise ValueError("Can't specify both geometric parameters and pupil_plane_im.") + raise GalSimIncompatibleValuesError( + "Can't specify both geometric parameters and pupil_plane_im.", + circular_pupil=circular_pupil, nstruts=nstruts, strut_thick=strut_thick, + strut_angle=strut_angle, pupil_plane_im=pupil_plane_im) if screen_list is not None and lam is None: - raise ValueError("Wavelength `lam` must be specified with `screen_list`.") + raise GalSimIncompatibleValuesError( + "Wavelength `lam` must be specified with `screen_list`.", + screen_list=screen_list, lam=lam) # Although the user can set the pupil plane size and scale directly if desired, in most # cases it's nicer to have GalSim try to pick good values for these. @@ -282,40 +289,28 @@ def __init__(self, diam, lam=None, circular_pupil=True, obscuration=0.0, else: # Use geometric parameters. if pupil_plane_scale is not None: # Check input scale and warn if looks suspicious. - if pupil_plane_scale > good_pupil_scale: # pragma: no cover - import warnings + if pupil_plane_scale > good_pupil_scale: ratio = good_pupil_scale / pupil_plane_scale - warnings.warn("Input pupil_plane_scale may be too large for good sampling.\n" - "Consider decreasing pupil_plane_scale by a factor %f, and/or " - "check PhaseScreenPSF outputs for signs of folding in real " - "space."%(1./ratio)) + galsim_warn("Input pupil_plane_scale may be too large for good sampling.\n" + "Consider decreasing pupil_plane_scale by a factor %f, and/or " + "check PhaseScreenPSF outputs for signs of folding in real " + "space."%(1./ratio)) else: pupil_plane_scale = good_pupil_scale if pupil_plane_size is not None: # Check input size and warn if looks suspicious - if pupil_plane_size < good_pupil_size: # pragma: no cover - import warnings + if pupil_plane_size < good_pupil_size: ratio = good_pupil_size / pupil_plane_size - warnings.warn("Input pupil_plane_size may be too small for good focal-plane" - "sampling.\n" - "Consider increasing pupil_plane_size by a factor %f, and/or " - "check PhaseScreenPSF outputs for signs of undersampling."%ratio) + galsim_warn("Input pupil_plane_size may be too small for good focal-plane" + "sampling.\n" + "Consider increasing pupil_plane_size by a factor %f, and/or " + "check PhaseScreenPSF outputs for signs of undersampling."%ratio) else: pupil_plane_size = good_pupil_size self._generate_pupil_plane(circular_pupil, nstruts, strut_thick, strut_angle, pupil_plane_scale, pupil_plane_size) - # Check FFT size - if self._gsparams is not None: - maximum_fft_size = self._gsparams.maximum_fft_size - else: - maximum_fft_size = GSParams().maximum_fft_size - if self.npix > maximum_fft_size: - raise RuntimeError("Created pupil plane array that is too large, {0} " - "If you can handle the large FFT, you may update " - "gsparams.maximum_fft_size".format(self.npix)) - def _generate_pupil_plane(self, circular_pupil, nstruts, strut_thick, strut_angle, pupil_plane_scale, pupil_plane_size): """ Create an array of illuminated pixels parameterically. @@ -324,6 +319,11 @@ def _generate_pupil_plane(self, circular_pupil, nstruts, strut_thick, strut_angl # Fudge a little to prevent good_fft_size() from turning 512.0001 into 768. ratio *= (1.0 - 1.0/2**14) self.npix = Image.good_fft_size(int(np.ceil(ratio))) + + # Check FFT size + if self.npix > self._gsparams.maximum_fft_size: + raise GalSimFFTSizeError("Created pupil plane array that is too large.",self.npix) + self.pupil_plane_size = pupil_plane_size # Shrink scale such that size = scale * npix exactly. self.pupil_plane_scale = pupil_plane_size / self.npix @@ -384,11 +384,17 @@ def _load_pupil_plane(self, pupil_plane_im, pupil_angle, pupil_plane_scale, good pp_arr = pupil_plane_im.array self.npix = pp_arr.shape[0] + # Check FFT size + if self.npix > self._gsparams.maximum_fft_size: + raise GalSimFFTSizeError("Loaded pupil plane array that is too large.", self.npix) + # Sanity checks if pupil_plane_im.array.shape[0] != pupil_plane_im.array.shape[1]: - raise ValueError("We require square input pupil plane arrays!") + raise GalSimValueError("Input pupil_plane_im must be square.", + pupil_plane_im.array.shape) if pupil_plane_im.array.shape[0] % 2 == 1: - raise ValueError("Even-sized input arrays are required for the pupil plane!") + raise GalSimValueError("Input pupil_plane_im must have even sizes.", + pupil_plane_im.array.shape) # Set the scale, priority is: # 1. pupil_plane_scale kwarg @@ -419,12 +425,11 @@ def _load_pupil_plane(self, pupil_plane_im, pupil_angle, pupil_plane_scale, good self.pupil_plane_size = self.pupil_plane_scale * self.npix # Check sampling interval and warn if it's not good enough. - if self.pupil_plane_scale > good_pupil_scale: # pragma: no cover - import warnings + if self.pupil_plane_scale > good_pupil_scale: ratio = self.pupil_plane_scale / good_pupil_scale - warnings.warn("Input pupil plane image may not be sampled well enough!\n" - "Consider increasing sampling by a factor %f, and/or check " - "PhaseScreenPSF outputs for signs of folding in real space."%ratio) + galsim_warn("Input pupil plane image may not be sampled well enough!\n" + "Consider increasing sampling by a factor %f, and/or check " + "PhaseScreenPSF outputs for signs of folding in real space."%ratio) if pupil_angle.rad == 0.: self._illuminated = pp_arr.astype(bool) @@ -496,7 +501,7 @@ def __repr__(self): tmp = self.illuminated.astype(np.int16).tolist() s += ", pupil_plane_im=array(%r"%tmp+", dtype='int16')" s += ", pupil_plane_scale=%r"%self.pupil_plane_scale - if hasattr(self, '_gsparams') and self._gsparams is not None: + if self._gsparams != GSParams(): s += ", gsparams=%r"%self._gsparams s += ")" return s @@ -523,39 +528,35 @@ def illuminated(self): """ return self._illuminated - @property + @lazy_property def rho(self): """ Unit-disk normalized pupil plane coordinate as a complex number: (x, y) => x + 1j * y. """ - if not hasattr(self, '_rho') or self._rho is None: - u = np.fft.fftshift(np.fft.fftfreq(self.npix, self.diam/self.pupil_plane_size/2.0)) - u, v = np.meshgrid(u, u) - self._rho = u + 1j * v - return self._rho + u = np.fft.fftshift(np.fft.fftfreq(self.npix, self.diam/self.pupil_plane_size/2.0)) + u, v = np.meshgrid(u, u) + return u + 1j * v + + @lazy_property + def _uv(self): + u = np.fft.fftshift(np.fft.fftfreq(self.npix, 1./self.pupil_plane_size)) + u, v = np.meshgrid(u, u) + return u, v @property def u(self): """Pupil horizontal coordinate array in meters.""" - if not hasattr(self, '_u'): - u = np.fft.fftshift(np.fft.fftfreq(self.npix, 1./self.pupil_plane_size)) - self._u, self._v = np.meshgrid(u, u) - return self._u + return self._uv[0] @property def v(self): """Pupil vertical coordinate array in meters.""" - if not hasattr(self, '_v'): - u = np.fft.fftshift(np.fft.fftfreq(self.npix, 1./self.pupil_plane_size)) - self._u, self._v = np.meshgrid(u, u) - return self._v + return self._uv[1] - @property + @lazy_property def rsqr(self): """Pupil radius squared array in meters squared.""" - if not hasattr(self, '_rsqr'): - self._rsqr = self.u**2 + self.v**2 - return self._rsqr + return self.u**2 + self.v**2 @property def obscuration(self): @@ -566,7 +567,7 @@ def __getstate__(self): # Let unpickled object reconstruct cached values on-the-fly instead of including them in the # pickle. d = self.__dict__.copy() - for k in ['_rho', '_u', '_v', '_rsqr']: + for k in ('_rho', '_u', '_v', '_rsqr'): d.pop(k, None) return d @@ -583,6 +584,9 @@ def __getstate__(self): # - Implies relation between aperture grid and real-space grid: # dL = lambda/theta # L = lambda/dtheta + # + # MJ: Of these four, only _sky_scale is still used. The rest are left here for informational + # purposes, but nothing actually calls them. def _getStepK(self, lam, scale_unit=arcsec): """Return the Fourier grid spacing for this aperture at given wavelength. @@ -649,20 +653,15 @@ def __init__(self, *layers): if len(layers) == 1: # First check if layers[0] is a PhaseScreenList, so we avoid nesting. if isinstance(layers[0], PhaseScreenList): - layers = layers[0]._layers + self._layers = layers[0]._layers else: # Next, see if layers[0] is iterable. E.g., to catch generator expressions. try: - layers = list(layers[0]) + self._layers = list(layers[0]) except TypeError: - # If that fails, check if layers[0] is a bare PhaseScreen. Should probably - # make an ABC for this (use __subclasshook__?), but for now, just check - # AtmosphericScreen and OpticalScreen. - if isinstance(layers[0], (AtmosphericScreen, OpticalScreen)): - layers = [layers[0]] - # else, layers is either empty or a tuple of PhaseScreens and so responds appropriately - # to list() below. - self._layers = list(layers) + self._layers = list(layers) + else: + self._layers = list(layers) self._update_attrs() self._pending = [] # Pending PSFs to calculate upon first drawImage. @@ -676,7 +675,7 @@ def __getitem__(self, index): return cls(self._layers[index]) elif isinstance(index, numbers.Integral): return self._layers[index] - else: # pragma: no cover + else: msg = "{cls.__name__} indices must be integers" raise TypeError(msg.format(cls=cls)) @@ -1068,7 +1067,7 @@ class PhaseScreenPSF(GSObject): The following are optional keywords to use to setup the aperture if `aper` is not provided: - @param diam Aperture diameter in meters. + @param diam Aperture diameter in meters. [default: None] @param circular_pupil Adopt a circular pupil? [default: True] @param obscuration Linear dimension of central obscuration as fraction of aperture linear dimension. [0., 1.). [default: 0.0] @@ -1124,7 +1123,8 @@ def __init__(self, screen_list, lam, t0=0.0, exptime=0.0, time_step=0.025, flux= if aper is None: # Check here for diameter. if 'diam' not in kwargs: - raise ValueError("Diameter required if aperture not specified directly.") + raise GalSimIncompatibleValuesError( + "Diameter required if aperture not specified directly.", diam=None, aper=aper) aper = Aperture(lam=lam, screen_list=self._screen_list, gsparams=gsparams, **kwargs) self.aper = aper @@ -1135,7 +1135,7 @@ def __init__(self, screen_list, lam, t0=0.0, exptime=0.0, time_step=0.025, flux= if isinstance(scale_unit, str): scale_unit = AngleUnit.from_name(scale_unit) self.scale_unit = scale_unit - self._gsparams = gsparams + self._gsparams = GSParams.check(gsparams) self.scale = aper._sky_scale(self.lam, self.scale_unit) self._force_stepk = _force_stepk @@ -1144,7 +1144,7 @@ def __init__(self, screen_list, lam, t0=0.0, exptime=0.0, time_step=0.025, flux= self.img = np.zeros(self.aper.illuminated.shape, dtype=np.float64) if self.exptime < 0: - raise ValueError("Cannot integrate PSF for negative time.") + raise GalSimRangeError("Cannot integrate PSF for negative time.", self.exptime, 0.) self._ii_pad_factor = ii_pad_factor @@ -1220,7 +1220,7 @@ def __str__(self): (self._screen_list, self.lam, self.exptime)) def __repr__(self): - outstr = ("galsim.PhaseScreenPSF(%r, lam=%r, exptime=%r, flux=%r, aper=%r, theta=%r, " + + outstr = ("galsim.PhaseScreenPSF(%r, lam=%r, exptime=%r, flux=%r, aper=%r, theta=%r, " "interpolant=%r, scale_unit=%r, gsparams=%r)") return outstr % (self._screen_list, self.lam, self.exptime, self.flux, self.aper, self.theta, self.interpolant, self.scale_unit, self.gsparams) @@ -1284,12 +1284,10 @@ def _finalize(self): observed_stepk = self._ii.stepk if observed_stepk < specified_stepk: - import warnings - warnings.warn( - "The calculated stepk (%g) for PhaseScreenPSF is smaller "%observed_stepk + - "than what was used to build the wavefront (%g). "%specified_stepk + - "This could lead to aliasing problems. " + - "Increasing pad_factor is recommended.") + galsim_warn( + "The calculated stepk (%g) for PhaseScreenPSF is smaller than what was used " + "to build the wavefront (%g). This could lead to aliasing problems. " + "Increasing pad_factor is recommended."%(observed_stepk, specified_stepk)) @property def _sbp(self): @@ -1630,7 +1628,9 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, tip=0., tilt=0., def # OpticalScreen. if lam_over_diam is not None: if lam is not None or diam is not None: - raise TypeError("If specifying lam_over_diam, then do not specify lam or diam") + raise GalSimIncompatibleValuesError( + "If specifying lam_over_diam, then do not specify lam or diam", + lam_over_diam=lam_over_diam, lam=lam, diam=diam) # For combination of lam_over_diam and pupil_plane_im with a specified scale, it's # tricky to determine the actual diameter of the pupil needed by Aperture. So for now, # we just disallow this combination. Please feel free to raise an issue at @@ -1638,20 +1638,26 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, tip=0., tilt=0., def if pupil_plane_im is not None: if isinstance(pupil_plane_im, basestring): # Filename, therefore specific scale exists. - raise TypeError("If specifying lam_over_diam, then do not " - "specify pupil_plane_im as a filename.") - elif (isinstance(pupil_plane_im, Image) - and pupil_plane_im.scale is not None): - raise TypeError("If specifying lam_over_diam, then do not specify " - "pupil_plane_im with definite scale attribute.") + raise GalSimIncompatibleValuesError( + "If specifying lam_over_diam, then do not specify pupil_plane_im as " + "as a filename.", + lam_over_diam=lam_over_diam, pupil_plane_im=pupil_plane_im) + elif isinstance(pupil_plane_im, Image) and pupil_plane_im.scale is not None: + raise GalSimIncompatibleValuesError( + "If specifying lam_over_diam, then do not specify pupil_plane_im " + "with definite scale attribute.", + lam_over_diam=lam_over_diam, pupil_plane_im=pupil_plane_im) elif pupil_plane_scale is not None: - raise TypeError("If specifying lam_over_diam, then do not specify " - "pupil_plane_scale.") + raise GalSimIncompatibleValuesError( + "If specifying lam_over_diam, then do not specify pupil_plane_scale. ", + lam_over_diam=lam_over_diam, pupil_plane_scale=pupil_plane_scale) lam = 500. # Arbitrary diam = lam*1.e-9 / lam_over_diam * radians / scale_unit else: if lam is None or diam is None: - raise TypeError("If not specifying lam_over_diam, then specify lam AND diam") + raise GalSimIncompatibleValuesError( + "If not specifying lam_over_diam, then specify lam AND diam", + lam_over_diam=lam_over_diam, lam=lam, diam=diam) # Make the optical screen. optics_screen = OpticalScreen( @@ -1671,17 +1677,14 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, tip=0., tilt=0., def gsparams=gsparams) self.obscuration = obscuration else: - if hasattr(aper, '_obscuration'): - self.obscuration = aper._obscuration - else: - self.obscuration = 0.0 + self.obscuration = aper.obscuration # Save for pickling self._lam = float(lam) self._flux = float(flux) self._interpolant = interpolant self._scale_unit = scale_unit - self._gsparams = gsparams + self._gsparams = GSParams.check(gsparams) self._suppress_warning = suppress_warning self._geometric_shooting = geometric_shooting self._aper = aper @@ -1689,16 +1692,17 @@ def __init__(self, lam_over_diam=None, lam=None, diam=None, tip=0., tilt=0., def self._force_maxk = _force_maxk self._ii_pad_factor = ii_pad_factor - # Finally, put together to make the PSF. - self._psf = PhaseScreenPSF(self._screens, lam=self._lam, flux=self._flux, - aper=aper, interpolant=self._interpolant, + @lazy_property + def _psf(self): + psf = PhaseScreenPSF(self._screens, lam=self._lam, flux=self._flux, + aper=self._aper, interpolant=self._interpolant, scale_unit=self._scale_unit, gsparams=self._gsparams, suppress_warning=self._suppress_warning, geometric_shooting=self._geometric_shooting, - _force_stepk=_force_stepk, _force_maxk=_force_maxk, - ii_pad_factor=ii_pad_factor) - - self._psf._prepareDraw() # No need to delay an OpticalPSF. + _force_stepk=self._force_stepk, _force_maxk=self._force_maxk, + ii_pad_factor=self._ii_pad_factor) + psf._prepareDraw() # No need to delay an OpticalPSF. + return psf def __str__(self): screen = self._psf._screen_list[0] @@ -1761,21 +1765,11 @@ def __getstate__(self): # The SBProfile is picklable, but it is pretty inefficient, due to the large images being # written as a string. Better to pickle the psf and remake the PhaseScreenPSF. d = self.__dict__.copy() - d['aper'] = d['_psf'].aper - del d['_psf'] + d.pop('_psf', None) return d def __setstate__(self, d): self.__dict__ = d - aper = self.__dict__.pop('aper') - self._psf = PhaseScreenPSF(self._screens, lam=self._lam, flux=self._flux, - aper=aper, interpolant=self._interpolant, - scale_unit=self._scale_unit, gsparams=self._gsparams, - suppress_warning=self._suppress_warning, - _force_stepk=self._force_stepk, - _force_maxk=self._force_maxk, - ii_pad_factor=self._ii_pad_factor) - self._psf._prepareDraw() @property def _maxk(self): diff --git a/galsim/phase_screens.py b/galsim/phase_screens.py index 70871143a36..4274411e63d 100644 --- a/galsim/phase_screens.py +++ b/galsim/phase_screens.py @@ -17,8 +17,8 @@ # from builtins import range, zip - import numpy as np + from .random import BaseDeviate, GaussianDeviate from .image import Image from .angle import radians @@ -26,6 +26,7 @@ from . import utilities from . import fft from . import zernike +from .errors import GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError, galsim_warn class AtmosphericScreen(object): @@ -106,10 +107,13 @@ def __init__(self, screen_size, screen_scale=None, altitude=0.0, r0_500=0.2, L0= vx=0.0, vy=0.0, alpha=1.0, time_step=None, rng=None, suppress_warning=False): if (alpha != 1.0 and time_step is None): - raise ValueError("No time_step provided when alpha != 1.0") + raise GalSimIncompatibleValuesError( + "No time_step provided when alpha != 1.0", alpha=alpha, time_step=time_step) if (alpha == 1.0 and time_step is not None): - raise ValueError("Setting AtmosphericScreen time_step prohibited when alpha == 1.0. " - "Did you mean to set time_step in makePSF or PhaseScreenPSF?") + raise GalSimIncompatibleValuesError( + "Setting AtmosphericScreen time_step prohibited when alpha == 1.0. " + "Did you mean to set time_step in makePSF or PhaseScreenPSF?", + alpha=alpha, time_step=time_step) if screen_scale is None: # We copy Jee+Tyson(2011) and (arbitrarily) set the screen scale equal to r0 by default. screen_scale = r0_500 @@ -143,7 +147,7 @@ def __str__(self): return "galsim.AtmosphericScreen(altitude=%s)" % self.altitude def __repr__(self): - return ("galsim.AtmosphericScreen(%r, %r, altitude=%r, r0_500=%r, L0=%r, " + + return ("galsim.AtmosphericScreen(%r, %r, altitude=%r, r0_500=%r, L0=%r, " "vx=%r, vy=%r, alpha=%r, time_step=%r, rng=%r)") % ( self.screen_size, self.screen_scale, self.altitude, self.r0_500, self.L0, self.vx, self.vy, self.alpha, self.time_step, self._orig_rng) @@ -211,16 +215,12 @@ def instantiate(self, kmin=0., kmax=np.inf, check=None): if check is not None and not self._suppress_warning: if check == 'FFT': if self.kmax != np.inf: - import warnings - warnings.warn( - "Instantiating AtmosphericScreen with kmax != inf " - "may yield surprising results when drawing using Fourier optics.") + galsim_warn("AtmosphericScreen was instantiated for photon shooting. " + "Drawing now with FFT may yield surprising results.") if check == 'phot': if self.kmax == np.inf: - import warnings - warnings.warn( - "Instantiating AtmosphericScreen with kmax == inf " - "may yield surprising results when drawing using geometric optics.") + galsim_warn("AtmosphericScreen was instantiated for FFT drawing. " + "Drawing now with photon shooting may yield surprising results.") # Note the magic number 0.00058 is actually ... wait for it ... @@ -270,7 +270,7 @@ def _seek(self, t): # Can't reverse, so reset and move forward. if t < self._time: if t < 0.0: - raise ValueError("Can't rewind irreversible screen to t < 0.0") + raise GalSimRangeError("Can't rewind irreversible screen to t < 0.0", t, 0.) self._reset() # Find number of boiling updates we need to perform. previous_update_number = int(self._time // self.time_step) @@ -336,7 +336,7 @@ def wavefront(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): u = np.array(u, dtype=float) v = np.array(v, dtype=float) if u.shape != v.shape: - raise ValueError("u.shape not equal to v.shape") + raise GalSimIncompatibleValuesError("u.shape not equal to v.shape",u=u,v=v) if t is None: t = self._time @@ -349,7 +349,8 @@ def wavefront(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): else: t = np.array(t, dtype=float) if t.shape != u.shape: - raise ValueError("t.shape must match u.shape if t is not a scalar") + raise GalSimIncompatibleValuesError( + "t.shape must match u.shape if t is not a scalar", t=t, u=u) self.instantiate() # noop if already instantiated @@ -396,7 +397,7 @@ def wavefront_gradient(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): u = np.array(u, dtype=float) v = np.array(v, dtype=float) if u.shape != v.shape: - raise ValueError("u.shape not equal to v.shape") + raise GalSimIncompatibleValuesError("u.shape not equal to v.shape", u=u, v=v) from numbers import Real if isinstance(t, Real): @@ -406,7 +407,8 @@ def wavefront_gradient(self, u, v, t=None, theta=(0.0*radians, 0.0*radians)): else: t = np.array(t, dtype=float) if t.shape != u.shape: - raise ValueError("t.shape must match u.shape if t is not a scalar") + raise GalSimIncompatibleValuesError( + "t.shape must match u.shape if t is not a scalar", t=t, u=u) self.instantiate() # noop if already instantiated @@ -578,7 +580,9 @@ def Atmosphere(screen_size, rng=None, _bar=None, **kwargs): kwargs['r0_500'] = [r0_500 * w**(-3./5) for w in r0_weights] # kwargs['r0_500'] = [nmax**(3./5) * kwargs['r0_500'][0]] * nmax elif 'r0_weights' in kwargs: - raise ValueError("Cannot use r0_weights if r0_500 is specified as a list.") + raise GalSimIncompatibleValuesError( + "Cannot use r0_weights if r0_500 is specified as a list.", + r0_weights=kwargs['r0_weights'], r0_500=kwargs['r0_500']) if rng is None: rng = BaseDeviate() @@ -646,15 +650,17 @@ def __init__(self, diam, tip=0.0, tilt=0.0, defocus=0.0, astig1=0.0, astig2=0.0, else: # Make sure no individual aberrations were passed in, since they will be ignored. if any([tip, tilt, defocus, astig1, astig2, coma1, coma2, trefoil1, trefoil2, spher]): - raise TypeError("Cannot pass in individual aberrations and array!") + raise GalSimIncompatibleValuesError( + "Cannot pass in individual aberrations and array.", + tip=tip, tilt=tilt, defocus=defocus, astig1=astig1, astig2=astig2, + coma1=coma1, coma2=coma2, trefoil1=trefoil1, trefoil2=trefoil2, + spher=spher, aberrations=aberrations) # Aberrations were passed in, so check for right number of entries. if len(aberrations) <= 2: - raise ValueError("Aberrations keyword must have length > 2") + raise GalSimValueError("Aberrations keyword must have length > 2", aberrations) # Check for non-zero value in first two places. Probably a mistake. if aberrations[0] != 0.0: - import warnings - warnings.warn( - "Detected non-zero value in aberrations[0] -- this value is ignored!") + galsim_warn("Detected non-zero value in aberrations[0] -- this value is ignored!") aberrations = np.array(aberrations) self.aberrations = aberrations @@ -741,7 +747,7 @@ def wavefront(self, u, v, t=None, theta=None): u = np.array(u, dtype=float) v = np.array(v, dtype=float) if u.shape != v.shape: - raise ValueError("u.shape not equal to v.shape") + raise GalSimIncompatibleValuesError("u.shape not equal to v.shape", u=u, v=v) return self._wavefront(u, v, t, theta) def _wavefront(self, u, v, t, theta): @@ -763,7 +769,7 @@ def wavefront_gradient(self, u, v, t=None, theta=None): u = np.array(u, dtype=float) v = np.array(v, dtype=float) if u.shape != v.shape: - raise ValueError("u.shape not equal to v.shape") + raise GalSimIncompatibleValuesError("u.shape not equal to v.shape", u=u, v=v) return self._wavefront_gradient(u, v, t, theta) diff --git a/galsim/photon_array.py b/galsim/photon_array.py index f98b62c4ec3..1531ff5e741 100644 --- a/galsim/photon_array.py +++ b/galsim/photon_array.py @@ -21,9 +21,12 @@ """ import numpy as np + from . import _galsim from .random import UniformDeviate from .angle import radians, arcsec +from .errors import GalSimError, GalSimRangeError, GalSimValueError, GalSimUndefinedBoundsError +from .errors import GalSimIncompatibleValuesError, convert_cpp_errors # Add on more methods in the python layer @@ -188,13 +191,14 @@ def scaleFlux(self, scale): self._flux *= scale def scaleXY(self, scale): - self._x *= scale - self._y *= scale + self._x *= float(scale) + self._y *= float(scale) def assignAt(self, istart, rhs): "Assign the contents of another PhotonArray to this one starting at istart." if istart + rhs.size() > self.size(): - raise IndexError("The given rhs does not fit into this array starting at %d"%istart) + raise GalSimValueError( + "The given rhs does not fit into this array starting at %d"%istart, rhs) s = slice(istart, istart + rhs.size()) self.x[s] = rhs.x self.y[s] = rhs.y @@ -207,6 +211,9 @@ def assignAt(self, istart, rhs): def convolve(self, rhs, rng=None): "Convolve this PhotonArray with another." + if rhs.size() != self.size(): + raise GalSimIncompatibleValuesError("PhotonArray.convolve with unequal size arrays", + self_pa=self, rhs=rhs) ud = UniformDeviate(rng) self._pa.convolve(rhs._pa, ud._rng) @@ -258,8 +265,9 @@ def _pa(self): if self.hasAllocatedWavelengths(): assert(self._wave.strides[0] == self._wave.itemsize) _wave = self._wave.ctypes.data - return _galsim.PhotonArray(int(self.size()), _x, _y, _flux, _dxdz, _dydz, _wave, - self._is_corr) + with convert_cpp_errors(): + return _galsim.PhotonArray(int(self.size()), _x, _y, _flux, _dxdz, _dydz, _wave, + self._is_corr) def addTo(self, image): """Add flux of photons to an image by binning into pixels. @@ -273,6 +281,9 @@ def addTo(self, image): @returns the total flux of photons the landed inside the image bounds. """ + if not image.bounds.isDefined(): + raise GalSimUndefinedBoundsError( + "Attempting to PhotonArray::addTo an Image with undefined Bounds") return self._pa.addTo(image._image) @classmethod @@ -296,7 +307,7 @@ def makeFromImage(cls, image, max_flux=1., rng=None): ud = UniformDeviate(rng) max_flux = float(max_flux) if (max_flux <= 0): - raise ValueError("max_flux must be positive") + raise GalSimRangeError("max_flux must be positive", max_flux, 0.) total_flux = image.array.sum(dtype=float) # This goes a bit over what we actually need, but not by much. Worth it to not have to @@ -309,7 +320,7 @@ def makeFromImage(cls, image, max_flux=1., rng=None): photons._y = photons.y[:N] photons._flux = photons.flux[:N] - if image.scale != 1.: + if image.scale != 1. and image.scale is not None: photons.scaleXY(image.scale) return photons @@ -363,14 +374,11 @@ def read(cls, file_name): @param file_name The file name of the input FITS file. """ - from ._pyfits import pyfits, pyfits_version + from ._pyfits import pyfits with pyfits.open(file_name) as fits: data = fits[1].data N = len(data) - if pyfits_version > '3.0': - names = data.columns.names - else: # pragma: no cover - names = data.dtype.names + names = data.columns.names photons = cls(N, x=data['x'], y=data['y'], flux=data['flux']) if 'dxdz' in names: @@ -422,9 +430,9 @@ class FRatioAngles(object): def __init__(self, fratio, obscuration=0.0, rng=None): if fratio < 0: - raise ValueError("The f-ratio must be positive.") + raise GalSimRangeError("The f-ratio must be positive.", fratio, 0.) if obscuration < 0 or obscuration >= 1: - raise ValueError("The obscuration fraction must be between 0 and 1.") + raise GalSimRangeError("Invalid obscuration.", obscuration, 0., 1.) ud = UniformDeviate(rng) self.fratio = fratio @@ -537,7 +545,7 @@ def __init__(self, base_wavelength, scale_unit=arcsec, **kwargs): # Any remaining kwargs will get forwarded to galsim.dcr.get_refraction # Check that they're valid for kw in self.kw: - if kw not in ['temperature', 'pressure', 'H2O_pressure']: + if kw not in ('temperature', 'pressure', 'H2O_pressure'): raise TypeError("Got unexpected keyword: {0}".format(kw)) self.base_refraction = dcr.get_refraction(self.base_wavelength, self.zenith_angle, @@ -548,7 +556,7 @@ def applyTo(self, photon_array, local_wcs): """ from . import dcr if not photon_array.hasAllocatedWavelengths(): - raise RuntimeError("PhotonDCR requires that wavelengths be set") + raise GalSimError("PhotonDCR requires that wavelengths be set") w = photon_array.wavelength cenx = local_wcs.origin.x diff --git a/galsim/position.py b/galsim/position.py index 65599251158..e45e2e8983c 100644 --- a/galsim/position.py +++ b/galsim/position.py @@ -71,7 +71,7 @@ class Position(object): a PositionI by a float or add a PositionI to a PositionD. """ def __init__(self): - raise NotImplementedError("Cannot instantiate the base class. " + + raise NotImplementedError("Cannot instantiate the base class. " "Use either PositionD or PositionI.") def _parse_args(self, *args, **kwargs): @@ -88,8 +88,8 @@ def _parse_args(self, *args, **kwargs): try: self.x, self.y = args[0] except (TypeError, ValueError): - raise TypeError(("Single argument to %s must be either a Position "+ - "or a tuple.")%self.__class__) + raise TypeError("Single argument to %s must be either a Position " + "or a tuple."%self.__class__) else: raise TypeError("%s takes at most 2 arguments (%d given)"%( self.__class__, len(args))) @@ -116,8 +116,7 @@ def __div__(self, other): self._check_scalar(other, 'divide') return self.__class__(self.x / other, self.y / other) - def __truediv__(self, other): - return self.__div__(other) + __truediv__ = __div__ def __neg__(self): return self.__class__(-self.x, -self.y) @@ -127,7 +126,7 @@ def __add__(self, other): if isinstance(other,Bounds): return other + self elif not isinstance(other,Position): - raise ValueError("Can only add a Position to a %s"%self.__class__.__name__) + raise TypeError("Can only add a Position to a %s"%self.__class__.__name__) elif isinstance(other, self.__class__): return self.__class__(self.x + other.x, self.y + other.y) else: @@ -135,7 +134,7 @@ def __add__(self, other): def __sub__(self, other): if not isinstance(other,Position): - raise ValueError("Can only subtract a Position from a %s"%self.__class__.__name__) + raise TypeError("Can only subtract a Position from a %s"%self.__class__.__name__) elif isinstance(other, self.__class__): return self.__class__(self.x - other.x, self.y - other.y) else: @@ -163,8 +162,6 @@ class PositionD(Position): """ def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) - if self.x != float(self.x) or self.y != float(self.y): - raise ValueError("PositionD must be initialized with float values") self.x = float(self.x) self.y = float(self.y) @@ -177,7 +174,7 @@ def _check_scalar(self, other, op): if other == float(other): return except (TypeError, ValueError): pass - raise ValueError("Can only %s a PositionD by float values"%op) + raise TypeError("Can only %s a PositionD by float values"%op) class PositionI(Position): @@ -190,7 +187,7 @@ class PositionI(Position): def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) if self.x != int(self.x) or self.y != int(self.y): - raise ValueError("PositionI must be initialized with integer values") + raise TypeError("PositionI must be initialized with integer values") self.x = int(self.x) self.y = int(self.y) @@ -205,4 +202,4 @@ def _check_scalar(self, other, op): if other == int(other): return except (TypeError, ValueError): pass - raise ValueError("Can only %s a PositionI by integer values"%op) + raise TypeError("Can only %s a PositionI by integer values"%op) diff --git a/galsim/pse.py b/galsim/pse.py index 954582fd2d9..240d88eb214 100644 --- a/galsim/pse.py +++ b/galsim/pse.py @@ -27,12 +27,15 @@ import os import sys +from .errors import GalSimError, GalSimValueError, GalSimIncompatibleValuesError + + class PowerSpectrumEstimator(object): """ Class for estimating the shear power spectrum from gridded shears. - The PowerSpectrumEstimator class can be used even on systems where GalSim is not installed. It - just requires Python v2.6 or 2.7 and NumPy. + The PowerSpectrumEstimator class can be used even on systems where GalSim is not installed. + It just requires NumPy. This class stores all the data used in power spectrum estimation that is fixed with the geometry of the problem - the binning and spin weighting factors. @@ -181,7 +184,7 @@ def _bin_power(self, C, ell_weight=None): P,_ = np.histogram(self.l_abs, self.bin_edges, weights=C) count,_ = np.histogram(self.l_abs, self.bin_edges) if (count == 0).any(): - raise RuntimeError("Logarithmic bin definition resulted in >=1 empty bin!") + raise GalSimError("Logarithmic bin definition resulted in >=1 empty bin!") return P/count def estimate(self, g1, g2, weight_EE=False, weight_BB=False, theory_func=None): @@ -206,14 +209,15 @@ def estimate(self, g1, g2, weight_EE=False, weight_BB=False, theory_func=None): from .table import LookupTable # Check for the expected square geometry consistent with the previously-defined grid size. if g1.shape != g2.shape: - raise ValueError("g1 and g2 grids do not have the same shape!") + raise GalSimIncompatibleValuesError( + "g1 and g2 grids do not have the same shape.", g1=g1, g2=g2) if g1.shape[0] != g1.shape[1]: - raise ValueError("Input shear arrays are not square!") + raise GalSimValueError("Input shear arrays must be square.", g1.shape) if g1.shape[0] != self.N: - raise ValueError("Input shear array size is not correct!") + raise GalSimValueError("Input shear array size is not correct!", g1.shape) if not isinstance(weight_EE, bool) or not isinstance(weight_BB, bool): - raise ValueError("Input weight flags must be bools!") + raise TypeError("Input weight flags must be bools!") # Transform g1+j*g2 into Fourier space and rotate into E-B, then separate into E and B. EB = np.fft.ifft2(self.eb_rot * np.fft.fft2(g1 + 1j*g2)) diff --git a/galsim/random.py b/galsim/random.py index fb1736cb354..03b1c66889f 100644 --- a/galsim/random.py +++ b/galsim/random.py @@ -22,7 +22,10 @@ import numpy as np import weakref + from . import _galsim +from .errors import GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError +from .errors import convert_cpp_errors class BaseDeviate(object): """Base class for all the various random deviates. @@ -116,9 +119,11 @@ def reset(self, seed=None): if isinstance(seed, BaseDeviate): self._reset(seed) elif isinstance(seed, (str, int)): - self._rng = self._rng_type(_galsim.BaseDeviateImpl(seed), *self._rng_args) + with convert_cpp_errors(): + self._rng = self._rng_type(_galsim.BaseDeviateImpl(seed), *self._rng_args) elif seed is None: - self._rng = self._rng_type(_galsim.BaseDeviateImpl(0), *self._rng_args) + with convert_cpp_errors(): + self._rng = self._rng_type(_galsim.BaseDeviateImpl(0), *self._rng_args) else: raise TypeError("BaseDeviate must be initialized with either an int or another " "BaseDeviate") @@ -127,7 +132,8 @@ def _reset(self, rng): """Equivalent to self.reset(rng), but rng must be a BaseDeviate (not an int), and there is no type checking. """ - self._rng = self._rng_type(rng._rng, *self._rng_args) + with convert_cpp_errors(): + self._rng = self._rng_type(rng._rng, *self._rng_args) def duplicate(self): """Create a duplicate of the current Deviate object. The subsequent series from each copy @@ -155,8 +161,9 @@ def duplicate(self): """ ret = BaseDeviate.__new__(self.__class__) ret.__dict__.update(self.__dict__) - rng = _galsim.BaseDeviateImpl(self.serialize()) - ret._rng = self._rng_type(rng, *ret._rng_args) + with convert_cpp_errors(): + rng = _galsim.BaseDeviateImpl(self.serialize()) + ret._rng = self._rng_type(rng, *ret._rng_args) return ret def __copy__(self): @@ -170,8 +177,9 @@ def __getstate__(self): def __setstate__(self, d): self.__dict__ = d - rng = _galsim.BaseDeviateImpl(d['rng_str']) - self._rng = self._rng_type(rng, *self._rng_args) + with convert_cpp_errors(): + rng = _galsim.BaseDeviateImpl(d['rng_str']) + self._rng = self._rng_type(rng, *self._rng_args) def clearCache(self): """Clear the internal cache of the Deviate, if any. This is currently only relevant for @@ -298,7 +306,7 @@ class GaussianDeviate(BaseDeviate): """ def __init__(self, seed=None, mean=0., sigma=1.): if sigma < 0.: - raise ValueError("GaussianDeviate sigma must be > 0.") + raise GalSimRangeError("GaussianDeviate sigma must be > 0.", sigma, 0.) self._rng_type = _galsim.GaussianDeviateImpl self._rng_args = (float(mean), float(sigma)) self.reset(seed) @@ -683,8 +691,9 @@ def __init__(self, seed=None, function=None, x_min=None, if interpolant is None: interpolant='linear' if x_min or x_max: - raise TypeError('Cannot pass x_min or x_max alongside a ' - 'filename in arguments to DistDeviate') + raise GalSimIncompatibleValuesError( + "Cannot pass x_min or x_max with a filename argument", + function=function, x_min=x_min, x_max=x_max) function = LookupTable.from_file(function, interpolant=interpolant) x_min = function.x_min x_max = function.x_max @@ -698,28 +707,33 @@ def __init__(self, seed=None, function=None, x_min=None, # but we'd like to throw reasonable errors in that case anyway function(0.6) # A value unlikely to be a singular point of a function except Exception as e: - raise ValueError( - "String function must either be a valid filename or something that "+ - "can eval to a function of x.\n"+ - "Input provided: {0}\n".format(self.__function)+ - "Caught error: {0}".format(e)) + raise GalSimValueError( + "String function must either be a valid filename or something that " + "can eval to a function of x.\n" + "Caught error: {0}".format(e), self.__function) else: - self.__function = weakref.ref(function) # Save the inputs to be used in repr # Check that the function is actually a function - if not (isinstance(function, LookupTable) or hasattr(function, '__call__')): - raise TypeError('Keyword function must be a callable function or a string') + if not hasattr(function, '__call__'): + raise TypeError('function must be a callable function or a string') if interpolant: - raise TypeError('Cannot provide an interpolant with a callable function argument') + raise GalSimIncompatibleValuesError( + "Cannot provide an interpolant with a callable function argument", + interpolant=interpolant, function=function) if isinstance(function, LookupTable): if x_min or x_max: - raise TypeError('Cannot provide x_min or x_max with a LookupTable function '+ - 'argument') + raise GalSimIncompatibleValuesError( + "Cannot provide x_min or x_max with a LookupTable function", + function=function, x_min=x_min, x_max=x_max) x_min = function.x_min x_max = function.x_max else: if x_min is None or x_max is None: - raise TypeError('Must provide x_min and x_max when function argument is a '+ - 'regular python callable function') + raise GalSimIncompatibleValuesError( + "Must provide x_min and x_max when function argument is a regular " + "python callable function", + function=function, x_min=x_min, x_max=x_max) + + self.__function = weakref.ref(function) # Save the inputs to be used in repr # Compute the probability distribution function, pdf(x) if (npoints is None and isinstance(function, LookupTable) and @@ -743,15 +757,13 @@ def __init__(self, seed=None, function=None, x_min=None, # Check that the probability is nonnegative if not np.all(pdf >= 0.): - raise ValueError('Negative probability passed to DistDeviate: %s'%function) + raise GalSimValueError('Negative probability found in DistDeviate.',function) # Compute the cumulative distribution function = int(pdf(x),x) cdf = np.cumsum(pdf) # Quietly renormalize the probability if it wasn't already normalized totalprobability = cdf[-1] - if totalprobability < 0.: - raise ValueError('Negative probability passed to DistDeviate: %s'%function) cdf /= totalprobability self._inverse_cdf = LookupTable(cdf, xarray, interpolant='linear') @@ -773,8 +785,7 @@ def val(self, p): @returns the corresponding x such that p = cdf(x). """ if p<0 or p>1: - raise ValueError('Cannot request cumulative probability value from DistDeviate for ' - 'p<0 or p>1! You entered: %f'%p) + raise GalSimRangeError('Invalid cumulative probability for DistDeviate', p, 0., 1.) return self._inverse_cdf(p) def __call__(self): @@ -799,7 +810,7 @@ def _function(self): return self.__function if isinstance(self.__function, str) else self.__function() def __repr__(self): - return ('galsim.DistDeviate(seed=%r, function=%r, x_min=%r, x_max=%r, interpolant=%r, '+ + return ('galsim.DistDeviate(seed=%r, function=%r, x_min=%r, x_max=%r, interpolant=%r, ' 'npoints=%r)')%(self._seed_repr(), self._function, self._xmin, self._xmax, self._interpolant, self._npoints) def __str__(self): diff --git a/galsim/randwalk.py b/galsim/randwalk.py index c0ea31afe07..b6137961cef 100644 --- a/galsim/randwalk.py +++ b/galsim/randwalk.py @@ -23,6 +23,7 @@ from .gsobject import GSObject from .position import PositionD from .utilities import lazy_property, doc_inherit +from .errors import GalSimRangeError, convert_cpp_errors class RandomWalk(GSObject): """ @@ -124,9 +125,10 @@ def deltas(self): fluxper=self._flux/self._npoints for p in self._points: - d = _galsim.SBDeltaFunction(fluxper, self.gsparams._gsp) - d = _galsim.SBTransform(d, 1.0, 0.0, 0.0, 1.0, _galsim.PositionD(p[0],p[1]), 1.0, - self.gsparams._gsp) + with convert_cpp_errors(): + d = _galsim.SBDeltaFunction(fluxper, self.gsparams._gsp) + d = _galsim.SBTransform(d, 1.0, 0.0, 0.0, 1.0, _galsim.PositionD(p[0],p[1]), 1.0, + self.gsparams._gsp) deltas.append(d) return deltas @@ -135,7 +137,8 @@ def deltas(self): @lazy_property def _sbp(self): - return _galsim.SBAdd(self.deltas, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBAdd(self.deltas, self.gsparams._gsp) @property def input_half_light_radius(self): @@ -195,17 +198,15 @@ def _verify(self): """ from .random import BaseDeviate if not isinstance(self._rng, BaseDeviate): - raise TypeError("rng must be an instance of galsim.BaseDeviate, " - "got %s" % str(self._rng)) + raise TypeError("rng must be an instance of galsim.BaseDeviate, got %s"%self._rng) if self._npoints <= 0: - raise ValueError("npoints must be > 0, got %s" % str(self._npoints)) + raise GalSimRangeError("npoints must be > 0", self._npoints, 1) if self._half_light_radius <= 0.0: - raise ValueError("half light radius must be > 0" - ", got %s" % str(self._half_light_radius)) + raise GalSimRangeError("half light radius must be > 0", self._half_light_radius, 0.) if self._flux < 0.0: - raise ValueError("flux must be >= 0, got %s" % str(self._flux)) + raise GalSimRangeError("flux must be >= 0", self._flux, 0.) def __str__(self): rep='galsim.RandomWalk(%(npoints)d, %(hlr)g, flux=%(flux)g, gsparams=%(gsparams)s)' diff --git a/galsim/real.py b/galsim/real.py index 167713093b7..914ecfd5cf8 100644 --- a/galsim/real.py +++ b/galsim/real.py @@ -32,12 +32,22 @@ """ +import os +import numpy as np + from .gsobject import GSObject +from .gsparams import GSParams from .chromatic import ChromaticSum from .position import PositionD -import os -import numpy as np -from .utilities import doc_inherit +from .utilities import doc_inherit, convert_interpolant +from .interpolant import Quintic +from .interpolatedimage import InterpolatedImage, _InterpolatedKImage +from .image import ImageCD +from .correlatednoise import CovarianceSpectrum +from . import _galsim +from .errors import GalSimError, GalSimValueError, GalSimIncompatibleValuesError +from .errors import GalSimIndexError, convert_cpp_errors + HST_area = 45238.93416 # Area of HST primary mirror in cm^2 from Synphot User's Guide. @@ -195,34 +205,41 @@ def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, from .correlatednoise import UncorrelatedNoise, _BaseCorrelatedNoise from .interpolatedimage import InterpolatedImage from .convolve import Convolve, Deconvolve + from .config import LoggerWrapper if rng is None: rng = BaseDeviate() elif not isinstance(rng, BaseDeviate): - raise TypeError("The rng provided to RealGalaxy constructor is not a BaseDeviate") + raise TypeError("The rng provided to RealGalaxy is not a BaseDeviate") self.rng = rng if flux is not None and flux_rescale is not None: - raise TypeError("Cannot supply a flux and a flux rescaling factor!") + raise GalSimIncompatibleValuesError( + "Cannot supply a flux and a flux rescaling factor.", + flux=flux, flux_rescale=flux_rescale) + + logger = LoggerWrapper(logger) # So don't need to check `if logger:` all the time. if isinstance(real_galaxy_catalog, tuple): # Special (undocumented) way to build a RealGalaxy without needing the rgc directly # by providing the things we need from it. Used by COSMOSGalaxy. self.gal_image, self.psf_image, noise_image, pixel_scale, var = real_galaxy_catalog use_index = 0 # For the logger statements below. - if logger: - logger.debug('RealGalaxy %d: Start RealGalaxy constructor.',use_index) + logger.debug('RealGalaxy %d: Start RealGalaxy constructor.',use_index) self.catalog_file = None self.catalog = '' else: # Get the index to use in the catalog if index is not None: if id is not None or random: - raise AttributeError('Too many methods for selecting a galaxy!') + raise GalSimIncompatibleValuesError( + "Too many methods for selecting a galaxy.", + index=index, id=id, random=random) use_index = index elif id is not None: if random: - raise AttributeError('Too many methods for selecting a galaxy!') + raise GalSimIncompatibleValuesError( + "Too many methods for selecting a galaxy.", id=id, random=random) use_index = real_galaxy_catalog.getIndexForID(id) elif random: ud = UniformDeviate(self.rng) @@ -235,26 +252,24 @@ def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, # Pick another one to try. use_index = int(real_galaxy_catalog.nobjects * ud()) else: - raise AttributeError('No method specified for selecting a galaxy!') - if logger: - logger.debug('RealGalaxy %d: Start RealGalaxy constructor.',use_index) + raise GalSimIncompatibleValuesError( + "No method specified for selecting a galaxy.", + index=index, id=id, random=random) + logger.debug('RealGalaxy %d: Start RealGalaxy constructor.',use_index) # Read in the galaxy, PSF images; for now, rely on pyfits to make I/O errors. self.gal_image = real_galaxy_catalog.getGalImage(use_index) - if logger: - logger.debug('RealGalaxy %d: Got gal_image',use_index) + logger.debug('RealGalaxy %d: Got gal_image',use_index) self.psf_image = real_galaxy_catalog.getPSFImage(use_index) - if logger: - logger.debug('RealGalaxy %d: Got psf_image',use_index) + logger.debug('RealGalaxy %d: Got psf_image',use_index) #self._noise = real_galaxy_catalog.getNoise(use_index, self.rng, gsparams) # We need to duplication some of the RealGalaxyCatalog.getNoise() function, since we # want it to be possible to have the RealGalaxyCatalog in another process, and the # BaseCorrelatedNoise object is not picklable. So we just build it here instead. noise_image, pixel_scale, var = real_galaxy_catalog.getNoiseProperties(use_index) - if logger: - logger.debug('RealGalaxy %d: Got noise_image',use_index) + logger.debug('RealGalaxy %d: Got noise_image',use_index) self.catalog_file = real_galaxy_catalog.getFileName() self.catalog = real_galaxy_catalog @@ -267,8 +282,7 @@ def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, x_interpolant='linear', gsparams=gsparams) self._noise = _BaseCorrelatedNoise(self.rng, ii, noise_image.wcs) self._noise = self._noise.withVariance(var) - if logger: - logger.debug('RealGalaxy %d: Finished building noise',use_index) + logger.debug('RealGalaxy %d: Finished building noise',use_index) # Save any other relevant information as instance attributes self.index = use_index @@ -280,7 +294,7 @@ def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, self._input_flux = flux self._flux_rescale = flux_rescale self._area_norm = area_norm - self._gsparams = gsparams + self._gsparams = GSParams.check(gsparams) # Convert noise_pad to the right noise to pass to InterpolatedImage if noise_pad_size: @@ -292,8 +306,7 @@ def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, self.original_psf = InterpolatedImage( self.psf_image, x_interpolant=x_interpolant, k_interpolant=k_interpolant, flux=1.0, gsparams=gsparams) - if logger: - logger.debug('RealGalaxy %d: Made original_psf',use_index) + logger.debug('RealGalaxy %d: Made original_psf',use_index) # Build the InterpolatedImage of the galaxy. # Use the stepk value of the PSF as a maximum value for stepk of the galaxy. @@ -305,8 +318,7 @@ def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, calculate_stepk=self.original_psf.stepk, calculate_maxk=self.original_psf.maxk, noise_pad=noise_pad, rng=self.rng, gsparams=gsparams) - if logger: - logger.debug('RealGalaxy %d: Made original_gal',use_index) + logger.debug('RealGalaxy %d: Made original_gal',use_index) # Only alter normalization if a change is requested if flux is not None or flux_rescale is not None or area_norm != 1: @@ -324,13 +336,11 @@ def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, # Initialize the _sbp attribute self._conv = Convolve([self.original_gal, psf_inv], gsparams=gsparams) self._sbp = self._conv._sbp - if logger: - logger.debug('RealGalaxy %d: Made gsobject',use_index) + logger.debug('RealGalaxy %d: Made gsobject',use_index) # Save the noise in the image as an accessible attribute self._noise = self._noise.convolvedWith(psf_inv, gsparams) - if logger: - logger.debug('RealGalaxy %d: Finished building RealGalaxy',use_index) + logger.debug('RealGalaxy %d: Finished building RealGalaxy',use_index) @classmethod def makeFromImage(cls, image, PSF, xi, **kwargs): @@ -526,11 +536,15 @@ class RealGalaxyCatalog(object): # So skip any other calculations that might normally be necessary on construction. def __init__(self, file_name=None, sample=None, dir=None, preload=False, logger=None, _nobjects_only=False): + from ._pyfits import pyfits + from .config import LoggerWrapper + if sample is not None and file_name is not None: - raise ValueError("Cannot specify both the sample and file_name!") + raise GalSimIncompatibleValuesError( + "Cannot specify both the sample and file_name.", + sample=sample, file_name=file_name) - from ._pyfits import pyfits - self.file_name, self.image_dir, _ = _parse_files_dirs(file_name, dir, sample) + self.file_name, self.image_dir, self.sample = _parse_files_dirs(file_name, dir, sample) with pyfits.open(self.file_name) as fits: self.cat = fits[1].data @@ -582,7 +596,7 @@ def __init__(self, file_name=None, sample=None, dir=None, preload=False, self.saved_noise_im = {} self.loaded_files = {} - self.logger = logger + self.logger = LoggerWrapper(logger) # The pyfits commands aren't thread safe. So we need to make sure the methods that # use pyfits are not run concurrently from multiple threads. @@ -627,7 +641,7 @@ def getIndexForID(self, id): if id in self.ident: return self.ident.index(id) else: - raise ValueError('ID %s not found in list of IDs'%id) + raise GalSimValueError('ID not found in list of IDs',id, self.ident) def preload(self): """Preload the files into memory. @@ -636,15 +650,13 @@ def preload(self): a big speedup if memory isn't an issue. """ from ._pyfits import pyfits - if self.logger: - self.logger.debug('RealGalaxyCatalog: start preload') + self.logger.debug('RealGalaxyCatalog: start preload') for file_name in np.concatenate((self.gal_file_name , self.psf_file_name)): # numpy sometimes add a space at the end of the string that is not present in # the original file. Stupid. But this next line removes it. file_name = file_name.strip() if file_name not in self.loaded_files: - if self.logger: - self.logger.debug('RealGalaxyCatalog: preloading %s',file_name) + self.logger.debug('RealGalaxyCatalog: preloading %s',file_name) # I use memmap=False, because I was getting problems with running out of # file handles in the great3 real_gal run, which uses a lot of rgc files. # I think there must be a bug in pyfits that leaves file handles open somewhere @@ -660,19 +672,16 @@ def preload(self): def _getFile(self, file_name): from ._pyfits import pyfits if file_name in self.loaded_files: - if self.logger: - self.logger.debug('RealGalaxyCatalog: File %s is already open',file_name) + self.logger.debug('RealGalaxyCatalog: File %s is already open',file_name) f = self.loaded_files[file_name] else: self.loaded_lock.acquire() # Check again in case two processes both hit the else at the same time. if file_name in self.loaded_files: # pragma: no cover - if self.logger: - self.logger.debug('RealGalaxyCatalog: File %s is already open',file_name) + self.logger.debug('RealGalaxyCatalog: File %s is already open',file_name) f = self.loaded_files[file_name] else: - if self.logger: - self.logger.debug('RealGalaxyCatalog: open file %s',file_name) + self.logger.debug('RealGalaxyCatalog: open file %s',file_name) f = pyfits.open(file_name,memmap=False) self.loaded_files[file_name] = f self.loaded_lock.release() @@ -685,21 +694,18 @@ def getBandpass(self): try: bp = real_galaxy_bandpasses[self.band[0].upper()] except KeyError: - raise ValueError("Bandpass not found. To use bandpass '{0}', please add an entry to " - "the galsim.real.real_galaxy_bandpasses " - "dictionary.".format(self.band[0])) + raise GalSimValueError("Bandpass not found. To use this bandpass, please add an entry " + "to the galsim.real.real_galaxy_bandpasses dictionary.", + self.band[0], real_galaxy_bandpasses.keys()) return Bandpass(bp[0], wave_type='nm', zeropoint=bp[1]) def getGalImage(self, i): """Returns the galaxy at index `i` as an Image object. """ from .image import Image - if self.logger: - self.logger.debug('RealGalaxyCatalog %d: Start getGalImage',i) + self.logger.debug('RealGalaxyCatalog %d: Start getGalImage',i) if i >= len(self.gal_file_name): - raise IndexError( - 'index %d given to getGalImage is out of range (0..%d)' - % (i,len(self.gal_file_name)-1)) + raise GalSimIndexError('index out of range (0..%d)'%(len(self.gal_file_name)-1),i) f = self._getFile(self.gal_file_name[i]) # For some reason the more elegant `with gal_lock:` syntax isn't working for me. # It gives an EOFError. But doing an explicit acquire and release seems to work fine. @@ -713,12 +719,9 @@ def getPSFImage(self, i): """Returns the PSF at index `i` as an Image object. """ from .image import Image - if self.logger: - self.logger.debug('RealGalaxyCatalog %d: Start getPSFImage',i) + self.logger.debug('RealGalaxyCatalog %d: Start getPSFImage',i) if i >= len(self.psf_file_name): - raise IndexError( - 'index %d given to getPSFImage is out of range (0..%d)' - % (i,len(self.psf_file_name)-1)) + raise GalSimIndexError('index out of range (0..%d)'%(len(self.psf_file_name)-1),i) f = self._getFile(self.psf_file_name[i]) self.psf_lock.acquire() array = f[self.psf_hdu[i]].data @@ -740,26 +743,21 @@ def getNoiseProperties(self, i): as a tuple (im, scale, var). """ from .image import Image - if self.logger: - self.logger.debug('RealGalaxyCatalog %d: Start getNoise',i) + self.logger.debug('RealGalaxyCatalog %d: Start getNoise',i) if self.noise_file_name is None: im = None else: if i >= len(self.noise_file_name): - raise IndexError( - 'index %d given to getNoise is out of range (0..%d)'%( - i,len(self.noise_file_name)-1)) + raise GalSimIndexError('index out of range (0..%d)'%(len(self.noise_file_name)-1),i) if self.noise_file_name[i] in self.saved_noise_im: im = self.saved_noise_im[self.noise_file_name[i]] - if self.logger: - self.logger.debug('RealGalaxyCatalog %d: Got saved noise im',i) + self.logger.debug('RealGalaxyCatalog %d: Got saved noise im',i) else: self.noise_lock.acquire() # Again, a second check in case two processes get here at the same time. - if self.noise_file_name[i] in self.saved_noise_im: + if self.noise_file_name[i] in self.saved_noise_im: # pragma: no cover im = self.saved_noise_im[self.noise_file_name[i]] - if self.logger: - self.logger.debug('RealGalaxyCatalog %d: Got saved noise im',i) + self.logger.debug('RealGalaxyCatalog %d: Got saved noise im',i) else: from ._pyfits import pyfits with pyfits.open(self.noise_file_name[i]) as fits: @@ -767,8 +765,7 @@ def getNoiseProperties(self, i): im = Image(np.ascontiguousarray(array.astype(np.float64)), scale=self.pixel_scale[i]) self.saved_noise_im[self.noise_file_name[i]] = im - if self.logger: - self.logger.debug('RealGalaxyCatalog %d: Built noise im',i) + self.logger.debug('RealGalaxyCatalog %d: Built noise im',i) self.noise_lock.release() return im, self.pixel_scale[i], self.variance[i] @@ -834,30 +831,30 @@ def _parse_files_dirs(file_name, image_dir, sample): use_sample = None else: use_sample = sample - if use_sample != '25.2' and use_sample != '23.5': - raise ValueError("Sample name not recognized: %s"%use_sample) - # after that piece of code, use_sample is either "23.5", "25.2" (if using one of the default - # catalogs) or it is still None, if a file_name was given. if file_name is None: file_name = 'real_galaxy_catalog_' + use_sample + '.fits' if image_dir is None: + use_meta_dir = True # Used to give a more helpful error message image_dir = os.path.join(meta_data.share_dir, 'COSMOS_'+use_sample+'_training_sample') + else: + use_meta_dir = False full_file_name = os.path.join(image_dir,file_name) - if not os.path.isfile(full_file_name): - raise RuntimeError('No RealGalaxy catalog found in %s. '%image_dir + - 'Run the program galsim_download_cosmos -s %s '%use_sample + - 'to download catalog and accompanying image files.') + if not os.path.isfile(full_file_name) and use_meta_dir: + if use_sample not in ('23.5', '25.2'): + raise GalSimValueError("Sample name not recognized.",use_sample, ('23.5', '25.2')) + else: + raise OSError('No RealGalaxy catalog found in %s. Run the program ' + 'galsim_download_cosmos -s %s to download catalog and accompanying ' + 'image files.'%(image_dir, use_sample)) elif image_dir is None: full_file_name = file_name image_dir = os.path.dirname(file_name) else: full_file_name = os.path.join(image_dir,file_name) if not os.path.isfile(full_file_name): - raise IOError(full_file_name+' not found.') - if not os.path.isdir(image_dir): - raise IOError(image_dir+' directory does not exist!') + raise OSError(full_file_name+' not found.') return full_file_name, image_dir, use_sample @@ -998,49 +995,48 @@ class ChromaticRealGalaxy(ChromaticSum): There are no additional methods for ChromaticRealGalaxy beyond the usual ChromaticObject methods. """ - def __init__(self, real_galaxy_catalogs=None, index=None, id=None, random=False, rng=None, + def __init__(self, real_galaxy_catalogs, index=None, id=None, random=False, rng=None, gsparams=None, logger=None, **kwargs): from .random import BaseDeviate, UniformDeviate from .bounds import BoundsI from .interpolatedimage import InterpolatedImage from .correlatednoise import _BaseCorrelatedNoise + from .config import LoggerWrapper + if rng is None: rng = BaseDeviate() elif not isinstance(rng, BaseDeviate): - raise TypeError("The rng provided to ChromaticRealGalaxy constructor " - "is not a BaseDeviate") + raise TypeError("The rng provided to ChromaticRealGalaxy is not a BaseDeviate") self.rng = rng - if real_galaxy_catalogs is None: - raise ValueError("No RealGalaxyCatalog(s) specified!") + logger = LoggerWrapper(logger) # So don't need to check `if logger:` all the time. # Get the index to use in the catalog if index is not None: if id is not None or random: - raise AttributeError('Too many methods for selecting a galaxy!') + raise GalSimIncompatibleValuesError( + "Too many methods for selecting a galaxy.", index=index, id=id, random=random) use_index = index elif id is not None: if random: - raise AttributeError('Too many methods for selecting a galaxy!') + raise GalSimIncompatibleValuesError( + "Too many methods for selecting a galaxy.", id=id, random=random) use_index = real_galaxy_catalogs[0].getIndexForID(id) elif random: uniform_deviate = UniformDeviate(self.rng) use_index = int(real_galaxy_catalogs[0].nobjects * uniform_deviate()) else: - raise AttributeError('No method specified for selecting a galaxy!') - if logger: - logger.debug('ChromaticRealGalaxy %d: Start ChromaticRealGalaxy constructor.', - use_index) + raise GalSimIncompatibleValuesError( + "No method specified for selecting a galaxy.", index=index, id=id, random=random) + logger.debug('ChromaticRealGalaxy %d: Start ChromaticRealGalaxy constructor.', use_index) self.index = use_index # Read in the galaxy, PSF images; for now, rely on pyfits to make I/O errors. imgs = [rgc.getGalImage(use_index) for rgc in real_galaxy_catalogs] - if logger: - logger.debug('ChromaticRealGalaxy %d: Got gal_image', use_index) + logger.debug('ChromaticRealGalaxy %d: Got gal_image', use_index) PSFs = [rgc.getPSF(use_index) for rgc in real_galaxy_catalogs] - if logger: - logger.debug('ChromaticRealGalaxy %d: Got psf', use_index) + logger.debug('ChromaticRealGalaxy %d: Got psf', use_index) bands = [rgc.getBandpass() for rgc in real_galaxy_catalogs] @@ -1058,8 +1054,7 @@ def __init__(self, real_galaxy_catalogs=None, index=None, id=None, random=False, xi = _BaseCorrelatedNoise(self.rng, ii, noise_image.wcs) xi = xi.withVariance(var) xis.append(xi) - if logger: - logger.debug('ChromaticRealGalaxy %d: Got noise_image',use_index) + logger.debug('ChromaticRealGalaxy %d: Got noise_image',use_index) self.catalog_files = [rgc.getFileName() for rgc in real_galaxy_catalogs] self._initialize(imgs, bands, xis, PSFs, gsparams=gsparams, **kwargs) @@ -1134,12 +1129,6 @@ def makeFromImages(cls, images, bands, PSFs, xis, **kwargs): def _initialize(self, imgs, bands, xis, PSFs, SEDs=None, k_interpolant=None, maxk=None, pad_factor=4., area_norm=1.0, noise_pad_size=0, gsparams=None): - from .interpolant import Quintic - from .interpolatedimage import InterpolatedImage, _InterpolatedKImage - from .image import ImageCD - from . import utilities - from . import _galsim - from .correlatednoise import CovarianceSpectrum if SEDs is None: SEDs = self._poly_SEDs(bands) @@ -1148,11 +1137,11 @@ def _initialize(self, imgs, bands, xis, PSFs, if k_interpolant is None: k_interpolant = Quintic(tol=1e-4) else: - k_interpolant = utilities.convert_interpolant(k_interpolant) + k_interpolant = convert_interpolant(k_interpolant) self._area_norm = area_norm self._k_interpolant = k_interpolant - self._gsparams = gsparams + self._gsparams = GSParams.check(gsparams) NSED = len(self.SEDs) Nim = len(imgs) @@ -1214,8 +1203,13 @@ def _initialize(self, imgs, bands, xis, PSFs, # Get Fourier-space representations of input imgs. kimgs = np.empty((Nim, nk, nk), dtype=np.complex128) + if noise_pad_size == 0: + noise_pad = 0. + for i, (img, xi) in enumerate(zip(imgs, xis)): - ii = InterpolatedImage(img, noise_pad_size=noise_pad_size, noise_pad=xi, + if noise_pad_size != 0: + noise_pad = xi + ii = InterpolatedImage(img, noise_pad_size=noise_pad_size, noise_pad=noise_pad, rng=self.rng, pad_factor=pad_factor) kimgs[i] = ii.drawKImage(nx=nk, ny=nk, scale=stepk).array @@ -1235,10 +1229,11 @@ def _initialize(self, imgs, bands, xis, PSFs, # Solve the weighted linear least squares problem for each Fourier mode. This is # effectively a constrained chromatic deconvolution. Take advantage of symmetries. - _galsim.ComputeCRGCoefficients( - coef.ctypes.data, Sigma.ctypes.data, - w.ctypes.data, kimgs.ctypes.data, PSF_eff_kimgs.ctypes.data, - NSED, Nim, nk, nk) + with convert_cpp_errors(): + _galsim.ComputeCRGCoefficients( + coef.ctypes.data, Sigma.ctypes.data, + w.ctypes.data, kimgs.ctypes.data, PSF_eff_kimgs.ctypes.data, + NSED, Nim, nk, nk) # Reorder these so they correspond to (NSED, nky, nkx) and (NSED, NSED, nky, nkx) shapes. coef = np.transpose(coef, (2,0,1)) diff --git a/galsim/scene.py b/galsim/scene.py index 1822022dd0c..bbbd9d8ba78 100644 --- a/galsim/scene.py +++ b/galsim/scene.py @@ -26,6 +26,8 @@ import os from .real import RealGalaxy, RealGalaxyCatalog +from .errors import GalSimError, GalSimValueError, GalSimIncompatibleValuesError +from .errors import GalSimNotImplementedError, galsim_warn # Below is a number that is needed to relate the COSMOS parametric galaxy fits to quantities that # GalSim needs to make a GSObject representing that fit. It is simply the pixel scale, in arcsec, @@ -165,17 +167,24 @@ def __init__(self, file_name=None, sample=None, dir=None, preload=False, use_real=True, exclusion_level='marginal', min_hlr=0, max_hlr=0., min_flux=0., max_flux=0., _nobjects_only=False): if sample is not None and file_name is not None: - raise ValueError("Cannot specify both the sample and file_name!") + raise GalSimIncompatibleValuesError( + "Cannot specify both the sample and file_name.", + sample=sample, file_name=file_name) from ._pyfits import pyfits from .real import _parse_files_dirs self.use_real = use_real - if exclusion_level not in ['none', 'bad_stamp', 'bad_fits', 'marginal']: - raise ValueError("Invalid value of exclusion_level: %s"%exclusion_level) + # We'll set these up if and when we need them. + self._bandpass = None + self._sed = None + + if exclusion_level not in ('none', 'bad_stamp', 'bad_fits', 'marginal'): + raise GalSimValueError("Invalid value of exclusion_level.", exclusion_level, + ('none', 'bad_stamp', 'bad_fits', 'marginal')) # Start by parsing the file name - full_file_name, _, self.use_sample = _parse_files_dirs(file_name, dir, sample) + self.full_file_name, _, self.use_sample = _parse_files_dirs(file_name, dir, sample) if self.use_real and not _nobjects_only: # First, do the easy thing: real galaxies. We make the galsim.RealGalaxyCatalog() @@ -185,37 +194,24 @@ def __init__(self, file_name=None, sample=None, dir=None, preload=False, # The fits name has _fits inserted before the .fits ending. # Note: don't just use k = -5 in case it actually ends with .fits.fz - k = self.real_cat.file_name.find('.fits') - param_file_name = self.real_cat.file_name[:k] + '_fits' + self.real_cat.file_name[k:] + param_file_name = self.real_cat.file_name.replace('.fits', '_fits.fits') with pyfits.open(param_file_name) as fits: self.param_cat = fits[1].data else: + self.real_cat = None try: # Read in data. - with pyfits.open(full_file_name) as fits: + with pyfits.open(self.full_file_name) as fits: self.param_cat = fits[1].data # Check if this was the right file. It should have a 'fit_status' column. self.param_cat['fit_status'] - except KeyError: # pragma: no cover + except KeyError: # But if that doesn't work, then the name might be the name of the real catalog, # so try adding _fits to it as above. - k = full_file_name.find('.fits') - param_file_name = full_file_name[:k] + '_fits' + full_file_name[k:] + param_file_name = self.full_file_name.replace('.fits', '_fits.fits') with pyfits.open(param_file_name) as fits: self.param_cat = fits[1].data - # Check for the old-style parameter catalog - if 'fit_dvc_btt' not in self.param_cat.dtype.names: # pragma: no cover - # This will fail if they try to make a parametric galaxy. - # Don't raise an exception here, since they might not care about that. - # But give them some guidance about the error they will get if they - # do try to make a parametric galaxy. - import warnings - warnings.warn( - 'You seem to have an old version of the COSMOS parameter file. '+ - 'Please run `galsim_download_cosmos -s %s` '%self.use_sample+ - 'to re-download the COSMOS catalog.') - # NB. The pyfits FITS_Rec class has a bug where it makes a copy of the full # record array in each record (e.g. in getParametricRecord) and then doesn't # garbage collect it until the top-level FITS_Record goes out of scope. @@ -228,81 +224,69 @@ def __init__(self, file_name=None, sample=None, dir=None, preload=False, self.param_cat = np.array(self.param_cat, copy=True) self.orig_index = np.arange(len(self.param_cat)) - mask = np.ones(len(self.orig_index), dtype=bool) + self._apply_exclusion(exclusion_level, min_hlr, max_hlr, min_flux, max_flux) + - if exclusion_level in ['marginal', 'bad_stamp']: + def _apply_exclusion(self, exclusion_level, min_hlr=0, max_hlr=0, min_flux=0, max_flux=0): + from ._pyfits import pyfits + mask = np.ones(len(self.orig_index), dtype=bool) + if exclusion_level in ('marginal', 'bad_stamp'): # First, read in what we need to impose selection criteria, if the appropriate # exclusion_level was chosen. - k = full_file_name.find('.fits') + + # This should work if the user passed in (or we defaulted to) the real galaxy + # catalog name: + selection_file_name = self.full_file_name.replace('.fits', '_selection.fits') try: - # This should work if the user passed in (or we defaulted to) the real galaxy - # catalog name: - selection_file_name = full_file_name[:k] + '_selection' + full_file_name[k:] + with pyfits.open(selection_file_name) as fits: + self.selection_cat = fits[1].data + except (IOError, OSError): + # There's one more option: full_file_name might be the parametric fit file, so + # we have to strip off the _fits.fits (instead of just the .fits) + selection_file_name = self.full_file_name.replace('_fits', '_selection') try: with pyfits.open(selection_file_name) as fits: self.selection_cat = fits[1].data - except IOError: - # There's one more option: full_file_name might be the parametric fit file, so - # we have to strip off the _fits.fits (instead of just the .fits) - selection_file_name = full_file_name[:k-5] + '_selection' + full_file_name[k:] - with pyfits.open(selection_file_name) as fits: - self.selection_cat = fits[1].data - - - # At this point we've read in the catalog one way or another (otherwise we would - # have gotten tossed out of this part of the code to throw an IOError). So, we can - # proceed to select galaxies in a way that excludes suspect postage stamps (e.g., - # with deblending issues), suspect parametric model fits, or both of the above plus - # marginal ones. These two options for 'exclusion_level' involve placing cuts on - # the S/N of the object detection in the original postage stamp, and on issues with - # masking that can indicate deblending or detection failures. These cuts were used - # in GREAT3. In the case of the masking cut, in some cases there are messed up ones - # that have a 0 for self.selection_cat['peak_image_pixel_count']. To make sure we - # don't divide by zero (generating a RuntimeWarning), and still eliminate those, we - # will first set that column to 1.e-5. We choose a sample-dependent mask ratio cut, - # since this depends on the peak object flux, which will differ for the two samples - # (and we can't really cut on this for arbitrary user-defined samples). - if self.use_sample == "23.5": - cut_ratio = 0.2 - sn_limit = 20.0 - else: - cut_ratio = 0.8 - sn_limit = 12.0 - div_val = self.selection_cat['peak_image_pixel_count'] - div_val[div_val == 0.] = 1.e-5 - mask &= ( (self.selection_cat['sn_ellip_gauss'] >= sn_limit) & - ((self.selection_cat['min_mask_dist_pixels'] > 11.0) | - (self.selection_cat['average_mask_adjacent_pixel_count'] / \ - div_val < cut_ratio)) ) - except IOError: - # We can't make any of the above cuts (or any later ones that depend on the - # selection catalog) because we couldn't find the selection catalog. Bummer. Warn - # the user, and move on. - self.selection_cat = None - import warnings - warnings.warn( - 'File with GalSim selection criteria not found! '+ - 'Not all of the requested exclusions will be performed. '+ - 'Run the program `galsim_download_cosmos -s %s` '%self.use_sample+ - 'to get the necessary selection file.') + except (IOError, OSError): # pragma: no cover + raise OSError("File with GalSim selection criteria not found. " + "Run the program `galsim_download_cosmos -s %s` to get the " + "necessary selection file."%(self.use_sample)) + + # We proceed to select galaxies in a way that excludes suspect postage stamps (e.g., + # with deblending issues), suspect parametric model fits, or both of the above plus + # marginal ones. These two options for 'exclusion_level' involve placing cuts on + # the S/N of the object detection in the original postage stamp, and on issues with + # masking that can indicate deblending or detection failures. These cuts were used + # in GREAT3. In the case of the masking cut, in some cases there are messed up ones + # that have a 0 for self.selection_cat['peak_image_pixel_count']. To make sure we + # don't divide by zero (generating a RuntimeWarning), and still eliminate those, we + # will first set that column to 1.e-5. We choose a sample-dependent mask ratio cut, + # since this depends on the peak object flux, which will differ for the two samples + # (and we can't really cut on this for arbitrary user-defined samples). + if self.use_sample == "23.5": + cut_ratio = 0.2 + sn_limit = 20.0 + else: + cut_ratio = 0.8 + sn_limit = 12.0 + div_val = self.selection_cat['peak_image_pixel_count'] + div_val[div_val == 0.] = 1.e-5 + mask &= ( (self.selection_cat['sn_ellip_gauss'] >= sn_limit) & + ((self.selection_cat['min_mask_dist_pixels'] > 11.0) | + (self.selection_cat['average_mask_adjacent_pixel_count'] / \ + div_val < cut_ratio)) ) # Finally, impose a cut that the total flux in the postage stamp should be positive, # which excludes a tiny number of galaxies (of order 10 in each sample) with some sky # subtraction or deblending errors. Some of these are eliminated by other cuts when # using exclusion_level='marginal'. - if hasattr(self,'real_cat'): - if hasattr(self.real_cat, 'stamp_flux'): - mask &= self.real_cat.stamp_flux > 0 - else: - import warnings - warnings.warn( - 'This version of the COSMOS catalog does not have info about total flux in '+ - 'the galaxy postage stamps. Exclusion of negative-flux stamps in advance '+ - 'cannot be done. '+ - 'Run the program `galsim_download_cosmos -s %s` '%self.use_sample+ - 'to get the updated catalog with this information precomputed.') - - if exclusion_level in ['bad_fits', 'marginal']: + if self.real_cat is not None: + if not hasattr(self.real_cat, 'stamp_flux'): # pragma: no cover + raise OSError("You still have the old COSMOS catalog. Run the program " + "`galsim_download_cosmos -s %s` to upgrade."%(self.use_sample)) + mask &= self.real_cat.stamp_flux > 0 + + if exclusion_level in ('bad_fits', 'marginal'): # This 'exclusion_level' involves eliminating failed parametric fits (bad fit status # flags). In this case we only get rid of those with failed bulge+disk AND failed # Sersic fits, so there is no viable parametric model for the galaxy. @@ -326,52 +310,23 @@ def __init__(self, file_name=None, sample=None, dir=None, preload=False, # Some fit parameters can indicate a likely sky subtraction error: very high sersic n # AND abnormally large half-light radius (>1 arcsec). if 'hlr' not in self.param_cat.dtype.names: # pragma: no cover - # This is the circularized HLR in arcsec, which we have to compute from the stored - # parametric fits. - hlr = cosmos_pix_scale * self.param_cat['sersicfit'][:,1] * \ - np.sqrt(self.param_cat['sersicfit'][:,2]) - else: - # This is the pre-computed circularized HLR in arcsec. - hlr = self.param_cat['hlr'][:,0] + raise OSError("You still have the old COSMOS catalog. Run the program " + "`galsim_download_cosmos -s %s` to upgrade."%(self.use_sample)) + hlr = self.param_cat['hlr'][:,0] n = self.param_cat['sersicfit'][:,2] mask &= ( (n < 5) | (hlr < 1.) ) # Major flux differences in the parametric model vs. the COSMOS catalog can indicate fit # issues, deblending problems, etc. - if self.selection_cat is not None: - mask &= ( np.abs(self.selection_cat['dmag']) < 0.8) + mask &= ( np.abs(self.selection_cat['dmag']) < 0.8) if min_hlr > 0. or max_hlr > 0. or min_flux > 0. or max_flux > 0.: if 'hlr' not in self.param_cat.dtype.names: # pragma: no cover - # Check if they have a new version of the selection catalog that has precomputed - # fluxes etc. If not, do the calculations, which include some approximations in - # getting the flux. - import warnings - warnings.warn( - 'You seem to have an old version of the COSMOS parameter file. '+ - 'Please run `galsim_download_cosmos -s %s` '%self.use_sample+ - 'to re-download the COSMOS catalog to get faster and more accurate selection.') - - sparams = self.param_cat['sersicfit'] - hlr_pix = sparams[:,1] - n = sparams[:,2] - q = sparams[:,3] - hlr = cosmos_pix_scale*hlr_pix*np.sqrt(q) - if min_flux > 0. or max_flux > 0.: - flux_hlr = sparams[:,0] - # The prefactor for n=4 is 3.607. For n=1, it is 1.901. - # It's not linear in these values, but for the sake of efficiency and the - # ability to work on the whole array at once, just linearly interpolate. - # This was improved as part of issue #693, for which part of the work involved - # updating the catalogs to include precomputed fluxes and radii. So as shown - # below, if that info is in the catalog we just use it directly instead of using - # this approximate calculation. - #prefactor = ( (n-1.)*3.607 + (4.-n)*1.901 ) / (4.-1.) - prefactor = ((3.607-1.901)/3.) * n + (4.*1.901 - 1.*3.607)/3. - flux = 2.0*np.pi*prefactor*(hlr**2)*flux_hlr/cosmos_pix_scale**2 - else: - hlr = self.param_cat['hlr'][:,0] # sersic half-light radius - flux = self.param_cat['flux'][:,0] + raise OSError("You still have the old COSMOS catalog. Run the program " + "`galsim_download_cosmos -s %s` to upgrade."%(self.use_sample)) + + hlr = self.param_cat['hlr'][:,0] # sersic half-light radius + flux = self.param_cat['flux'][:,0] if min_hlr > 0.: mask &= (hlr > min_hlr) @@ -388,10 +343,10 @@ def __init__(self, file_name=None, sample=None, dir=None, preload=False, # We need this method because the config apparatus will use this via a Proxy, and they cannot # access attributes directly -- just call methods. So this is how we get nobjects there. def getNObjects(self) : return self.nobjects - + def getUseSample(self): return self.use_sample def getOrigIndex(self, index): return self.orig_index[index] - def getNTot(self) : return len(self.param_cat) + def __len__(self): return self.nobjects def makeGalaxy(self, index=None, gal_type=None, chromatic=False, noise_pad_size=5, deep=False, sersic_prec=0.05, rng=None, n_random=None, gsparams=None): @@ -470,22 +425,26 @@ def makeGalaxy(self, index=None, gal_type=None, chromatic=False, noise_pad_size= @returns Either a GSObject or a ChromaticObject depending on the value of `chromatic`, or a list of them if `index` is an iterable. """ + return self._makeGalaxy(self, index, gal_type, chromatic, noise_pad_size, + deep, sersic_prec, rng, n_random, gsparams) + + @staticmethod + def _makeGalaxy(self, index=None, gal_type=None, chromatic=False, noise_pad_size=5, + deep=False, sersic_prec=0.05, rng=None, n_random=None, gsparams=None): from .random import BaseDeviate - if not self.use_real: + if not self.canMakeReal(): if gal_type is None: gal_type = 'parametric' elif gal_type != 'parametric': - raise ValueError("Only 'parametric' galaxy type is allowed when use_real == False") + raise GalSimIncompatibleValuesError( + "Only 'parametric' galaxy type is allowed when use_real == False", + gal_type=gal_type, use_real=self.canMakeReal()) else: if gal_type is None: gal_type = 'real' - if gal_type not in ['real', 'parametric']: - raise ValueError("Invalid galaxy type %r"%gal_type) - - # We'll set these up if and when we need them. - self._bandpass = None - self._sed = None + if gal_type not in ('real', 'parametric'): + raise GalSimValueError("Invalid galaxy type %r", gal_type, ('real', 'parametric')) # Make rng if we will need it. if index is None or gal_type == 'real': @@ -500,8 +459,8 @@ def makeGalaxy(self, index=None, gal_type=None, chromatic=False, noise_pad_size= index = self.selectRandomIndex(n_random, rng=rng) else: if n_random is not None: - import warnings - warnings.warn("Ignoring input n_random, since indices were specified!") + raise GalSimIncompatibleValuesError( + "Cannot specify both index and n_random", n_random=n_random, index=index) if hasattr(index, '__iter__'): indices = index @@ -512,51 +471,52 @@ def makeGalaxy(self, index=None, gal_type=None, chromatic=False, noise_pad_size= # call the appropriate helper routine for that case. if gal_type == 'real': if chromatic: - raise RuntimeError("Cannot yet make real chromatic galaxies!") - gal_list = self._makeReal(indices, noise_pad_size, rng, gsparams) - else: - # If no pre-selection was done based on radius or flux, then we won't have checked - # whether we're using the old or new catalog (the latter of which has a lot of - # precomputations done). Just in case, let's check here, though it does seem like a bit - # of overkill to emit this warning each time. - if 'hlr' not in self.param_cat.dtype.names: # pragma: no cover - import warnings - warnings.warn( - 'You seem to have an old version of the COSMOS parameter file. '+ - 'Please run `galsim_download_cosmos -s %s` '%self.use_sample+ - 'to re-download the COSMOS catalog '+ - 'and take advantage of pre-computation of many quantities..') + raise GalSimNotImplementedError("Cannot yet make real chromatic galaxies!") + gal_list = [] + for idx in indices: + real_params = self.getRealParams(idx) + gal = RealGalaxy(real_params, noise_pad_size=noise_pad_size, rng=rng, + gsparams=gsparams) + gal_list.append(gal) - gal_list = self._makeParametric(indices, chromatic, sersic_prec, gsparams) + else: + if chromatic: + bandpass = self.getBandpass() + sed = self.getSED() + else: + bandpass = None + sed = None + gal_list = [] + for idx in indices: + record = self.getParametricRecord(idx) + gal = COSMOSCatalog._buildParametric(record, sersic_prec, gsparams, + chromatic, bandpass, sed) + gal_list.append(gal) # If trying to use the 23.5 sample and "fake" a deep sample, rescale the size and flux as # suggested in the GREAT3 handbook. if deep: - if self.use_sample == '23.5': + if self.getUseSample() == '23.5': # Rescale the flux to get a limiting mag of 25 in F814W when starting with a # limiting mag of 23.5. Make the galaxies a factor of 0.6 smaller and appropriately # fainter. flux_factor = 10.**(-0.4*1.5) size_factor = 0.6 gal_list = [ gal.dilate(size_factor) * flux_factor for gal in gal_list ] - elif self.use_sample == '25.2': - import warnings - warnings.warn( - 'Ignoring `deep` argument, because the sample being used already '+ - 'corresponds to a flux limit of F814W<25.2') + elif self.getUseSample() == '25.2': + galsim_warn("Ignoring `deep` argument, because the sample being used already " + "corresponds to a flux limit of F814W<25.2") else: - import warnings - warnings.warn( - 'Ignoring `deep` argument, because the sample being used does not '+ - 'corresponds to a flux limit of F814W<23.5') + galsim_warn("Ignoring `deep` argument, because the sample being used does not " + "corresponds to a flux limit of F814W<23.5") # Store the orig_index as gal.index regardless of whether we have a RealGalaxy or not. - # It gets set by _makeReal, but not by _makeParametric. + # It gets set as part of making a real galaxy, but not by _buildParametric. # And if we are doing the deep scaling, then it gets messed up by that. # So just put it in here at the end to be sure. for gal, idx in zip(gal_list, indices): - gal.index = self.orig_index[idx] - if hasattr(gal, 'original'): gal.original.index = self.orig_index[idx] + gal.index = self.getOrigIndex(idx) + if hasattr(gal, 'original'): gal.original.index = gal.index if hasattr(index, '__iter__'): return gal_list @@ -582,13 +542,12 @@ def selectRandomIndex(self, n_random=1, rng=None, _n_rng_calls=False): if rng is None: rng = BaseDeviate() - if hasattr(self, 'real_cat') and hasattr(self.real_cat, 'weight'): + if hasattr(self.real_cat, 'weight'): use_weights = self.real_cat.weight[self.orig_index] else: - import warnings - warnings.warn('Selecting random object without correcting for catalog-level ' - 'selection effects. This correction requires the existence of ' - 'real catalog with valid weights in addition to parametric one.') + galsim_warn("Selecting random object without correcting for catalog-level " + "selection effects. This correction requires the existence of " + "real catalog with valid weights in addition to parametric one.") use_weights = None # By default, get the number of RNG calls. We then decide whether or not to return them @@ -607,49 +566,40 @@ def selectRandomIndex(self, n_random=1, rng=None, _n_rng_calls=False): else: return index[0] - def _makeReal(self, indices, noise_pad_size, rng, gsparams): - return [ RealGalaxy(self.real_cat, index=self.orig_index[i], - noise_pad_size=noise_pad_size, rng=rng, gsparams=gsparams) - for i in indices ] - - def _makeParametric(self, indices, chromatic, sersic_prec, gsparams): + def getBandpass(self): from .bandpass import Bandpass + # Defer making the Bandpass and reading in SEDs until we actually are going to use them. + # It's not a huge calculation, but the thin() call especially isn't trivial. + + if self._bandpass is None: + # We have to set an appropriate zeropoint. This is slightly complicated: The + # nominal COSMOS zeropoint for single-orbit depth (2000s of usable exposure time, + # across 4 dithered exposures) is supposedly 25.94. But the science images that we + # are using were normalized to count rate, not counts, meaning that an object with + # mag=25.94 has a count rate of 1 photon/sec, not 1 photon total. Since we've + # declared our flux normalization for the outputs to be appropriate for a 1s + # exposure, we use this zeropoint directly. + # This means that when drawing chromatic parametric galaxies, the outputs will be + # properly normalized in terms of counts. + zp = 25.94 + self._bandpass = Bandpass('ACS_wfc_F814W.dat', wave_type='nm').withZeropoint(zp) + return self._bandpass + + def getSED(self): from .sed import SED - if chromatic: - # Defer making the Bandpass and reading in SEDs until we actually are going to use them. - # It's not a huge calculation, but the thin() call especially isn't trivial. - if self._bandpass is None: - # We have to set an appropriate zeropoint. This is slightly complicated: The - # nominal COSMOS zeropoint for single-orbit depth (2000s of usable exposure time, - # across 4 dithered exposures) is supposedly 25.94. But the science images that we - # are using were normalized to count rate, not counts, meaning that an object with - # mag=25.94 has a count rate of 1 photon/sec, not 1 photon total. Since we've - # declared our flux normalization for the outputs to be appropriate for a 1s - # exposure, we use this zeropoint directly. - zp = 25.94 - self._bandpass = Bandpass('ACS_wfc_F814W.dat', wave_type='nm').withZeropoint(zp) - # This means that when drawing chromatic parametric galaxies, the outputs will be - # properly normalized in terms of counts. - - # Read in some SEDs. We are using some fairly truncated and thinned ones, because - # in any case the SED assignment here is somewhat arbitrary and should not be taken - # too seriously. - self._sed = [ - # bulge - SED('CWW_E_ext_more.sed', wave_type='Ang', flux_type='flambda'), - # disk - SED('CWW_Scd_ext_more.sed', wave_type='Ang', flux_type='flambda'), - # intermediate - SED('CWW_Sbc_ext_more.sed', wave_type='Ang', flux_type='flambda')] - - gal_list = [] - for index in indices: - record = self.getParametricRecord(index) - gal = self._buildParametric(record, sersic_prec, gsparams, - chromatic, self._bandpass, self._sed) - gal_list.append(gal) - - return gal_list + if self._sed is None: + # Read in some SEDs. We are using some fairly truncated and thinned ones, because + # in any case the SED assignment here is somewhat arbitrary and should not be taken + # too seriously. + self._sed = [ + # bulge + SED('CWW_E_ext_more.sed', wave_type='Ang', flux_type='flambda'), + # disk + SED('CWW_Scd_ext_more.sed', wave_type='Ang', flux_type='flambda'), + # intermediate + SED('CWW_Sbc_ext_more.sed', wave_type='Ang', flux_type='flambda') + ] + return self._sed @staticmethod def _round_sersic(n, sersic_prec): @@ -679,45 +629,12 @@ def _buildParametric(record, sersic_prec, gsparams, chromatic, bandpass=None, se bparams = record['bulgefit'] sparams = record['sersicfit'] if 'hlr' not in record: # pragma: no cover - # This code is here for backwards compatibility; if they have an old version of the - # catalog, then we have to do all calculations. - # - # Get the status flag for the fits. Entries 0 and 4 in 'fit_status' are relevant for - # bulgefit and sersicfit, respectively. - bstat = record['fit_status'][0] - sstat = record['fit_status'][4] - # Get the precomputed bulge-to-total flux ratio for the 2-component fits. - dvc_btt = record['fit_dvc_btt'] - # Get the precomputed median absolute deviation for the 1- and 2-component fits. These - # quantities are used to ascertain whether the 2-component fit is really justified, or - # if the 1-component Sersic fit is sufficient to describe the galaxy light profile. - bmad = record['fit_mad_b'] - smad = record['fit_mad_s'] - - # First decide if we can / should use bulgefit, otherwise sersicfit. This decision - # process depends on: the status flags for the fits, the bulge-to-total ratios (if near - # 0 or 1, just use single component fits), the sizes for the bulge and disk (if <=0 then - # use single component fits), the axis ratios for the bulge and disk (if <0.051 then use - # single component fits), and a comparison of the median absolute deviations to see - # which is better. The reason for the 0.051 cutoff is that the fits were bound at 0.05 - # as a minimum, so anything below 0.051 generally means that the fitter hit the boundary - # for the 2-component fits, typically meaning that we don't have enough information to - # make reliable 2-component fits. - use_bulgefit = True - if ( bstat < 1 or bstat > 4 or dvc_btt < 0.1 or dvc_btt > 0.9 or - np.isnan(dvc_btt) or bparams[9] <= 0 or - bparams[1] <= 0 or bparams[11] < 0.051 or bparams[3] < 0.051 or - smad < bmad ): - use_bulgefit = False - # Then check if sersicfit is viable; if not, this galaxy is a total failure. - # Note that we can avoid including these in the catalog in the first place by using - # `exclusion_level=bad_fits` or `exclusion_level=marginal` when making the catalog. - if sstat < 1 or sstat > 4 or sparams[1] <= 0 or sparams[0] <= 0: - raise RuntimeError("Cannot make parametric model for this galaxy!") - else: - use_bulgefit = record['use_bulgefit'] - if not use_bulgefit and not record['viable_sersic']: - raise RuntimeError("Cannot make parametric model for this galaxy!") + raise OSError("You still have the old COSMOS catalog. Run the program " + "`galsim_download_cosmos -s %s` to upgrade."%(self.use_sample)) + + use_bulgefit = record['use_bulgefit'] + if not use_bulgefit and not record['viable_sersic']: # pragma: no cover + raise GalSimError("Cannot make parametric model for this galaxy!") if use_bulgefit: # Bulge parameters: @@ -727,28 +644,15 @@ def _buildParametric(record, sersic_prec, gsparams, chromatic, bandpass=None, se bulge_beta = bparams[15]*radians disk_q = bparams[3] disk_beta = bparams[7]*radians - if 'hlr' not in record: # pragma: no cover - # If we're supposed to use the 2-component fits, get all the parameters. - # We have to convert from the stored half-light radius along the major axis, to an - # azimuthally averaged one (multiplying by sqrt(bulge_q)). We also have to convert - # to our native units of arcsec, from units of COSMOS pixels. - bulge_hlr = cosmos_pix_scale*np.sqrt(bulge_q)*bparams[9] - # The stored quantity is the surface brightness at the half-light radius. We have - # to convert to total flux within an n=4 surface brightness profile. - bulge_flux = 2.0*np.pi*3.607*(bulge_hlr**2)*bparams[8]/cosmos_pix_scale**2 - # Disk parameters, defined analogously: - disk_hlr = cosmos_pix_scale*np.sqrt(disk_q)*bparams[1] - disk_flux = 2.0*np.pi*1.901*(disk_hlr**2)*bparams[0]/cosmos_pix_scale**2 - else: - bulge_hlr = record['hlr'][1] - bulge_flux = record['flux'][1] - disk_hlr = record['hlr'][2] - disk_flux = record['flux'][2] + bulge_hlr = record['hlr'][1] + bulge_flux = record['flux'][1] + disk_hlr = record['hlr'][2] + disk_flux = record['flux'][2] # Make sure the bulge-to-total flux ratio is not nonsense. bfrac = bulge_flux/(bulge_flux+disk_flux) - if bfrac < 0 or bfrac > 1 or np.isnan(bfrac): - raise RuntimeError("Cannot make parametric model for this galaxy") + if bfrac < 0 or bfrac > 1 or np.isnan(bfrac): # pragma: no cover + raise GalSimError("Cannot make parametric model for this galaxy") # Then combine the two components of the galaxy. if chromatic: @@ -771,9 +675,9 @@ def _buildParametric(record, sersic_prec, gsparams, chromatic, bandpass=None, se gsparams=gsparams) # Apply shears for intrinsic shape. - if bulge_q < 1.: + if bulge_q < 1.: # pragma: no branch bulge = bulge.shear(q=bulge_q, beta=bulge_beta) - if disk_q < 1.: + if disk_q < 1.: # pragma: no branch disk = disk.shear(q=disk_q, beta=disk_beta) gal = bulge + disk @@ -793,19 +697,8 @@ def _buildParametric(record, sersic_prec, gsparams, chromatic, bandpass=None, se gal_n = COSMOSCatalog._round_sersic(gal_n, sersic_prec) gal_q = sparams[3] gal_beta = sparams[7]*radians - - if 'hlr' not in record: # pragma: no cover - gal_hlr = cosmos_pix_scale*np.sqrt(gal_q)*sparams[1] - # Below is the calculation of the full Sersic n-dependent quantity that goes into - # the conversion from surface brightness to flux, which here we're calling - # 'prefactor'. In the n=4 and n=1 cases above, this was precomputed, but here we - # have to calculate for each value of n. - tmp_ser = Sersic(gal_n, half_light_radius=gal_hlr, gsparams=gsparams) - gal_flux = sparams[0] / tmp_ser.xValue(0,gal_hlr) / cosmos_pix_scale**2 - else: - gal_hlr = record['hlr'][0] - gal_flux = record['flux'][0] - + gal_hlr = record['hlr'][0] + gal_flux = record['flux'][0] if chromatic: gal = Sersic(gal_n, flux=1., half_light_radius=gal_hlr, gsparams=gsparams) @@ -822,7 +715,7 @@ def _buildParametric(record, sersic_prec, gsparams, chromatic, bandpass=None, se gal = Sersic(gal_n, flux=gal_flux, half_light_radius=gal_hlr, gsparams=gsparams) # Apply shears for intrinsic shape. - if gal_q < 1.: + if gal_q < 1.: # pragma: no branch gal = gal.shear(q=gal_q, beta=gal_beta) return gal @@ -839,86 +732,21 @@ def getRealParams(self, index): def getParametricRecord(self, index): """Get the parametric record for a given index""" - # Used by _makeSingleGalaxy to circumvent pickling the result. + # Used by _makeGalaxy to circumvent pickling the result. record = self.param_cat[self.orig_index[index]] # Convert to a dict, since on some systems, the numpy record doesn't seem to # pickle correctly. - #record_dict = { k:record[k] for k in record.dtype.names } # doesn't work in python 2.6 - record_dict = dict( (k,record[k]) for k in record.dtype.names ) # equivalent. + record_dict = { k:record[k] for k in record.dtype.names } return record_dict def canMakeReal(self): """Is it permissible to call makeGalaxy with gal_type='real'?""" return self.use_real - @staticmethod - def _makeSingleGalaxy(cosmos_catalog, index, gal_type, noise_pad_size=5, deep=False, - rng=None, sersic_prec=0.05, gsparams=None): - from .random import BaseDeviate - # A static function that mimics the functionality of COSMOSCatalog.makeGalaxy() - # for single index and chromatic=False. - # The only point of this class is to circumvent some pickling issues when using - # config objects with type : COSMOSGalaxy. It's a staticmethod, which means it - # cannot use any self attributes. Just methods. (Which also means we can use it - # through a proxy COSMOSCatalog object, which we need for the config layer.) - - if not cosmos_catalog.canMakeReal(): - if gal_type is None: - gal_type = 'parametric' - elif gal_type != 'parametric': - raise ValueError("Only 'parametric' galaxy type is allowed when use_real == False") - - if gal_type not in ['real', 'parametric']: - raise ValueError("Invalid galaxy type %r"%gal_type) - - if gal_type == 'real' and rng is None: - rng = BaseDeviate() - - if gal_type == 'real': - real_params = cosmos_catalog.getRealParams(index) - gal = RealGalaxy(real_params, noise_pad_size=noise_pad_size, rng=rng, gsparams=gsparams) - else: - record = cosmos_catalog.getParametricRecord(index) - gal = COSMOSCatalog._buildParametric(record, sersic_prec, gsparams, chromatic=False) - - # If trying to use the 23.5 sample and "fake" a deep sample, rescale the size and flux as - # suggested in the GREAT3 handbook. - if deep: - if self.use_sample == '23.5': - # Rescale the flux to get a limiting mag of 25 in F814W when starting with a - # limiting mag of 23.5. Make the galaxies a factor of 0.6 smaller and appropriately - # fainter. - flux_factor = 10.**(-0.4*1.5) - size_factor = 0.6 - gal = gal.dilate(size_factor) * flux_factor - else: - import warnings - warnings.warn( - 'Ignoring `deep` argument, because the sample being used already '+ - 'corresponds to a flux limit of F814W<25.2') - - # Store the orig_index as gal.index, since the above RealGalaxy initialization just sets it - # as 0. Plus, it isn't set at all if we make a parametric galaxy. And if we are doing the - # deep scaling, then it gets messed up by that. If we have done some transformations, and - # are also doing later transformation, it will take the `original` attribute that is already - # there. So having `index` doesn't help, and we also need `original.index`. - gal.index = cosmos_catalog.getOrigIndex(index) - if hasattr(gal, 'original'): - gal.original.index = cosmos_catalog.getOrigIndex(index) - - return gal - - # Since this is a function, not a class, need to use an unconventional location for defining - # these config parameters. Also, I thought it would make sense to attach them to the - # _makeSingleGalaxy method. But that doesn't work, since it is technically a staticmethod - # object, not a normal function. So we attach these to makeGalaxy instead. - makeGalaxy._req_params = {} - makeGalaxy._opt_params = { "index" : int, - "gal_type" : str, - "noise_pad_size" : float, - "deep" : bool, - "sersic_prec": float, - "n_random": int - } - makeGalaxy._single_params = [] - makeGalaxy._takes_rng = True + def __eq__(self, other): + return (isinstance(other, COSMOSCatalog) and + self.use_real == other.use_real and + self.use_sample == other.use_sample and + self.real_cat == other.real_cat and + np.array_equal(self.param_cat, other.param_cat) and + np.array_equal(self.orig_index, other.orig_index)) diff --git a/galsim/second_kick.py b/galsim/second_kick.py index ef8e1293ad0..642a69725c2 100644 --- a/galsim/second_kick.py +++ b/galsim/second_kick.py @@ -34,6 +34,7 @@ from .position import PositionD from .angle import arcsec, AngleUnit, radians from .deltafunction import DeltaFunction +from .errors import convert_cpp_errors class SecondKick(GSObject): """Class describing the expectation value of the high-k turbulence portion of an atmospheric PSF @@ -118,21 +119,25 @@ def __init__(self, lam, r0, diam, obscuration=0, kcrit=0.2, flux=1, @lazy_property def _sbs(self): lam_over_r0 = (1.e-9*self._lam/self._r0)*self._scale - return _galsim.SBSecondKick(lam_over_r0, self._kcrit, self._flux, self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBSecondKick(lam_over_r0, self._kcrit, self._flux, self._gsparams._gsp) @lazy_property def _sba(self): lam_over_diam = (1.e-9*self._lam/self._diam)*self._scale - return _galsim.SBAiry(lam_over_diam, self._obscuration, 1., self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBAiry(lam_over_diam, self._obscuration, 1., self._gsparams._gsp) @lazy_property def _sbd(self): - return _galsim.SBDeltaFunction(self._sbs.getDelta(), self._gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBDeltaFunction(self._sbs.getDelta(), self._gsparams._gsp) @lazy_property def _sbp(self): - full_sbs = _galsim.SBAdd([self._sbs, self._sbd], self._gsparams._gsp) - return _galsim.SBConvolve([full_sbs, self._sba], False, self._gsparams._gsp) + with convert_cpp_errors(): + full_sbs = _galsim.SBAdd([self._sbs, self._sbd], self._gsparams._gsp) + return _galsim.SBConvolve([full_sbs, self._sba], False, self._gsparams._gsp) @property def flux(self): diff --git a/galsim/sed.py b/galsim/sed.py index dee960a2cba..09b4e7afb0e 100644 --- a/galsim/sed.py +++ b/galsim/sed.py @@ -31,6 +31,8 @@ from . import integ from . import dcr from .utilities import WeakMethod, lazy_property, combine_wave_list +from .errors import GalSimError, GalSimValueError, GalSimRangeError, GalSimSEDError +from .errors import GalSimIncompatibleValuesError class SED(object): """Object to represent the spectral energy distributions of stars and galaxies. @@ -127,14 +129,14 @@ def __init__(self, spec, wave_type, flux_type, redshift=0., fast=True, self._flux_type = flux_type # Need to save the original for repr # Parse the various options for wave_type if isinstance(wave_type, str): - if wave_type.lower() in ['nm', 'nanometer', 'nanometers']: + if wave_type.lower() in ('nm', 'nanometer', 'nanometers'): self.wave_type = 'nm' self.wave_factor = 1. - elif wave_type.lower() in ['a', 'ang', 'angstrom', 'angstroms']: + elif wave_type.lower() in ('a', 'ang', 'angstrom', 'angstroms'): self.wave_type = 'Angstrom' self.wave_factor = 10. else: - raise ValueError("Unknown wave_type '{0}'".format(wave_type)) + raise GalSimValueError("Unknown wave_type", wave_type, ('nm', 'Angstrom')) else: self.wave_type = wave_type try: @@ -172,12 +174,15 @@ def __init__(self, spec, wave_type, flux_type, redshift=0., fast=True, self.flux_type = '1' self.spectral = False else: - raise ValueError("Unknown flux_type '{0}'".format(flux_type)) + raise GalSimValueError("Unknown flux_type", flux_type, + ('flambda', 'fnu', 'fphotons', '1')) else: self.flux_type = flux_type self.spectral = self.check_spectral() if not self.spectral and not self.check_dimensionless(): - raise TypeError("Flux_type must be equivalent to a spectral density or dimensionless.") + raise GalSimValueError( + "Flux_type must be equivalent to a spectral density or dimensionless.", + flux_type) try: if self.wave_factor and self.spectral: self.flux_factor = (1*self.flux_type).to(SED._fphotons).value @@ -299,7 +304,7 @@ def _initialize_spec(self): self._const = False if isinstance(self._orig_spec, (int, float)): if not self.dimensionless: - raise ValueError("Attempt to set spectral SED using float or integer.") + raise GalSimSEDError("Attempt to set spectral SED using float or integer.", self) self._const = True self._spec = lambda w: float(self._orig_spec) elif isinstance(self._orig_spec, basestring): @@ -319,16 +324,15 @@ def _initialize_spec(self): except ArithmeticError: test_value = 0 except Exception as e: - raise ValueError( - "String spec must either be a valid filename or something that "+ - "can eval to a function of wave.\n" + - "Input provided: {0!r}\n".format(self._orig_spec) + - "Caught error: {0}".format(e)) + raise GalSimValueError( + "String spec must either be a valid filename or something that " + "can eval to a function of wave.\n" + "Caught error: {0}".format(e), self._orig_spec) from numbers import Real if not isinstance(test_value, Real): - raise ValueError("The given SED function, %r, did not return a valid" - " number at test wavelength %s: got %s"%( - self._spec, 700.0, test_value)) + raise GalSimValueError("The given SED function did not return a valid number " + "at test wavelength %s: got %s"%(700.0, test_value), + self._orig_spec) else: self._spec = self._orig_spec @@ -368,11 +372,11 @@ def _check_bounds(self, wave): extrapolation_slop = 1.e-6 # allow a small amount of extrapolation if wmin < self.blue_limit - extrapolation_slop: - raise ValueError("Requested wavelength ({0}) is bluer than blue_limit ({1})" - .format(wmin, self.blue_limit)) + raise GalSimRangeError("Requested wavelength is bluer than blue_limit.", + wave, self.blue_limit, self.red_limit) if wmax > self.red_limit + extrapolation_slop: - raise ValueError("Requested wavelength ({0}) is redder than red_limit ({1})" - .format(wmax, self.red_limit)) + raise GalSimRangeError("Requested wavelength is redder than red_limit.", + wave, self.blue_limit, self.red_limit) @lazy_property def _fast_spec(self): @@ -533,7 +537,8 @@ def __mul__(self, other): # Product of two SEDs if isinstance(other, SED): if self.spectral and other.spectral: - raise TypeError("Cannot multiply two spectral densities together.") + raise GalSimIncompatibleValuesError( + "Cannot multiply two spectral densities together.", self_sed=self, other=other) if other._const: return self._mul_scalar(other._spec(42.0)) # const, so can eval anywhere. @@ -574,7 +579,7 @@ def __rmul__(self, other): def __div__(self, other): # Enable division by scalars or dimensionless callables (including dimensionless SEDs.) if isinstance(other, SED) and other.spectral: - raise TypeError("Cannot divide by spectral SED.") + raise GalSimSEDError("Cannot divide by spectral SED.", other) if hasattr(other, '__call__'): spec = lambda w: self(w * (1.0 + self.redshift)) / other(w * (1.0 + self.redshift)) elif isinstance(self._spec, LookupTable): @@ -593,8 +598,7 @@ def __div__(self, other): _wave_list=self.wave_list, _blue_limit=self.blue_limit, _red_limit=self.red_limit) - def __truediv__(self, other): - return self.__div__(other) + __truediv__ = __div__ def __add__(self, other): # Add together two SEDs, with the following caveats: @@ -606,7 +610,8 @@ def __add__(self, other): # These conditions ensure that SED addition is commutative. if self.redshift != other.redshift: - raise ValueError("Can only add SEDs with same redshift.") + raise GalSimIncompatibleValuesError( + "Can only add SEDs with same redshift.", self_sed=self, other=other) if self.dimensionless and other.dimensionless: flux_type = '1' @@ -615,7 +620,8 @@ def __add__(self, other): flux_type = 'fphotons' _spectral = True else: - raise TypeError("Cannot add SEDs with incompatible dimensions.") + raise GalSimIncompatibleValuesError( + "Cannot add SEDs with incompatible dimensions.", self_sed=self, other=other) wave_list, blue_limit, red_limit = combine_wave_list(self, other) @@ -661,7 +667,7 @@ def withFluxDensity(self, target_flux_density, wavelength): @returns the new normalized SED. """ if self.dimensionless: - raise TypeError("Cannot set flux density of dimensionless SED.") + raise GalSimSEDError("Cannot set flux density of dimensionless SED.", self) if isinstance(wavelength, units.Quantity): wavelength_nm = wavelength.to(units.nm, units.spectral()) current_flux_density = self._call(wavelength_nm.value) @@ -700,8 +706,8 @@ def withMagnitude(self, target_magnitude, bandpass): @returns the new normalized SED. """ if bandpass.zeropoint is None: - raise RuntimeError("Cannot call SED.withMagnitude on this bandpass, because it does not" - " have a zeropoint. See Bandpass.withZeropoint()") + raise GalSimError("Cannot call SED.withMagnitude on this bandpass, because it does " + "not have a zeropoint. See Bandpass.withZeropoint()") current_magnitude = self.calculateMagnitude(bandpass) norm = 10**(-0.4*(target_magnitude - current_magnitude)) return self * norm @@ -714,7 +720,7 @@ def atRedshift(self, redshift): @returns the redshifted SED. """ if redshift <= -1: - raise ValueError("Invalid redshift {0}".format(redshift)) + raise GalSimRangeError("Invalid redshift", redshift, -1.) zfactor = (1.0 + redshift) / (1.0 + self.redshift) wave_list = self.wave_list * zfactor blue_limit = self.blue_limit * zfactor @@ -735,12 +741,15 @@ def calculateFlux(self, bandpass): """ from . import integ if self.dimensionless: - raise TypeError("Cannot calculate flux of dimensionless SED.") + raise GalSimSEDError("Cannot calculate flux of dimensionless SED.", self) if len(bandpass.wave_list) > 0 or len(self.wave_list) > 0: slop = 1e-6 # nm if (self.blue_limit > bandpass.blue_limit + slop or self.red_limit < bandpass.red_limit - slop): - raise ValueError("SED undefined within Bandpass") + raise GalSimRangeError("Bandpass is not completely within defined wavelength " + "range for this SED.", + (bandpass.blue_limit, bandpass.red_limit), + self.blue_limit, self.red_limit) x, _, _ = combine_wave_list(self, bandpass) return np.trapz(bandpass(x) * self(x), x) else: @@ -759,10 +768,10 @@ def calculateMagnitude(self, bandpass): @returns the bandpass magnitude. """ if self.dimensionless: - raise TypeError("Cannot calculate magnitude of dimensionless SED.") + raise GalSimSEDError("Cannot calculate magnitude of dimensionless SED.", self) if bandpass.zeropoint is None: - raise RuntimeError("Cannot do this calculation for a bandpass without an assigned" - " zeropoint") + raise GalSimError("Cannot do this calculation for a bandpass without an assigned " + "zeropoint") flux = self.calculateFlux(bandpass) return -2.5 * np.log10(flux) + bandpass.zeropoint @@ -831,14 +840,14 @@ def calculateDCRMomentShifts(self, bandpass, **kwargs): """ from .dcr import parse_dcr_angles if self.dimensionless: - raise TypeError("Cannot calculate DCR shifts of dimensionless SED.") + raise GalSimSEDError("Cannot calculate DCR shifts of dimensionless SED.", self) zenith_angle, parallactic_angle, kwargs = parse_dcr_angles(**kwargs) # Any remaining kwargs will get forwarded to galsim.dcr.get_refraction # Check that they're valid for kw in kwargs: - if kw not in ['temperature', 'pressure', 'H2O_pressure']: + if kw not in ('temperature', 'pressure', 'H2O_pressure'): raise (TypeError("Got unexpected keyword in calculateDCRMomentShifts: {0}" .format(kw))) @@ -881,7 +890,7 @@ def calculateSeeingMomentRatio(self, bandpass, alpha=-0.2, base_wavelength=500): @returns the ratio of the PSF second moments to the second moments of the reference PSF. """ if self.dimensionless: - raise TypeError("Cannot calculate seeing moment ratio of dimensionless SED.") + raise GalSimSEDError("Cannot calculate seeing moment ratio of dimensionless SED.", self) flux = self.calculateFlux(bandpass) if len(bandpass.wave_list) > 0: x, _, _ = combine_wave_list([self, bandpass]) @@ -962,7 +971,7 @@ def __hash__(self): return self._hash def __repr__(self): - outstr = ('galsim.SED(%r, wave_type=%r, flux_type=%r, redshift=%r, fast=%r,' + + outstr = ('galsim.SED(%r, wave_type=%r, flux_type=%r, redshift=%r, fast=%r,' ' _wave_list=%r, _blue_limit=%r, _red_limit=%s)')%( self._orig_spec, self.wave_type, self._flux_type, self.redshift, self.fast, self.wave_list, self.blue_limit, diff --git a/galsim/sensor.py b/galsim/sensor.py index 9adcc4d162d..c085865d224 100644 --- a/galsim/sensor.py +++ b/galsim/sensor.py @@ -36,6 +36,7 @@ from .table import LookupTable from .random import UniformDeviate from . import meta_data +from .errors import GalSimUndefinedBoundsError, convert_cpp_errors class Sensor(object): """ @@ -67,6 +68,8 @@ def accumulate(self, photons, image, orig_center=None, resume=False): @returns the total flux that fell onto the image. """ + if not image.bounds.isDefined(): + raise GalSimUndefinedBoundsError("Calling accumulate on image with undefined bounds") return photons.addTo(image) def __repr__(self): @@ -166,11 +169,11 @@ def __init__(self, name='lsst_itl_8', strength=1.0, rng=None, diffusion_factor=1 if not os.path.isfile(self.config_file): cfg_file = os.path.join(meta_data.share_dir, 'sensors', self.config_file) if not os.path.isfile(cfg_file): - raise IOError("Cannot locate file %s or %s"%(self.config_file, cfg_file)) + raise OSError("Cannot locate file %s or %s"%(self.config_file, cfg_file)) self.config_file = cfg_file self.vertex_file = os.path.join(meta_data.share_dir, 'sensors', self.vertex_file) - if not os.path.isfile(self.vertex_file): - raise IOError("Cannot locate vertex file %s"%(self.vertex_file)) + if not os.path.isfile(self.vertex_file): # pragma: no cover + raise OSError("Cannot locate vertex file %s"%(self.vertex_file)) self.config = self._read_config_file(self.config_file) @@ -180,9 +183,9 @@ def __init__(self, name='lsst_itl_8', strength=1.0, rng=None, diffusion_factor=1 # A bit kludgy, but it works self.treering_func = LookupTable(x=[0.0,1.0], f=[0.0,0.0], interpolant='linear') elif not isinstance(treering_func, LookupTable): - raise ValueError("treering_func must be a galsim.LookupTable") + raise TypeError("treering_func must be a galsim.LookupTable") if not isinstance(treering_center, PositionD): - raise ValueError("treering_center must be a galsim.PositionD") + raise TypeError("treering_center must be a galsim.PositionD") # Now we read in the absorption length table: abs_file = os.path.join(meta_data.share_dir, 'sensors', 'abs_length.dat') @@ -202,15 +205,16 @@ def _init_silicon(self): nrecalc = float(self.nrecalc) / self.strength vertex_data = np.loadtxt(self.vertex_file, skiprows = 1) - if vertex_data.shape != (Nx * Ny * (4 * NumVertices + 4), 5): - raise IOError("Vertex file %s does not match config file %s"%( + if vertex_data.shape != (Nx * Ny * (4 * NumVertices + 4), 5): # pragma: no cover + raise OSError("Vertex file %s does not match config file %s"%( self.vertex_file, self.config_file)) - self._silicon = _galsim.Silicon(NumVertices, num_elec, Nx, Ny, self.qdist, nrecalc, - diff_step, PixelSize, SensorThickness, - vertex_data.ctypes.data, - self.treering_func._tab, self.treering_center._p, - self.abs_length_table._tab, self.transpose) + with convert_cpp_errors(): + self._silicon = _galsim.Silicon(NumVertices, num_elec, Nx, Ny, self.qdist, nrecalc, + diff_step, PixelSize, SensorThickness, + vertex_data.ctypes.data, + self.treering_func._tab, self.treering_center._p, + self.abs_length_table._tab, self.transpose) def __str__(self): s = 'galsim.SiliconSensor(%r'%self.name @@ -275,6 +279,8 @@ def accumulate(self, photons, image, orig_center=PositionI(0,0), resume=False): raise RuntimeError("accumulate called with resume, but provided image does " "not match one used in the previous accumulate call.") self._last_image = image + if not image.bounds.isDefined(): + raise GalSimUndefinedBoundsError("Calling accumulate on image with undefined bounds") return self._silicon.accumulate(photons._pa, self.rng._rng, image._image, orig_center._p, resume) @@ -311,8 +317,8 @@ def _read_config_file(self, filename): lines=file.readlines() lines = [ l.strip() for l in lines ] lines = [ l.split() for l in lines if len(l) > 0 and l[0] != '#' ] - if any([l[1] != '=' for l in lines]): - raise IOError("Error reading config file %s"%filename) + if any([l[1] != '=' for l in lines]): # pragma: no cover + raise OSError("Error reading config file %s"%filename) config = dict([(l[0], l[2]) for l in lines]) # convert strings to int or float values when appropriate for k in config: diff --git a/galsim/sersic.py b/galsim/sersic.py index 695a892108b..00e22b74068 100644 --- a/galsim/sersic.py +++ b/galsim/sersic.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import GalSimRangeError, GalSimIncompatibleValuesError, convert_cpp_errors class Sersic(GSObject): """A class describing a Sersic profile. @@ -196,6 +197,9 @@ class Sersic(GSObject): _is_analytic_x = True _is_analytic_k = True + _minimum_n = 0.3 # Lower bounds has hard limit at ~0.29 + _maximum_n = 6.2 # Upper bounds is just where we have tested that code works well. + # The conversion from hlr to scale radius is complicated for Sersic, especially since we # allow it to be truncated. So we do these calculations in the C++-layer constructor. def __init__(self, n, half_light_radius=None, scale_radius=None, @@ -205,26 +209,39 @@ def __init__(self, n, half_light_radius=None, scale_radius=None, self._trunc = float(trunc) self._gsparams = GSParams.check(gsparams) + if self._n < Sersic._minimum_n: + raise GalSimRangeError("Requested Sersic index is too small", + self._n, Sersic._minimum_n, Sersic._maximum_n) + if self._n > Sersic._maximum_n: + raise GalSimRangeError("Requested Sersic index is too large", + self._n, Sersic._minimum_n, Sersic._maximum_n) + + if self._trunc < 0: + raise GalSimRangeError("Sersic trunc must be > 0", self._trunc, 0.) + # Parse the radius options if half_light_radius is not None: if scale_radius is not None: - raise TypeError( - "Only one of scale_radius or half_light_radius may be " + - "specified for Spergel") + raise GalSimIncompatibleValuesError( + "Only one of scale_radius or half_light_radius may be specified for Spergel", + half_light_radius=half_light_radius, scale_radius=scale_radius) self._hlr = float(half_light_radius) if self._trunc == 0. or flux_untruncated: self._flux_fraction = 1. self._r0 = self._hlr / self.calculateHLRFactor() else: if self._trunc <= math.sqrt(2.) * self._hlr: - raise ValueError("Sersic trunc must be > sqrt(2) * half_light_radius") - self._r0 = _galsim.SersicTruncatedScale(self._n, self._hlr, self._trunc) + raise GalSimRangeError("Sersic trunc must be > sqrt(2) * half_light_radius", + self._trunc, math.sqrt(2.) * self._hlr) + with convert_cpp_errors(): + self._r0 = _galsim.SersicTruncatedScale(self._n, self._hlr, self._trunc) elif scale_radius is not None: self._r0 = float(scale_radius) self._hlr = 0. else: - raise TypeError( - "Either scale_radius or half_light_radius must be specified for Spergel") + raise GalSimIncompatibleValuesError( + "Either scale_radius or half_light_radius must be specified for Spergel", + half_light_radius=half_light_radius, scale_radius=scale_radius) if self._trunc > 0.: self._flux_fraction = self.calculateIntegratedFlux(self._trunc) @@ -237,16 +254,19 @@ def __init__(self, n, half_light_radius=None, scale_radius=None, def calculateIntegratedFlux(self, r): """Return the fraction of the total flux enclosed within a given radius, r""" - return _galsim.SersicIntegratedFlux(self._n, float(r)/self._r0) + with convert_cpp_errors(): + return _galsim.SersicIntegratedFlux(self._n, float(r)/self._r0) def calculateHLRFactor(self): """Calculate the half-light-radius in units of the scale radius. """ - return _galsim.SersicHLR(self._n, self._flux_fraction) + with convert_cpp_errors(): + return _galsim.SersicHLR(self._n, self._flux_fraction) @lazy_property def _sbp(self): - return _galsim.SBSersic(self._n, self._r0, self._flux, self._trunc, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBSersic(self._n, self._r0, self._flux, self._trunc, self.gsparams._gsp) @property def n(self): return self._n diff --git a/galsim/shapelet.py b/galsim/shapelet.py index 88f4f55d2b9..7486265b671 100644 --- a/galsim/shapelet.py +++ b/galsim/shapelet.py @@ -28,6 +28,9 @@ from .image import Image from .utilities import doc_inherit from . import _galsim +from .errors import GalSimValueError, GalSimIncompatibleValuesError, GalSimNotImplementedError +from .errors import convert_cpp_errors + class Shapelet(GSObject): """A class describing polar shapelet surface brightness profiles. @@ -134,11 +137,13 @@ def __init__(self, sigma, order, bvec=None, gsparams=None): self._bvec = np.empty(bvec_size, dtype=float) else: if len(bvec) != bvec_size: - raise ValueError("bvec is the wrong size for the provided order") + raise GalSimIncompatibleValuesError( + "bvec is the wrong size for the provided order", bvec=bvec, order=order) self._bvec = np.ascontiguousarray(bvec, dtype=float) - self._sbp = _galsim.SBShapelet(self._sigma, self._order, self._bvec.ctypes.data, - self.gsparams._gsp) + with convert_cpp_errors(): + self._sbp = _galsim.SBShapelet(self._sigma, self._order, self._bvec.ctypes.data, + self.gsparams._gsp) @classmethod def size(cls, order): @@ -187,8 +192,9 @@ def __getstate__(self): def __setstate__(self, d): self.__dict__ = d - self._sbp = _galsim.SBShapelet(self._sigma, self._order, self._bvec.ctypes.data, - self.gsparams._gsp) + with convert_cpp_errors(): + self._sbp = _galsim.SBShapelet(self._sigma, self._order, self._bvec.ctypes.data, + self.gsparams._gsp) @property def _maxk(self): @@ -272,27 +278,29 @@ def fit(cls, sigma, order, image, center=None, normalization='flux', gsparams=No center = PositionD(center.x,center.y) if not normalization.lower() in ("flux", "f", "surface brightness", "sb"): - raise ValueError(("Invalid normalization requested: '%s'. Expecting one of 'flux', "+ - "'f', 'surface brightness' or 'sb'.") % normalization) + raise GalSimValueError("Invalid normalization requested.", normalization, + ('flux', 'f', 'surface brightneess', 'sb')) ret = Shapelet(sigma, order, bvec=None, gsparams=gsparams) if image.wcs is not None and not image.wcs.isPixelScale(): # TODO: Add ability for ShapeletFitImage to take jacobian matrix. - raise NotImplementedError("Sorry, cannot (yet) fit a shapelet model to an image "+ - "with a non-trivial WCS.") + raise GalSimNotImplementedError("Sorry, cannot (yet) fit a shapelet model to an image " + "with a non-trivial WCS.") # Make it double precision if it is not. image = Image(image, dtype=np.float64, copy=False) - _galsim.ShapeletFitImage(ret._sigma, ret._order, ret._bvec.ctypes.data, - image._image, image.scale, center._p) + with convert_cpp_errors(): + _galsim.ShapeletFitImage(ret._sigma, ret._order, ret._bvec.ctypes.data, + image._image, image.scale, center._p) if normalization.lower() == "flux" or normalization.lower() == "f": ret._bvec /= image.scale**2 # Update the SBProfile, since it doesn't have the right bvector anymore. - ret._sbp = _galsim.SBShapelet(ret._sigma, ret._order, ret._bvec.ctypes.data, - ret.gsparams._gsp) + with convert_cpp_errors(): + ret._sbp = _galsim.SBShapelet(ret._sigma, ret._order, ret._bvec.ctypes.data, + ret.gsparams._gsp) return ret diff --git a/galsim/shear.py b/galsim/shear.py index b3fc98458e6..c64954d4d41 100644 --- a/galsim/shear.py +++ b/galsim/shear.py @@ -22,6 +22,7 @@ import numpy as np from .angle import Angle, _Angle, radians +from .errors import GalSimRangeError, GalSimIncompatibleValuesError class Shear(object): """A class to represent shears in a variety of ways. @@ -114,7 +115,7 @@ def __init__(self, *args, **kwargs): g2 = kwargs.pop('g2', 0.) self._g = g1 + 1j * g2 if abs(self._g) > 1.: - raise ValueError("Requested shear exceeds 1: %f"%abs(self._g)) + raise GalSimRangeError("Requested shear exceeds 1.", self._g, 0., 1.) # e1,e2 elif 'e1' in kwargs or 'e2' in kwargs: @@ -122,7 +123,7 @@ def __init__(self, *args, **kwargs): e2 = kwargs.pop('e2', 0.) absesq = e1**2 + e2**2 if absesq > 1.: - raise ValueError("Requested distortion exceeds 1: %s"%np.sqrt(absesq)) + raise GalSimRangeError("Requested distortion exceeds 1.",np.sqrt(absesq), 0., 1.) self._g = (e1 + 1j * e2) * self._e2g(absesq) # eta1,eta2 @@ -136,62 +137,64 @@ def __init__(self, *args, **kwargs): # g,beta elif 'g' in kwargs: if 'beta' not in kwargs: - raise TypeError( - "Shear constructor requires position angle when g is specified!") + raise GalSimIncompatibleValuesError( + "Shear constructor requires beta when g is specified.", + g=kwargs['g'], beta=None) beta = kwargs.pop('beta') if not isinstance(beta, Angle): - raise TypeError( - "The position angle that was supplied is not an Angle instance!") + raise TypeError("beta must be an Angle instance.") g = kwargs.pop('g') if g > 1 or g < 0: - raise ValueError("Requested |shear| is outside [0,1]: %f"%g) + raise GalSimRangeError("Requested |shear| is outside [0,1].",g, 0., 1.) self._g = g * np.exp(2j * beta.rad) # e,beta elif 'e' in kwargs: if 'beta' not in kwargs: - raise TypeError( - "Shear constructor requires position angle when e is specified!") + raise GalSimIncompatibleValuesError( + "Shear constructor requires beta when e is specified.", + e=kwargs['e'], beta=None) beta = kwargs.pop('beta') if not isinstance(beta, Angle): - raise TypeError( - "The position angle that was supplied is not an Angle instance!") + raise TypeError("beta must be an Angle instance.") e = kwargs.pop('e') if e > 1 or e < 0: - raise ValueError("Requested distortion is outside [0,1]: %f"%e) + raise GalSimRangeError("Requested distortion is outside [0,1].", e, 0., 1.) self._g = self._e2g(e**2) * e * np.exp(2j * beta.rad) # eta,beta elif 'eta' in kwargs: if 'beta' not in kwargs: - raise TypeError( - "Shear constructor requires position angle when eta is specified!") + raise GalSimIncompatibleValuesError( + "Shear constructor requires beta when eta is specified.", + eta=kwargs['eta'], beta=None) beta = kwargs.pop('beta') if not isinstance(beta, Angle): - raise TypeError( - "The position angle that was supplied is not an Angle instance!") + raise TypeError("beta must be an Angle instance.") eta = kwargs.pop('eta') if eta < 0: - raise ValueError("Requested eta is below 0: %f"%eta) + raise GalSimRangeError("Requested eta is below 0.", eta, 0.) self._g = self._eta2g(eta) * eta * np.exp(2j * beta.rad) # q,beta elif 'q' in kwargs: if 'beta' not in kwargs: - raise TypeError( - "Shear constructor requires position angle when q is specified!") + raise GalSimIncompatibleValuesError( + "Shear constructor requires beta when q is specified.", + q=kwargs['q'], beta=None) beta = kwargs.pop('beta') if not isinstance(beta, Angle): - raise TypeError( - "The position angle that was supplied is not an Angle instance!") + raise TypeError("beta must be an Angle instance.") q = kwargs.pop('q') if q <= 0 or q > 1: - raise ValueError("Cannot use requested axis ratio of %f!"%q) + raise GalSimRangeError("Cannot use requested axis ratio.", q, 0., 1.) eta = -np.log(q) self._g = self._eta2g(eta) * eta * np.exp(2j * beta.rad) elif 'beta' in kwargs: - raise TypeError("beta provided to Shear constructor, but not g/e/eta/q") + raise GalSimIncompatibleValuesError( + "beta provided to Shear constructor, but not g/e/eta/q", + beta=kwargs['beta'], e=None, g=None, q=None, eta=None) # check for the case where there are 1 or 2 kwargs that are not valid ones for # initializing a Shear diff --git a/galsim/spergel.py b/galsim/spergel.py index a76b68bae1e..c29ef03715b 100644 --- a/galsim/spergel.py +++ b/galsim/spergel.py @@ -24,6 +24,7 @@ from .gsparams import GSParams from .utilities import lazy_property, doc_inherit from .position import PositionD +from .errors import GalSimRangeError, GalSimIncompatibleValuesError, convert_cpp_errors class Spergel(GSObject): @@ -103,30 +104,47 @@ class Spergel(GSObject): _is_analytic_x = True _is_analytic_k = True + # Constrain range of allowed Spergel index nu. Spergel (2010) Table 1 lists values of nu + # from -0.9 to +0.85. We found that nu = -0.9 is too tricky for the GKP integrator to + # handle, however, so the lower limit is -0.85 instead. The upper limit is set by the + # cyl_bessel_k function, which runs into overflow errors for nu larger than about 4.0. + _minimum_nu = -0.85 + _maximum_nu = 4.0 + def __init__(self, nu, half_light_radius=None, scale_radius=None, flux=1., gsparams=None): self._nu = float(nu) self._flux = float(flux) self._gsparams = GSParams.check(gsparams) + if self._nu < Spergel._minimum_nu: + raise GalSimRangeError("Requested Spergel index is too small", + self._nu, Spergel._minimum_nu, Spergel._maximum_nu) + if self._nu > Spergel._maximum_nu: + raise GalSimRangeError("Requested Spergel index is too large", + self._nu, Spergel._minimum_nu, Spergel._maximum_nu) + # Parse the radius options if half_light_radius is not None: if scale_radius is not None: - raise TypeError( - "Only one of scale_radius or half_light_radius may be " + - "specified for Spergel") + raise GalSimIncompatibleValuesError( + "Only one of scale_radius or half_light_radius may be specified", + half_light_radius=half_light_radius, scale_radius=scale_radius) self._hlr = float(half_light_radius) - self._r0 = self._hlr / _galsim.SpergelCalculateHLR(self._nu) + with convert_cpp_errors(): + self._r0 = self._hlr / _galsim.SpergelCalculateHLR(self._nu) elif scale_radius is not None: self._r0 = float(scale_radius) self._hlr = 0. else: - raise TypeError( - "Either scale_radius or half_light_radius must be specified for Spergel") + raise GalSimIncompatibleValuesError( + "Either scale_radius or half_light_radius must be specified for Spergel", + half_light_radius=half_light_radius, scale_radius=scale_radius) @lazy_property def _sbp(self): - return _galsim.SBSpergel(self._nu, self._r0, self._flux, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBSpergel(self._nu, self._r0, self._flux, self.gsparams._gsp) @property def nu(self): return self._nu @@ -135,7 +153,8 @@ def scale_radius(self): return self._r0 @property def half_light_radius(self): if self._hlr == 0.: - self._hlr = self._r0 * _galsim.SpergelCalculateHLR(self._nu) + with convert_cpp_errors(): + self._hlr = self._r0 * _galsim.SpergelCalculateHLR(self._nu) return self._hlr def calculateIntegratedFlux(self, r): diff --git a/galsim/sum.py b/galsim/sum.py index a3d49ca46ae..2301de1c0be 100644 --- a/galsim/sum.py +++ b/galsim/sum.py @@ -22,6 +22,8 @@ from .gsobject import GSObject from .chromatic import ChromaticObject, ChromaticSum from .utilities import lazy_property, doc_inherit +from . import _galsim +from .errors import convert_cpp_errors def Add(*args, **kwargs): """A function for adding 2 or more GSObject or ChromaticObject instances. @@ -149,10 +151,10 @@ def obj_list(self): return self._obj_list @property def _sbp(self): - from . import _galsim # NB. I only need this until compound and transform are reimplemented in Python... sb_list = [obj._sbp for obj in self.obj_list] - return _galsim.SBAdd(sb_list, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBAdd(sb_list, self.gsparams._gsp) @lazy_property def _flux(self): diff --git a/galsim/table.py b/galsim/table.py index 09da51ef79c..2d805d518a6 100644 --- a/galsim/table.py +++ b/galsim/table.py @@ -26,6 +26,10 @@ from . import _galsim from .utilities import lazy_property +from .position import PositionD +from .bounds import BoundsD +from .errors import GalSimRangeError, GalSimBoundsError, GalSimValueError +from .errors import GalSimIncompatibleValuesError, convert_cpp_errors class LookupTable(object): """ @@ -84,28 +88,26 @@ class LookupTable(object): that all inputs / outputs will still be f, it's just a question of how the interpolation is done. [default: False] """ - def __init__(self, x=None, f=None, interpolant=None, x_log=False, f_log=False): + def __init__(self, x, f, interpolant=None, x_log=False, f_log=False): self.x_log = x_log self.f_log = f_log - if x is None or f is None: - raise TypeError("x and f are required for LookupTable") - # check for proper interpolant if interpolant is None: interpolant = 'spline' else: - if interpolant not in ['spline', 'linear', 'ceil', 'floor', 'nearest']: - raise ValueError("Unknown interpolant: %s" % interpolant) + if interpolant not in ('spline', 'linear', 'ceil', 'floor', 'nearest'): + raise GalSimValueError("Unknown interpolant", interpolant, + ('spline', 'linear', 'ceil', 'floor', 'nearest')) self.interpolant = interpolant # Sanity checks if len(x) != len(f): - raise ValueError("Input array lengths don't match") + raise GalSimIncompatibleValuesError("Input array lengths don't match", x=x, f=f) if interpolant == 'spline' and len(x) < 3: - raise ValueError("Input arrays too small to spline interpolate") - if interpolant in ['linear', 'ceil', 'floor', 'nearest'] and len(x) < 2: - raise ValueError("Input arrays too small to interpolate") + raise GalSimValueError("Input arrays too small to spline interpolate", x) + if interpolant in ('linear', 'ceil', 'floor', 'nearest') and len(x) < 2: + raise GalSimValueError("Input arrays too small to interpolate", x) # turn x and f into numpy arrays so that all subsequent math is possible (unlike for # lists, tuples). Also make sure the dtype is float @@ -118,11 +120,11 @@ def __init__(self, x=None, f=None, interpolant=None, x_log=False, f_log=False): self._x_min = self.x[0] self._x_max = self.x[-1] if self._x_min == self._x_max: - raise ValueError("All x values are equal") + raise GalSimValueError("All x values are equal", x) if self.x_log and self.x[0] <= 0.: - raise ValueError("Cannot interpolate in log(x) when table contains x<=0!") + raise GalSimValueError("Cannot interpolate in log(x) when table contains x<=0.", x) if self.f_log and np.any(self.f <= 0.): - raise ValueError("Cannot interpolate in log(f) when table contains f<=0!") + raise GalSimValueError("Cannot interpolate in log(f) when table contains f<=0.", f) @lazy_property def _tab(self): @@ -132,8 +134,9 @@ def _tab(self): if self.x_log: self._x = np.log(self._x) if self.f_log: self._f = np.log(self._f) - return _galsim._LookupTable(self._x.ctypes.data, self._f.ctypes.data, - len(self._x), self.interpolant) + with convert_cpp_errors(): + return _galsim._LookupTable(self._x.ctypes.data, self._f.ctypes.data, + len(self._x), self.interpolant) @property def x_min(self): return self._x_min @@ -164,8 +167,6 @@ def __call__(self, x): # Handle the log(x) if necessary if self.x_log: - if np.any(np.asarray(x) <= 0.): - raise ValueError("Cannot interpolate x<=0 when using log(x) interpolation.") x = np.log(x) x = np.asarray(x) @@ -173,9 +174,7 @@ def __call__(self, x): f = self._tab.interp(float(x)) else: dimen = len(x.shape) - if dimen > 2: - raise ValueError("Arrays with dimension larger than 2 not allowed!") - elif dimen == 2: + if dimen > 1: f = np.empty_like(x.ravel(), dtype=float) xx = x.astype(float,copy=False).ravel() self._tab.interpMany(xx.ctypes.data, f.ctypes.data, len(xx)) @@ -193,11 +192,11 @@ def __call__(self, x): def _check_range(self, x): slop = (self.x_max - self.x_min) * 1.e-6 if np.min(x) < self.x_min - slop: - raise ValueError("x value(s) below the range of the LookupTable: %s < %s"%( - x, self.x_min)) + raise GalSimRangeError("x value(s) below the range of the LookupTable.", + x, self.x_min, self.x_max) if np.max(x) > self.x_max + slop: - raise ValueError("x value(s) above the range of the LookupTable: %s > %s"%( - x, self.x_max)) + raise GalSimRangeError("x value(s) above the range of the LookupTable.", + x, self.x_min, self.x_max) def getArgs(self): return self.x @@ -271,15 +270,16 @@ def from_file(cls, file_name, interpolant='spline', x_log=False, f_log=False, am try: # version >= 0.20 from pandas.io.common import CParserError - except ImportError: # pragma: no cover + except ImportError: # version < 0.20 from pandas.parser import CParserError data = pandas.read_csv(file_name, comment='#', delim_whitespace=True, header=None) data = data.values.transpose() except (ImportError, AttributeError, CParserError): # pragma: no cover data = np.loadtxt(file_name).transpose() - if data.shape[0] != 2: - raise ValueError("File %s provided for LookupTable does not have 2 columns"%file_name) + if data.shape[0] != 2: # pragma: no cover + raise GalSimValueError("File provided for LookupTable does not have 2 columns", + file_name) x=data[0] f=data[1] if amplitude != 1.0: @@ -415,8 +415,8 @@ class LookupTable2D(object): `edge_mode='constant'`. [default: 0] """ def __init__(self, x, y, f, interpolant='linear', edge_mode='raise', constant=0): - if edge_mode not in ['raise', 'wrap', 'constant']: - raise ValueError("Unknown edge_mode: {:0}".format(edge_mode)) + if edge_mode not in ('raise', 'wrap', 'constant'): + raise GalSimValueError("Unknown edge_mode.", edge_mode, ('raise', 'wrap', 'constant')) self.x = np.ascontiguousarray(x, dtype=float) self.y = np.ascontiguousarray(y, dtype=float) @@ -425,15 +425,19 @@ def __init__(self, x, y, f, interpolant='linear', edge_mode='raise', constant=0) dx = np.diff(self.x) dy = np.diff(self.y) - if not (all(dx > 0) and all(dy > 0)): - raise ValueError("x and y input grids are not strictly increasing.") + if not all(dx > 0): + raise GalSimValueError("x input grids is not strictly increasing.", x) + if not all(dy > 0): + raise GalSimValueError("y input grids is not strictly increasing.", y) fshape = self.f.shape if fshape != (len(x), len(y)): - raise ValueError("Shape of `f` must be (len(`x`), len(`y`)).") + raise GalSimIncompatibleValuesError( + "Shape of f incompatible with lengths of x,y", f=f, x=x, y=y) - if interpolant not in ['linear', 'ceil', 'floor', 'nearest']: - raise ValueError("Unknown interpolant: %s" % interpolant) + if interpolant not in ('linear', 'ceil', 'floor', 'nearest'): + raise GalSimValueError("Unknown interpolant.", interpolant, + ('linear', 'ceil', 'floor', 'nearest')) self.interpolant = interpolant @@ -452,14 +456,17 @@ def __init__(self, x, y, f, interpolant='linear', edge_mode='raise', constant=0) self.xperiod = self.x[-1] - self.x[0] self.yperiod = self.y[-1] - self.y[0] else: - raise ValueError("Cannot use edge_mode='wrap' unless either x and y are equally " - "spaced or first/last row/column of f are identical.") + raise GalSimIncompatibleValuesError( + "Cannot use edge_mode='wrap' unless either x and y are equally " + "spaced or first/last row/column of f are identical.", + edge_mode=edge_mode, x=x, y=y, f=f) @lazy_property def _tab(self): - return _galsim._LookupTable2D(self.x.ctypes.data, self.y.ctypes.data, - self.f.ctypes.data, len(self.x), len(self.y), - self.interpolant) + with convert_cpp_errors(): + return _galsim._LookupTable2D(self.x.ctypes.data, self.y.ctypes.data, + self.f.ctypes.data, len(self.x), len(self.y), + self.interpolant) def getXArgs(self): return self.x @@ -480,9 +487,14 @@ def _wrap_args(self, x, y): return ((x-self.x[0]) % self.xperiod + self.x[0], (y-self.y[0]) % self.yperiod + self.y[0]) + @property + def _bounds(self): + return BoundsD(self.x[0], self.x[-1], self.y[0], self.y[-1]) + def _call_raise(self, x, y): if not self._inbounds(x, y): - raise ValueError("Extrapolating beyond input range.") + raise GalSimBoundsError("Extrapolating beyond input range.", + PositionD(x,y), self._bounds) if isinstance(x, numbers.Real): return self._tab.interp(x, y) @@ -528,7 +540,8 @@ def __call__(self, x, y): def _gradient_raise(self, x, y): if not self._inbounds(x, y): - raise ValueError("Extrapolating beyond input range.") + raise GalSimBoundsError("Extrapolating beyond input range.", + PositionD(x,y), self._bounds) if isinstance(x, numbers.Real): grad = np.empty(2, dtype=float) diff --git a/galsim/transform.py b/galsim/transform.py index 06ebcf465b8..11fcbc151d5 100644 --- a/galsim/transform.py +++ b/galsim/transform.py @@ -22,11 +22,13 @@ import numpy as np import math import cmath + from . import _galsim from .gsobject import GSObject from .gsparams import GSParams from .utilities import lazy_property, doc_inherit, WeakMethod from .position import PositionD +from .errors import GalSimError, convert_cpp_errors def Transform(obj, jac=(1.,0.,0.,1.), offset=PositionD(0.,0.), flux_ratio=1., gsparams=None): """A function for transforming either a GSObject or ChromaticObject. @@ -161,8 +163,9 @@ def _flux(self): @lazy_property def _sbp(self): dudx, dudy, dvdx, dvdy = self._jac.ravel() - return _galsim.SBTransform(self._original._sbp, dudx, dudy, dvdx, dvdy, - self._offset._p, self._flux_ratio, self.gsparams._gsp) + with convert_cpp_errors(): + return _galsim.SBTransform(self._original._sbp, dudx, dudy, dvdx, dvdy, + self._offset._p, self._flux_ratio, self.gsparams._gsp) @lazy_property def _noise(self): @@ -194,39 +197,45 @@ def __repr__(self): return 'galsim.Transformation(%r, jac=%r, offset=%r, flux_ratio=%r, gsparams=%r)'%( self.original, self._jac.tolist(), self.offset, self.flux_ratio, self.gsparams) - def __str__(self): + @classmethod + def _str_from_jac(cls, jac): from .wcs import JacobianWCS - s = str(self.original) - dudx, dudy, dvdx, dvdy = self._jac.ravel() + dudx, dudy, dvdx, dvdy = jac.ravel() if dudx != 1 or dudy != 0 or dvdx != 0 or dvdy != 1: # Figure out the shear/rotate/dilate calls that are equivalent. jac = JacobianWCS(dudx,dudy,dvdx,dvdy) scale, shear, theta, flip = jac.getDecomposition() - single = None + s = None if flip: - single = 0 # Special value indicating to just use transform. + s = 0 # Special value indicating to just use transform. if abs(theta.rad) > 1.e-12: - if single is None: - single = '.rotate(%s)'%theta + if s is None: + s = '.rotate(%s)'%theta else: - single = 0 + s = 0 if shear.g > 1.e-12: - if single is None: - single = '.shear(%s)'%shear + if s is None: + s = '.shear(%s)'%shear else: - single = 0 + s = 0 if abs(scale-1.0) > 1.e-12: - if single is None: - single = '.expand(%s)'%scale + if s is None: + s = '.expand(%s)'%scale else: - single = 0 - if single == 0: + s = 0 + if s == 0: # If flip or there are two components, then revert to transform as simpler. - single = '.transform(%s,%s,%s,%s)'%(dudx,dudy,dvdx,dvdy) - if single is None: + s = '.transform(%s,%s,%s,%s)'%(dudx,dudy,dvdx,dvdy) + if s is None: # If nothing is large enough to show up above, give full detail of transform - single = '.transform(%r,%r,%r,%r)'%(dudx,dudy,dvdx,dvdy) - s += single + s = '.transform(%r,%r,%r,%r)'%(dudx,dudy,dvdx,dvdy) + return s + else: + return '' + + def __str__(self): + s = str(self.original) + s += self._str_from_jac(self._jac) if self.offset.x != 0 or self.offset.y != 0: s += '.shift(%s,%s)'%(self.offset.x,self.offset.y) if self.flux_ratio != 1.: diff --git a/galsim/utilities.py b/galsim/utilities.py index d8e126cdce8..efc2487d626 100644 --- a/galsim/utilities.py +++ b/galsim/utilities.py @@ -23,9 +23,12 @@ from future.utils import iteritems from builtins import range, object import weakref +import os +import numpy as np +from .errors import GalSimError, GalSimValueError, GalSimIncompatibleValuesError, GalSimRangeError +from .errors import galsim_warn -import numpy as np def roll2d(image, shape): """Perform a 2D roll (circular shift) on a supplied 2D NumPy array, conveniently. @@ -100,9 +103,6 @@ def rotate_xy(x, y, theta): @return the rotated coordinates `(x_rot,y_rot)`. """ - from .angle import Angle - if not isinstance(theta, Angle): - raise TypeError("Input rotation angle theta must be a galsim.Angle instance.") sint, cost = theta.sincos() x_rot = x * cost - y * sint y_rot = x * sint + y * cost @@ -131,9 +131,11 @@ def canindex(arg): other_vals = [] if len(args) == 0: # Then name1,name2 need to be kwargs - # If not, then python will raise an appropriate error. - x = kwargs.pop(name1) - y = kwargs.pop(name2) + try: + x = kwargs.pop(name1) + y = kwargs.pop(name2) + except KeyError: + raise TypeError('Expecting kwargs %s, %s. Got %s'%(name1, name2, kwargs.keys())) elif ( ( isinstance(args[0], PositionI) or (not integer and isinstance(args[0], PositionD)) ) and len(args) <= 1+len(others) ): @@ -148,11 +150,11 @@ def canindex(arg): for arg in args[1:]: other_vals.append(arg) others.pop(0) - elif len(args) == 1: # pragma: no cover + elif len(args) == 1: if integer: - raise TypeError("Cannot parse argument "+str(args[0])+" as a PositionI") + raise TypeError("Cannot parse argument %s as a PositionI"%(args[0])) else: - raise TypeError("Cannot parse argument "+str(args[0])+" as a PositionD") + raise TypeError("Cannot parse argument %s as a PositionD"%(args[0])) elif len(args) <= 2 + len(others): x = args[0] y = args[1] @@ -261,16 +263,19 @@ def _convertPositions(pos, units, func): This is used by the functions getShear(), getConvergence(), getMagnification(), and getLensing() for both PowerSpectrum and NFWHalo. """ - from .position import PositionD, PositionI + from .position import Position from .angle import AngleUnit, arcsec # Check for PositionD or PositionI: - if isinstance(pos, PositionD) or isinstance(pos, PositionI): + if isinstance(pos, Position): pos = [ pos.x, pos.y ] - # Check for list of PositionD or PositionI: + elif len(pos) == 0: + raise TypeError("Unable to parse the input pos argument for %s."%func) + + # Check for list of Position: # The only other options allow pos[0], so if this is invalid, an exception # will be raised: - elif isinstance(pos[0], PositionD) or isinstance(pos[0], PositionI): + elif isinstance(pos[0], Position): pos = [ np.array([p.x for p in pos], dtype='float'), np.array([p.y for p in pos], dtype='float') ] @@ -292,7 +297,8 @@ def _convertPositions(pos, units, func): # if the string is invalid, this raises a reasonable error message. units = AngleUnit.from_name(units) if not isinstance(units, AngleUnit): - raise ValueError("units must be either an AngleUnit or a string") + raise GalSimValueError("units must be either an AngleUnit or a string", units, + ('arcsec', 'arcmin', 'degree', 'hour', 'radian')) # Convert pos to arcsec if units != arcsec: @@ -390,27 +396,27 @@ def thin_tabulated_values(x, f, rel_err=1.e-4, trim_zeros=True, preserve_range=T # Check for valid inputs if len(x) != len(f): - raise ValueError("len(x) != len(f)") + raise GalSimIncompatibleValuesError("len(x) != len(f)", x=x, f=f) if rel_err <= 0 or rel_err >= 1: - raise ValueError("rel_err must be between 0 and 1") + raise GalSimRangeError("rel_err must be between 0 and 1", rel_err, 0., 1.) if not (np.diff(x) >= 0).all(): - raise ValueError("input x is not sorted.") + raise GalSimValueError("input x is not sorted.", x) # Check for trivial noop. if len(x) <= 2: # Nothing to do return x,f - if trim_zeros: - first = max(f.nonzero()[0][0]-1, 0) # -1 to keep one non-redundant zero. - last = min(f.nonzero()[0][-1]+1, len(x)-1) # +1 to keep one non-redundant zero. - x, f = x[first:last+1], f[first:last+1] - total_integ = np.trapz(abs(f), x) if total_integ == 0: return np.array([ x[0], x[-1] ]), np.array([ f[0], f[-1] ]) thresh = total_integ * rel_err + if trim_zeros: + first = max(f.nonzero()[0][0]-1, 0) # -1 to keep one non-redundant zero. + last = min(f.nonzero()[0][-1]+1, len(x)-1) # +1 to keep one non-redundant zero. + x, f = x[first:last+1], f[first:last+1] + x_range = x[-1] - x[0] if not preserve_range: # Remove values from the front that integrate to less than thresh. @@ -488,11 +494,11 @@ def old_thin_tabulated_values(x, f, rel_err=1.e-4, preserve_range=False): # prag # Check for valid inputs if len(x) != len(f): - raise ValueError("len(x) != len(f)") + raise GalSimIncompatibleValuesError("len(x) != len(f)", x=x, f=f) if rel_err <= 0 or rel_err >= 1: - raise ValueError("rel_err must be between 0 and 1") + raise GalSimRangeError("rel_err must be between 0 and 1", rel_err, 0., 1.) if not (np.diff(x) >= 0).all(): - raise ValueError("input x is not sorted.") + raise GalSimValueError("input x is not sorted.", x) # Check for trivial noop. if len(x) <= 2: @@ -570,37 +576,6 @@ def old_thin_tabulated_values(x, f, rel_err=1.e-4, preserve_range=False): # prag return newx, newf -def _gammafn(x): # pragma: no cover - """ - This code is not currently used, but in case we need a gamma function at some point, it will be - here in the utilities module. - - The gamma function is present in python2.7's math module, but not 2.6. So try using that, - and if it fails, use some code from RosettaCode: - http://rosettacode.org/wiki/Gamma_function#Python - """ - try: - import math - return math.gamma(x) - except AttributeError: - y = float(x) - 1.0 - sm = _gammafn._a[-1] - for an in _gammafn._a[-2::-1]: - sm = sm * y + an - return 1.0 / sm - -_gammafn._a = ( 1.00000000000000000000, 0.57721566490153286061, -0.65587807152025388108, - -0.04200263503409523553, 0.16653861138229148950, -0.04219773455554433675, - -0.00962197152787697356, 0.00721894324666309954, -0.00116516759185906511, - -0.00021524167411495097, 0.00012805028238811619, -0.00002013485478078824, - -0.00000125049348214267, 0.00000113302723198170, -0.00000020563384169776, - 0.00000000611609510448, 0.00000000500200764447, -0.00000000118127457049, - 0.00000000010434267117, 0.00000000000778226344, -0.00000000000369680562, - 0.00000000000051003703, -0.00000000000002058326, -0.00000000000000534812, - 0.00000000000000122678, -0.00000000000000011813, 0.00000000000000000119, - 0.00000000000000000141, -0.00000000000000000023, 0.00000000000000000002 - ) - def horner(x, coef, dtype=None): """Evaluate univariate polynomial using Horner's method. @@ -697,25 +672,20 @@ def deInterleaveImage(image, N, conserve_flux=False,suppress_warnings=False): from .wcs import JacobianWCS, PixelScale if isinstance(N,int): n1,n2 = N,N - elif hasattr(N,'__iter__'): - if len(N)==2: - n1,n2 = N - else: - raise TypeError("'N' has to be a list or a tuple of two integers") - if not (n1 == int(n1) and n2 == int(n2)): - raise TypeError("'N' has to be of type int or a list or a tuple of two integers") - n1 = int(n1) - n2 = int(n2) else: - raise TypeError("'N' has to be of type int or a list or a tuple of two integers") + try: + n1,n2 = N + except (TypeError, ValueError): + raise TypeError("N must be an integer or a tuple of two integers") if not isinstance(image, Image): - raise TypeError("'image' has to be an instance of galsim.Image") + raise TypeError("image must be an instance of galsim.Image") y_size,x_size = image.array.shape if x_size%n1 or y_size%n2: - raise ValueError("The value of 'N' is incompatible with the dimensions of the image to "+ - +"be 'deinterleaved'") + raise GalSimIncompatibleValuesError( + "The value of N is incompatible with the dimensions of the image to be deinterleaved", + N=N, image=image) im_list, offsets = [], [] for i in range(n1): @@ -749,14 +719,13 @@ def deInterleaveImage(image, N, conserve_flux=False,suppress_warnings=False): img.setOrigin(image.origin) elif suppress_warnings is False: - import warnings - warnings.warn("Individual images could not be assigned a WCS automatically.") + galsim_warn("Individual images could not be assigned a WCS automatically.") return im_list, offsets def interleaveImages(im_list, N, offsets, add_flux=True, suppress_warnings=False, - catch_offset_errors=True): + catch_offset_errors=True): """ Interleaves the pixel values from two or more images and into a single larger image. @@ -834,33 +803,29 @@ def interleaveImages(im_list, N, offsets, add_flux=True, suppress_warnings=False from .wcs import PixelScale, JacobianWCS if isinstance(N,int): n1,n2 = N,N - elif hasattr(N,'__iter__'): - if len(N)==2: - n1,n2 = N - else: - raise TypeError("'N' has to be a list or a tuple of two integers") - if not (n1 == int(n1) and n2 == int(n2)): - raise TypeError("'N' has to be of type int or a list or a tuple of two integers") - n1 = int(n1) - n2 = int(n2) else: - raise TypeError("'N' has to be of type int or a list or a tuple of two integers") + try: + n1,n2 = N + except (TypeError, ValueError): + raise TypeError("N must be an integer or a tuple of two integers") if len(im_list)<2: - raise TypeError("'im_list' needs to have at least two instances of galsim.Image") + raise GalSimValueError("im_list must have at least two instances of galsim.Image", im_list) if (n1*n2 != len(im_list)): - raise ValueError("'N' is incompatible with the number of images in 'im_list'") + raise GalSimIncompatibleValuesError( + "N is incompatible with the number of images in im_list", N=N, im_list=im_list) if len(im_list)!=len(offsets): - raise ValueError("'im_list' and 'offsets' must be lists of same length") + raise GalSimIncompatibleValuesError( + "im_list and offsets must be lists of same length", im_list=im_list, offsets=offsets) for offset in offsets: if not isinstance(offset, PositionD): - raise TypeError("'offsets' must be a list of galsim.PositionD instances") + raise TypeError("offsets must be a list of galsim.PositionD instances") if not isinstance(im_list[0], Image): - raise TypeError("'im_list' must be a list of galsim.Image instances") + raise TypeError("im_list must be a list of galsim.Image instances") # These should be the same for all images in `im_list'. y_size, x_size = im_list[0].array.shape @@ -868,14 +833,15 @@ def interleaveImages(im_list, N, offsets, add_flux=True, suppress_warnings=False for im in im_list[1:]: if not isinstance(im, Image): - raise TypeError("'im_list' must be a list of galsim.Image instances") + raise TypeError("im_list must be a list of galsim.Image instances") if im.array.shape != (y_size,x_size): - raise ValueError("All galsim.Image instances in 'im_list' must be of the same size") + raise GalSimIncompatibleValuesError( + "All galsim.Image instances in im_list must be of the same size", im_list=im_list) if im.wcs != wcs: - raise ValueError( - "All galsim.Image instances in 'im_list' must have the same WCS") + raise GalSimIncompatibleValuesError( + "All galsim.Image instances in im_list must have the same WCS", im_list=im_list) img_array = np.zeros((n2*y_size,n1*x_size)) # The tricky part - going from (x,y) Image coordinates to array indices @@ -892,16 +858,23 @@ def interleaveImages(im_list, N, offsets, add_flux=True, suppress_warnings=False err_j = (n2-1)*0.5-n2*dy - round((n2-1)*0.5-n2*dy) tol = 1.e-6 if abs(err_i)>tol or abs(err_j)>tol: - raise ValueError( - "'offsets' must be a list of galsim.PositionD instances with x values "+ - "spaced by 1/{0} and y values by 1/{1} around 0 for N = ".format(n1,n2)+str(N)) - - if i<0 or j<0 or i>=x_size or j>=y_size: - raise ValueError( - "'offsets' must be a list of galsim.PositionD instances with x values "+ - "spaced by 1/{0} and y values by 1/{1} around 0 for N = ".format(n1,n2)+str(N)) + raise GalSimIncompatibleValuesError( + "offsets must be a list of galsim.PositionD instances with x values " + "spaced by 1/{0} and y values by 1/{1} around 0.".format(n1,n2), + N=N, offsets=offsets) + + if i<0 or j<0 or i>=n1 or j>=n2: + raise GalSimIncompatibleValuesError( + "offsets must be a list of galsim.PositionD instances with x values " + "spaced by 1/{0} and y values by 1/{1} around 0.".format(n1,n2), + N=N, offsets=offsets) + else: + # If we're told to just trust the offsets, at least make sure the slice will be + # the right shape. + i = i%n1 + j = j%n2 - img_array[j::n2,i::n1] = im_list[k].array[:,:] + img_array[j::n2,i::n1] = im_list[k].array img = Image(img_array) if not add_flux: @@ -921,17 +894,15 @@ def interleaveImages(im_list, N, offsets, add_flux=True, suppress_warnings=False img.wcs = img_wcs elif suppress_warnings is False: - import warnings - warnings.warn("Interleaved image could not be assigned a WCS automatically.") + galsim_warn("Interleaved image could not be assigned a WCS automatically.") # Assign a possibly non-trivial origin and warn if individual image have different origins. orig = im_list[0].origin img.setOrigin(orig) for im in im_list[1:]: if not im.origin==orig: # pragma: no cover - import warnings - warnings.warn("Images in `im_list' have multiple values for origin. Assigning the \ - origin of the first Image instance in 'im_list' to the interleaved image.") + galsim_warn("Images in `im_list' have multiple values for origin. Assigning the " + "origin of the first Image instance in 'im_list' to the interleaved image.") break return img @@ -1016,6 +987,8 @@ def resize(self, maxsize): else: root = self.root cache = self.cache + if maxsize <= 0: + raise GalSimValueError("Invalid maxsize", maxsize) if maxsize < oldsize: for i in range(oldsize - maxsize): # Delete root.next @@ -1023,15 +996,13 @@ def resize(self, maxsize): new_next_link = root[1] = root[1][1] new_next_link[0] = root del cache[current_next_link[2]] - elif maxsize > oldsize: + else: # maxsize > oldsize: for i in range(maxsize - oldsize): # Insert between root and root.next key = object() cache[key] = link = [root, root[1], key, None] root[1][0] = link root[1] = link - else: - raise ValueError("Invalid maxsize: {0:}".format(maxsize)) # http://stackoverflow.com/questions/2891790/pretty-printing-of-numpy-array @@ -1068,14 +1039,16 @@ def dol_to_lod(dol, N=None): out[k] = v[i] except IndexError: # It's list-like, but too short. if len(v) != 1: - raise ValueError("Cannot broadcast kwargs of different non-length-1 lengths.") + raise GalSimIncompatibleValuesError( + "Cannot broadcast kwargs of different non-length-1 lengths.", dol=dol) out[k] = v[0] except TypeError: # Value is not list-like, so broadcast it in its entirety. out[k] = v except KeyboardInterrupt: raise except: # pragma: no cover - raise ValueError("Cannot broadcast kwarg {0}={1}".format(k, v)) + raise GalSimIncompatibleValuesError( + "Cannot broadcast kwarg {0}={1}".format(k, v), dol=dol) yield out def structure_function(image): @@ -1087,8 +1060,11 @@ def structure_function(image): where the x and r on the RHS are 2D vectors, but the |r| on the LHS is just a scalar length. - @param image Image containing random field realization. The `.scale` attribute here *is* used - in the calculation. If it's `None`, then the code will use 1.0 for the scale. + The image must have its `scale` attribute defined. It will be used in the calculations to + set the scale of the radial distances. + + @param image Image containing random field realization. + @returns A python callable mapping a separation length r to the estimate of the structure function D(r). """ @@ -1096,8 +1072,6 @@ def structure_function(image): array = image.array nx, ny = array.shape scale = image.scale - if scale is None: - scale = 1.0 # The structure function can be derived from the correlation function B(r) as: # D(r) = 2 * [B(0) - B(r)] @@ -1133,8 +1107,8 @@ def combine_wave_list(*args): elif isinstance(args[0], (list, tuple)): args = args[0] else: - raise TypeError("Single input argument must be a SED, Bandpass, GSObject, " - " ChromaticObject or a (possibly mixed) list of them.") + raise TypeError("Single input argument must be an SED, Bandpass, GSObject, " + "ChromaticObject or a (possibly mixed) list of them.") blue_limit = 0.0 red_limit = np.inf @@ -1147,7 +1121,7 @@ def combine_wave_list(*args): wave_list = np.union1d(wave_list, obj.wave_list) wave_list = wave_list[(wave_list >= blue_limit) & (wave_list <= red_limit)] if blue_limit > red_limit: - raise RuntimeError("Empty wave_list intersection.") + raise GalSimError("Empty wave_list intersection.") # Make sure both limits are included. if len(wave_list) > 0 and (wave_list[0] != blue_limit or wave_list[-1] != red_limit): wave_list = np.union1d([blue_limit, red_limit], wave_list) @@ -1323,26 +1297,27 @@ def rand_with_replacement(n, n_choices, rng, weight=None, _n_rng_calls=False): from .random import BaseDeviate, UniformDeviate # Make sure we got a proper RNG. if not isinstance(rng, BaseDeviate): - raise TypeError("The rng provided to rand_with_replacement() is not a BaseDeviate") + raise TypeError("The rng provided to rand_with_replacement() must be a BaseDeviate") ud = UniformDeviate(rng) # Sanity check the requested number of random indices. # Note: we do not require that the type be an int, as long as the value is consistent with # an integer value (i.e., it could be a float 1.0 or 1). - if not n-int(n) == 0 or n < 1: - raise ValueError("n must be an integer >= 1.") - if not n_choices-int(n_choices) == 0 or n_choices < 1: - raise ValueError("n_choices must be an integer >= 1.") + if n != int(n) or n < 1: + raise GalSimValueError("n must be an integer >= 1.", n) + if n_choices != int(n_choices) or n_choices < 1: + raise GalSimValueError("n_choices must be an integer >= 1.", n_choices) # Sanity check the input weight. if weight is not None: # We need some sanity checks here in case people passed in weird values. if len(weight) != n_choices: - raise ValueError("Array of weights has wrong length: %d instead of %d"% - (len(weight), n_choices)) - if np.min(weight)<0 or np.max(weight)>1 or np.any(np.isnan(weight)) or \ - np.any(np.isinf(weight)): - raise ValueError("Supplied weights include values outside [0,1] or inf/NaN values!") + raise GalSimIncompatibleValuesError( + "Array of weights has wrong length", weight=weight, n_choices=n_choices) + if (np.min(weight)<0 or np.max(weight)>1 or np.any(np.isnan(weight)) or + np.any(np.isinf(weight))): + raise GalSimValueError("Supplied weights include values outside [0,1] or inf/NaN.", + weight) # We first make a random list of integer indices. index = np.zeros(n) @@ -1485,6 +1460,34 @@ def __init__(self, f): self.f = f.__func__ self.c = weakref.ref(f.__self__) def __call__(self, *args): - if self.c() is None : + if self.c() is None : # pragma: no cover raise TypeError('Method called on dead object') return self.f(self.c(), *args) + +def ensure_dir(target): + """ + Make sure the directory for the target location exists, watching for a race condition + + In particular check if the OS reported that the directory already exists when running + makedirs, which can happen if another process creates it before this one can + """ + + _ERR_FILE_EXISTS=17 + dir = os.path.dirname(target) + if dir == '': return + + exists = os.path.exists(dir) + if not exists: + try: + os.makedirs(dir) + except OSError as err: # pragma: no cover + # check if the file now exists, which can happen if some other + # process created the directory between the os.path.exists call + # above and the time of the makedirs attempt. This is OK + if err.errno != _ERR_FILE_EXISTS: + raise err + + elif exists and not os.path.isdir(dir): # pragma: no cover + raise OSError("tried to make directory '%s' " + "but a non-directory file of that " + "name already exists" % dir) diff --git a/galsim/vonkarman.py b/galsim/vonkarman.py index 3dbe1a05904..3f11a7d011e 100644 --- a/galsim/vonkarman.py +++ b/galsim/vonkarman.py @@ -27,6 +27,7 @@ from .utilities import lazy_property, doc_inherit from .position import PositionD from .angle import arcsec, AngleUnit +from .errors import GalSimError, convert_cpp_errors, galsim_warn class VonKarman(GSObject): @@ -116,19 +117,21 @@ def __init__(self, lam, r0, L0=25.0, flux=1, scale_unit=arcsec, @lazy_property def _sbvk(self): - sbvk = _galsim.SBVonKarman(self._lam, self._r0, self._L0, self._flux, - self._scale, self._do_delta, self._gsparams._gsp) + with convert_cpp_errors(): + sbvk = _galsim.SBVonKarman(self._lam, self._r0, self._L0, self._flux, + self._scale, self._do_delta, self._gsparams._gsp) + self._delta = sbvk.getDelta() if not self._suppress: if self._delta > self._gsparams.maxk_threshold: - import warnings - warnings.warn("VonKarman delta-function component is larger than maxk_threshold. " - "Please see docstring for information about this component and how " - "to toggle it.") + galsim_warn("VonKarman delta-function component is larger than maxk_threshold. " + "Please see docstring for information about this component and how " + "to toggle it.") if self._do_delta: - sbvk = _galsim.SBVonKarman(self._lam, self._r0, self._L0, - self._flux-self._delta, self._scale, - self._do_delta, self._gsparams._gsp) + with convert_cpp_errors(): + sbvk = _galsim.SBVonKarman(self._lam, self._r0, self._L0, + self._flux-self._delta, self._scale, + self._do_delta, self._gsparams._gsp) return sbvk @lazy_property @@ -136,8 +139,9 @@ def _sbp(self): # Add in a delta function with appropriate amplitude if requested. if self._do_delta: sbvk = self._sbvk - sbdelta = _galsim.SBDeltaFunction(self._delta, self._gsparams._gsp) - return _galsim.SBAdd([sbvk, sbdelta], self._gsparams._gsp) + with convert_cpp_errors(): + sbdelta = _galsim.SBDeltaFunction(self._delta, self._gsparams._gsp) + return _galsim.SBAdd([sbvk, sbdelta], self._gsparams._gsp) else: return self._sbvk diff --git a/galsim/wcs.py b/galsim/wcs.py index d0804d046b1..c9a876d76b3 100644 --- a/galsim/wcs.py +++ b/galsim/wcs.py @@ -48,10 +48,12 @@ """ import numpy as np + from .gsobject import GSObject -from .position import PositionI, PositionD +from .position import Position, PositionI, PositionD from .celestial import CelestialCoord from .shear import Shear +from .errors import GalSimError, GalSimIncompatibleValuesError, GalSimNotImplementedError class BaseWCS(object): """The base class for all other kinds of WCS transformations. @@ -231,10 +233,8 @@ def posToWorld(self, image_pos, color=None, **kwargs): CelestialCoord.project for the valid options. [default: 'gnomonic'] """ if color is None: color = self._color - if isinstance(image_pos, PositionI): - image_pos = PositionD(image_pos.x, image_pos.y) - elif not isinstance(image_pos, PositionD): - raise TypeError("toWorld requires a PositionD or PositionI argument") + if not isinstance(image_pos, Position): + raise TypeError("image_pos must be a PositionD or PositionI argument") return self._posToWorld(image_pos, color=color, **kwargs) def profileToWorld(self, image_profile, image_pos=None, world_pos=None, color=None): @@ -285,11 +285,9 @@ def posToImage(self, world_pos, color=None): """ if color is None: color = self._color if self.isCelestial() and not isinstance(world_pos, CelestialCoord): - raise TypeError("toImage requires a CelestialCoord argument") - elif not self.isCelestial() and isinstance(world_pos, PositionI): - world_pos = PositionD(world_pos.x, world_pos.y) - elif not self.isCelestial() and not isinstance(world_pos, PositionD): - raise TypeError("toImage requires a PositionD or PositionI argument") + raise TypeError("world_pos must be a CelestialCoord argument") + elif not self.isCelestial() and not isinstance(world_pos, Position): + raise TypeError("world_pos must be a PositionD or PositionI argument") return self._posToImage(world_pos, color=color) def profileToImage(self, world_profile, image_pos=None, world_pos=None, color=None): @@ -407,9 +405,15 @@ def local(self, image_pos=None, world_pos=None, color=None): @returns a LocalWCS instance. """ if color is None: color = self._color - if image_pos and world_pos: - raise TypeError("Only one of image_pos or world_pos may be provided") - return self._local(image_pos, world_pos, color) + if world_pos is not None: + if image_pos is not None: + raise GalSimIncompatibleValuesError( + "Only one of image_pos or world_pos may be provided", + image_pos=image_pos, world_pos=world_pos) + image_pos = self.posToImage(world_pos, color) + if image_pos is not None and not isinstance(image_pos, Position): + raise TypeError("image_pos must be a PositionD or PositionI argument") + return self._local(image_pos, color) def jacobian(self, image_pos=None, world_pos=None, color=None): """Return the local JacobianWCS of the WCS at a given point. @@ -538,9 +542,7 @@ def withOrigin(self, origin, world_origin=None, color=None): @returns the new recentered WCS """ if color is None: color = self._color - if isinstance(origin, PositionI): - origin = PositionD(origin.x, origin.y) - elif not isinstance(origin, PositionD): + if not isinstance(origin, Position): raise TypeError("origin must be a PositionD or PositionI argument") return self._withOrigin(origin, world_origin, color) @@ -584,13 +586,11 @@ def writeToFitsHeader(self, header, bounds): 5. We haven't thought much about the security implications of this, so beware using GalSim to open FITS files from untrusted sources. - @param header A FitsHeader object to write the data to. + @param header A FitsHeader (or dict-like) object to write the data to. @param bounds The bounds of the image. """ from . import fits # First write the XMIN, YMIN values - if not isinstance(header, fits.FitsHeader): - header = fits.FitsHeader(header) header["GS_XMIN"] = (bounds.xmin, "GalSim image minimum x coordinate") header["GS_YMIN"] = (bounds.ymin, "GalSim image minimum y coordinate") @@ -633,15 +633,13 @@ def _set_origin(self, origin, world_origin=None): if origin is None: self._origin = PositionD(0,0) else: - if isinstance(origin, PositionI): - origin = PositionD(origin) - elif not isinstance(origin, PositionD): + if not isinstance(origin, Position): raise TypeError("origin must be a PositionD or PositionI argument") self._origin = origin if world_origin is None: self._world_origin = PositionD(0,0) else: - if not isinstance(world_origin, PositionD): + if not isinstance(world_origin, Position): raise TypeError("world_origin must be a PositionD argument") self._world_origin = world_origin @@ -698,20 +696,9 @@ def readFromFitsHeader(header): if wcs_name is not None: wcs_type = eval('galsim.' + wcs_name) wcs = wcs_type._readHeader(header) - elif 'GS_SCALE' in header: - # Old versions of GalSim didn't write GS_WCS, but did write GS_SCALE, which implies that - # the wcs is just a PixelScale: - wcs = PixelScale(header['GS_SCALE']) - elif 'CTYPE1' in header: - try: - wcs = FitsWCS(header=header, suppress_warning=True) - except KeyboardInterrupt: - raise - except: # pragma: no cover - # This shouldn't ever happen, but just in case... - wcs = PixelScale(1.) else: - wcs = PixelScale(1.) + # If we aren't told which type to use, this should find something appropriate + wcs = FitsWCS(header=header, suppress_warning=True) if xmin != 1 or ymin != 1: # ds9 always assumes the image has an origin at (1,1), so convert back to actual @@ -817,9 +804,7 @@ def _withOrigin(self, origin, world_origin, color): # v1 = v0 + v2 - v(x0,y0) else: - if isinstance(world_origin, PositionI): - world_origin = PositionD(world_origin.x, world_origin.y) - elif not isinstance(origin, PositionD): + if not isinstance(world_origin, Position): raise TypeError("world_origin must be a PositionD or PositionI argument") if not self.isLocal(): world_origin += self.world_origin - self._posToWorld(self.origin, color=color) @@ -827,11 +812,10 @@ def _withOrigin(self, origin, world_origin, color): # If the class doesn't define something else, then we can approximate the local Jacobian # from finite differences for the derivatives. This will be overridden by UniformWCS. - def _local(self, image_pos, world_pos, color): + def _local(self, image_pos, color): + if image_pos is None: - if world_pos is None: - raise TypeError("Either image_pos or world_pos must be provided") - image_pos = self._posToImage(world_pos, color=color) + raise TypeError("origin must be a PositionD or PositionI argument") # Calculate the Jacobian using finite differences for the derivatives. x0 = image_pos.x - self.x0 @@ -913,7 +897,7 @@ def _y(self, u, v, color=None): return self._local_wcs._y(u,v) # For UniformWCS, the local WCS is an attribute. Just return it. - def _local(self, image_pos=None, world_pos=None, color=None): + def _local(self, image_pos, color): return self._local_wcs # UniformWCS transformations can be inverted easily, so might as well provide that function. @@ -966,7 +950,7 @@ def _posToImage(self, world_pos, color): return PositionD(self._x(u,v),self._y(u,v)) # For LocalWCS, this is of course trivial. - def _local(self, image_pos, world_pos, color): + def _local(self, image_pos, color): return self @@ -1003,12 +987,11 @@ def _withOrigin(self, origin, world_origin, color): # from finite differences for the derivatives of ra and dec. Very similar to the # version for EuclideanWCS, but convert from dra, ddec to du, dv locallat at the given # position. - def _local(self, image_pos, world_pos, color): + def _local(self, image_pos, color): from .angle import radians, arcsec + if image_pos is None: - if world_pos is None: - raise TypeError("Either image_pos or world_pos must be provided") - image_pos = self._posToImage(world_pos, color) + raise TypeError("origin must be a PositionD or PositionI argument") x0 = image_pos.x - self.x0 y0 = image_pos.y - self.y0 @@ -1162,7 +1145,7 @@ class PixelScale(LocalWCS): def __init__(self, scale): self._color = None - self._scale = scale + self._scale = float(scale) # Help make sure PixelScale is read-only. @property @@ -1267,7 +1250,7 @@ class ShearWCS(LocalWCS): def __init__(self, scale, shear): self._color = None - self._scale = scale + self._scale = float(scale) self._shear = shear self._g1 = shear.g1 self._g2 = shear.g2 @@ -1281,11 +1264,6 @@ def scale(self): return self._scale @property def shear(self): return self._shear - @property - def origin(self): return PositionD(0,0) - @property - def world_origin(self): return PositionD(0,0) - def _u(self, x, y, color=None): u = x * (1.-self._g1) - y * self._g2 u *= self._gfactor * self._scale @@ -1403,10 +1381,10 @@ class JacobianWCS(LocalWCS): def __init__(self, dudx, dudy, dvdx, dvdy): self._color = None - self._dudx = dudx - self._dudy = dudy - self._dvdx = dvdx - self._dvdy = dvdy + self._dudx = float(dudx) + self._dudy = float(dudy) + self._dvdx = float(dvdx) + self._dvdy = float(dvdy) self._det = dudx * dvdy - dudy * dvdx # Help make sure JacobianWCS is read-only. @@ -1419,11 +1397,6 @@ def dvdx(self): return self._dvdx @property def dvdy(self): return self._dvdy - @property - def origin(self): return PositionD(0,0) - @property - def world_origin(self): return PositionD(0,0) - def _u(self, x, y, color=None): return self._dudx * x + self._dudy * y @@ -1435,10 +1408,16 @@ def _x(self, u, v, color=None): # ( dvdx dvdy ) # J^-1 = (1/det) ( dvdy -dudy ) # ( -dvdx dudx ) - return (self._dvdy * u - self._dudy * v)/self._det + try: + return (self._dvdy * u - self._dudy * v)/self._det + except ZeroDivisionError: + raise GalSimError("Transformation is singular") def _y(self, u, v, color=None): - return (-self._dvdx * u + self._dudx * v)/self._det + try: + return (-self._dvdx * u + self._dudx * v)/self._det + except ZeroDivisionError: + raise GalSimError("Transformation is singular") def _profileToWorld(self, image_profile): from .transform import _Transform @@ -1447,10 +1426,13 @@ def _profileToWorld(self, image_profile): def _profileToImage(self, world_profile): from .transform import _Transform - return _Transform(world_profile, - (self._dvdy/self._det, -self._dudy/self._det, - -self._dvdx/self._det, self._dudx/self._det), - flux_ratio=self._pixelArea()) + try: + return _Transform(world_profile, + (self._dvdy/self._det, -self._dudy/self._det, + -self._dvdx/self._det, self._dudx/self._det), + flux_ratio=self._pixelArea()) + except ZeroDivisionError: + raise GalSimError("Transformation is singular") def _pixelArea(self): return abs(self._det) @@ -1500,7 +1482,7 @@ def getDecomposition(self): # First we need to see whether or not the transformation includes a flip. The evidence # for a flip is that the determinant is negative. if self._det == 0.: - raise RuntimeError("Transformation is singular") + raise GalSimError("Transformation is singular") elif self._det < 0.: flip = True scale = math.sqrt(-self._det) @@ -1560,8 +1542,11 @@ def _maxScale(self): return 0.5 * (h1 + h2) def _inverse(self): - return JacobianWCS(self._dvdy/self._det, -self._dudy/self._det, - -self._dvdx/self._det, self._dudx/self._det) + try: + return JacobianWCS(self._dvdy/self._det, -self._dudy/self._det, + -self._dvdx/self._det, self._dudx/self._det) + except ZeroDivisionError: + raise GalSimError("Transformation is singular") def _toJacobian(self): return self @@ -1867,7 +1852,7 @@ def _readHeader(header): dudy = header.get("CD1_2",0.) dvdx = header.get("CD2_1",0.) dvdy = header.get("CD2_2",1.) - elif 'CDELT1' in header or 'CDELT2' in header: + else: dudx = header.get("CDELT1",1.) dudy = 0. dvdx = 0. @@ -1949,18 +1934,10 @@ def _writeFuncToHeader(func, letter, header): import pickle import types, marshal, base64 if type(func) == types.FunctionType: - try: - # Python3 and usually Python2 - code = marshal.dumps(func.__code__) - name = func.__name__ - defaults = func.__defaults__ - closure = func.__closure__ - except AttributeError: # pragma: no cover - # Older Python2 syntax, just in case. - code = marshal.dumps(func.func_code) - name = func.func_name - defaults = func.func_defaults - closure = func.func_closure + code = marshal.dumps(func.__code__) + name = func.__name__ + defaults = func.__defaults__ + closure = func.__closure__ # Functions may also have something called closure cells. If there are any, we need # to include them as well. Help for this part came from: @@ -1994,7 +1971,7 @@ def _writeFuncToHeader(func, letter, header): # Fits can't handle arbitrary strings. Shrink to a base-64 alphabet that is printable. # (This is like UUencoding for those of you who remember that...) - s = base64.b64encode(s) + s = base64.b64encode(s).decode() first_key = 'GS_'+letter+'_FN' else: # Nothing to write. @@ -2014,7 +1991,8 @@ def _writeFuncToHeader(func, letter, header): else: key = 'GS_%s%04d'%(letter,i) header[key] = s_array[i] -def _makecell(value): +def _makecell(value): # pragma: no cover + # (codecov gets confused, because the lambda function is never called.) # This is a little trick to make a closure cell. # We make a function that has the given value in closure, then then get the # first (only) closure item, which will be the closure cell we need. @@ -2192,7 +2170,7 @@ def _v(self, x, y, color=None): def _x(self, u, v, color=None): if self._xfunc is None: - raise NotImplementedError( + raise GalSimNotImplementedError( "World -> Image direction not implemented for this UVFunction") else: if self._uses_color: @@ -2202,7 +2180,7 @@ def _x(self, u, v, color=None): def _y(self, u, v, color=None): if self._yfunc is None: - raise NotImplementedError( + raise GalSimNotImplementedError( "World -> Image direction not implemented for this UVFunction") else: if self._uses_color: @@ -2365,7 +2343,8 @@ def _radec(self, x, y, color=None): return self._radec_func(x,y) def _xy(self, ra, dec, color=None): - raise NotImplementedError("World -> Image direction not implemented for RaDecFunction") + raise GalSimNotImplementedError( + "World -> Image direction not implemented for RaDecFunction") def _newOrigin(self, origin): return RaDecFunction(self._orig_ra_func, self._orig_dec_func, origin) diff --git a/galsim/wfirst/__init__.py b/galsim/wfirst/__init__.py index 66a1697b37a..356e957c11d 100644 --- a/galsim/wfirst/__init__.py +++ b/galsim/wfirst/__init__.py @@ -262,8 +262,7 @@ def _parse_SCAs(SCAs): SCAs = [SCAs] # Then check for reasonable values. if min(SCAs) <= 0 or max(SCAs) > galsim.wfirst.n_sca: - raise ValueError( - "Invalid SCA! Indices must be positive and <=%d."%galsim.wfirst.n_sca) + raise galsim.GalSimRangeError("Invalid SCA.", SCAs, 1, galsim.wfirst.n_sca) # Check for uniqueness. If not unique, make it unique. SCAs = list(set(SCAs)) else: diff --git a/galsim/wfirst/wfirst_backgrounds.py b/galsim/wfirst/wfirst_backgrounds.py index 087df8dcbd9..30741a5712d 100644 --- a/galsim/wfirst/wfirst_backgrounds.py +++ b/galsim/wfirst/wfirst_backgrounds.py @@ -77,8 +77,8 @@ def getSkyLevel(bandpass, world_pos=None, exptime=None, epoch=2025, date=None): """ # Check for cached sky level information for this filter. If not, raise exception if not hasattr(bandpass, '_sky_level'): - raise RuntimeError("Only bandpasses returned from galsim.wfirst.getBandpasses() are" - " allowed here!") + raise galsim.GalSimError("Only bandpasses returned from galsim.wfirst.getBandpasses() are " + "allowed here!") # Check for proper type for position, and extract the ecliptic coordinates. if world_pos is None: @@ -87,7 +87,7 @@ def getSkyLevel(bandpass, world_pos=None, exptime=None, epoch=2025, date=None): ecliptic_lon = 90.*galsim.degrees else: if not isinstance(world_pos, galsim.CelestialCoord): - raise ValueError("Position (world_pos) must be supplied as a CelestialCoord!") + raise TypeError("world_pos must be supplied as a CelestialCoord.") if date is not None: epoch = date.year ecliptic_lon, ecliptic_lat = world_pos.ecliptic(epoch=epoch, date=date) @@ -99,12 +99,9 @@ def getSkyLevel(bandpass, world_pos=None, exptime=None, epoch=2025, date=None): # The table only includes longitude in the range [0, 180] because there is symmetry in that a # negative longitude in the range[-180, 0] should have the same sky level as at the positive # value of longitude (given that the Sun is at 0). - if ecliptic_lon/galsim.degrees > 180.: - ecliptic_lon -= 360.*galsim.degrees - ecliptic_lon = abs(ecliptic_lon/galsim.degrees)*galsim.degrees - # And latitude symmetries: - if ecliptic_lat/galsim.degrees < 0.: - ecliptic_lat = abs(ecliptic_lat/galsim.degrees)*galsim.degrees + ecliptic_lon = ecliptic_lon.wrap() + ecliptic_lon = abs(ecliptic_lon.rad)*galsim.radians + ecliptic_lat = abs(ecliptic_lat.rad)*galsim.radians sin_ecliptic_lat = np.sin(ecliptic_lat) # Take the lookup table, and turn negative numbers (indicating failure because of proximity to @@ -121,14 +118,15 @@ def getSkyLevel(bandpass, world_pos=None, exptime=None, epoch=2025, date=None): ilon = int(xlon) xlat -= ilat xlon -= ilon - sky_val = ( s[ilat, ilon] * (1.-xlat)*(1.-xlon) + \ - s[ilat, ilon+1] * (1.-xlat)*xlon + \ - s[ilat+1, ilon] * xlat*(1.-xlon) + \ - s[ilat+1, ilon+1] * xlat*xlon) + sky_val = (s[ilat, ilon] * (1.-xlat)*(1.-xlon) + + s[ilat, ilon+1] * (1.-xlat)*xlon + + s[ilat+1, ilon] * xlat*(1.-xlon) + + s[ilat+1, ilon+1] * xlat*xlon) # If the result is too large, then raise an exception: we should not look at this position! if sky_val > max_sky: - raise ValueError("Position (world_pos) is too close to sun! Would not observe here.") + raise galsim.GalSimValueError("world_pos is too close to sun. Would not observe here.", + world_pos) # Now, convert to the right units, and return. (See docstring for explanation.) # First, multiply by the effective collecting area in m^2. diff --git a/galsim/wfirst/wfirst_bandpass.py b/galsim/wfirst/wfirst_bandpass.py index adf0b8eaf77..200e3ff1b65 100644 --- a/galsim/wfirst/wfirst_bandpass.py +++ b/galsim/wfirst/wfirst_bandpass.py @@ -107,16 +107,16 @@ def getBandpasses(AB_zeropoint=True, default_thin_trunc=True, **kwargs): import warnings warnings.warn('default_thin_trunc is true, but other arguments have been passed' ' to getBandpasses(). Using the other arguments and ignoring' - ' default_thin_trunc.') + ' default_thin_trunc.', galsim.GalSimWarning) default_thin_trunc = False if len(kwargs) > 0: - for key in kwargs: + for key in list(kwargs.keys()): if key in truncate_kwargs: tmp_truncate_dict[key] = kwargs.pop(key) if key in thin_kwargs: tmp_thin_dict[key] = kwargs.pop(key) if len(kwargs) != 0: - raise ValueError("Unknown kwargs: %s"%(' '.join(kwargs.keys()))) + raise TypeError("Unknown kwargs: %s"%(' '.join(kwargs.keys()))) # Set up a dictionary. bandpass_dict = {} diff --git a/galsim/wfirst/wfirst_detectors.py b/galsim/wfirst/wfirst_detectors.py index 97a129d2ffa..2282de88293 100644 --- a/galsim/wfirst/wfirst_detectors.py +++ b/galsim/wfirst/wfirst_detectors.py @@ -27,6 +27,9 @@ import numpy as np import os +from . import exptime as default_exptime + + def applyNonlinearity(img): """ Applies the WFIRST nonlinearity function to the supplied image `im`. @@ -41,7 +44,7 @@ def applyNonlinearity(img): """ img.applyNonlinearity(NLfunc=galsim.wfirst.NLfunc) -def addReciprocityFailure(img, exptime=None): +def addReciprocityFailure(img, exptime=default_exptime): """ Accounts for the reciprocity failure for the WFIRST directors and includes it in the original Image `img` directly. @@ -55,10 +58,8 @@ def addReciprocityFailure(img, exptime=None): @param exptime The exposure time (t) in seconds, which goes into the expression for reciprocity failure given in the docstring. If None, then the routine will use the default WFIRST exposure time in galsim.wfirst.exptime. - [default: None] - """ - if exptime is None: - exptime=galsim.wfirst.exptime + [default: {exptime}] + """.format(exptime=default_exptime) img.addReciprocityFailure(exp_time=exptime, alpha=galsim.wfirst.reciprocity_alpha, base_flux=1.0) @@ -95,21 +96,15 @@ def applyPersistence(img, prev_exposures): @param img The Image to be transformed. @param prev_exposures List of up to {ncoeff} Image instances in the order of exposures, with the recent exposure being the first element. - """.format(ncoeff=galsim.wfirst.persistence_coefficients) + """.format(ncoeff=len(galsim.wfirst.persistence_coefficients)) if not hasattr(prev_exposures,'__iter__'): - raise TypeError("In wfirst.applyPersistence, 'prev_exposures' must be a list of Image" - " instances") + raise TypeError("In wfirst.applyPersistence, prev_exposures must be a list of Image " + "instances") n_exp = min(len(prev_exposures),len(galsim.wfirst.persistence_coefficients)) - if n_exp > len(galsim.wfirst.persistence_coefficients): - import warnings - warnings.warn("More than {ncoeff} Image instances were passed to" - " galsim.wfirst.applyPersistence routine. Ignoring the earlier" - " exposures".format(ncoeff=galsim.wfirst.persistence_coefficients)) - img.applyPersistence(prev_exposures[:n_exp],galsim.wfirst.persistence_coefficients[:n_exp]) -def allDetectorEffects(img, rng=None, exptime=None, prev_exposures=[]): +def allDetectorEffects(img, prev_exposures=(), rng=None, exptime=default_exptime): """ This utility applies all sources of noise and detector effects for WFIRST that are implemented in GalSim. In terms of noise, this includes the Poisson noise due to the signal (sky + @@ -124,25 +119,22 @@ def allDetectorEffects(img, rng=None, exptime=None, prev_exposures=[]): recent exposures. @param img The Image to be modified. + @param prev_exposures List of up to {ncoeff} Image instances in the order of exposures, with + the recent exposure being the first element. [default: []] @param rng An optional galsim.BaseDeviate to use for the addition of noise. If None, a new one will be initialized. [default: None] @param exptime The exposure time, in seconds. If None, then the WFIRST default - exposure time will be used. [default: None] - @param prev_exposures List of up to {ncoeff} Image instances in the order of exposures, with - the recent exposure being the first element. [default: []] + exposure time will be used. [default: {exptime}] @returns prev_exposures Updated list of previous exposures containing up to {ncoeff} Image instances. - """.format(ncoeff=galsim.wfirst.persistence_coefficients) - # Deal appropriately with passed-in RNG, exposure time. - if rng is None: - rng = galsim.BaseDeviate() - elif not isinstance(rng, galsim.BaseDeviate): - raise TypeError("The rng provided to RealGalaxy constructor is not a BaseDeviate") - if exptime is None: - exptime=galsim.wfirst.exptime + """.format(ncoeff=len(galsim.wfirst.persistence_coefficients), exptime=default_exptime) + + # Make sure we don't have any negative values. + img.replaceNegative(0.) # Add Poisson noise. + rng = galsim.BaseDeviate(rng) poisson_noise = galsim.PoissonNoise(rng) img.addNoise(poisson_noise) @@ -158,12 +150,12 @@ def allDetectorEffects(img, rng=None, exptime=None, prev_exposures=[]): img.addNoise(dark_noise) # Persistence (use WFIRST coefficients) + prev_exposures = list(prev_exposures) applyPersistence(img,prev_exposures) # Update the 'prev_exposures' queue - if len(prev_exposures)>=len(galsim.wfirst.persistence_coefficients): - prev_exposures.pop() - prev_exposures.insert(0,img.copy()) + ncoeff = len(galsim.wfirst.persistence_coefficients) + prev_exposures = [img.copy()] + prev_exposures[:ncoeff-1] # Nonlinearity (use WFIRST routine). applyNonlinearity(img) diff --git a/galsim/wfirst/wfirst_psfs.py b/galsim/wfirst/wfirst_psfs.py index f02283d2345..3d24d8d5ce1 100644 --- a/galsim/wfirst/wfirst_psfs.py +++ b/galsim/wfirst/wfirst_psfs.py @@ -185,20 +185,18 @@ def getPSF(SCAs=None, approximate_struts=False, n_waves=None, extra_aberrations= blue_limit, red_limit = _find_limits(default_bandpass_list, bandpass_dict) else: if not isinstance(wavelength_limits, tuple): - raise ValueError("Wavelength limits must be entered as a tuple!") + raise TypeError("Wavelength limits must be entered as a tuple.") blue_limit, red_limit = wavelength_limits if red_limit <= blue_limit: - raise ValueError("Wavelength limits must have red_limit > blue_limit." - "Input: blue limit=%f, red limit=%f nanometers"% - (blue_limit, red_limit)) + raise galsim.GalSimIncompatibleValuesError( + "Wavelength limits must have red_limit > blue_limit.", + blue_limit=blue_limit, red_limit=red_limit) + elif isinstance(wavelength, float): + wavelength_nm = wavelength + elif isinstance(wavelength, galsim.Bandpass): + wavelength_nm = wavelength.effective_wavelength else: - if isinstance(wavelength, galsim.Bandpass): - wavelength_nm = wavelength.effective_wavelength - elif isinstance(wavelength, float): - wavelength_nm = wavelength - else: - raise TypeError("Keyword 'wavelength' should either be a Bandpass, float," - " or None.") + raise TypeError("wavelength should either be a Bandpass, float, or None.") # Start reading in the aberrations for the relevant SCAs. aberration_dict = {} @@ -280,15 +278,14 @@ def storePSFImages(PSF_dict, filename, bandpass_list=None, clobber=False): # Check for sane input PSF_dict. if len(PSF_dict) == 0 or len(PSF_dict) > galsim.wfirst.n_sca or \ min(PSF_dict.keys()) < 1 or max(PSF_dict.keys()) > galsim.wfirst.n_sca: - raise ValueError("PSF_dict must come from getPSF()!") + raise galsim.GalSimError("PSF_dict must come from getPSF().") # Check if file already exists and warn about clobbering. - if os.path.exists(filename): - if clobber is False: - raise ValueError("Output file already exists, and clobber is not set!") + if os.path.isfile(filename): + if clobber: + os.remove(filename) else: - import warnings - warnings.warn("Output file already exists, and will be clobbered.") + raise OSError("Output file %r already exists"%filename) # Check that bandpass list input is okay. It should be strictly a subset of the default list of # bandpasses. @@ -296,13 +293,10 @@ def storePSFImages(PSF_dict, filename, bandpass_list=None, clobber=False): bandpass_list = default_bandpass_list else: if not isinstance(bandpass_list[0], basestring): - raise ValueError("Expected input list of bandpass names!") + raise TypeError("Expected input list of bandpass names.") if not set(bandpass_list).issubset(default_bandpass_list): - err_msg = '' - for item in default_bandpass_list: - err_msg += item+' ' - raise ValueError("Bandpass list must be a subset of the default list, containing %s" - %err_msg) + raise galsim.GalSimValueError("Invalid values in bandpass_list", bandpass_list, + default_bandpass_list) # Get all the WFIRST bandpasses. bandpass_dict = galsim.wfirst.getBandpasses() @@ -315,7 +309,7 @@ def storePSFImages(PSF_dict, filename, bandpass_list=None, clobber=False): PSF = PSF_dict[SCA] if not isinstance(PSF, galsim.ChromaticOpticalPSF) and \ not isinstance(PSF, galsim.InterpolatedChromaticObject): - raise RuntimeError("Error, PSFs are not ChromaticOpticalPSFs.") + raise galsim.GalSimValueError("PSFs are not ChromaticOpticalPSFs.", PSF_dict) star = galsim.Gaussian(sigma=1.e-8, flux=1.) for bp_name in bandpass_list: @@ -377,20 +371,19 @@ def loadPSFImages(filename): # Find all indices in `bp_list` that correspond to this bandpass. bp_indices = [] - if band_name in bp_list: - idx = -1 - while True: - try: - idx = bp_list.index(band_name, idx+1) - bp_indices.append(idx) - except ValueError: - break + idx = -1 + while True: + try: + idx = bp_list.index(band_name, idx+1) + bp_indices.append(idx) + except ValueError: + break for SCA in SCA_list: # Now find which element has both the right band_name and is for this SCA. There might # not be any, depending on what exactly was stored. use_idx = -1 - for index in bp_indices: + for index in bp_indices: # pragma: no branch if SCA_list[index] == SCA: use_idx = index break @@ -413,9 +406,6 @@ def _read_aberrations(SCA): @returns a NumPy array containing the aberrations, in the required format for ChromaticOpticalPSF. """ - if SCA < 1 or SCA > galsim.wfirst.n_sca: - raise ValueError("SCA requested is out of range: %d"%SCA) - # Construct filename. sca_str = '%02d'%SCA infile = os.path.join(galsim.meta_data.share_dir, diff --git a/galsim/wfirst/wfirst_wcs.py b/galsim/wfirst/wfirst_wcs.py index de9caa66457..12e18ffea78 100644 --- a/galsim/wfirst/wfirst_wcs.py +++ b/galsim/wfirst/wfirst_wcs.py @@ -147,7 +147,7 @@ def getWCS(world_pos, PA=None, date=None, SCAs=None, PA_is_FPA=False): # Parse input position if not isinstance(world_pos, galsim.CelestialCoord): - raise TypeError("Position on the sky must be given as a galsim.CelestialCoord!") + raise TypeError("Position on the sky must be given as a galsim.CelestialCoord.") # Get the date. (Vernal equinox in 2025, taken from # http://www.astropixels.com/ephemeris/soleq2001.html, if none was supplied.) @@ -157,7 +157,7 @@ def getWCS(world_pos, PA=None, date=None, SCAs=None, PA_is_FPA=False): # Are we allowed to look here? if not allowedPos(world_pos, date): - raise RuntimeError("Error, WFIRST cannot look at this position on this date!") + raise galsim.GalSimError("Error, WFIRST cannot look at this position on this date.") # If position angle was not given, then get the optimal one: if PA is None: @@ -166,7 +166,7 @@ def getWCS(world_pos, PA=None, date=None, SCAs=None, PA_is_FPA=False): else: # Just enforce type if not isinstance(PA, galsim.Angle): - raise TypeError("Position angle must be a galsim.Angle!") + raise TypeError("Position angle must be a galsim.Angle.") # Check which SCAs are to be done using a helper routine in this module. SCAs = galsim.wfirst._parse_SCAs(SCAs) @@ -192,22 +192,22 @@ def getWCS(world_pos, PA=None, date=None, SCAs=None, PA_is_FPA=False): wcs_dict = {} for i_sca in SCAs: # Set up the header. - header = galsim.FitsHeader() + header = [] # Populate some necessary variables in the FITS header that are always the same, regardless of # input and SCA number. _populate_required_fields(header) # And populate some things that just depend on the overall locations or other input, not on # the SCA. - header['RA_TARG'] = (world_pos.ra / galsim.degrees, - "right ascension of the target (deg) (J2000)") - header['DEC_TARG'] = (world_pos.dec / galsim.degrees, - "declination of the target (deg) (J2000)") - header['PA_OBSY'] = (pa_obsy / galsim.degrees, "position angle of observatory Y axis (deg)") - header['PA_FPA'] = (pa_fpa / galsim.degrees, "position angle of FPA Y axis (deg)") - - # Finally do all the SCA-specific stuff. - header['SCA_NUM'] = (i_sca, "SCA number (1 - 18)") + header.extend([ + ('RA_TARG', world_pos.ra / galsim.degrees, + "right ascension of the target (deg) (J2000)"), + ('DEC_TARG', world_pos.dec / galsim.degrees, + "declination of the target (deg) (J2000)"), + ('PA_OBSY', pa_obsy / galsim.degrees, "position angle of observatory Y axis (deg)"), + ('PA_FPA', pa_fpa / galsim.degrees, "position angle of FPA Y axis (deg)"), + ('SCA_NUM', i_sca, "SCA number (1 - 18)"), + ]) # Set the position of center of this SCA in focal plane angular coordinates. sca_xc_fpa = np.arctan(sca_xc_mm[i_sca]/focal_length)*galsim.radians @@ -240,8 +240,6 @@ def getWCS(world_pos, PA=None, date=None, SCAs=None, PA_is_FPA=False): crval = world_pos.deproject(u, v, projection='gnomonic') crval1 = crval.ra crval2 = crval.dec - header['CRVAL1'] = (crval1 / galsim.degrees, "first axis value at reference pixel") - header['CRVAL2'] = (crval2 / galsim.degrees, "second axis value at reference pixel") # Compute the position angle of the local pixel Y axis. # This requires projecting local North onto the detector axes. @@ -269,26 +267,30 @@ def getWCS(world_pos, PA=None, date=None, SCAs=None, PA_is_FPA=False): # Rotate by pa_fpa. cos_pa_sca = np.cos(pa_sca) sin_pa_sca = np.sin(pa_sca) - header['CD1_1'] = (cos_pa_sca * a10 + sin_pa_sca * b10, - "partial of first axis coordinate w.r.t. x") - header['CD1_2'] = (cos_pa_sca * a11 + sin_pa_sca * b11, - "partial of first axis coordinate w.r.t. y") - header['CD2_1'] = (-sin_pa_sca * a10 + cos_pa_sca * b10, - "partial of second axis coordinate w.r.t. x") - header['CD2_2'] = (-sin_pa_sca * a11 + cos_pa_sca * b11, - "partial of second axis coordinate w.r.t. y") - header['ORIENTAT'] = (pa_sca / galsim.degrees, - "position angle of image y axis (deg. e of n)") - header['LONPOLE'] = (phi_p / galsim.degrees, - "Native longitude of celestial pole") + + header.extend([ + ('CRVAL1', crval1 / galsim.degrees, "first axis value at reference pixel"), + ('CRVAL2', crval2 / galsim.degrees, "second axis value at reference pixel"), + ('CD1_1', cos_pa_sca * a10 + sin_pa_sca * b10, + "partial of first axis coordinate w.r.t. x"), + ('CD1_2', cos_pa_sca * a11 + sin_pa_sca * b11, + "partial of first axis coordinate w.r.t. y"), + ('CD2_1', -sin_pa_sca * a10 + cos_pa_sca * b10, + "partial of second axis coordinate w.r.t. x"), + ('CD2_2', -sin_pa_sca * a11 + cos_pa_sca * b11, + "partial of second axis coordinate w.r.t. y"), + ('ORIENTAT', pa_sca / galsim.degrees, "position angle of image y axis (deg. e of n)"), + ('LONPOLE', phi_p / galsim.degrees, "Native longitude of celestial pole"), + ]) for i in range(n_sip): for j in range(n_sip): if i+j >= 2 and i+j < n_sip: sipstr = "A_%d_%d"%(i,j) - header[sipstr] = a_sip[i_sca,i,j] + header.append( (sipstr, a_sip[i_sca,i,j]) ) sipstr = "B_%d_%d"%(i,j) - header[sipstr] = b_sip[i_sca,i,j] + header.append( (sipstr, b_sip[i_sca,i,j]) ) + header = galsim.FitsHeader(header) wcs = galsim.GSFitsWCS(header=header) # Store the original header as an attribute of the WCS. This ensures that we have all the # extra keywords for whenever an image with this WCS is written to file. @@ -324,11 +326,10 @@ def findSCA(wcs_dict, world_pos, include_border=False): """ # Sanity check args. if not isinstance(wcs_dict, dict): - raise ValueError("wcs_dict should be a dict containing WCS output by" - " galsim.wfirst.getWCS!"%galsim.wfirst.n_sca) + raise TypeError("wcs_dict should be a dict containing WCS output by galsim.wfirst.getWCS.") if not isinstance(world_pos, galsim.CelestialCoord): - raise TypeError("Position on the sky must be given as a galsim.CelestialCoord!") + raise TypeError("Position on the sky must be given as a galsim.CelestialCoord.") # Set up the minimum and maximum pixel values, depending on whether or not to include the # border. We put it immediately into a galsim.BoundsI(), since the routine returns xmin, xmax, @@ -340,7 +341,7 @@ def findSCA(wcs_dict, world_pos, include_border=False): for i_sca in wcs_dict: wcs = wcs_dict[i_sca] image_pos = wcs.toImage(world_pos) - if bounds_list[i_sca-1].includes(int(image_pos.x), int(image_pos.y)): + if bounds_list[i_sca].includes(image_pos): sca = i_sca break @@ -370,28 +371,28 @@ def _calculate_minmax_pix(include_border=False): # are the same, but that won't always be the case, so for the sake of generality we only # group together those that are forced to be the same. # - # Negative side of 1/2/3, same as positive side of 10/11/12 + # Positive side of 1/2/3, same as negative side of 10/11/12 border_mm = abs(sca_xc_mm[1]-sca_xc_mm[10])-galsim.wfirst.n_pix*pixel_size_mm half_border_pix = int(0.5*border_mm / pixel_size_mm) - min_x_pix[1:4] -= half_border_pix - max_x_pix[10:13] += half_border_pix + max_x_pix[1:4] += half_border_pix + min_x_pix[10:13] -= half_border_pix - # Positive side of 1/2/3 and 13/14/15, same as negative side of 10/11/12, 4/5/6 + # Negative side of 1/2/3 and 13/14/15, same as positive side of 10/11/12, 4/5/6 border_mm = abs(sca_xc_mm[1]-sca_xc_mm[4])-galsim.wfirst.n_pix*pixel_size_mm half_border_pix = int(0.5*border_mm / pixel_size_mm) - max_x_pix[1:4] += half_border_pix - max_x_pix[13:16] += half_border_pix - min_x_pix[10:13] -= half_border_pix - min_x_pix[4:7] -= half_border_pix + min_x_pix[1:4] -= half_border_pix + min_x_pix[13:16] -= half_border_pix + max_x_pix[10:13] += half_border_pix + max_x_pix[4:7] += half_border_pix - # Positive side of 4/5/6, 16/17/18, 7/8/9, same as negative side of 13/14/15, 7/8/9, - # 16/17/18 + # Negative side of 4/5/6, 16/17/18, same as positive side of 13/14/15, 7/8/9 + # Also add this same chip gap to the outside chips. Neg side of 7/8/9, pos 16/17/18. border_mm = abs(sca_xc_mm[7]-sca_xc_mm[4])-galsim.wfirst.n_pix*pixel_size_mm half_border_pix = int(0.5*border_mm / pixel_size_mm) - max_x_pix[4:10] += half_border_pix - max_x_pix[16:19] += half_border_pix - min_x_pix[7:10] -= half_border_pix - min_x_pix[13:19] -= half_border_pix + min_x_pix[4:10] -= half_border_pix + min_x_pix[16:19] -= half_border_pix + max_x_pix[7:10] += half_border_pix + max_x_pix[13:19] += half_border_pix # In the vertical direction, the gaps vary, with the gap between one pair of rows being # significantly larger than between the other pair of rows. The reason for this has to do @@ -399,22 +400,24 @@ def _calculate_minmax_pix(include_border=False): # and choices in which way to arrange each SCA to maximize the usable space in the focal # plane. - # Top of 2/5/8/11/14/17, same as bottom of 1/4/7/10/13/16 and 2/5/8/11/14/17 + # Top of 2/5/8/11/14/17, same as bottom of 1/4/7/10/13/16. + # Also use this for top of top row: 1/4/7/10/13/16. border_mm = abs(sca_yc_mm[1]-sca_yc_mm[2])-galsim.wfirst.n_pix*pixel_size_mm half_border_pix = int(0.5*border_mm / pixel_size_mm) - list_1 = np.linspace(1,16,6).astype(int) + list_1 = np.arange(1,18,3) list_2 = list_1 + 1 list_3 = list_1 + 2 - min_y_pix[list_1] -= half_border_pix - min_y_pix[list_2] -= half_border_pix max_y_pix[list_2] += half_border_pix + min_y_pix[list_1] -= half_border_pix + max_y_pix[list_1] += half_border_pix - # Top of 1/4/7/10/13/16, same as bottom of 3/6/9/12/15/18 and top of same - border_mm = abs(sca_yc_mm[1]-sca_yc_mm[3])-galsim.wfirst.n_pix*pixel_size_mm + # Top of 3/6/9/12/15/18, same as bottom of 2/5/8/11/14/17. + # Also use this for bottom of bottom row: 3/6/9/12/15/18. + border_mm = abs(sca_yc_mm[2]-sca_yc_mm[3])-galsim.wfirst.n_pix*pixel_size_mm half_border_pix = int(0.5*border_mm / pixel_size_mm) - min_y_pix[list_3] -= half_border_pix - max_y_pix[list_1] += half_border_pix max_y_pix[list_3] += half_border_pix + min_y_pix[list_2] -= half_border_pix + min_y_pix[list_3] -= half_border_pix return min_x_pix, max_x_pix, min_y_pix, max_y_pix @@ -423,32 +426,33 @@ def _populate_required_fields(header): Utility routine to do populate some of the basic fields for the WCS headers for WFIRST that don't require any interesting calculation. """ - header['EQUINOX'] = (2000.0, "equinox of celestial coordinate system") - header['WCSAXES'] = (2, "number of World Coordinate System axes") - header['A_ORDER'] = 4 - header['B_ORDER'] = 4 - header['WCSNAME'] = 'wfiwcs_'+optics_design_ver+'_'+prog_version - header['CRPIX1'] = (galsim.wfirst.n_pix/2, "x-coordinate of reference pixel") - header['CRPIX2'] = (galsim.wfirst.n_pix/2, "y-coordinate of reference pixel") - header['CTYPE1'] = ("RA---TAN-SIP", "coordinate type for the first axis") - header['CTYPE2'] = ("DEC--TAN-SIP", "coordinate type for the second axis") - header['SIMPLE'] = 'True' - header['BITPIX'] = 16 - header['NAXIS'] = 0 - header['EXTEND'] = 'True' - header['BZERO'] = 0 - header['BSCALE'] = 1 - header['TELESCOP'] = (tel_name, "telescope used to acquire data") - header['INSTRUME'] = (instr_name, "identifier for instrument used to acquire data") - -def _parse_sip_file(file): + header.extend([ + ('EQUINOX', 2000.0, "equinox of celestial coordinate system"), + ('WCSAXES', 2, "number of World Coordinate System axes"), + ('A_ORDER', 4), + ('B_ORDER', 4), + ('WCSNAME', 'wfiwcs_'+optics_design_ver+'_'+prog_version), + ('CRPIX1', galsim.wfirst.n_pix/2, "x-coordinate of reference pixel"), + ('CRPIX2', galsim.wfirst.n_pix/2, "y-coordinate of reference pixel"), + ('CTYPE1', "RA---TAN-SIP", "coordinate type for the first axis"), + ('CTYPE2', "DEC--TAN-SIP", "coordinate type for the second axis"), + ('SIMPLE', 'True'), + ('BITPIX', 16), + ('NAXIS', 0), + ('EXTEND', 'True'), + ('BZERO', 0), + ('BSCALE', 1), + ('TELESCOP', tel_name, "telescope used to acquire data"), + ('INSTRUME', instr_name, "identifier for instrument used to acquire data"), + ]) + +def _parse_sip_file(file): # pragma: no cover """ Utility routine to parse the file with the SIP coefficients and hand back some arrays to be used for later calculations. """ if not os.path.exists(file): - raise RuntimeError("Error, cannot find file that should have WFIRST SIP" - " coefficients: %s"%file) + raise OSError("Cannot find file that should have WFIRST SIP coefficients: %s"%file) # Parse the file, which comes from wfi_wcs_sip_gen_0.1.c provided by Jeff Kruk. data = np.loadtxt(file, usecols=[0, 3, 4, 5, 6, 7]).transpose() @@ -488,8 +492,6 @@ def _det_to_tangplane_positions(x_in, y_in): """ img_dist_coeff = np.array([-1.0873e-2, 3.5597e-03, 3.6515e-02, -1.8691e-4]) - if not isinstance(x_in, galsim.Angle) or not isinstance(y_in, galsim.Angle): - raise ValueError("Input x_in and y_in are not galsim.Angles.") # The optical distortion model is defined in terms of separations in *degrees*. r_sq = (x_in/galsim.degrees)**2 + (y_in/galsim.degrees)**2 r = np.sqrt(r_sq) diff --git a/galsim/zernike.py b/galsim/zernike.py index c24bec1dfad..01b870b2e8e 100644 --- a/galsim/zernike.py +++ b/galsim/zernike.py @@ -22,6 +22,7 @@ import numpy as np from .utilities import LRU_Cache, binomial, horner2d, nCr, lazy_property +from .errors import GalSimValueError, GalSimRangeError # Some utilities for working with Zernike polynomials @@ -101,7 +102,7 @@ def _zern_coef_array(n, m, obscuration, shape): elif obscuration == 0: coefs = np.array(_zern_rho_coefs(n, m), dtype=np.complex128) else: - raise ValueError("Illegal obscuration: {}".format(obscuration)) + raise GalSimRangeError("Invalid obscuration.", obscuration, 0., 1.) coefs /= _zern_norm(n, m) if m < 0: coefs *= -1j @@ -431,17 +432,6 @@ def __init__(self, coef, R_outer=1.0, R_inner=0.0): self.R_outer = float(R_outer) self.R_inner = float(R_inner) - # _coef_array property only exists to support the deprecated OpticalPSF.coef_array attribute. - # It can be deleted in version 2.0. - @lazy_property - def _coef_array(self): - arr = _noll_coef_array(len(self.coef)-1, self.R_inner/self.R_outer).dot(self.coef[1:]) - - if self.R_outer != 1.0: - shape = arr.shape - arr /= self.R_outer**np.sum(np.mgrid[0:2*shape[0]:2, 0:shape[1]], axis=0) - return arr - @lazy_property def _coef_array_xy(self): arr = _noll_coef_array_xy(len(self.coef)-1, self.R_inner/self.R_outer).dot(self.coef[1:]) @@ -576,7 +566,7 @@ def zernikeRotMatrix(jmax, theta): if m_jmax != 0: n_jmaxp1, m_jmaxp1 = noll_to_zern(jmax+1) if n_jmax == n_jmaxp1 and abs(m_jmaxp1) == abs(m_jmax): - raise ValueError("Cannot construct Zernike rotation matrix for jmax={}".format(jmax)) + raise GalSimValueError("Cannot construct Zernike rotation matrix for this jmax.", jmax) R = np.zeros((jmax+1, jmax+1), dtype=np.float64) R[0, 0] = 1.0 diff --git a/include/galsim/GSParams.h b/include/galsim/GSParams.h index dd2d169086c..bc5dcc9c728 100644 --- a/include/galsim/GSParams.h +++ b/include/galsim/GSParams.h @@ -123,7 +123,7 @@ namespace galsim { */ GSParams() : minimum_fft_size(128), - maximum_fft_size(4096), + maximum_fft_size(8192), folding_threshold(5.e-3), stepk_minimum_hlr(5.), maxk_threshold(1.e-3), diff --git a/include/galsim/Laguerre.h b/include/galsim/Laguerre.h index 3f739e989b4..9bf9f4dd3fa 100644 --- a/include/galsim/Laguerre.h +++ b/include/galsim/Laguerre.h @@ -32,6 +32,9 @@ typedef tmv::Matrix MatrixXd; typedef tmv::Vector > VectorXcd; typedef tmv::Matrix > MatrixXcd; #else +#if defined(__GNUC__) && __GNUC__ >= 6 +#pragma GCC diagnostic ignored "-Wint-in-bool-context" +#endif #include "Eigen/Dense" using Eigen::VectorXd; using Eigen::MatrixXd; diff --git a/include/galsim/OneDimensionalDeviate.h b/include/galsim/OneDimensionalDeviate.h index 2d4da0180c7..7c64f1c570d 100644 --- a/include/galsim/OneDimensionalDeviate.h +++ b/include/galsim/OneDimensionalDeviate.h @@ -108,7 +108,11 @@ namespace galsim { _xUpper(xUpper), _isRadial(isRadial), _gsparams(gsparams), - _fluxIsReady(false) {} + _fluxIsReady(false), + _useRejectionMethod(false), + _invMaxAbsDensity(0.), + _invMeanAbsDensity(0.) + {} Interval(const Interval& rhs) : _fluxDensityPtr(rhs._fluxDensityPtr), @@ -120,7 +124,7 @@ namespace galsim { _useRejectionMethod(rhs._useRejectionMethod), _invMaxAbsDensity(rhs._invMaxAbsDensity), _invMeanAbsDensity(rhs._invMeanAbsDensity) - {} + {} Interval& operator=(const Interval& rhs) { diff --git a/include/galsim/hsm/PSFCorr.h b/include/galsim/hsm/PSFCorr.h index 1120e824458..8ad028f3a13 100644 --- a/include/galsim/hsm/PSFCorr.h +++ b/include/galsim/hsm/PSFCorr.h @@ -237,7 +237,7 @@ namespace hsm { */ class HSMError : public std::runtime_error { public: - HSMError(const std::string& m) : std::runtime_error("HSM Error: " + m) {} + HSMError(const std::string& m) : std::runtime_error(m) {} }; //! @endcond diff --git a/pysrc/Integ.cpp b/pysrc/Integ.cpp index 5032035e3a0..97a34b53a55 100644 --- a/pysrc/Integ.cpp +++ b/pysrc/Integ.cpp @@ -24,6 +24,11 @@ namespace galsim { namespace integ { +#if defined(__GNUC__) && __GNUC__ >= 6 +// Workaround for a bug in some versions of gcc 6-8. +// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80947 +#pragma GCC visibility push(hidden) +#endif // A C++ function object that just calls a python function. class PyFunc : public std::unary_function @@ -35,6 +40,9 @@ namespace integ { private: const py::object& _func; }; +#if defined(__GNUC__) && __GNUC__ >= 6 +#pragma GCC visibility pop +#endif // Integrate a python function using int1d. py::tuple PyInt1d(const py::object& func, double min, double max, diff --git a/requirements.txt b/requirements.txt index 0126b13338b..f8c6562434f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ pybind11>=2.2 pip==9.0.3 # For now, pybind11 in conjunction with pip version 10.0 is broken. Use 9.0.3. # Not technically required, but useful. -pyyaml>=3.12 +pyyaml>=3.12 # This one is required to run tests. pandas>=0.20 # This is not in conda. Let pip install these. diff --git a/share/SConscript b/share/SConscript index d1ce939c3b4..39426cfdb11 100644 --- a/share/SConscript +++ b/share/SConscript @@ -26,7 +26,7 @@ else: meta_data_file = os.path.join('..','galsim','meta_data.py') try: f = open(meta_data_file,'w') -except IOError: +except (IOError, OSError): # Probably the user ran sudo scons install without first running plain old scons # (without sudo), so the meta_data.py file is owned by root now. # However, it should still be removable, since the directory should be owned diff --git a/src/RealGalaxy.cpp b/src/RealGalaxy.cpp index d12d1d0c063..2b8e6546214 100644 --- a/src/RealGalaxy.cpp +++ b/src/RealGalaxy.cpp @@ -20,6 +20,9 @@ #ifdef USE_TMV #include "TMV.h" #else +#if defined(__GNUC__) && __GNUC__ >= 6 +#pragma GCC diagnostic ignored "-Wint-in-bool-context" +#endif #include "Eigen/Dense" #endif diff --git a/src/WCS.cpp b/src/WCS.cpp index f95c0fb6d95..7bcf23b5df3 100644 --- a/src/WCS.cpp +++ b/src/WCS.cpp @@ -27,6 +27,9 @@ typedef tmv::Vector VectorXd; typedef tmv::Matrix MatrixXd; typedef tmv::VectorView MapVectorXd; #else +#if defined(__GNUC__) && __GNUC__ >= 6 +#pragma GCC diagnostic ignored "-Wint-in-bool-context" +#endif #include "Eigen/Dense" using Eigen::VectorXd; using Eigen::MatrixXd; diff --git a/src/hsm/PSFCorr.cpp b/src/hsm/PSFCorr.cpp index 6c1d21cf3b7..f5a682ce8b8 100644 --- a/src/hsm/PSFCorr.cpp +++ b/src/hsm/PSFCorr.cpp @@ -54,6 +54,9 @@ damages of any kind. typedef tmv::Matrix MatrixXd; typedef tmv::Vector VectorXd; #else +#if defined(__GNUC__) && __GNUC__ >= 6 +#pragma GCC diagnostic ignored "-Wint-in-bool-context" +#endif #include "Eigen/Dense" using Eigen::MatrixXd; using Eigen::VectorXd; diff --git a/test_requirements.txt b/test_requirements.txt index cf91a082b96..d660888f576 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,3 +1,4 @@ +pyyaml>=3.12 pytest>=3.4 pytest-xdist>=1.19 pytest-timeout>=1.2 diff --git a/tests/.coveragerc b/tests/.coveragerc index cb662bed3f2..94ab1652641 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -4,9 +4,6 @@ branch = True include = *galsim/* omit = - # Tests here require the DM stack, so don't include them in travis coverage tests. - *lsst/* - # These are mostly still tested, but we don't really care if the tests are complete. *deprecated/* @@ -28,34 +25,14 @@ exclude_lines = # If you put this in a comment, you can manually exclude code from being covered. pragma: no cover - # Don't complain about missing debug-only code: - logger.debug - - # Don't complain if tests don't hit defensive checks of user input - raise NotImplementedError - raise ValueError - raise RuntimeError - raise TypeError - raise AttributeError - raise IndexError - raise IOError - raise KeyError - - # Don't complain about not hitting warning code - if suppress_warnings is False: - import warnings - warnings.warn - # Don't complain if non-runnable code isn't run: if False: if 0: if __name__ == .__main__.: # Don't complain about exceptional circumstances not under control of the test suite - except KeyboardInterrupt - except IOError - except OSError + except .*KeyboardInterrupt + except .*OSError - # Or code for special cases of older versions of things. + # Or checks for alternate versions when some package is not available except ImportError - if .*pyfits_version diff --git a/tests/config_input/catalog2.fits b/tests/config_input/catalog2.fits new file mode 100644 index 00000000000..e04d6253440 Binary files /dev/null and b/tests/config_input/catalog2.fits differ diff --git a/tests/config_input/catalog2.txt b/tests/config_input/catalog2.txt new file mode 100644 index 00000000000..1bb96e6014a --- /dev/null +++ b/tests/config_input/catalog2.txt @@ -0,0 +1,5 @@ +% input catalog for test_config_value.py +% 0 1 2 3 4 5 6 7 8 9 10 11 +1.234 4.131 9 -3 1 yes He's "ceased to be" 1.2 23 +2.345 -900 0 8 0 No bleedin' "bereft of life" 0.1 15 +3.456 8.e3 -4 17 1 false demised! "kicked the bucket" -0.9 82 diff --git a/tests/config_input/catalog3.txt b/tests/config_input/catalog3.txt new file mode 100644 index 00000000000..e669804caca --- /dev/null +++ b/tests/config_input/catalog3.txt @@ -0,0 +1,3 @@ +1.234 4.131 9 -3 1 yes He's "ceased to be" 1.2 23 +2.345 -900 0 8 0 No bleedin' "bereft of life" 0.1 15 +3.456 8.e3 -4 17 1 false demised! "kicked the bucket" -0.9 82 diff --git a/tests/config_input/dict.txt b/tests/config_input/dict.txt new file mode 100644 index 00000000000..b6be92ee37f --- /dev/null +++ b/tests/config_input/dict.txt @@ -0,0 +1,33 @@ +# Copyright (c) 2012-2018 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. +# + + +b: false + +f: 0.1 + +i: 1 + +s: Brian + +noise : + models : + - { variance : 0.12 } + - { gain : 1.9, read_noise : 0.3, sky_level : 100 } + + diff --git a/tests/config_input/dict.yaml b/tests/config_input/dict.yaml index b6be92ee37f..8a8f8686d99 100644 --- a/tests/config_input/dict.yaml +++ b/tests/config_input/dict.yaml @@ -29,5 +29,3 @@ noise : models : - { variance : 0.12 } - { gain : 1.9, read_noise : 0.3, sky_level : 100 } - - diff --git a/tests/des_data/invalid_psfcat.psf b/tests/des_data/invalid_psfcat.psf new file mode 100644 index 00000000000..bbb279c0e32 Binary files /dev/null and b/tests/des_data/invalid_psfcat.psf differ diff --git a/tests/galsim_test_helpers.py b/tests/galsim_test_helpers.py index f26054c451e..fe69c0e0e35 100644 --- a/tests/galsim_test_helpers.py +++ b/tests/galsim_test_helpers.py @@ -22,6 +22,7 @@ import sys import logging import coord +import copy path, filename = os.path.split(__file__) try: @@ -34,6 +35,21 @@ # This file has some helper functions that are used by tests from multiple files to help # avoid code duplication. +# These are the default GSParams used when unspecified. We'll check that specifying +# these explicitly produces the same results. +default_params = galsim.GSParams( + minimum_fft_size = 128, + maximum_fft_size = 8192, + folding_threshold = 5.e-3, + maxk_threshold = 1.e-3, + kvalue_accuracy = 1.e-5, + xvalue_accuracy = 1.e-5, + shoot_accuracy = 1.e-5, + realspace_relerr = 1.e-4, + realspace_abserr = 1.e-6, + integration_relerr = 1.e-6, + integration_abserr = 1.e-8) + def gsobject_compare(obj1, obj2, conv=None, decimal=10): """Helper function to check that two GSObjects are equivalent """ @@ -440,11 +456,11 @@ def do_pickle(obj1, func = lambda x : x, irreprable=False): #print(repr(obj1)) with galsim.utilities.printoptions(precision=18, threshold=np.inf): obj5 = eval(repr(obj1)) - print('obj1 = ',repr(obj1)) - print('obj5 = ',repr(obj5)) + #print('obj1 = ',repr(obj1)) + #print('obj5 = ',repr(obj5)) f5 = func(obj5) - print('f1 = ',f1) - print('f5 = ',f5) + #print('f1 = ',f1) + #print('f5 = ',f5) assert f5 == f1, "func(obj1) = %r\nfunc(obj5) = %r"%(f1, f5) else: # Even if we're not actually doing the test, still make the repr to check for syntax errors. @@ -553,51 +569,6 @@ def all_obj_diff(objs, check_hash=True): raise e -def check_chromatic_invariant(obj, bps=None, waves=None): - """ Helper function to check that ChromaticObjects satisfy intended invariants. - """ - if bps is None: - # load a filter - bppath = os.path.join(galsim.meta_data.share_dir, 'bandpasses') - bandpass = (galsim.Bandpass(os.path.join(bppath, 'LSST_r.dat'), 'nm') - .truncate(relative_throughput=1e-3) - .thin(rel_err=1e-3)) - bps = [bandpass] - - if waves is None: - waves = [500.] - - assert isinstance(obj.wave_list, np.ndarray) - assert isinstance(obj.separable, bool) - assert isinstance(obj.interpolated, bool) - assert isinstance(obj.deinterpolated, (galsim.ChromaticObject, galsim.GSObject)) - - for wave in waves: - desired = obj.SED(wave) - # Since InterpolatedChromaticObject.evaluateAtWavelength involves actually drawing an - # image, which implies flux can be lost off of the edges of the image, we don't expect - # its accuracy to be nearly as good as for other objects. - decimal = 2 if obj.interpolated else 7 - np.testing.assert_almost_equal(obj.evaluateAtWavelength(wave).flux, desired, - decimal) - # Don't bother trying to draw a deconvolution. - if isinstance(obj, galsim.ChromaticDeconvolution): - continue - np.testing.assert_allclose( - obj.evaluateAtWavelength(wave).drawImage().array.sum(dtype=float), - desired, - rtol=1e-2) - - if obj.SED.spectral: - for bp in bps: - calc_flux = obj.calculateFlux(bp) - np.testing.assert_equal(obj.SED.calculateFlux(bp), calc_flux) - np.testing.assert_allclose(calc_flux, obj.drawImage(bp).array.sum(dtype=float), rtol=1e-2) - # Also try manipulating exptime and area. - np.testing.assert_allclose( - calc_flux * 10, obj.drawImage(bp, exptime=5, area=2).array.sum(dtype=float), rtol=1e-2) - - def funcname(): import inspect return inspect.stack()[1][3] @@ -692,3 +663,21 @@ def assert_warns(wtype, *args, **kwargs): del Dummy del _t + +# Context to make it easier to profile bits of the code +class profile(object): + def __init__(self, sortby='tottime', nlines=30): + self.sortby = sortby + self.nlines = nlines + + def __enter__(self): + import cProfile, pstats + self.pr = cProfile.Profile() + self.pr.enable() + return self + + def __exit__(self, type, value, traceback): + import pstats + self.pr.disable() + ps = pstats.Stats(self.pr).sort_stats(self.sortby) + ps.print_stats(self.nlines) diff --git a/tests/test_airy.py b/tests/test_airy.py index 9af812c3ae7..d4edb9d26ff 100644 --- a/tests/test_airy.py +++ b/tests/test_airy.py @@ -28,21 +28,6 @@ imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference # images. -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_airy(): """Test the generation of a specific Airy profile against a known result. @@ -173,6 +158,13 @@ def test_airy_radii(): assert_raises(AttributeError, getattr, test_gal_shear, "half_light_radius") assert_raises(AttributeError, getattr, test_gal_shear, "lam_over_diam") + # hlr and fwhm not implemented for obscuration != 0 + airy2 = galsim.Airy(lam_over_diam= 1./0.8, flux=1., obscuration=0.2) + with assert_raises(galsim.GalSimNotImplementedError): + airy2.half_light_radius + with assert_raises(galsim.GalSimNotImplementedError): + airy2.fwhm + @timer def test_airy_flux_scaling(): diff --git a/tests/test_bandpass.py b/tests/test_bandpass.py index 73d925f6254..b35cff14401 100644 --- a/tests/test_bandpass.py +++ b/tests/test_bandpass.py @@ -128,6 +128,10 @@ def test_Bandpass_basic(): blue_limit=700, red_limit=400) assert_raises(ValueError, galsim.Bandpass, throughput=lambda w: 1, wave_type='inches') assert_raises(ValueError, galsim.Bandpass, throughput=lambda w: 1, wave_type=units.Unit('Hz')) + assert_raises(ValueError, galsim.Bandpass, galsim.LookupTable([400,550], [0.4, 0.55], 'linear'), + wave_type='nm', blue_limit=300, red_limit=500) + assert_raises(ValueError, galsim.Bandpass, galsim.LookupTable([400,550], [0.4, 0.55], 'linear'), + wave_type='nm', blue_limit=500, red_limit=600) @timer @@ -300,6 +304,11 @@ def test_ne(): galsim.Bandpass(throughput=lt, wave_type='nm').withZeropoint(sed)] all_obj_diff(bps) + with assert_raises(galsim.GalSimValueError): + galsim.Bandpass(throughput=lt, wave_type='nm').withZeropoint('invalid') + with assert_raises(TypeError): + galsim.Bandpass(throughput=lt, wave_type='nm').withZeropoint(None) + @timer def test_thin(): @@ -359,6 +368,13 @@ def test_zp(): assert bp_tr.zeropoint is None, \ "Zeropoint erroneously preserved after truncating with explicit blue_limit" + with assert_raises(galsim.GalSimValueError): + bp_tr = bp.truncate(preserve_zp = 'False') + with assert_raises(galsim.GalSimValueError): + bp_tr = bp.truncate(preserve_zp = 43) + with assert_raises(galsim.GalSimIncompatibleValuesError): + galsim.Bandpass('1', 'nm', 400, 550).truncate(relative_throughput=1.e-4) + @timer def test_truncate_inputs(): diff --git a/tests/test_box.py b/tests/test_box.py index b0d8e4e46e3..a959b93efb5 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -28,22 +28,6 @@ imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference # images. -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - - @timer def test_box(): """Test the generation of a specific box profile against a known result. diff --git a/tests/test_calc.py b/tests/test_calc.py index 947345af9e3..d3a669fd448 100644 --- a/tests/test_calc.py +++ b/tests/test_calc.py @@ -159,6 +159,8 @@ def test_sigma(): np.testing.assert_equal( (g1.sigma, g1.sigma), g1.calculateMomentRadius(rtype='both'), err_msg="Gaussian.calculateMomentRadius(both) returned wrong value.") + with assert_raises(galsim.GalSimValueError): + g1.calculateMomentRadius(rtype='invalid') # Check for a convolution of two Gaussians. Should be equivalent, but now will need to # do the calculation. @@ -259,6 +261,8 @@ def test_sigma(): np.testing.assert_almost_equal( test_sigma/e1_sigma, 1.0, decimal=4, err_msg="image.calculateMomentRadius is not accurate.") + with assert_raises(galsim.GalSimValueError): + im.calculateMomentRadius(rtype='invalid') # Check that a non-square image works correctly. Also, not centered anywhere in particular. bounds = galsim.BoundsI(-1234, -1234+size*2, 8234, 8234+size) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index f5e484ac3ab..c8a6ed92f7d 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -26,9 +26,9 @@ @timer -def test_basic_catalog(): - """Test basic operations on Catalog.""" - # First the ASCII version +def test_ascii_catalog(): + """Test basic operations on an ASCII Catalog.""" + cat = galsim.Catalog(dir='config_input', file_name='catalog.txt') np.testing.assert_equal(cat.ncols, 12) np.testing.assert_equal(cat.nobjects, 3) @@ -39,7 +39,54 @@ def test_basic_catalog(): do_pickle(cat) - # Next the FITS version + cat2 = galsim.Catalog('catalog.txt', 'config_input', comments='#', file_type='ASCII') + assert cat2 == cat + assert len(cat2) == cat2.nobjects == cat2.getNObjects() == cat.nobjects + assert cat2.ncols == cat.ncols + + # Special _nobjects_only option sets nobjects, but doesn't finish setting up object. + cat3 = galsim.Catalog('catalog.txt', 'config_input', _nobjects_only=True) + assert cat3 == cat + assert len(cat3) == cat3.nobjects == cat3.getNObjects() == cat.nobjects + with assert_raises(AttributeError): + assert cat3.ncols + with assert_raises(AttributeError): + assert cat3.get(1,11) + + cat2 = galsim.Catalog('catalog2.txt', 'config_input', comments='%') + assert cat2.nobjects == cat.nobjects + np.testing.assert_array_equal(cat2.data, cat.data) + assert cat2 != cat + do_pickle(cat2) + + cat3 = galsim.Catalog('catalog3.txt', 'config_input', comments='') + assert len(cat3) == cat3.nobjects == cat.nobjects + np.testing.assert_array_equal(cat3.data, cat.data) + assert cat3 != cat + do_pickle(cat3) + + cat3n = galsim.Catalog('catalog3.txt', 'config_input', comments=None, _nobjects_only=True) + assert cat3n.nobjects == 3 + + # Check construction errors + assert_raises(galsim.GalSimValueError, galsim.Catalog, 'catalog.txt', file_type='invalid') + assert_raises(ValueError, galsim.Catalog, 'catalog3.txt', 'config_input', comments="#%") + assert_raises((IOError, OSError), galsim.Catalog, 'catalog.txt') # Wrong dir + assert_raises((IOError, OSError), galsim.Catalog, 'invalid.txt', 'config_input') + + # Check indexing errors + assert_raises(IndexError, cat.get, -1, 11) + assert_raises(IndexError, cat.get, 3, 11) + assert_raises(IndexError, cat.get, 1, -1) + assert_raises(IndexError, cat.get, 1, 50) + assert_raises(IndexError, cat.get, 'val', 11) + assert_raises(IndexError, cat.get, 3, 'val') + + +@timer +def test_fits_catalog(): + """Test basic operations on a FITS Catalog.""" + cat = galsim.Catalog(dir='config_input', file_name='catalog.fits') np.testing.assert_equal(cat.ncols, 12) np.testing.assert_equal(cat.nobjects, 3) @@ -50,10 +97,56 @@ def test_basic_catalog(): do_pickle(cat) + cat2 = galsim.Catalog('catalog.fits', 'config_input', hdu=1, file_type='FITS') + assert cat2 == cat + assert len(cat2) == cat2.nobjects == cat2.getNObjects() == cat.nobjects + assert cat2.ncols == cat.ncols + + # Special _nobjects_only option sets nobjects, but doesn't finish setting up object. + cat3 = galsim.Catalog('catalog.fits', 'config_input', _nobjects_only=True) + assert cat3 == cat + assert len(cat3) == cat3.nobjects == cat3.getNObjects() == cat.nobjects + with assert_raises(AttributeError): + assert cat3.ncols + with assert_raises(AttributeError): + cat3.get(1, 'angle2') + + # Check construction errors + assert_raises(galsim.GalSimValueError, galsim.Catalog, 'catalog.fits', file_type='invalid') + assert_raises((IOError, OSError), galsim.Catalog, 'catalog.fits') # Wrong dir + assert_raises((IOError, OSError), galsim.Catalog, 'invalid.fits', 'config_input') + + # Check indexing errors + assert_raises(IndexError, cat.get, -1, 'angle2') + assert_raises(IndexError, cat.get, 3, 'angle2') + assert_raises(KeyError, cat.get, 1, 'invalid') + assert_raises(KeyError, cat.get, 1, 3) + assert_raises(IndexError, cat.get, 'val', 'angle2') + + # Check non-default hdu + cat2 = galsim.Catalog('catalog2.fits', 'config_input', hdu=2) + assert len(cat2) == cat2.nobjects == cat.nobjects + np.testing.assert_array_equal(cat2.data, cat.data) + assert cat2 != cat + do_pickle(cat2) + + cat3 = galsim.Catalog('catalog2.fits', 'config_input', hdu='data') + assert cat3.nobjects == cat.nobjects + np.testing.assert_array_equal(cat3.data, cat.data) + assert cat3 != cat + assert cat3 != cat2 # Even though these are the same, it doesn't know 'data' is hdu 2. + do_pickle(cat3) + + cat2n = galsim.Catalog('catalog2.fits', 'config_input', hdu=2, _nobjects_only=True) + assert cat2n.nobjects == 3 + + @timer def test_basic_dict(): """Test basic operations on Dict.""" + import yaml + # Pickle d = galsim.Dict(dir='config_input', file_name='dict.p') np.testing.assert_equal(len(d), 4) @@ -79,24 +172,53 @@ def test_basic_dict(): do_pickle(d) # YAML - try: - import yaml - except ImportError as e: - # Raise a warning so this message shows up when doing pytest (or scons tests). - import warnings - warnings.warn("Unable to import yaml. Skipping yaml tests") - print("Caught ",e) - else: - d = galsim.Dict(dir='config_input', file_name='dict.yaml') - np.testing.assert_equal(len(d), 5) - np.testing.assert_equal(d.file_type, 'YAML') - np.testing.assert_equal(d['i'], 1) - np.testing.assert_equal(d.get('s'), 'Brian') - np.testing.assert_equal(d.get('s2', 'Grail'), 'Grail') # Not in dict. Use default. - np.testing.assert_almost_equal(d.get('f', 999.), 0.1) # In dict. Ignore default. - d2 = galsim.Dict(dir='config_input', file_name='dict.yaml', file_type='yaml') - assert d == d2 - do_pickle(d) + d = galsim.Dict(dir='config_input', file_name='dict.yaml') + np.testing.assert_equal(len(d), 5) + np.testing.assert_equal(d.file_type, 'YAML') + np.testing.assert_equal(d['i'], 1) + np.testing.assert_equal(d.get('s'), 'Brian') + np.testing.assert_equal(d.get('s2', 'Grail'), 'Grail') # Not in dict. Use default. + np.testing.assert_almost_equal(d.get('f', 999.), 0.1) # In dict. Ignore default. + d2 = galsim.Dict(dir='config_input', file_name='dict.yaml', file_type='yaml') + assert d == d2 + do_pickle(d) + + # We also have longer chained keys in dict.yaml + np.testing.assert_equal(d.get('noise.models.0.variance'), 0.12) + np.testing.assert_equal(d.get('noise.models.1.gain'), 1.9) + with assert_raises(KeyError): + d.get('invalid') + with assert_raises(KeyError): + d.get('noise.models.invalid') + with assert_raises(KeyError): + d.get('noise.models.1.invalid') + with assert_raises(IndexError): + d.get('noise.models.2.invalid') + with assert_raises(TypeError): + d.get('noise.models.1.gain.invalid') + + # It's really hard to get to this error. I think this is the only (contrived) way. + d3 = galsim.Dict('dict.yaml', 'config_input', key_split=None) + with assert_raises(KeyError): + d3.get('') + do_pickle(d3) + + with assert_raises(galsim.GalSimValueError): + galsim.Dict(dir='config_input', file_name='dict.yaml', file_type='invalid') + with assert_raises(galsim.GalSimValueError): + galsim.Dict(dir='config_input', file_name='dict.txt') + with assert_raises((IOError, OSError)): + galsim.Catalog('invalid.yaml', 'config_input') + + # Check some dict equivalences. + assert 'noise' in d + assert len(d) == 5 + assert sorted(d.keys()) == ['b', 'f', 'i', 'noise', 's'] + assert all( d[k] == v for k,v in d.items() ) + assert all( d[k] == v for k,v in zip(d.keys(), d.values()) ) + assert all( d[k] == v for k,v in d.iteritems() ) + assert all( d[k] == v for k,v in zip(d.iterkeys(), d.itervalues()) ) + assert all( k in d for k in d ) @timer @@ -117,21 +239,45 @@ def test_output_catalog(): """Test basic operations on Catalog.""" names = [ 'float1', 'float2', 'int1', 'int2', 'bool1', 'bool2', 'str1', 'str2', 'str3', 'str4', 'angle', 'posi', 'posd', 'shear' ] - types = [ float, float, int, int, bool, bool, str, str, str, str, + types = [ float, 'f8', int, 'i4', bool, 'bool', str, 'str', 'S', 'S0', galsim.Angle, galsim.PositionI, galsim.PositionD, galsim.Shear ] out_cat = galsim.OutputCatalog(names, types) - out_cat.addRow( [1.234, 4.131, 9, -3, 1, True, "He's", '"ceased', 'to', 'be"', - 1.2 * galsim.degrees, galsim.PositionI(5,6), - galsim.PositionD(0.3,-0.4), galsim.Shear(g1=0.2, g2=0.1) ]) - out_cat.addRow( (2.345, -900, 0.0, 8, False, 0, "bleedin'", '"bereft', 'of', 'life"', - 11 * galsim.arcsec, galsim.PositionI(-35,106), - galsim.PositionD(23.5,55.1), galsim.Shear(e1=-0.1, e2=0.15) )) - out_cat.addRow( [3.4560001, 8.e3, -4, 17.0, 1, 0, 'demised!', '"kicked', 'the', 'bucket"', - 0.4 * galsim.radians, galsim.PositionI(88,99), - galsim.PositionD(-0.99,-0.88), galsim.Shear() ]) - - # First the ASCII version + row1 = (1.234, 4.131, 9, -3, 1, True, "He's", '"ceased', 'to', 'be"', + 1.2 * galsim.degrees, galsim.PositionI(5,6), + galsim.PositionD(0.3,-0.4), galsim.Shear(g1=0.2, g2=0.1)) + row2 = (2.345, -900, 0.0, 8, False, 0, "bleedin'", '"bereft', 'of', 'life"', + 11 * galsim.arcsec, galsim.PositionI(-35,106), + galsim.PositionD(23.5,55.1), galsim.Shear(e1=-0.1, e2=0.15)) + row3 = (3.4560001, 8.e3, -4, 17.0, 1, 0, 'demised!', '"kicked', 'the', 'bucket"', + 0.4 * galsim.radians, galsim.PositionI(88,99), + galsim.PositionD(-0.99,-0.88), galsim.Shear()) + + out_cat.addRow(row1) + out_cat.addRow(row2) + out_cat.addRow(row3) + + assert out_cat.names == out_cat.getNames() == names + assert out_cat.types == out_cat.getTypes() == types + assert len(out_cat) == out_cat.getNObjects() == out_cat.nobjects == 3 + assert out_cat.getNCols() == out_cat.ncols == len(names) + + # Can also set the types after the fact. + # MJ: I think this used to be used by the "truth" catalog extra output. + # But it doesn't seem to be used there anymore. Probably not by anything then. + # I'm not sure how useful it is, I guess it doesn't hurt to leave it in. + out_cat2 = galsim.OutputCatalog(names) + assert out_cat2.types == [float] * len(names) + out_cat2.setTypes(types) + assert out_cat2.types == out_cat2.getTypes() == types + + # Another feature that doesn't seem to be used anymore is you can add the rows out of order + # and just give a key to use for sorting at the end. + out_cat2.addRow(row3, 3) + out_cat2.addRow(row1, 1) + out_cat2.addRow(row2, 2) + + # Check ASCII round trip out_cat.write(dir='output', file_name='catalog.dat') cat = galsim.Catalog(dir='output', file_name='catalog.dat') np.testing.assert_equal(cat.ncols, 17) @@ -155,9 +301,7 @@ def test_output_catalog(): np.testing.assert_almost_equal(cat.getFloat(0,15), 0.2) np.testing.assert_almost_equal(cat.getFloat(0,16), 0.1) - # Next the FITS version - if os.path.isfile('output/catalog.fits'): - os.remove('output/catalog.fits') + # Check FITS round trip out_cat.write(dir='output', file_name='catalog.fits') cat = galsim.Catalog(dir='output', file_name='catalog.fits') np.testing.assert_equal(cat.ncols, 17) @@ -182,6 +326,12 @@ def test_output_catalog(): np.testing.assert_almost_equal(cat.getFloat(0,'shear.g1'), 0.2) np.testing.assert_almost_equal(cat.getFloat(0,'shear.g2'), 0.1) + # The one that was made out of order should write the same file. + out_cat2.write(dir='output', file_name='catalog2.fits') + cat2 = galsim.Catalog(dir='output', file_name='catalog2.fits') + np.testing.assert_array_equal(cat2.data, cat.data) + assert cat2 != cat # Because file_name is different. + # Check that it properly overwrites an existing output file. out_cat.addRow( [1.234, 4.131, 9, -3, 1, True, "He's", '"ceased', 'to', 'be"', 1.2 * galsim.degrees, galsim.PositionI(5,6), @@ -199,9 +349,16 @@ def test_output_catalog(): out_cat2 = galsim.OutputCatalog(names, types) # No data. do_pickle(out_cat2) + # Check errors + with assert_raises(galsim.GalSimValueError): + out_cat.addRow((1,2,3)) # Wrong length + with assert_raises(galsim.GalSimValueError): + out_cat.write(dir='output', file_name='catalog.txt', file_type='invalid') + if __name__ == "__main__": - test_basic_catalog() + test_ascii_catalog() + test_fits_catalog() test_basic_dict() test_single_row() test_output_catalog() diff --git a/tests/test_cdmodel.py b/tests/test_cdmodel.py index bc40bc43f71..3cd237a77b7 100644 --- a/tests/test_cdmodel.py +++ b/tests/test_cdmodel.py @@ -199,6 +199,29 @@ def test_simplegeometry(): "itcdrx array is not 0 where it should be") +@timer +def test_cdmodel_errors(): + """Test some invalid usage of CDModel""" + + # I don't think these errors are possible from the PowerLawCD constructor, so test + # them directly in the base class. + with assert_raises(galsim.GalSimValueError): + # Must be odd x odd + galsim.cdmodel.BaseCDModel( + np.zeros((4,4)), np.zeros((4,4)), np.zeros((4,4)), np.zeros((4,4)) ) + with assert_raises(galsim.GalSimValueError): + # Must be square + galsim.cdmodel.BaseCDModel( + np.zeros((5,3)), np.zeros((5,3)), np.zeros((5,3)), np.zeros((5,3)) ) + with assert_raises(galsim.GalSimValueError): + # Must be same shape + galsim.cdmodel.BaseCDModel( + np.zeros((3,3)), np.zeros((3,3)), np.zeros((3,3)), np.zeros((5,5)) ) + with assert_raises(galsim.GalSimValueError): + # Must be >= 3x3 + galsim.cdmodel.BaseCDModel( + np.zeros((1,1)), np.zeros((1,1)), np.zeros((1,1)), np.zeros((1,1)) ) + @timer def test_fluxconservation(): """Test flux conservation of charge deflection model for galaxy and flat image. @@ -347,6 +370,7 @@ def test_exampleimage(): if __name__ == "__main__": test_simplegeometry() + test_cdmodel_errors() test_fluxconservation() test_forwardbackward() test_gainratio() diff --git a/tests/test_chromatic.py b/tests/test_chromatic.py index 2037fea38f9..adf204c6d18 100644 --- a/tests/test_chromatic.py +++ b/tests/test_chromatic.py @@ -168,21 +168,13 @@ def test_draw_add_commutativity(): # similar times in the test suite where we want to force it to use the base class # implementation, so those had to be switched as well. galsim.ChromaticObject.drawImage(chromatic_final, bandpass, image=chromatic_image, - integrator=integrator) + integrator=integrator, add_to_image=True) galsim.ChromaticObject.drawKImage(chromatic_final, bandpass, image=chromatic_kimage, integrator=integrator) t5 = time.time() print('ChromaticObject drawImage, drawKImage took {0} seconds.'.format(t5-t4)) # plotme(chromatic_image) - # Check error handling of too few sample points - integrator = galsim.integ.ContinuousIntegrator(galsim.integ.midptRule, N=1, use_endpoints=False) - with assert_raises(ValueError): - chromatic_final.drawImage(bandpass, integrator=integrator) - integrator = galsim.integ.ContinuousIntegrator(galsim.integ.trapzRule, N=1, use_endpoints=False) - with assert_raises(ValueError): - chromatic_final.drawImage(bandpass, integrator=integrator) - peak = chromatic_image.array.max() printval(GS_image, chromatic_image) np.testing.assert_array_almost_equal( @@ -196,16 +188,36 @@ def test_draw_add_commutativity(): err_msg="Directly computed chromatic kimage disagrees with kimage created using " +"galsim.chromatic") + # Repeat with multiple inseparable profiles. + delta = galsim.ChromaticObject(galsim.DeltaFunction()).rotate(lambda wave: wave*galsim.degrees) + chromatic_final2 = galsim.Convolve(chromatic_gal, chromatic_PSF, delta) + chromatic_final2.drawImage(bandpass, image=chromatic_image, integrator=integrator) + chromatic_final2.drawKImage(bandpass, image=chromatic_kimage, integrator=integrator) + # Note: fft vs real space differences now, so only accurate to 1.e-3 + np.testing.assert_array_almost_equal(chromatic_image.array/peak, GS_image.array/peak, 3) + np.testing.assert_array_almost_equal(chromatic_kimage.array/kpeak, GS_kimage.array/kpeak, 6) + + # Check error handling of too few sample points + integrator = galsim.integ.ContinuousIntegrator(galsim.integ.midptRule, N=1, use_endpoints=False) + with assert_raises(ValueError): + chromatic_final.drawImage(bandpass, integrator=integrator) + integrator = galsim.integ.ContinuousIntegrator(galsim.integ.trapzRule, N=1, use_endpoints=False) + with assert_raises(ValueError): + chromatic_final.drawImage(bandpass, integrator=integrator) + # As an aside, check for appropriate tests of 'integrator' argument. - assert_raises(TypeError, chromatic_final.drawImage, bandpass, method='no_pixel', + assert_raises(ValueError, chromatic_final.drawImage, bandpass, method='no_pixel', integrator='midp') # minor misspelling - assert_raises(TypeError, chromatic_final.drawKImage, bandpass, + assert_raises(ValueError, chromatic_final.drawKImage, bandpass, integrator='midp') # minor misspelling assert_raises(TypeError, chromatic_final.drawImage, bandpass, method='no_pixel', integrator=galsim.integ.midpt) assert_raises(TypeError, chromatic_final.drawKImage, bandpass, integrator=galsim.integ.midpt) + # Can't use base class directly. + assert_raises(NotImplementedError, galsim.integ.ImageIntegrator) + @timer def test_ChromaticConvolution_InterpolatedImage(): @@ -533,8 +545,28 @@ def test_chromatic_flux(): int_flux/analytic_flux, 1.0, 3, err_msg="Drawn ChromaticConvolve flux (interpolated) doesn't match analytic prediction") # As an aside, check for appropriate tests of 'integrator' argument. - assert_raises(TypeError, final_int.drawImage, bandpass, integrator='midp') # minor misspelling + assert_raises(ValueError, final_int.drawImage, bandpass, integrator='midp') # minor misspelling + assert_raises(TypeError, final_int.drawImage, bandpass, integrator=galsim.integ.midpt) + do_pickle(PSF) + + # Check option to not use exact SED + PSF = PSF.deinterpolated + PSF = PSF * 1.0 + PSF = PSF.interpolate(waves=np.linspace(bandpass.blue_limit, bandpass.red_limit, 30), + use_exact_SED=False) + final_int = galsim.Convolve([star, PSF]) + image3 = galsim.ImageD(stamp_size, stamp_size, scale=pixel_scale) + final_int.drawImage(bandpass, image=image3) + int_flux = image3.array.sum() + # Be *slightly* less stringent in this test given that we did use interpolation. + printval(image, image3) + np.testing.assert_almost_equal( + int_flux/analytic_flux, 1.0, 3, + err_msg="Drawn ChromaticConvolve flux (interpolated) doesn't match analytic prediction") + # As an aside, check for appropriate tests of 'integrator' argument. + assert_raises(ValueError, final_int.drawImage, bandpass, integrator='midp') # minor misspelling assert_raises(TypeError, final_int.drawImage, bandpass, integrator=galsim.integ.midpt) + do_pickle(PSF) # Go back to no interpolation (this will effect the PSFs that are used below). PSF = PSF.deinterpolated @@ -585,12 +617,17 @@ def test_chromatic_flux(): "using flux_ratio * ChromaticObject") # As should this. - star4 = star.withScaledFlux(flux_ratio) + star4 = star.withScaledFlux(lambda wave: flux_ratio) final = galsim.Convolve([star4, PSF]) final.drawImage(bandpass, image=image) np.testing.assert_almost_equal(image.array.sum()/target_flux, 1.0, 4, err_msg="Drawn ChromaticConvolve flux doesn't match " + "using ChromaticObject.withScaledFlux(flux_ratio)") + # Can't scale GSObject by function (just SED) + with assert_raises(TypeError): + galsim.Gaussian(fwhm=1e-8).withScaledFlux(lambda wave: flux) + with assert_raises(TypeError): + galsim.Gaussian(fwhm=1e-8) * (lambda wave: flux) # Test ChromaticObject.withFlux star5 = star.withFlux(1.0, bandpass) @@ -610,6 +647,7 @@ def test_chromatic_flux(): np.testing.assert_almost_equal(image.array.sum(), 1.0, 4, err_msg="Drawn ChromaticConvolve flux doesn't match " "using ChromaticObject.withMagnitude(0.0)") + assert_raises(galsim.GalSimError, star.withMagnitude, 25.0, bandpass) # Some very simple tests of withFluxDensity. star7 = star.withFluxDensity(5.0, 500) @@ -628,7 +666,6 @@ def test_chromatic_flux(): assert star7 == star9 - @timer def test_double_ChromaticSum(): ''' Test logic section of ChromaticConvolve that splits apart ChromaticSums for the case that @@ -673,6 +710,11 @@ def test_ChromaticConvolution_of_ChromaticConvolution(): if any(isinstance(h, galsim.ChromaticConvolution) for h in g.obj_list): raise AssertionError("ChromaticConvolution did not expand ChromaticConvolution argument") + assert_raises(TypeError, galsim.ChromaticConvolution) + assert_raises(TypeError, galsim.ChromaticConvolution, bulge_SED) + assert_raises(TypeError, galsim.ChromaticConvolution, [a,b], invalid=True) + assert_raises(NotImplementedError, galsim.ChromaticConvolution, [a,b], real_space=True) + @timer def test_ChromaticAutoConvolution(): @@ -1043,6 +1085,11 @@ def test_ChromaticObject_shear(): np.testing.assert_almost_equal(mom['Myy'] / (sigma/pixel_scale)**2, sh_myy, decimal=4) np.testing.assert_almost_equal(mom['Mxy'] / (sigma/pixel_scale)**2, sh_mxy, decimal=4) + assert_raises(TypeError, cgal.shear, 0.1, 0.3) + assert_raises(TypeError, cgal.shear, 0.1) + assert_raises(TypeError, cgal.shear, shear, g1=0.1, g2=0.2) + assert_raises(TypeError, cgal.shear, shear=shear, g1=0.1, g2=0.2) + @timer def test_ChromaticObject_shift(): @@ -1070,39 +1117,63 @@ def test_ChromaticObject_shift(): flux2, 2.*flux, 5, err_msg="rotated ChromaticObject * 2 resulted in wrong flux.") + cgal = galsim.Gaussian(fwhm=1.0) * bulge_SED + assert_raises(TypeError, cgal.shift) + assert_raises(TypeError, cgal.shift, 0.1) + assert_raises(TypeError, cgal.shift, shift, 0.1, 0.2) + assert_raises(TypeError, cgal.shift, shift, dx=0.1, dy=0.2) + assert_raises(TypeError, cgal.shift, shift=shift) @timer def test_ChromaticObject_compound_affine_transformation(): """ Check that making a (separable) object chromatic before a bunch of transformations is equivalent to making it chromatic after a bunch of transformations. """ - im1 = galsim.ImageD(32, 32, scale=0.2) - im2 = galsim.ImageD(32, 32, scale=0.2) shear = galsim.Shear(eta=1.0, beta=0.3*galsim.radians) scale = 1.1 theta = 0.1 * galsim.radians shift = (0.1, 0.3) + sed = galsim.SED('wave**0.3', 'nm', 'fphotons') + bandpass = galsim.Bandpass('1', 'nm', blue_limit=400, red_limit=550) a = galsim.Gaussian(fwhm=1.0) a = a.shear(shear).shift(shift).rotate(theta).dilate(scale) a = a.shear(shear).shift(shift).rotate(theta).expand(scale) a = a.lens(g1=0.1, g2=0.1, mu=1.1).shift(shift).rotate(theta).magnify(scale) - a = a * bulge_SED + a = a * sed - b = galsim.Gaussian(fwhm=1.0) * bulge_SED + b = galsim.Gaussian(fwhm=1.0) * sed b = b.shear(shear).shift(shift).rotate(theta).dilate(scale) b = b.shear(shear).shift(shift).rotate(theta).expand(scale) b = b.lens(g1=0.1, g2=0.1, mu=1.1).shift(shift).rotate(theta).magnify(scale) - a.drawImage(bandpass, image=im1, method='no_pixel') - b.drawImage(bandpass, image=im2, method='no_pixel') + # Include a few gratuitous combinations of functional and static values. + pshift = galsim.PositionD(*shift) + c = galsim.Gaussian(fwhm=1.0) * sed + c = c.shear(shear).shift(lambda w: shift).rotate(theta).dilate(lambda w: scale) + c = c.shear(shear).shift(lambda w: pshift).rotate(theta).expand(scale) + c = c.lens(g1=lambda w:0.1, g2=0.1, mu=lambda w:1.1).shift(shift).rotate(theta).magnify(scale) + + d = galsim.Gaussian(fwhm=1.0) * sed + d = d.shear(lambda w: shear).shift(pshift).rotate(lambda w: theta).dilate(scale) + d = d.shear(shear).shift(shift).rotate(theta).transform(scale, lambda w:0, lambda w:0, scale) + d = d.lens(g1=0.1, g2=lambda w:0.1, mu=1.1).shift(shift).rotate(theta).magnify(scale) + + im1 = galsim.ImageD(32, 32, scale=0.2) + im1 = a.drawImage(bandpass, image=im1.copy(), method='no_pixel') + im2 = b.drawImage(bandpass, image=im1.copy(), method='no_pixel') + im3 = c.drawImage(bandpass, image=im1.copy(), method='no_pixel') + im4 = d.drawImage(bandpass, image=im1.copy(), method='no_pixel') printval(im1, im2) - np.testing.assert_array_almost_equal(im1.array, im2.array, 5, + np.testing.assert_array_almost_equal(im2.array, im1.array, 5, + "ChromaticObject affine transformation not equal to " + "GSObject affine transformation") + np.testing.assert_array_almost_equal(im3.array, im1.array, 5, + "ChromaticObject affine transformation not equal to " + "GSObject affine transformation") + np.testing.assert_array_almost_equal(im4.array, im1.array, 5, "ChromaticObject affine transformation not equal to " "GSObject affine transformation") - - do_pickle(a) - do_pickle(b) # Check flux scaling flux = im2.array.sum() @@ -1112,6 +1183,13 @@ def test_ChromaticObject_compound_affine_transformation(): flux2, 2.*flux, 5, err_msg="transformed ChromaticObject * 2 resulted in wrong flux.") + # Just check that the cache resizing routines are what the docs say they are. + galsim.ChromaticObject.resize_multiplier_cache(100) + galsim.ChromaticConvolution.resize_effective_prof_cache(100) + + # Check some branches in repr that we wouldn't hit otherwise. + repr(a); repr(b); repr(c); repr(d) + @timer def test_analytic_integrator(): @@ -1177,7 +1255,7 @@ def test_analytic_integrator(): "Analytic integrator doesn't match sample integrator") # Test that attempting to use SampleIntegrator with analytic sed, bandpass raises an Error: - with assert_raises(AttributeError): + with assert_raises(ValueError): final1.drawImage(band1, integrator=galsim.integ.SampleIntegrator(rule=galsim.integ.trapzRule)) @@ -1191,14 +1269,14 @@ def test_gsparam(): # getting properly forwarded through the internals of ChromaticObjects. gsparams = galsim.GSParams(maximum_fft_size=16) gal = galsim.Gaussian(fwhm=1, gsparams=gsparams) * bulge_SED - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimFFTSizeError): gal.drawImage(bandpass) # Repeat, putting the gsparams argument in after the ChromaticObject constructor. gal = galsim.Gaussian(fwhm=1) * bulge_SED psf = galsim.Gaussian(sigma=0.4) final = galsim.Convolve([gal, psf], gsparams=gsparams) - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimError): final.drawImage(bandpass) do_pickle(final) @@ -1301,6 +1379,13 @@ def test_separable_ChromaticSum(): np.testing.assert_array_almost_equal(img1.array, img4.array, 5, "separable ChromaticSum not correctly drawn") + assert_raises(TypeError, galsim.ChromaticSum, + [mono_gal1 * bulge_SED, mono_gal2 * bulge_SED], invalid=3) + assert_raises(TypeError, galsim.ChromaticSum) + assert_raises(TypeError, galsim.ChromaticSum, bulge_SED) + with assert_raises(galsim.GalSimIncompatibleValuesError): + sum = mono_gal1 * bulge_SED + mono_gal2 + @timer def test_centroid(): @@ -1535,7 +1620,7 @@ def __repr__(self): ' when including achromatic transformations after precomputation') # Check that the routine does not interpolate outside of its original bounds. - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimError): obj_interp.drawImage(bandpass_z) # Make sure it behaves appropriately when asked to apply chromatic transformations after @@ -1551,26 +1636,22 @@ def __repr__(self): interp_psf = exact_psf.interpolate(waves, oversample_fac=oversample_fac) trans_exact_psf = \ exact_psf.shear(shear=chrom_shear).shift(dx=0.,dy=chrom_shift_y).dilate(chrom_dilate) - # The object is going to emit a warning that we don't want to worry about (it's good for code - # users, but a nuisance when testing), so let's deliberately ignore it by going into a - # `catch_warnings` context. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - trans_interp_psf = \ - interp_psf.shear(shear=chrom_shear).shift(dx=0.,dy=chrom_shift_y).dilate(chrom_dilate) - exact_obj = galsim.Convolve(star, trans_exact_psf) - interp_obj = galsim.Convolve(star, trans_interp_psf) - im_exact = exact_obj.drawImage(bandpass, scale=atm_scale) - im_interp = im_exact.copy() - im_interp = interp_obj.drawImage(bandpass, image=im_interp, scale=atm_scale) - # Note: since the image rendering should have been done in exactly the same way (it should - # have trashed the interpolation entirely), test to high precision. - np.testing.assert_array_almost_equal( - im_interp.array, im_exact.array, decimal=9, - err_msg='Did not do exact chromatic transformation by discarding interpolation') - # Also make sure that it ditched the interpolation. - assert not hasattr(trans_interp_psf, 'waves') + + with assert_warns(galsim.GalSimWarning): + trans_interp_psf = interp_psf.shear( + shear=chrom_shear).shift(dx=0.,dy=chrom_shift_y).dilate(chrom_dilate) + exact_obj = galsim.Convolve(star, trans_exact_psf) + interp_obj = galsim.Convolve(star, trans_interp_psf) + im_exact = exact_obj.drawImage(bandpass, scale=atm_scale) + im_interp = im_exact.copy() + im_interp = interp_obj.drawImage(bandpass, image=im_interp, scale=atm_scale) + # Note: since the image rendering should have been done in exactly the same way (it should + # have trashed the interpolation entirely), test to high precision. + np.testing.assert_array_almost_equal( + im_interp.array, im_exact.array, decimal=9, + err_msg='Did not do exact chromatic transformation by discarding interpolation') + # Also make sure that it ditched the interpolation. + assert not hasattr(trans_interp_psf, 'waves') @timer @@ -1616,6 +1697,9 @@ def test_ChromaticOpticalPSF(): obscuration=obscuration, nstruts=nstruts) do_pickle(psf) + with assert_raises(galsim.GalSimIncompatibleValuesError): + galsim.ChromaticOpticalPSF(lam=lam, diam=diam, aberrations=aberrations, lam_over_diam=0.02) + if not os.path.isfile(os.path.join(refdir, 'r_exact.fits')): import warnings warnings.warn("Could not find file r_exact.fits, so generating it from scratch. This " @@ -1643,8 +1727,10 @@ def test_ChromaticOpticalPSF(): obj = galsim.Convolve(star, psf) if __name__ == '__main__': - # This is slow, but it worth testing the pickling of InterpolatedChromaticObjects. + # This is slow, but it is worth testing the pickling of InterpolatedChromaticObjects. do_pickle(psf) + else: + repr(psf) im_r_ref = galsim.fits.read(os.path.join(refdir, 'r_exact.fits')) im_r = im_r_ref.copy() @@ -1741,6 +1827,9 @@ def test_ChromaticAiry(): im_r_2.array, im_r.array, decimal=8, err_msg='Inconsistent ChromaticAiry image when initializing a different way') + with assert_raises(galsim.GalSimIncompatibleValuesError): + galsim.ChromaticAiry(lam=lam, diam=diam, lam_over_diam=lam_over_diam/galsim.arcsec) + # Also check evaluation at a single wavelength. chromatic_psf_400 = psf.evaluateAtWavelength(400.) new_lam_over_diam = (1.e-9*400/diam)*galsim.radians @@ -1788,6 +1877,13 @@ def test_chromatic_fiducial_wavelength(): assert np.isfinite(img1.array.sum()), "drawImage failed to identify fiducial wavelength" assert np.isfinite(img2.array.sum()), "drawImage failed to identify fiducial wavelength" + # Pathalogical sed that is zero across the band. + bad_sed = galsim.SED(galsim.LookupTable([300,498,499,601,602,800], + [ 1, 1, 0, 0, 1, 1], 'linear'), 'nm', 'flambda') + gal3 = galsim.Gaussian(fwhm=1) * bad_sed + with assert_raises(galsim.GalSimError): + gal3.drawImage(bp) + @timer def test_chromatic_image_setup(): @@ -1839,6 +1935,79 @@ def test_convolution_of_spectral(): assert_raises(ValueError, galsim.Convolve, cgal1, cgal1, cgal2, cgal3) +def check_chromatic_invariant(obj, bps=None, waves=None): + """ Helper function to check that ChromaticObjects satisfy intended invariants. + """ + if bps is None: + # load a filter + bppath = os.path.join(galsim.meta_data.share_dir, 'bandpasses') + bandpass = (galsim.Bandpass(os.path.join(bppath, 'LSST_r.dat'), 'nm') + .truncate(relative_throughput=1e-3) + .thin(rel_err=1e-3)) + bps = [bandpass] + + if waves is None: + waves = [500.] + + assert isinstance(obj.wave_list, np.ndarray) + assert isinstance(obj.separable, bool) + assert isinstance(obj.interpolated, bool) + assert isinstance(obj.deinterpolated, (galsim.ChromaticObject, galsim.GSObject)) + + for wave in waves: + desired = obj.SED(wave) + # Since InterpolatedChromaticObject.evaluateAtWavelength involves actually drawing an + # image, which implies flux can be lost off of the edges of the image, we don't expect + # its accuracy to be nearly as good as for other objects. + decimal = 2 if obj.interpolated else 7 + np.testing.assert_almost_equal(obj.evaluateAtWavelength(wave).flux, desired, + decimal) + # Don't bother trying to draw a deconvolution. + if isinstance(obj, galsim.ChromaticDeconvolution): + continue + np.testing.assert_allclose( + obj.evaluateAtWavelength(wave).drawImage().array.sum(dtype=float), + desired, + rtol=1e-2) + + if obj.SED.spectral: + for bp in bps: + calc_flux = obj.calculateFlux(bp) + np.testing.assert_equal(obj.SED.calculateFlux(bp), calc_flux) + np.testing.assert_allclose(calc_flux, + obj.drawImage(bp).array.sum(dtype=float), rtol=1e-2) + # Also try manipulating exptime and area. + np.testing.assert_allclose( + calc_flux * 10, + obj.drawImage(bp, exptime=5, area=2).array.sum(dtype=float), rtol=1e-2) + + assert_raises(galsim.GalSimSEDError, galsim.Deconvolve, obj) + assert_raises(galsim.GalSimSEDError, galsim.AutoConvolve, obj) + assert_raises(galsim.GalSimSEDError, galsim.AutoCorrelate, obj) + assert_raises(galsim.GalSimSEDError, galsim.FourierSqrt, obj) + + try: + obj = copy.copy(obj) + obj.SED = galsim.SED('1', 'nm', '1') + except AttributeError: + return + if isinstance(obj, galsim.GSObject): return + + # Test errors for dimensionless SEDs. + with assert_raises(galsim.GalSimSEDError): + obj.drawImage(bps[0]) + with assert_raises(galsim.GalSimSEDError): + obj.drawKImage(bps[0]) + with assert_raises(galsim.GalSimSEDError): + obj.withFluxDensity(100., 500) + with assert_raises(galsim.GalSimSEDError): + obj.withFluxDensity(100., 500) + with assert_raises(galsim.GalSimSEDError): + obj.calculateFlux(bps[0]) + with assert_raises(galsim.GalSimSEDError): + obj.calculateMagnitude(bps[0]) + + @timer def test_chromatic_invariant(): # Test atomic and non-transformed objects first. @@ -1857,6 +2026,9 @@ def test_chromatic_invariant(): do_pickle(chrom3) do_pickle(galsim.ChromaticObject(gsobj)) + with assert_raises(TypeError): + galsim.ChromaticObject(bulge_SED) + check_chromatic_invariant(chrom1) check_chromatic_invariant(chrom2) check_chromatic_invariant(chrom3) @@ -1876,22 +2048,30 @@ def test_chromatic_invariant(): np.testing.assert_almost_equal(img1.array, img3.array, decimal=5) # ChromaticAtmosphere - chrom_atm = galsim.ChromaticAtmosphere(gsobj, 500.0, zenith_angle=20.0 * galsim.degrees) + chrom_atm = galsim.ChromaticAtmosphere(gsobj, 500.0, zenith_angle=20.0 * galsim.degrees, + pressure=70., temperature=285., H2O_pressure=1.05) check_chromatic_invariant(chrom_atm) do_pickle(chrom_atm) + assert_raises(TypeError, galsim.ChromaticAtmosphere, gsobj, + 500.0, zenith_angle=20.0 * galsim.degrees, invalid=3) + # ChromaticTransformation formed from __mul__ chrom = gsobj * bulge_SED check_chromatic_invariant(chrom) do_pickle(chrom) + with assert_raises(galsim.GalSimError): + chrom.noise + # ChromaticOpticalPSF - chrom_opt = galsim.ChromaticOpticalPSF(lam=500.0, diam=2.0, tip=2.0, tilt=3.0, defocus=0.2) + chrom_opt = galsim.ChromaticOpticalPSF(lam=500.0, diam=2.0, tip=2.0, tilt=3.0, defocus=0.2, + scale_unit='arcmin') check_chromatic_invariant(chrom_opt) do_pickle(chrom_opt) # ChromaticAiry - chrom_airy = galsim.ChromaticAiry(lam=500.0, diam=3.0) + chrom_airy = galsim.ChromaticAiry(lam=500.0, diam=3.0, scale_unit=galsim.arcmin) check_chromatic_invariant(chrom_airy) do_pickle(chrom_airy) @@ -1904,6 +2084,8 @@ def test_chromatic_invariant(): # e.g. autoconv2 has no hope. But there are a few do_pickle calls that are commented # out that we should probably try to make work. A job for another day, though... #do_pickle(chrom_sum_noSED) + repr(chrom_sum_noSED) + str(chrom_sum_noSED) chrom_sum_SED = chrom + chrom # also separable check_chromatic_invariant(chrom_sum_SED) @@ -1922,6 +2104,9 @@ def test_chromatic_invariant(): check_chromatic_invariant(conv1) do_pickle(conv1) + with assert_raises(galsim.GalSimError): + conv1.noise + conv2 = galsim.Convolve(chrom_airy, chrom_opt) # Non-SEDed check_chromatic_invariant(conv2) do_pickle(conv2) @@ -2002,6 +2187,7 @@ def test_ne(): gal2 = galsim.Gaussian(fwhm=1.1) cgal1 = galsim.ChromaticObject(gal1).dilate(lambda w:1) cgal2 = galsim.ChromaticObject(gal2).dilate(lambda w:1) + cgal3 = cgal1.interpolate(np.arange(400, 550, 10)) # ChromaticObject. Only param is the GSObject to chromaticize. # The following should test unequal: @@ -2017,6 +2203,8 @@ def test_ne(): # oversample_fac. # Also get a copy of cgal1 and make it interpolatable, but with a different waves argument. gals = [cgal1, + cgal2, + cgal3, galsim.InterpolatedChromaticObject(cgal1, np.arange(500, 700, 50)), galsim.InterpolatedChromaticObject(cgal2, np.arange(500, 700, 50)), galsim.InterpolatedChromaticObject(cgal1, np.arange(500, 700, 25)), @@ -2062,24 +2250,40 @@ def test_ne(): sed2 = galsim.SED(lambda w: 2*w, 'nm', 'flambda') # The following should test unequal. gals = [gal1 * sed1, - gal2 * sed2, - gal1 * sed2] + gal1 * sed2, + gal2 * sed1, + gal2 * sed2] all_obj_diff(gals) # ChromaticTransformation. Params are an object (possibly chromatic), a jacobian jac, an # offset, a flux_ratio, and gsparams. For coverage, test jac, offset, and flux_ratio as # consts and functions. - jac = lambda w: [[w, 0], [0, 1]] - offset = lambda w: (0, w) - flux_ratio = lambda w: w + jac1 = lambda w: [[w, 0], [0, 1]] + jac2 = lambda w: [[w, 0], [0, w]] + offset1 = lambda w: (0, w) + offset2 = lambda w: (w, 0) + flux_ratio1 = lambda w: w + flux_ratio2 = lambda w: w**2 # The following should test unequal. + with assert_warns(galsim.GalSimWarning): + trans_cgal3 = galsim.ChromaticTransformation( + cgal3, jac=jac1, offset=offset1, flux_ratio=flux_ratio1), gals = [galsim.ChromaticTransformation(cgal1), - galsim.ChromaticTransformation(cgal1, jac=[[1, 1.1], [0.1, 1]]), - galsim.ChromaticTransformation(cgal1, jac=jac), - galsim.ChromaticTransformation(cgal1, offset=(0.1, 0.0)), - galsim.ChromaticTransformation(cgal1, offset=offset), - galsim.ChromaticTransformation(cgal1, flux_ratio=1.1), - galsim.ChromaticTransformation(cgal1, flux_ratio=flux_ratio), + galsim.ChromaticTransformation(cgal3), + galsim.ChromaticTransformation(gal1, jac=[[1, 1.1], [0.1, 1]]), + galsim.ChromaticTransformation(gal1, jac=[[1, 0.1], [0.1, 1]]), + galsim.ChromaticTransformation(gal1, jac=jac1), + galsim.ChromaticTransformation(gal1, jac=jac2), + galsim.ChromaticTransformation(gal1, offset=(0.1, 0.0)), + galsim.ChromaticTransformation(gal1, offset=(0.0, 0.1)), + galsim.ChromaticTransformation(gal1, offset=offset1), + galsim.ChromaticTransformation(gal1, offset=offset2), + galsim.ChromaticTransformation(gal1, flux_ratio=1.1), + galsim.ChromaticTransformation(gal1, flux_ratio=1.4), + galsim.ChromaticTransformation(gal1, flux_ratio=flux_ratio1), + galsim.ChromaticTransformation(gal1, flux_ratio=flux_ratio2), + galsim.ChromaticTransformation(cgal1, jac=jac1, offset=offset1, flux_ratio=flux_ratio1), + trans_cgal3, galsim.ChromaticTransformation(cgal1, gsparams=gsp)] all_obj_diff(gals) @@ -2087,6 +2291,7 @@ def test_ne(): # The following should test unequal. gals = [galsim.ChromaticSum(cgal1), galsim.ChromaticSum(cgal1, cgal2), + galsim.ChromaticSum(cgal3, cgal2), galsim.ChromaticSum(cgal2, cgal1), # Not! commutative. galsim.ChromaticSum(galsim.ChromaticSum(cgal1, cgal2), cgal2), galsim.ChromaticSum(cgal1, galsim.ChromaticSum(cgal2, cgal2)), # Not! associative. @@ -2095,8 +2300,11 @@ def test_ne(): # ChromaticConvolution. Params are objs to convolve and potentially gsparams. # The following should test unequal + with assert_warns(galsim.GalSimWarning): + conv_32 = galsim.ChromaticConvolution(cgal3, cgal2), gals = [galsim.ChromaticConvolution(cgal1), galsim.ChromaticConvolution(cgal1, cgal2), + conv_32, galsim.ChromaticConvolution(cgal2, cgal1), # Not! commutative. galsim.ChromaticConvolution(galsim.ChromaticConvolution(cgal1, cgal2), cgal2), # ChromaticConvolution is associative! (unlike galsim.Convolution) @@ -2107,21 +2315,31 @@ def test_ne(): # ChromaticDeconvolution. Only params here are obj to deconvolve and gsparams. gals = [galsim.ChromaticDeconvolution(cgal1), galsim.ChromaticDeconvolution(cgal2), + galsim.ChromaticDeconvolution(cgal3), galsim.ChromaticDeconvolution(cgal1, gsparams=gsp)] all_obj_diff(gals) - # ChromaticAutoConvolution. Only params here are obj to deconvolve and gsparams. + # ChromaticAutoConvolution. gals = [galsim.ChromaticAutoConvolution(cgal1), galsim.ChromaticAutoConvolution(cgal2), + galsim.ChromaticAutoConvolution(cgal3), galsim.ChromaticAutoConvolution(cgal1, gsparams=gsp)] all_obj_diff(gals) - # ChromaticAutoCorrelation. Only params here are obj to deconvolve and gsparams. + # ChromaticAutoCorrelation. gals = [galsim.ChromaticAutoCorrelation(cgal1), galsim.ChromaticAutoCorrelation(cgal2), + galsim.ChromaticAutoCorrelation(cgal3), galsim.ChromaticAutoCorrelation(cgal1, gsparams=gsp)] all_obj_diff(gals) + # ChromaticFourierSqrt. + gals = [galsim.ChromaticFourierSqrtProfile(cgal1), + galsim.ChromaticFourierSqrtProfile(cgal2), + galsim.ChromaticFourierSqrtProfile(cgal3), + galsim.ChromaticFourierSqrtProfile(cgal1, gsparams=gsp)] + all_obj_diff(gals) + # ChromaticOpticalPSF. Params include: lam, (diam or lam_over_diam), aberrations, nstruts, # strut_thick, strut_angle, obscuration, oversampling, pad_factor, flux, gsparams, ... # Most of these get tested in the same way, (via a kwargs dict comparison), so only test a few diff --git a/tests/test_config_gsobject.py b/tests/test_config_gsobject.py index 598aef520b9..46d9adb5697 100644 --- a/tests/test_config_gsobject.py +++ b/tests/test_config_gsobject.py @@ -47,6 +47,14 @@ def test_gaussian(): 'shear' : galsim.Shear(g1=-0.15, g2=0.2) }, 'gal6' : { 'type' : 'DeltaFunction' , 'flux' : 72.5 }, + 'bad1' : { 'type' : 'Gaussian' , 'fwhm' : 2, 'sigma' : 3, 'flux' : 100 }, + 'bad2' : { 'type' : 'Gaussian' }, + 'bad3' : { 'type' : 'Gaussian', 'sig' : 4 }, + 'bad4' : { 'sigma' : 2 }, + 'bad5' : { 'type' : 'Gauss', 'sigma' : 2 }, + 'bad6' : { 'type' : 'Gaussian', 'resolution' : 1.5 }, # requires psf field. + 'bad7' : { 'type' : 'Gaussian', 'sigma' : 2, 'resolution' : 1.5 }, # can't give sigma + 'bad8' : { 'type' : 'Gaussian', 'half_light_radius' : 2, 'resolution' : 1.5 }, # or hlr } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -80,6 +88,41 @@ def test_gaussian(): gal6c = galsim.Gaussian(sigma = 1.e-10, flux = 72.5) gsobject_compare(gal6a, gal6c, conv=galsim.Gaussian(sigma=0.01)) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad4') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad5') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad6') + + # Test various invalid ways to use resolution. + # This psf cannot be used for resolution, since no half_light_radius field. + psf_file = os.path.join('SBProfile_comparison_images','gauss_smallshear.fits') + config['psf'] = { 'type' : 'InterpolatedImage', 'image' : psf_file } + psf = galsim.config.BuildGSObject(config, 'psf') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad6') + # This has half_light_radius, but it raises an exception for obscuration != 1 + config['psf'] = { 'type' : 'Airy' , 'lam_over_diam' : 0.4, 'obscuration' : 0.3 } + psf = galsim.config.BuildGSObject(config, 'psf') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad6') + # This finally works. + config['psf'] = { 'type' : 'Airy' , 'lam_over_diam' : 0.4 } + psf = galsim.config.BuildGSObject(config, 'psf') + gal = galsim.config.BuildGSObject(config, 'bad6') + # Can't give a different size along with resolution. + with assert_raises(galsim.GalSimConfigError): + gal = galsim.config.BuildGSObject(config, 'bad7') + with assert_raises(galsim.GalSimConfigError): + gal = galsim.config.BuildGSObject(config, 'bad8') + @timer def test_moffat(): @@ -101,6 +144,9 @@ def test_moffat(): 'shear' : galsim.Shear(g1=-0.15, g2=0.2), 'gsparams' : { 'maxk_threshold' : 1.e-2 } }, + 'bad1' : { 'type' : 'Moffat' , 'beta' : 1.4, 'scale_radius' : 2, 'fwhm' : 3 }, + 'bad2' : { 'type' : 'Moffat' , 'beta' : 1.4 }, + 'bad3' : { 'type' : 'Moffat' , 'beth' : 1.4, 'fwhm' : 8 }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -137,6 +183,13 @@ def test_moffat(): with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c, conv=galsim.Gaussian(sigma=0.01)) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + @timer def test_airy(): @@ -157,7 +210,10 @@ def test_airy(): 'gal5' : { 'type' : 'Airy' , 'lam_over_diam' : 45, 'gsparams' : { 'xvalue_accuracy' : 1.e-2 } }, - 'gal6' : { 'type' : 'Airy' , 'lam' : 400., 'diam' : 4.0, 'scale_unit' : 'arcmin' } + 'gal6' : { 'type' : 'Airy' , 'lam' : 400., 'diam' : 4.0, 'scale_unit' : 'arcmin' }, + 'bad1' : { 'type' : 'Airy' , 'lam_over_diam' : 0.4, 'lam' : 400, 'diam' : 10 }, + 'bad2' : { 'type' : 'Airy' , 'flux' : 1.3 }, + 'bad3' : { 'type' : 'Airy' , 'lam_over_diam' : 0.4, 'obsc' : 0.3, 'flux' : 100 }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -196,6 +252,13 @@ def test_airy(): with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + @timer def test_kolmogorov(): @@ -207,7 +270,7 @@ def test_kolmogorov(): 'gal3' : { 'type' : 'Kolmogorov' , 'half_light_radius' : 2, 'flux' : 1.e6, 'ellip' : { 'type' : 'QBeta' , 'q' : 0.6, 'beta' : 0.39 * galsim.radians } }, - 'gal4' : { 'type' : 'Kolmogorov' , 'lam_over_r0' : 1, 'flux' : 50, + 'gal4' : { 'type' : 'Kolmogorov' , 'lam' : 400, 'r0_500' : 0.15, 'flux' : 50, 'dilate' : 3, 'ellip' : galsim.Shear(e1=0.3), 'rotate' : 12 * galsim.degrees, 'magnify' : 1.03, 'shear' : galsim.Shear(g1=0.03, g2=-0.05), @@ -215,7 +278,10 @@ def test_kolmogorov(): }, 'gal5' : { 'type' : 'Kolmogorov' , 'lam_over_r0' : 1, 'flux' : 50, 'gsparams' : { 'integration_relerr' : 1.e-2, 'integration_abserr' : 1.e-4 } - } + }, + 'bad1' : { 'type' : 'Kolmogorov' , 'fwhm' : 2, 'lam_over_r0' : 3, 'flux' : 100 }, + 'bad2' : { 'type' : 'Kolmogorov', 'flux' : 100 }, + 'bad3' : { 'type' : 'Kolmogorov' , 'lam_over_r0' : 2, 'lam' : 400, 'r0' : 0.15 }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -232,7 +298,7 @@ def test_kolmogorov(): gsobject_compare(gal3a, gal3b) gal4a = galsim.config.BuildGSObject(config, 'gal4')[0] - gal4b = galsim.Kolmogorov(lam_over_r0 = 1, flux = 50) + gal4b = galsim.Kolmogorov(lam=400, r0_500=0.15, flux = 50) gal4b = gal4b.dilate(3).shear(e1 = 0.3).rotate(12 * galsim.degrees) gal4b = gal4b.lens(0.03, -0.05, 1.03).shift(dx = 0.7, dy = -1.2) gsobject_compare(gal4a, gal4b) @@ -247,6 +313,12 @@ def test_kolmogorov(): with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') @timer def test_opticalpsf(): @@ -279,7 +351,13 @@ def test_opticalpsf(): os.path.join(".","Optics_comparison_images","sample_pupil_rolled.fits"), 'pupil_angle' : 27.*galsim.degrees }, 'gal6' : {'type' : 'OpticalPSF' , 'lam' : 874.0, 'diam' : 7.4, 'flux' : 70., - 'obscuration' : 0.1 } + 'aberrations' : [0.06, 0.12, -0.08, 0.07, 0.04, 0.0, 0.0, -0.13], + 'obscuration' : 0.1 }, + 'gal7' : {'type' : 'OpticalPSF' , 'lam' : 874.0, 'diam' : 7.4, 'aberrations' : []}, + 'bad1' : {'type' : 'OpticalPSF' , 'lam' : 874.0, 'diam' : 7.4, 'lam_over_diam' : 0.2}, + 'bad2' : {'type' : 'OpticalPSF' , 'lam_over_diam' : 0.2, + 'aberrations' : "0.06, 0.12, -0.08, 0.07, 0.04, 0.0, 0.0, -0.13"}, + 'bad3' : {'type' : 'OpticalPSF' , 'lam_over_diam' : 0.2, 'aberr' : []}, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -317,9 +395,23 @@ def test_opticalpsf(): gsobject_compare(gal5a, gal5b) gal6a = galsim.config.BuildGSObject(config, 'gal6')[0] - gal6b = galsim.OpticalPSF(lam=874., diam=7.4, flux=70., obscuration=0.1) + aberrations = np.zeros(12, dtype=float) + aberrations[4:] = [0.06, 0.12, -0.08, 0.07, 0.04, 0.0, 0.0, -0.13] + gal6b = galsim.OpticalPSF(lam=874., diam=7.4, flux=70., obscuration=0.1, + aberrations=aberrations) gsobject_compare(gal6a, gal6b) + gal7a = galsim.config.BuildGSObject(config, 'gal7')[0] + gal7b = galsim.OpticalPSF(lam=874., diam=7.4) + gsobject_compare(gal7a, gal7b) + + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + @timer def test_exponential(): @@ -339,7 +431,9 @@ def test_exponential(): }, 'gal5' : { 'type' : 'Exponential' , 'scale_radius' : 1, 'flux' : 50, 'gsparams' : { 'kvalue_accuracy' : 1.e-2 } - } + }, + 'bad1' : { 'type' : 'Exponential' , 'scale_radius' : 2, 'half_light_radius' : 3 }, + 'bad2' : { 'type' : 'Exponential' }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -371,6 +465,10 @@ def test_exponential(): with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c, conv=galsim.Gaussian(sigma=1)) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') @timer def test_sersic(): @@ -397,7 +495,11 @@ def test_sersic(): 'gal7' : { 'type' : 'Sersic' , 'n' : 3.2, 'half_light_radius' : 1.7, 'flux' : 50, 'trunc' : 4.3, 'gsparams' : { 'realspace_relerr' : 1.e-2 , 'realspace_abserr' : 1.e-4 } - } + }, + 'bad1' : { 'type' : 'Sersic' , 'n' : 0.1, 'half_light_radius' : 3.5 }, + 'bad2' : { 'type' : 'Sersic' , 'n' : 11.1, 'half_light_radius' : 3.5 }, + 'bad3' : { 'type' : 'Sersic' , 'n' : 1.1 }, + 'bad4' : { 'type' : 'Sersic' , 'n' : 1.1, 'half_light_radius' : 3.5, 'scale_radius' : 2 }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -437,7 +539,7 @@ def test_sersic(): # and would be rather slow. gal6a = galsim.config.BuildGSObject(config, 'gal6')[0] gal6b = galsim.Sersic(n=0.7, half_light_radius=1, flux=50) - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimFFTSizeError): gsobject_compare(gal6a, gal6b, conv=galsim.Gaussian(sigma=1)) gal7a = galsim.config.BuildGSObject(config, 'gal7')[0] @@ -452,6 +554,15 @@ def test_sersic(): with assert_raises(AssertionError): gsobject_compare(gal7a, gal7c, conv=conv) + with assert_raises(galsim.GalSimRangeError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimRangeError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad4') + @timer def test_devaucouleurs(): @@ -527,6 +638,12 @@ def test_inclined_exponential(): 'half_light_radius' : 1, 'flux' : 50, 'gsparams' : { 'minimum_fft_size' : 256 } }, + 'bad1' : { 'type' : 'InclinedExponential' , 'inclination' : 0.7 * galsim.radians, + 'half_light_radius' : 1, 'scale_radius' : 2 }, + 'bad2' : { 'type' : 'InclinedExponential' , 'inclination' : 0.7 * galsim.radians, + 'scale_h_over_r' : 0.2 }, + 'bad3' : { 'type' : 'InclinedExponential' , 'inclination' : 0.7 * galsim.radians, + 'scale_radius' : 1, 'scale_h_over_r' : 0.2, 'scale_height' : 0.1 }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -552,14 +669,22 @@ def test_inclined_exponential(): gal5a = galsim.config.BuildGSObject(config, 'gal5')[0] gsparams = galsim.GSParams(minimum_fft_size=256) - gal5b = galsim.InclinedExponential(inclination=0.7 * galsim.radians, half_light_radius=1, flux=50, gsparams=gsparams) + gal5b = galsim.InclinedExponential(inclination=0.7 * galsim.radians, half_light_radius=1, + flux=50, gsparams=gsparams) gsobject_compare(gal5a, gal5b, conv=galsim.Gaussian(sigma=1)) # Make sure they don't match when using the default GSParams - gal5c = galsim.InclinedExponential(inclination=0.7 * galsim.radians, half_light_radius=1, flux=50) + gal5c = galsim.InclinedExponential(inclination=0.7 * galsim.radians, half_light_radius=1, + flux=50) with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c, conv=galsim.Gaussian(sigma=1)) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimError): + galsim.config.BuildGSObject(config, 'bad3') @timer def test_inclined_sersic(): @@ -592,7 +717,8 @@ def test_inclined_sersic(): gsobject_compare(gal1a, gal1b) gal2a = galsim.config.BuildGSObject(config, 'gal2')[0] - gal2b = galsim.InclinedSersic(n=3.5, inclination=21 * galsim.degrees, scale_radius=0.007, flux=100) + gal2b = galsim.InclinedSersic(n=3.5, inclination=21 * galsim.degrees, scale_radius=0.007, + flux=100) gsobject_compare(gal2a, gal2b) gal3a = galsim.config.BuildGSObject(config, 'gal3')[0] @@ -610,11 +736,13 @@ def test_inclined_sersic(): gal5a = galsim.config.BuildGSObject(config, 'gal5')[0] gsparams = galsim.GSParams(minimum_fft_size=256) - gal5b = galsim.InclinedSersic(n=0.7, inclination=0.7 * galsim.radians, half_light_radius=1, flux=50, gsparams=gsparams) + gal5b = galsim.InclinedSersic(n=0.7, inclination=0.7 * galsim.radians, half_light_radius=1, + flux=50, gsparams=gsparams) gsobject_compare(gal5a, gal5b, conv=galsim.Gaussian(sigma=1)) # Make sure they don't match when using the default GSParams - gal5c = galsim.InclinedSersic(n=0.7, inclination=0.7 * galsim.radians, half_light_radius=1, flux=50) + gal5c = galsim.InclinedSersic(n=0.7, inclination=0.7 * galsim.radians, half_light_radius=1, + flux=50) with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c, conv=galsim.Gaussian(sigma=1)) @@ -645,24 +773,18 @@ def test_pixel(): gal2b = galsim.Pixel(scale = 1.7, flux = 100) gsobject_compare(gal2a, gal2b) - # The config stuff emits a warning about the rectangular pixel. - # We suppress that here, since we're doing it on purpose. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - gal3a = galsim.config.BuildGSObject(config, 'gal3')[0] - gal3b = galsim.Box(width = 2, height = 2.1, flux = 1.e6) - gal3b = gal3b.shear(q = 0.6, beta = 0.39 * galsim.radians) - # Drawing sheared Pixel without convolution doesn't work, so we need to - # do the extra convolution by a Gaussian here - gsobject_compare(gal3a, gal3b, conv=galsim.Gaussian(0.1)) - - gal4a = galsim.config.BuildGSObject(config, 'gal4')[0] - gal4b = galsim.Box(width = 1, height = 1.2, flux = 50) - gal4b = gal4b.dilate(3).shear(e1 = 0.3).rotate(12 * galsim.degrees).magnify(1.03) - gal4b = gal4b.shear(g1 = 0.03, g2 = -0.05).shift(dx = 0.7, dy = -1.2) - gsobject_compare(gal4a, gal4b, conv=galsim.Gaussian(0.1)) + gal3a = galsim.config.BuildGSObject(config, 'gal3')[0] + gal3b = galsim.Box(width = 2, height = 2.1, flux = 1.e6) + gal3b = gal3b.shear(q = 0.6, beta = 0.39 * galsim.radians) + # Drawing sheared Pixel without convolution doesn't work, so we need to + # do the extra convolution by a Gaussian here + gsobject_compare(gal3a, gal3b, conv=galsim.Gaussian(0.1)) + + gal4a = galsim.config.BuildGSObject(config, 'gal4')[0] + gal4b = galsim.Box(width = 1, height = 1.2, flux = 50) + gal4b = gal4b.dilate(3).shear(e1 = 0.3).rotate(12 * galsim.degrees).magnify(1.03) + gal4b = gal4b.shear(g1 = 0.03, g2 = -0.05).shift(dx = 0.7, dy = -1.2) + gsobject_compare(gal4a, gal4b, conv=galsim.Gaussian(0.1)) @timer def test_realgalaxy(): @@ -695,7 +817,9 @@ def test_realgalaxy(): 'gal7' : { 'type' : 'RealGalaxy' , 'random' : True}, # I admit the one below is odd (why would you specify "random" and have it be False?) but # one could imagine setting it based on some probabilistic process... - 'gal8' : { 'type' : 'RealGalaxy' , 'random' : False} + 'gal8' : { 'type' : 'RealGalaxy' , 'random' : False}, + 'bad1' : { 'type' : 'RealGalaxy' , 'index' : -3 }, + 'bad2' : { 'type' : 'RealGalaxy' , 'index' : 3000 }, } rng = galsim.UniformDeviate(1234) config['rng'] = galsim.UniformDeviate(1234) # A second copy starting with the same seed. @@ -769,6 +893,12 @@ def test_realgalaxy(): gal8b = galsim.RealGalaxy(real_cat, index=7) gsobject_compare(gal8a, gal8b, conv=conv) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + + @timer def test_cosmosgalaxy(): """Test various ways to build a COSMOSGalaxy @@ -837,7 +967,7 @@ def test_cosmosgalaxy(): gsobject_compare(gal3a, gal3b, conv=conv) config['obj_num'] = 3 - with assert_raises(IndexError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildGSObject(config, 'gal4') # One more test: make sure that if we specified from the start not to use real galaxies, that @@ -866,9 +996,7 @@ def test_cosmosgalaxy(): config['obj_num'] = 0 # It is going to complain that it doesn't have weight factors. We want to ignore this. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with assert_warns(galsim.GalSimWarning): gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] gal1b = cosmos_cat.makeGalaxy(rng=rng) gsobject_compare(gal1a, gal1b, conv=conv) @@ -892,10 +1020,10 @@ def test_interpolated_image(): 'x_interpolant' : 'lanczos5', 'scale' : 0.7, 'flux' : 1.e5 }, 'gal5' : { 'type' : 'InterpolatedImage', 'image' : file_name, - 'noise_pad' : 0.001 + 'noise_pad' : 0.001, 'noise_pad_size' : 64, }, 'gal6' : { 'type' : 'InterpolatedImage', 'image' : file_name, - 'noise_pad' : 'fits_files/blankimg.fits' + 'noise_pad' : 'fits_files/blankimg.fits', 'noise_pad_size' : 64, }, 'gal7' : { 'type' : 'InterpolatedImage', 'image' : file_name, 'pad_image' : 'fits_files/blankimg.fits' @@ -926,11 +1054,12 @@ def test_interpolated_image(): gsobject_compare(gal4a, gal4b) gal5a = galsim.config.BuildGSObject(config, 'gal5')[0] - gal5b = galsim.InterpolatedImage(im, rng=rng, noise_pad=0.001) + gal5b = galsim.InterpolatedImage(im, rng=rng, noise_pad=0.001, noise_pad_size=64) gsobject_compare(gal5a, gal5b) gal6a = galsim.config.BuildGSObject(config, 'gal6')[0] - gal6b = galsim.InterpolatedImage(im, rng=rng, noise_pad='fits_files/blankimg.fits') + gal6b = galsim.InterpolatedImage(im, rng=rng, noise_pad='fits_files/blankimg.fits', + noise_pad_size=64) gsobject_compare(gal6a, gal6b) gal7a = galsim.config.BuildGSObject(config, 'gal7')[0] @@ -1037,6 +1166,21 @@ def test_add(): ], 'flux' : 170. }, + 'bad1' : { + 'type' : 'Add' , + 'items' : { 'type' : 'Gaussian', 'sigma' : 2, 'flux' : 0.3 }, + }, + 'bad2' : { + 'type' : 'Add' , + 'items' : [], + }, + 'bad3' : { + 'type' : 'Add', + 'items' : 'invalid', + }, + 'bad4' : { + 'type' : 'Add', + }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -1110,6 +1254,15 @@ def test_add(): assert ("Warning: Automatic flux for the last item in Sum (to make the total flux=1) " + "resulted in negative flux = -0.200000 for that item") in cl.output + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad4') + @timer def test_convolve(): @@ -1173,6 +1326,21 @@ def test_convolve(): { 'type' : 'Gaussian' , 'sigma' : 2 }, ] }, + 'bad1' : { + 'type' : 'Convolve' , + 'items' : { 'type' : 'Gaussian', 'sigma' : 2, 'flux' : 0.3 }, + }, + 'bad2' : { + 'type' : 'Convolve' , + 'items' : [], + }, + 'bad3' : { + 'type' : 'Convolve' , + 'items' : 'invalid', + }, + 'bad4' : { + 'type' : 'Convolve' , + }, } gal1a = galsim.config.BuildGSObject(config, 'gal1')[0] @@ -1224,6 +1392,15 @@ def test_convolve(): gal6b = galsim.Gaussian(sigma = 2) gsobject_compare(gal6a, gal6b) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad4') + @timer def test_list(): @@ -1296,6 +1473,38 @@ def test_list(): with assert_raises(AssertionError): gsobject_compare(gal5a, gal5c, conv=galsim.Gaussian(sigma=1)) + config = { + 'bad1' : { 'type' : 'List', + 'items' : { 'type' : 'Exponential' , 'scale_radius' : 3.4, 'flux' : 100 } }, + 'bad2' : { 'type' : 'List', 'items' : [], }, + 'bad3' : { 'type' : 'List', 'items' : 'invalid', }, + 'bad4' : { 'type' : 'List', }, + 'bad5' : { + 'type' : 'List' , + 'items' : [ { 'type' : 'Gaussian' , 'sigma' : 2 }, + { 'type' : 'Gaussian' , 'fwhm' : 2, 'flux' : 100 }, ], + 'index' : -1, + }, + 'bad6' : { + 'type' : 'List' , + 'items' : [ { 'type' : 'Gaussian' , 'sigma' : 2 }, + { 'type' : 'Gaussian' , 'fwhm' : 2, 'flux' : 100 }, ], + 'index' : 2, + }, + } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad1') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad2') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad3') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad4') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad5') + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'bad6') + @timer def test_repeat(): @@ -1333,9 +1542,13 @@ def test_repeat(): config['obj_num'] = 4 gal2a = galsim.config.BuildGSObject(config, 'gal')[0] gsobject_compare(gal2a, gal2b) + + # Also check that the logger reports why it is using the current object config['obj_num'] = 5 - gal2a = galsim.config.BuildGSObject(config, 'gal')[0] + with CaptureLog() as cl: + gal2a = galsim.config.BuildGSObject(config, 'gal', logger=cl.logger)[0] gsobject_compare(gal2a, gal2b) + assert "repeat = 3, index = 5, use current object" in cl.output @timer diff --git a/tests/test_config_image.py b/tests/test_config_image.py index c88f7f3a5c1..e339bc8efbc 100644 --- a/tests/test_config_image.py +++ b/tests/test_config_image.py @@ -96,6 +96,47 @@ def test_single(): im7 = im7_list[k] np.testing.assert_array_equal(im7.array, im1.array) + # Check some errors + config['stamp'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['stamp'] = { 'type' : 'Invalid' } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['stamp'] = { 'draw_method' : 'invalid' } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['stamp'] = { 'n_photons' : 200 } # These next few require draw_method = phot + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['stamp'] = { 'poisson_flux' : False } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['stamp'] = { 'max_extra_noise' : 20. } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + del config['stamp'] + config['image'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image'] = { 'type' : 'Invalid' } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImages(3,config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImages(0,config) + config['image'] = { 'type' : 'Single', 'xsize' : 32 } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image'] = { 'type' : 'Single', 'xsize' : 0, 'ysize' : 32 } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image'] = { 'type' : 'Single' } + del config['gal'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + @timer def test_positions(): @@ -160,6 +201,13 @@ def test_positions(): np.testing.assert_array_equal(im6.array, im1.array) assert im6.bounds == im1.bounds + del config['image']['image_pos'] + del config['image']['world_pos'] + config['stamp']['world_pos'] = { 'type' : 'Random' } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + @timer def test_phot(): """Test draw_method=phot, which has extra allowed kwargs @@ -204,7 +252,7 @@ def test_phot(): # If max_extra_noise is given with n_photons, then ignore it. del config['_copied_image_keys_to_stamp'] - config['image']['max_extra_noise'] = 0.1 + config['stamp']['max_extra_noise'] = 0.1 im3c = galsim.config.BuildImage(config) np.testing.assert_array_equal(im3c.array, im3a.array) @@ -217,7 +265,7 @@ def test_phot(): # So without the noise field, it will raise an exception. del config['image']['n_photons'] del config['stamp']['n_photons'] - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # Using this much extra noise with a sky noise variance of 50 cuts the number of photons @@ -231,6 +279,19 @@ def test_phot(): im4b = galsim.config.BuildImage(config) np.testing.assert_array_equal(im4b.array, im4a.array) + # max_extra noise < 0 is invalid + config['stamp']['max_extra_noise'] = -1. + galsim.config.RemoveCurrent(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # Also negative variance noise is invalid. + config['stamp']['max_extra_noise'] = 0.1 + config['image']['noise'] = { 'type' : 'Gaussian', 'variance' : -50 } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + @timer def test_reject(): """Test various ways that objects can be rejected. @@ -349,9 +410,8 @@ def HighN(config, base, value_type): assert "obj 1: Skipping because field skip=True" in cl.output assert "obj 1: Caught SkipThisObject: e = None" in cl.output assert "Skipping object 1" in cl.output - assert "Object 0: Caught exception 105 index has gone past the number of entries" in cl.output - assert ("Object 0: Caught exception inner_radius (5.369661) must be less than radius "+ - "(3.931733) for type=RandomCircle") in cl.output + assert "Object 0: Caught exception index=105 has gone past the number of entries" in cl.output + assert "Object 0: Caught exception inner_radius must be less than radius (3.931733)" in cl.output assert "Object 0: Caught exception Unable to evaluate string 'math.sqrt(x)'" in cl.output assert "obj 0: reject evaluated to True" in cl.output assert "Object 0: Rejecting this object and rebuilding" in cl.output @@ -372,12 +432,12 @@ def HighN(config, base, value_type): # If we lower the number of retries, we'll max out and abort the image config['stamp']['retry_failures'] = 10 galsim.config.RemoveCurrent(config) - with assert_raises((ValueError, IndexError, RuntimeError)): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildStamps(nimages, config, do_noise=False) try: with CaptureLog() as cl: galsim.config.BuildStamps(nimages, config, do_noise=False, logger=cl.logger) - except (ValueError,IndexError,RuntimeError): + except (galsim.GalSimConfigError): pass #print(cl.output) assert "Object 0: Too many exceptions/rejections for this object. Aborting." in cl.output @@ -388,7 +448,7 @@ def HighN(config, base, value_type): try: with CaptureLog() as cl: galsim.config.BuildImages(nimages, config, logger=cl.logger) - except (ValueError,IndexError,RuntimeError): + except (ValueError,IndexError,galsim.GalSimError): pass #print(cl.output) assert "Exception caught when building image" in cl.output @@ -398,7 +458,7 @@ def HighN(config, base, value_type): try: with CaptureLog() as cl: galsim.config.BuildStamps(nimages, config, do_noise=False, logger=cl.logger) - except (ValueError,IndexError,RuntimeError): + except (ValueError,IndexError,galsim.GalSimError): pass #print(cl.output) assert re.search("Process-.: Exception caught when building stamp",cl.output) @@ -406,7 +466,7 @@ def HighN(config, base, value_type): try: with CaptureLog() as cl: galsim.config.BuildImages(nimages, config, logger=cl.logger) - except (ValueError,IndexError,RuntimeError): + except (ValueError,IndexError,galsim.GalSimError): pass #print(cl.output) assert re.search("Process-.: Exception caught when building image",cl.output) @@ -546,6 +606,44 @@ def test_ring(): print('gal1b = ',gal1b) gsobject_compare(gal1a, gal1b) + # Make sure it all runs using the normal syntax + stamp = galsim.config.BuildStamp(config) + + # num <= 0 is invalid + config['stamp']['num'] = 0 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + config['stamp']['num'] = -1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + del config['stamp']['num'] + del config['stamp']['_get'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamps(10, config) # Different error is making multiple stamps. + # Check invalid index. (Ususally this is automatic and can't be wrong, but it is + # permissible to set it by hand.) + config['stamp']['num'] = 2 + config['stamp']['index'] = 2 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + config['stamp']['index'] = -1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + # Starting on an odd index is an error. It's hard to make this happen in practice, + # but her we can do it by manually deleting the first attribute. + del galsim.config.stamp.valid_stamp_types['Ring'].first + config['stamp']['index'] = 1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + # Invalid to just have a psf. + config['psf'] = config['gal'] + del config['gal'] + config['stamp']['index'] = 0 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + config = { 'stamp' : { 'type' : 'Ring', @@ -705,6 +803,10 @@ def test_scattered(): np.testing.assert_almost_equal(ixy, 0., decimal=3) np.testing.assert_almost_equal(iyy / (sigma/scale)**2, 1, decimal=1) + config['image']['index_convention'] = 'invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + # Check that stamp_xsize, stamp_ysize, image_pos use the object count, rather than the # image count. config = copy.deepcopy(base_config) @@ -746,26 +848,63 @@ def test_scattered(): # Check error message for missing nobjects del config['image']['nobjects'] - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # Also if there is an input field that doesn't have nobj capability config['input'] = { 'dict' : { 'dir' : 'config_input', 'file_name' : 'dict.p' } } - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # However, an input field that does have nobj will return something for nobjects. # This catalog has 3 rows, so equivalent to nobjects = 3 config['input'] = { 'catalog' : { 'dir' : 'config_input', 'file_name' : 'catalog.txt' } } - del config['input_objs'] - galsim.config.RemoveCurrent(config) + config = galsim.config.CleanConfig(config) image = galsim.config.BuildImage(config) np.testing.assert_almost_equal(image.array, image2.array) + del config['image']['size'] + del config['image']['_get'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['xsize'] = size + del config['image']['_get'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['ysize'] = size + del config['image']['_get'] + + # If doing datacube, sizes have to be consistent. + config['image_force_xsize'] = size + config['image_force_ysize'] = size + galsim.config.BuildImage(config) # This works + + # These don't. + config['image']['xsize'] = size-1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['xsize'] = size + config['image']['ysize'] = size+1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['ysize'] = size + + # Can't have both image_pos and world_pos + config['image']['world_pos'] = { + 'type' : 'List', + 'items' : [ galsim.PositionD(x1*scale, y1*scale), + galsim.PositionD(x2*scale, y2*scale), + galsim.PositionD(x3*scale, y3*scale) ] + } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + del config['image']['image_pos'] + image = galsim.config.BuildImage(config) # But just world_pos is fine. + np.testing.assert_almost_equal(image.array, image2.array) + # When starting from the file state, there is some extra code to test about this, so # check that here. config['output'] = { 'type' : 'MultiFits', 'file_name' : 'output/test_scattered.fits', 'nimages' : 2 } - del config['input_objs'] - galsim.config.RemoveCurrent(config) + config = galsim.config.CleanConfig(config) galsim.config.BuildFile(config) image = galsim.fits.read('output/test_scattered.fits') np.testing.assert_almost_equal(image.array, image2.array) @@ -906,7 +1045,7 @@ def test_tiled(): 'type' : 'Gaussian', 'sigma' : { 'type': 'Random', 'min': 1, 'max': 2 }, 'flux' : '$image_pos.x + image_pos.y', - } + }, } seed = 1234 @@ -1004,6 +1143,154 @@ def test_tiled(): im3b = galsim.config.BuildImage(config) np.testing.assert_array_equal(im3b.array, im3a.array) + # Check errors + # sizes need to be > 0 + config['image']['stamp_xsize'] = 0 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['stamp_xsize'] = xsize + config['image']['stamp_ysize'] = -30 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['stamp_ysize'] = ysize + config['image']['order'] = 'invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['order'] = 'col' + del config['image']['nx_tiles'] + del config['image']['_get'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImages(2,config) + config['image']['nx_tiles'] = nx + + # If doing datacube, sizes have to be consistent. + config['image']['stamp_xsize'] = xsize + config['image']['stamp_ysize'] = ysize + config['image_force_xsize'] = im3b.array.shape[1] + config['image_force_ysize'] = im3b.array.shape[0] + galsim.config.BuildImage(config) # This works. + + # These don't. + config['image']['stamp_xsize'] = xsize-1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['stamp_xsize'] = xsize + config['image']['stamp_ysize'] = ysize+1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['stamp_ysize'] = ysize + config['image']['yborder'] = xborder + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # If tile with a square grid, then PowerSpectrum can omit grid_spacing and ngrid. + size = 32 + config = { + 'image' : { + 'type' : 'Tiled', + 'nx_tiles' : nx, + 'ny_tiles' : ny, + 'stamp_size' : size, + 'pixel_scale' : scale, + + 'random_seed' : 1234, + }, + 'gal' : { + 'type' : 'Gaussian', + 'sigma' : { 'type': 'Random', 'min': 1, 'max': 2 }, + 'flux' : '$image_pos.x + image_pos.y', + 'shear' : { 'type' : 'PowerSpectrumShear' }, + }, + 'input' : { + 'power_spectrum' : { 'e_power_function' : 'np.exp(-k**0.2)' }, + }, + } + + seed = 1234 + ps = galsim.PowerSpectrum(e_power_function=lambda k: np.exp(-k**0.2)) + rng = galsim.BaseDeviate(seed) + im4a = galsim.Image(nx*size, ny*size, scale=scale) + center = im4a.true_center * scale + ps.buildGrid(grid_spacing=size*scale, ngrid=max(nx,ny), rng=rng, center=center) + for j in range(ny): + for i in range(nx): + seed += 1 + ud = galsim.UniformDeviate(seed) + xorigin = i * size + 1 + yorigin = j * size + 1 + x = xorigin + (size-1)/2. + y = yorigin + (size-1)/2. + stamp = galsim.Image(size,size, scale=scale) + stamp.setOrigin(xorigin,yorigin) + + sigma = ud() + 1 + flux = x + y + gal = galsim.Gaussian(sigma=sigma, flux=flux) + g1, g2 = ps.getShear(galsim.PositionD(x*scale,y*scale)) + gal = gal.shear(g1=g1, g2=g2) + gal.drawImage(stamp) + im4a[stamp.bounds] = stamp + + # Compare to what config builds + im4b = galsim.config.BuildImage(config) + np.testing.assert_array_equal(im4b.array, im4a.array) + + # If grid sizes aren't square, it also works properly, but with more complicated ngrid calc. + config = galsim.config.CleanConfig(config) + del config['image']['stamp_size'] + config['image']['stamp_xsize'] = xsize + config['image']['stamp_ysize'] = ysize + seed = 1234 + rng = galsim.BaseDeviate(seed) + im5a = galsim.Image(nx*xsize, ny*ysize, scale=scale) + center = im5a.true_center * scale + grid_spacing = min(xsize,ysize) * scale + ngrid = int(math.ceil(max(nx*xsize, ny*ysize) * scale / grid_spacing)) + ps.buildGrid(grid_spacing=grid_spacing, ngrid=ngrid, rng=rng, center=center) + for j in range(ny): + for i in range(nx): + seed += 1 + ud = galsim.UniformDeviate(seed) + xorigin = i * xsize + 1 + yorigin = j * ysize + 1 + x = xorigin + (xsize-1)/2. + y = yorigin + (ysize-1)/2. + stamp = galsim.Image(xsize,ysize, scale=scale) + stamp.setOrigin(xorigin,yorigin) + + sigma = ud() + 1 + flux = x + y + gal = galsim.Gaussian(sigma=sigma, flux=flux) + g1, g2 = ps.getShear(galsim.PositionD(x*scale,y*scale)) + gal = gal.shear(g1=g1, g2=g2) + gal.drawImage(stamp) + im5a[stamp.bounds] = stamp + + im5b = galsim.config.BuildImage(config) + np.testing.assert_array_equal(im5b.array, im5a.array) + + # Finally, if the image type isn't tiled, then grid_spacing is required. + config = { + 'image' : { + 'type' : 'Scattered', + 'size': nx*size, + 'nobjects' : nx*ny, + 'pixel_scale' : scale, + 'random_seed' : 1234, + }, + 'gal' : { + 'type' : 'Gaussian', + 'sigma' : { 'type': 'Random', 'min': 1, 'max': 2 }, + 'flux' : '$image_pos.x + image_pos.y', + 'shear' : { 'type' : 'PowerSpectrumShear' }, + }, + 'input' : { + 'power_spectrum' : { 'e_power_function' : 'np.exp(-k**0.2)' }, + }, + } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + @timer def test_njobs(): @@ -1205,7 +1492,20 @@ def test_wcs(): # This needs to be done after 'scale2', so call it zref to make sure it happens # alphabetically after scale2 in a sorted list. 'zref' : '$(@image.scale2).withOrigin(galsim.PositionD(22,33))', - 'invalid' : 34 + 'bad1' : 34, + 'bad2' : { 'type' : 'Invalid' }, + 'bad3' : { 'type' : 'List', 'items' : galsim.PixelScale(0.12), }, + 'bad4' : { 'type' : 'List', 'items' : "galsim.PixelScale(0.12)", }, + 'bad5' : { + 'type' : 'List', + 'items' : [ galsim.PixelScale(0.12), galsim.PixelScale(0.23) ], + 'index' : -1 + }, + 'bad6' : { + 'type' : 'List', + 'items' : [ galsim.PixelScale(0.12), galsim.PixelScale(0.23) ], + 'index' : 2 + }, } reference = { @@ -1291,8 +1591,13 @@ def test_wcs(): wcs = galsim.config.BuildWCS(config, 'wcs', config) assert wcs == galsim.PixelScale(1.0) - with assert_raises(ValueError): - galsim.config.BuildWCS(config['image'], 'invalid', config) + for bad in ['bad1', 'bad2', 'bad3', 'bad4', 'bad5', 'bad6']: + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildWCS(config['image'], bad, config) + + # Base class usage is invalid + builder = galsim.config.wcs.WCSBuilder() + assert_raises(NotImplementedError, builder.buildWCS, config, config, logger=None) @timer def test_index_key(): @@ -1446,6 +1751,11 @@ def test_index_key(): assert 'current' not in config1['gal']['ellip'] assert 'current' not in config1['gal']['shear'] + # Finally check for invalid index_key + config['psf']['index_key'] = 'psf_num' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildFile(config) + @timer def test_multirng(): @@ -1512,6 +1822,7 @@ def test_multirng(): v = rngb() * 50. - 25. world_pos = galsim.PositionD(u,v) with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") psf_g1, psf_g2 = psf_ps.getShear(world_pos) if len(w) > 0: assert not psf_ps.bounds.includes(world_pos) @@ -1535,6 +1846,29 @@ def test_multirng(): np.testing.assert_array_equal(im.array, images2[n].array) np.testing.assert_array_equal(im.array, images3[n].array) + # Finally, test invalid rng_num + config4 = galsim.config.CopyConfig(config) + config4['image']['world_pos']['rng_num'] = -1 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config4) + config5 = galsim.config.CopyConfig(config) + config5['image']['world_pos']['rng_num'] = 20 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config5) + config6 = galsim.config.CopyConfig(config) + config6['image']['world_pos']['rng_num'] = 1.3 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config6) + config7 = galsim.config.CopyConfig(config) + config7['image']['world_pos']['rng_num'] = 1 + config7['image']['random_seed'] = 12345 + del config7['input'] + del config7['psf']['ellip'] + del config7['gal']['shear'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config7) + + @timer def test_template(): """Test various uses of the template keyword @@ -1660,6 +1994,140 @@ def test_variable_cat_size(): np.testing.assert_array_equal(cfg_images[1], ref_images[1]) +class BlendSetBuilder(galsim.config.StampBuilder): + """This is a stripped-down version of the BlendSetBuilder in examples/des/blend.py. + Use this to test the validity of having a StampBuilder that doesn't use a simple + GSObject for its "prof". + """ + + def setup(self, config, base, xsize, ysize, ignore, logger): + """Do the appropriate setup for a Blend stamp. + """ + self.ngal = galsim.config.ParseValue(config, 'n_neighbors', base, int)[0] + 1 + self.sep = galsim.config.ParseValue(config, 'sep', base, float)[0] + ignore = ignore + ['n_neighbors', 'sep'] + return super(self.__class__, self).setup(config, base, xsize, ysize, ignore, logger) + + def buildProfile(self, config, base, psf, gsparams, logger): + """ + Build a list of galaxy profiles, each convolved with the psf, to use for the blend image. + """ + if (base['obj_num'] % self.ngal != 0): + return None + else: + self.neighbor_gals = [] + for i in range(self.ngal-1): + gal = galsim.config.BuildGSObject(base, 'gal', gsparams=gsparams, logger=logger)[0] + self.neighbor_gals.append(gal) + galsim.config.RemoveCurrent(base['gal'], keep_safe=True) + + rng = galsim.config.GetRNG(config, base, logger, 'BlendSet') + ud = galsim.UniformDeviate(rng) + self.neighbor_pos = [galsim.PositionI(int(ud()*2*self.sep-self.sep), + int(ud()*2*self.sep-self.sep)) + for i in range(self.ngal-1)] + #print('neighbor positions = ',self.neighbor_pos) + + self.main_gal = galsim.config.BuildGSObject(base, 'gal', gsparams=gsparams, + logger=logger)[0] + + self.profiles = [ self.main_gal ] + self.profiles += [ g.shift(p) for g, p in zip(self.neighbor_gals, self.neighbor_pos) ] + if psf: + self.profiles = [ galsim.Convolve(gal, psf) for gal in self.profiles ] + return self.profiles + + def draw(self, profiles, image, method, offset, config, base, logger): + nx = base['stamp_xsize'] + ny = base['stamp_ysize'] + wcs = base['wcs'] + + if profiles is not None: + bounds = galsim.BoundsI(galsim.PositionI(0,0)) + for pos in self.neighbor_pos: + bounds += pos + bounds = bounds.withBorder(max(nx,ny)//2 + 1) + + self.full_images = [] + for prof in profiles: + im = galsim.ImageF(bounds=bounds, wcs=wcs) + galsim.config.DrawBasic(prof, im, method, offset-im.true_center, config, base, + logger) + self.full_images.append(im) + + k = base['obj_num'] % self.ngal + if k == 0: + center_pos = galsim.PositionI(0,0) + else: + center_pos = self.neighbor_pos[k-1] + xmin = int(center_pos.x) - nx//2 + 1 + ymin = int(center_pos.y) - ny//2 + 1 + self.bounds = galsim.BoundsI(xmin, xmin+nx-1, ymin, ymin+ny-1) + + image.setZero() + image.wcs = wcs + for full_im in self.full_images: + assert full_im.bounds.includes(self.bounds) + image += full_im[self.bounds] + + return image + +@timer +def test_blend(): + """Test the functionality used by the BlendSet stamp type in examples/des/blend.py. + Especially that it's internal "prof" is not just a single GSObject. + """ + galsim.config.RegisterStampType('BlendSet', BlendSetBuilder()) + config = { + 'stamp' : { + 'type' : 'BlendSet', + 'n_neighbors' : 3, + 'sep' : 10, + 'size' : 64, + }, + 'gal' : { + 'type' : 'Gaussian', + 'sigma' : { 'type' : 'Random', 'min': 1, 'max': 3 }, + 'flux' : { 'type' : 'Random', 'min': 20, 'max': 300 }, + }, + 'image' : { + 'type' : 'Single', + 'pixel_scale' : 0.5, + 'random_seed' : 1234, + }, + } + + # First just check that this works correctly as is. + galsim.config.SetupConfigImageNum(config, 0, 0) + images = galsim.config.BuildImages(8,config) + for i, im in enumerate(images): + im.write('output/blend%02d.fits'%i) + # Within each blendset, the images are shifted copies of each other. + np.testing.assert_array_equal(images[1].array[3:64,16:64], images[0].array[0:61,9:57]) + np.testing.assert_array_equal(images[2].array[1:62,6:54], images[0].array[0:61,9:57]) + np.testing.assert_array_equal(images[3].array[0:61,0:48], images[0].array[0:61,9:57]) + + np.testing.assert_array_equal(images[5].array[0:55,9:64], images[4].array[9:64,9:64]) + np.testing.assert_array_equal(images[6].array[5:60,1:56], images[4].array[9:64,9:64]) + np.testing.assert_array_equal(images[7].array[0:55,0:55], images[4].array[9:64,9:64]) + + # If there is a current_image, then updateSkip requires special handling here. + config['current_image'] = galsim.Image(64,64) + im8 = galsim.config.BuildStamp(config, obj_num=8) + + # Some reject items are invalid when using this kind of stamp. + config['stamp']['min_flux_frac'] = 0.3 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config, obj_num=8) + del config['stamp']['min_flux_frac'] + config['stamp']['min_snr'] = 20 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config, obj_num=8) + del config['stamp']['min_snr'] + config['stamp']['max_snr'] = 200 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config, obj_num=8) + if __name__ == "__main__": test_single() @@ -1677,3 +2145,4 @@ def test_variable_cat_size(): test_multirng() test_template() test_variable_cat_size() + test_blend() diff --git a/tests/test_config_noise.py b/tests/test_config_noise.py index 6fe7fc7410d..4a17f95d6f5 100644 --- a/tests/test_config_noise.py +++ b/tests/test_config_noise.py @@ -85,6 +85,12 @@ def test_gaussian(): im1c = galsim.config.BuildImage(config) np.testing.assert_equal(im1c.array, im1a.array) + # Base class usage is invalid + builder = galsim.config.noise.NoiseBuilder() + assert_raises(NotImplementedError, builder.addNoise, config, config, im1a, rng, var, + draw_method='auto', logger=None) + assert_raises(NotImplementedError, builder.getNoiseVariance, config, config) + @timer def test_poisson(): @@ -175,6 +181,60 @@ def test_poisson(): im3b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6) + # Can't have both sky_level and sky_level_pixel + config['image']['noise']['sky_level_pixel'] = 2000. + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # Must have a valid noise type + del config['image']['noise']['sky_level_pixel'] + config['image']['noise']['type'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # noise must be a dict + config['image']['noise'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # Can't have signal_to_noise and flux + config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky } + config['gal']['signal_to_noise'] = 100 + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # This should work + del config['gal']['flux'] + galsim.config.BuildImage(config) + + # These now hit the errors in CalculateNoiseVariance rather than AddNoise + config['image']['noise']['type'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['noise'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + del config['image']['noise'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # If rather than signal_to_noise, we have an extra_weight output, then it hits + # a different error. + config['gal']['flux'] = 100 + del config['gal']['signal_to_noise'] + config['output'] = { 'weight' : {} } + config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky } + galsim.config.SetupExtraOutput(config) + galsim.config.SetupConfigFileNum(config, 0, 0, 0) + # This should work again. + galsim.config.BuildImage(config) + config['image']['noise']['type'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['image']['noise'] = 'Invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + @timer def test_ccdnoise(): @@ -604,8 +664,16 @@ def test_whiten(): # If whitening already added too much noise, raise an exception config['image']['noise']['variance'] = 1.e-5 - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildStamp(config) + + # Can't have both whiten and symmetrize + config['image']['noise']['variance'] = 50 + config['image']['noise']['symmetrize'] = 4 + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildStamp(config) + config['image']['noise']['symmetrize'] = False # OK if false though. + galsim.config.BuildStamp(config) # 2. Poisson noise ##### @@ -652,7 +720,7 @@ def test_whiten(): np.testing.assert_almost_equal(im3d.array, im3c.array, decimal=5) config['image']['noise']['sky_level_pixel'] = 1.e-5 - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildStamp(config) # 3. CCDNoise @@ -705,7 +773,7 @@ def test_whiten(): config['image']['noise']['sky_level_pixel'] = 1.e-5 config['image']['noise']['read_noise'] = 0 - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildStamp(config) # 4. COSMOSNoise @@ -729,7 +797,7 @@ def test_whiten(): config['image']['noise']['variance'] = 1.e-5 del config['_current_cn_tag'] - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildStamp(config) diff --git a/tests/test_config_output.py b/tests/test_config_output.py index 05e6f830af9..e3784f8b7a2 100644 --- a/tests/test_config_output.py +++ b/tests/test_config_output.py @@ -128,10 +128,31 @@ def test_fits(): # multithreading starts. But with a regular logger, there really is profiling output. assert "Starting separate profiling for each of the" in cl.output - # If there is no output field, the default behavior is to write to root.fits. + # Check some public API utility functions + assert galsim.config.GetNFiles(config) == 6 + assert galsim.config.GetNImagesForFile(config, 0) == 1 + assert galsim.config.GetNObjForFile(config, 0, 0) == [1] + + # Check invalid output type + config['output']['type'] = 'invalid' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildFile(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.Process(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.GetNImagesForFile(config, 0) + with assert_raises(galsim.GalSimConfigError): + galsim.config.GetNObjForFile(config, 0, 0) + + # If there is no output field, it raises an error when trying to do BuildFile. os.remove('output_fits/test_fits_0.fits') config = galsim.config.CopyConfig(config1) del config['output'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildFile(config) + + # However, when run from a real config file, the processing will write a 'root' field, + # which it will use for the default behavior to write to root.fits. config['root'] = 'output_fits/test_fits_0' galsim.config.BuildFile(config) im2 = galsim.fits.read('output_fits/test_fits_0.fits') @@ -169,6 +190,10 @@ def test_multifits(): im1_list.append(im1) print('multifit image shapes = ',[im.array.shape for im in im1_list]) + assert galsim.config.GetNFiles(config) == 1 + assert galsim.config.GetNImagesForFile(config, 0) == 6 + assert galsim.config.GetNObjForFile(config, 0, 0) == [1, 1, 1, 1, 1, 1] + galsim.config.Process(config) im2_list = galsim.fits.readMulti('output/test_multifits.fits') for k in range(nimages): @@ -183,16 +208,16 @@ def test_multifits(): # Check error message for missing nimages del config['output']['nimages'] - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildFile(config) # Also if there is an input field that doesn't have nobj capability config['input'] = { 'dict' : { 'dir' : 'config_input', 'file_name' : 'dict.p' } } - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildFile(config) # However, an input field that does have nobj will return something for nobjects. # This catalog has 3 rows, so equivalent to nobjects = 3 - del config['input_objs'] + config = galsim.config.CleanConfig(config) config['input'] = { 'catalog' : { 'dir' : 'config_input', 'file_name' : 'catalog.txt' } } galsim.config.BuildFile(config) im4_list = galsim.fits.readMulti('output/test_multifits.fits') @@ -237,6 +262,10 @@ def test_datacube(): im1_list.append(im1) print('datacube image shapes = ',[im.array.shape for im in im1_list]) + assert galsim.config.GetNFiles(config) == 1 + assert galsim.config.GetNImagesForFile(config, 0) == 6 + assert galsim.config.GetNObjForFile(config, 0, 0) == [1, 1, 1, 1, 1, 1] + galsim.config.Process(config) im2_list = galsim.fits.readCube('output/test_datacube.fits') for k in range(nimages): @@ -251,16 +280,16 @@ def test_datacube(): # Check error message for missing nimages del config['output']['nimages'] - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildFile(config) # Also if there is an input field that doesn't have nobj capability config['input'] = { 'dict' : { 'dir' : 'config_input', 'file_name' : 'dict.p' } } - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildFile(config) # However, an input field that does have nobj will return something for nobjects. # This catalog has 3 rows, so equivalent to nobjects = 3 - del config['input_objs'] + config = galsim.config.CleanConfig(config) config['input'] = { 'catalog' : { 'dir' : 'config_input', 'file_name' : 'catalog.txt' } } galsim.config.BuildFile(config) im4_list = galsim.fits.readCube('output/test_datacube.fits') @@ -273,7 +302,7 @@ def test_datacube(): config['output']['weight'] = { 'hdu' : 1 } config['output']['badpix'] = { 'file_name' : 'output/test_datacube_bp.fits' } config['image']['noise'] = { 'type' : 'Gaussian', 'variance' : 0.1 } - with assert_raises(AttributeError): + with assert_raises(galsim.GalSimConfigError): galsim.config.BuildFile(config) # But if both weight and badpix are files, then it should work. @@ -374,6 +403,15 @@ def test_skip(): assert "Splitting work into 3 jobs. Doing job 3" in cl.output assert "Building 2 out of 6 total files: file_num = 4 .. 5" in cl.output + # job < 1 or job > njobs is invalid + with assert_raises(galsim.GalSimValueError): + galsim.config.Process(config, njobs=3, job=0) + with assert_raises(galsim.GalSimValueError): + galsim.config.Process(config, njobs=3, job=4) + # Also njobs < 1 is invalid + with assert_raises(galsim.GalSimValueError): + galsim.config.Process(config, njobs=0) + @timer def test_extra_wt(): @@ -403,14 +441,53 @@ def test_extra_wt(): galsim.config.Process(config) + main_im = [ galsim.fits.read('output/test_main_%d.fits'%k) for k in range(nfiles) ] for k in range(nfiles): im_wt = galsim.fits.read('output/test_wt_%d.fits'%k) np.testing.assert_almost_equal(im_wt.array, 1./(0.7 + k)) im_bp = galsim.fits.read('output/test_bp_%d.fits'%k) np.testing.assert_array_equal(im_bp.array, 0) + os.remove('output/test_main_%d.fits'%k) + + # If noclobber = True, don't overwrite existing file. + config['noise'] = { 'type' : 'Poisson', 'sky_level_pixel' : 500 } + config['output']['noclobber'] = True + galsim.config.RemoveCurrent(config) + with CaptureLog() as cl: + galsim.config.Process(config, logger=cl.logger) + assert 'Not writing weight file 0 = output/test_wt_0.fits' in cl.output + for k in range(nfiles): + im = galsim.fits.read('output/test_main_%d.fits'%k) + np.testing.assert_equal(im.array, main_im[k].array) + im_wt = galsim.fits.read('output/test_wt_%d.fits'%k) + np.testing.assert_almost_equal(im_wt.array, 1./(0.7 + k)) + + # Can also add these as extra hdus rather than separate files. + config['output']['noclobber'] = False + config['output']['weight'] = { 'hdu' : 1 } + config['output']['badpix'] = { 'hdu' : 2 } + galsim.config.RemoveCurrent(config) + galsim.config.Process(config) + for k in range(nfiles): + im_wt = galsim.fits.read('output/test_main_%d.fits'%k, hdu=1) + np.testing.assert_almost_equal(im_wt.array, 1./(0.7 + k)) + im_bp = galsim.fits.read('output/test_main_%d.fits'%k, hdu=2) + np.testing.assert_array_equal(im_bp.array, 0) + + config['output']['badpix'] = { 'hdu' : 0 } + galsim.config.RemoveCurrent(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.Process(config, except_abort=True) + config['output']['badpix'] = { 'hdu' : 1 } + with assert_raises(galsim.GalSimConfigError): + galsim.config.Process(config, except_abort=True) + config['output']['badpix'] = { 'hdu' : 3 } + with assert_raises(galsim.GalSimConfigError): + galsim.config.Process(config, except_abort=True) # If include_obj_var = True, then weight image includes signal. config['output']['weight']['include_obj_var'] = True + config['output']['badpix'] = { 'hdu' : 2 } config['output']['nproc'] = 2 galsim.config.RemoveCurrent(config) galsim.config.Process(config) @@ -419,9 +496,35 @@ def test_extra_wt(): sigma = ud() + 1. gal = galsim.Gaussian(sigma=sigma, flux=100) im = gal.drawImage(scale=0.4) - im_wt = galsim.fits.read('output/test_wt_%d.fits'%k) + im_wt = galsim.fits.read('output/test_main_%d.fits'%k, hdu=1) np.testing.assert_almost_equal(im_wt.array, 1./(0.7 + k + im.array)) + # It is permissible for weight, badpix to have no output. Some use cases require building + # the weight and/or badpix information even if it is not associated with any output. + config['output']['weight'] = {} + config['output']['badpix'] = {} + galsim.config.RemoveCurrent(config) + galsim.config.Process(config) + for k in range(nfiles): + assert_raises(OSError, galsim.fits.read, 'output/test_main_%d.fits'%k, hdu=1) + os.remove('output/test_wt_%d.fits'%k) + os.remove('output/test_main_%d.fits'%k) + + # Can also have both outputs + config['output']['weight'] = { 'file_name': "$'output/test_wt_%d.fits'%file_num", 'hdu': 1 } + galsim.config.RemoveCurrent(config) + galsim.config.Process(config, except_abort=True) + for k in range(nfiles): + im_wt1 = galsim.fits.read('output/test_wt_%d.fits'%k) + np.testing.assert_almost_equal(im_wt1.array, 1./(0.7 + k)) + im_wt2 = galsim.fits.read('output/test_main_%d.fits'%k, hdu=1) + np.testing.assert_almost_equal(im_wt2.array, 1./(0.7 + k)) + + # Other such use cases would access the final weight or badpix image using GetFinalExtraOutput + galsim.config.BuildFile(config) + wt = galsim.config.extra.GetFinalExtraOutput('weight', config) + np.testing.assert_almost_equal(wt[0].array, 1./0.7) + # If the image is a Scattered type, then the weight adn badpix images are built by a # different code path. config = { @@ -698,6 +801,85 @@ def test_extra_psf(): assert "Not writing psf file 4 = output_psf/test_psf.fits because already written" in cl.output assert "Not writing psf file 5 = output_psf/test_psf.fits because already written" in cl.output +@timer +def test_extra_psf_sn(): + """Test the signal_to_noise option of the extra psf field + """ + config = { + 'image' : { + 'random_seed' : 1234, + 'pixel_scale' : 0.4, + 'size' : 64, + }, + 'gal' : { + 'type' : 'Gaussian', + 'sigma' : 2.3, + 'flux' : 100, + }, + 'psf' : { + 'type' : 'Moffat', + 'beta' : 3.5, + 'fwhm' : 0.7, + }, + 'output' : { + 'psf' : {} + }, + } + # First pure psf image with no noise. + gal_image = galsim.config.BuildImage(config) + pure_psf_image = galsim.config.extra.GetFinalExtraOutput('psf', config)[0] + np.testing.assert_almost_equal(pure_psf_image.array.sum(), 1., decimal=6) + + # Draw PSF at S/N = 100 + # (But first check that an error is raised if noise is missing. + config['output']['psf']['signal_to_noise'] = 100 + galsim.config.RemoveCurrent(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + noise_var = 20. + config['image']['noise'] = { 'type' : 'Gaussian', 'variance' : noise_var, } + gal_image = galsim.config.BuildImage(config) + sn100_psf_image = galsim.config.extra.GetFinalExtraOutput('psf', config)[0] + sn100_flux = sn100_psf_image.array.sum() + psf_noise = sn100_psf_image - sn100_flux * pure_psf_image + print('psf_noise.var = ',psf_noise.array.var(), noise_var) + np.testing.assert_allclose(psf_noise.array.var(), noise_var, rtol=0.02) + snr = np.sqrt( np.sum(sn100_psf_image.array**2, dtype=float) / noise_var ) + print('snr = ',snr, 100) + np.testing.assert_allclose(snr, 100, rtol=0.2) # Not super accurate for any single image. + + # Can also specify different draw_methods. + config['output']['psf']['draw_method'] = 'real_space' + galsim.config.RemoveCurrent(config) + gal_image = galsim.config.BuildImage(config) + real_psf_image = galsim.config.extra.GetFinalExtraOutput('psf', config)[0] + print('real flux = ', real_psf_image.array.sum(), sn100_flux) + np.testing.assert_allclose(real_psf_image.array.sum(), sn100_flux, rtol=1.e-4) + + # phot is invalid with signal_to_noise + config['output']['psf']['draw_method'] = 'phot' + galsim.config.RemoveCurrent(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # Check for other invalid input + config['output']['psf']['draw_method'] = 'input' + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + config['output']['psf']['draw_method'] = 'auto' + config['output']['psf']['flux'] = sn100_flux + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildImage(config) + + # OK to use phot with flux. + config['output']['psf']['draw_method'] = 'phot' + del config['output']['psf']['signal_to_noise'] + gal_image = galsim.config.BuildImage(config) + phot_psf_image = galsim.config.extra.GetFinalExtraOutput('psf', config)[0] + print('phot flux = ', phot_psf_image.array.sum(), sn100_flux) + np.testing.assert_allclose(phot_psf_image.array.sum(), sn100_flux, rtol=1.e-4) + @timer def test_extra_truth(): @@ -770,7 +952,7 @@ def __init__(self, rng): def writeFile(self, *args, **kwargs): p = self.ud() if p < 0.33: - raise IOError("p = %f"%p) + raise OSError("p = %f"%p) else: galsim.fits.writeMulti(*args, **kwargs) @@ -812,10 +994,10 @@ def writeFile(self, file_name, config, base, logger): with CaptureLog() as cl: galsim.config.Process(config, logger=cl.logger) #print(cl.output) - assert "File output/test_flaky_fits_0.fits: Caught IOError" in cl.output + assert "File output/test_flaky_fits_0.fits: Caught OSError" in cl.output assert "This is try 2/6, so sleep for 2 sec and try again." in cl.output assert "file 0: Wrote FlakyFits to file 'output/test_flaky_fits_0.fits'" in cl.output - assert "File output/test_flaky_wt_0.fits: Caught IOError: " in cl.output + assert "File output/test_flaky_wt_0.fits: Caught OSError: " in cl.output assert "This is try 1/6, so sleep for 1 sec and try again." in cl.output assert "file 0: Wrote flaky_weight to 'output/test_flaky_wt_0.fits'" in cl.output @@ -875,7 +1057,7 @@ def writeFile(self, file_name, config, base, logger): with CaptureLog() as cl: try: galsim.config.Process(config, logger=cl.logger, except_abort=True) - except IOError as e: + except OSError as e: assert str(e) == "p = 0.126989" #print(cl.output) assert "File output/test_flaky_fits_0.fits not written." in cl.output @@ -939,7 +1121,7 @@ def test_config(): assert config == config5 # Copying deep copies and removes any existing input_manager - config4['input_manager'] = 'an input manager' + config4['_input_manager'] = 'an input manager' config7 = galsim.config.CopyConfig(config4) assert config == config7 @@ -1074,7 +1256,6 @@ def test_eval_full_word(): 'units': 'arcsec', 'grid_spacing': 10, 'ngrid': '$math.ceil(2*focal_rmax / @input.power_spectrum.grid_spacing)', - 'center': "0,0", }, }, @@ -1183,7 +1364,7 @@ def test_eval_full_word(): logger = logging.getLogger('test_eval_full_word') logger.addHandler(logging.StreamHandler(sys.stdout)) logger.setLevel(logging.DEBUG) - galsim.config.Process(config, logger=logger) + galsim.config.Process(config, logger=logger, except_abort=True) # First check the truth catalogs data0 = np.genfromtxt('output/test_eval_full_word_0.dat', names=True, deletechars='') @@ -1245,6 +1426,7 @@ def test_eval_full_word(): test_skip() test_extra_wt() test_extra_psf() + test_extra_psf_sn() test_extra_truth() test_retry_io() test_config() diff --git a/tests/test_config_value.py b/tests/test_config_value.py index 890af82d1e2..8f3144c42f0 100644 --- a/tests/test_config_value.py +++ b/tests/test_config_value.py @@ -59,11 +59,16 @@ def test_float_value(): 'ran1' : { 'type' : 'Random', 'min' : 0.5, 'max' : 3 }, 'ran2' : { 'type' : 'Random', 'min' : -5, 'max' : 0 }, 'gauss1' : { 'type' : 'RandomGaussian', 'sigma' : 1 }, + 'gauss1b' : { 'type' : 'RandomGaussian', 'sigma' : 1 }, 'gauss2' : { 'type' : 'RandomGaussian', 'sigma' : 3, 'mean' : 4 }, 'gauss3' : { 'type' : 'RandomGaussian', 'sigma' : 1.5, 'min' : -2, 'max' : 2 }, 'gauss4' : { 'type' : 'RandomGaussian', 'sigma' : 0.5, 'min' : 0, 'max' : 0.8 }, 'gauss5' : { 'type' : 'RandomGaussian', 'sigma' : 0.3, 'mean' : 0.5, 'min' : 0, 'max' : 0.5 }, + 'gauss6' : { 'type' : 'RandomGaussian', + 'sigma' : 0.8, 'mean' : 0.3, 'min' : 2. }, + 'gauss7' : { 'type' : 'RandomGaussian', + 'sigma' : 1.3, 'mean' : 0.3, 'min' : -2., 'max' : 0. }, 'dist1' : { 'type' : 'RandomDistribution', 'function' : 'config_input/distribution.txt', 'interpolant' : 'linear' }, 'dist2' : { 'type' : 'RandomDistribution', @@ -95,9 +100,22 @@ def test_float_value(): 'sum1' : { 'type' : 'Sum', 'items' : [ 72, '2.33', { 'type' : 'Dict', 'key' : 'f' } ] }, 'nfw' : { 'type' : 'NFWHaloMagnification' }, 'ps' : { 'type' : 'PowerSpectrumMagnification' }, - 'no_type' : { 'value' : 34. }, - 'bad_key' : { 'type' : 'RandomGaussian', 'sig' : 1 }, - 'bad_value' : { 'type' : 'RandomGaussian', 'sigma' : 'not a number' }, + 'bad1' : { 'value' : 34. }, + 'bad2' : { 'type' : 'RandomGaussian', 'sig' : 1 }, + 'bad3' : { 'type' : 'RandomGaussian', 'sigma' : 'not a number' }, + 'bad4' : { 'type' : 'Invalid', 'sig' : 1 }, + 'bad5' : { 'type' : 'Sequence', 'first' : 1, 'last' : 2.1, 'repeat' : -2 }, + 'bad6' : { 'type' : 'Sequence', 'first' : 1, 'last' : 2.1, 'nitems' : 12 }, + 'bad7' : { 'type' : 'RandomDistribution', + 'x' : [ 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 ], + 'interpolant' : 'linear' }, + 'bad8' : { 'type' : 'RandomDistribution', 'function' : 'x*x', + 'x' : [ 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 ], + 'f' : [ 0.1, 0.1, 0.1, 0.1, 0.2, 0.2, 0.2, 0.1, 0.1, 0.1, 0.1 ], + 'interpolant' : 'linear' }, + 'bad9' : { 'type' : 'RandomDistribution', 'interpolant' : 'linear' }, + 'bad10' : { 'type' : 'RandomDistribution', 'function' : 'x*x', 'x_log' : True }, + 'bad11' : { 'type' : 'RandomDistribution', 'function' : 'x*x', 'f_log' : True }, # Some items that would normally be set by the config processing 'image_xsize' : 2000, @@ -106,14 +124,7 @@ def test_float_value(): 'image_center' : galsim.PositionD(0,0), } - test_yaml = True - try: - galsim.config.ProcessInput(config) - except ImportError: - # We don't require PyYAML as a dependency, so if this fails, just remove the YAML dict. - del config['input']['dict'][2] - galsim.config.ProcessInput(config) - test_yaml = False + galsim.config.ProcessInput(config) # Test direct values val1 = galsim.config.ParseValue(config,'val1',config, float)[0] @@ -205,6 +216,22 @@ def test_float_value(): gd_val = 1-gd_val np.testing.assert_almost_equal(gauss5, gd_val) + gauss6 = galsim.config.ParseValue(config,'gauss6',config, float)[0] + gd = galsim.GaussianDeviate(rng,mean=0.,sigma=0.8) + gd_val = abs(gd()) + while gd_val < 1.7: + gd_val = abs(gd()) + gd_val += 0.3 + np.testing.assert_almost_equal(gauss6, gd_val) + + gauss7 = galsim.config.ParseValue(config,'gauss7',config, float)[0] + gd = galsim.GaussianDeviate(rng,mean=0.,sigma=1.3) + gd_val = abs(gd()) + while gd_val < 0.3 or gd_val > 2.3: + gd_val = abs(gd()) + gd_val = -gd_val + 0.3 + np.testing.assert_almost_equal(gauss7, gd_val) + # Test values generated from a distribution in a file dd=galsim.DistDeviate(rng,function='config_input/distribution.txt',interpolant='linear') for k in range(6): @@ -301,12 +328,8 @@ def test_float_value(): dict = [] dict.append(galsim.config.ParseValue(config,'dict1',config, float)[0]) dict.append(galsim.config.ParseValue(config,'dict2',config, float)[0]) - if test_yaml: - dict.append(galsim.config.ParseValue(config,'dict3',config, float)[0]) - dict.append(galsim.config.ParseValue(config,'dict4',config, float)[0]) - else: - dict.append(0.1) - dict.append(1.9) + dict.append(galsim.config.ParseValue(config,'dict3',config, float)[0]) + dict.append(galsim.config.ParseValue(config,'dict4',config, float)[0]) np.testing.assert_array_almost_equal(dict, [ 23.17, -17.23, 0.1, 1.9 ]) sum1 = galsim.config.ParseValue(config,'sum1',config, float)[0] @@ -315,11 +338,11 @@ def test_float_value(): # Test NFWHaloMagnification galsim.config.SetupInputsForImage(config, None) # Raise an error because no world_pos - with assert_raises(ValueError): + with assert_raises(galsim.GalSimConfigError): galsim.config.ParseValue(config,'nfw',config, float) config['world_pos'] = galsim.PositionD(6,8) # Still raise an error because no redshift - with assert_raises(ValueError): + with assert_raises(galsim.GalSimConfigError): galsim.config.ParseValue(config,'nfw',config, float) # With this, it should work. config['gal'] = { 'redshift' : gal_z } @@ -359,10 +382,18 @@ def test_float_value(): assert "Warning: NFWHalo mu = -163.631846 means strong lensing." in cl.output np.testing.assert_almost_equal(nfw4, 3000.) + # Negative max_mu is invalid. + galsim.config.RemoveCurrent(config) + config['nfw']['max_mu'] = -3. + del config['nfw']['_get'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'nfw',config, float) + # Test PowerSpectrumMagnification ps = galsim.PowerSpectrum(e_power_function='np.exp(-k**0.2)') galsim.config.RemoveCurrent(config) - config['rng'] = rng.duplicate() # reset them back to be in sync + rng = galsim.BaseDeviate(31415) # reset this so changes to tests above don't mess this up. + config['rng'] = rng.duplicate() ps.buildGrid(grid_spacing=10, ngrid=20, interpolant='linear', rng=rng) print("ps mag = ",ps.getMagnification((0.1,0.2))) galsim.config.SetupInputsForImage(config, None) @@ -370,27 +401,43 @@ def test_float_value(): np.testing.assert_almost_equal(ps1, ps.getMagnification((0.1,0.2))) # Beef up the amplitude to get strong lensing. - ps = galsim.PowerSpectrum(e_power_function='500 * np.exp(-k**0.2)') + ps = galsim.PowerSpectrum(e_power_function='100 * np.exp(-k**0.2)') ps.buildGrid(grid_spacing=10, ngrid=20, interpolant='linear', rng=rng) print("strong lensing mag = ",ps.getMagnification((0.1,0.2))) - galsim.config.RemoveCurrent(config) - del config['input_objs'] - config['input']['power_spectrum']['e_power_function'] = '500 * np.exp(-k**0.2)' + config = galsim.config.CleanConfig(config) + config['input']['power_spectrum']['e_power_function'] = '100 * np.exp(-k**0.2)' with CaptureLog() as cl: galsim.config.SetupInputsForImage(config, logger=cl.logger) ps2a = galsim.config.ParseValue(config,'ps',config, float)[0] - assert 'PowerSpectrum mu = -3.643856 means strong lensing. Using mu=25.000000' in cl.output + print(cl.output) + assert 'PowerSpectrum mu = -2.778083 means strong lensing. Using mu=25.000000' in cl.output np.testing.assert_almost_equal(ps2a, 25.) - galsim.config.RemoveCurrent(config) # Need a different point that happens to have strong lensing, since the PS realization changed. - config['world_pos'] = galsim.PositionD(3.1,24.2) + config['world_pos'] = galsim.PositionD(5,75) + galsim.config.RemoveCurrent(config) with CaptureLog() as cl: - galsim.config.SetupInputsForImage(config, cl.logger) + galsim.config.SetupInputsForImage(config, logger=cl.logger) ps2b = galsim.config.ParseValue(config, 'ps', config, float)[0] - assert "Warning: PowerSpectrum mu = 29.287659 means strong lensing." in cl.output + print(cl.output) + assert "PowerSpectrum mu = 26.949446 means strong lensing. Using mu=25.000000" in cl.output np.testing.assert_almost_equal(ps2b, 25.) + # Or set a different maximum + galsim.config.RemoveCurrent(config) + config['ps']['max_mu'] = 30. + del config['ps']['_get'] + ps3 = galsim.config.ParseValue(config,'ps',config, float)[0] + np.testing.assert_almost_equal(ps3, 26.949445807939387) + + # Negative max_mu is invalid. + galsim.config.RemoveCurrent(config) + config['ps']['max_mu'] = -3. + del config['ps']['_get'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'ps',config, float) + config['ps']['max_mu'] = 25. + # Out of bounds results in shear = 0, and a warning. galsim.config.RemoveCurrent(config) config['world_pos'] = galsim.PositionD(1000,2000) @@ -401,10 +448,43 @@ def test_float_value(): assert "Warning: position (1000.000000,2000.000000) not within the bounds" in cl.output np.testing.assert_almost_equal(ps2c, 1.) - # Should raise an AttributeError if there is no type in the dict - assert_raises(AttributeError, galsim.config.ParseValue, config, 'no_type', config, float) - assert_raises(AttributeError, galsim.config.ParseValue, config, 'bad_key', config, float) - assert_raises(ValueError, galsim.config.ParseValue, config, 'bad_value', config, float) + # Error if no world_pos + del config['world_pos'] + galsim.config.RemoveCurrent(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'ps', config, float) + + # Should raise a GalSimConfigError if there is no type in the dict + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad1', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad2', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad3', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad4', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad5', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad6', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad7', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad8', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad9', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad10', config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'bad11', config, float) + + # Error if given the wrong type. Should be float, not np.float16. + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'gauss1b',config, np.float16) + # Different path to (different) error if already processed into a _gen_fn. + galsim.config.ParseValue(config,'gauss1',config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'gauss1',config, np.float16) @timer @@ -453,16 +533,16 @@ def test_int_value(): 'dict2' : { 'type' : 'Dict', 'num' : 1, 'key' : 'i' }, 'dict3' : { 'type' : 'Dict', 'num' : 2, 'key' : 'i' }, 'sum1' : { 'type' : 'Sum', 'items' : [ 72.3, '2', { 'type' : 'Dict', 'key' : 'i' } ] }, + 'cur1' : { 'type' : 'Current', 'key' : 'val1' }, + 'cur2' : { 'type' : 'Current', 'key' : 'list2.index.step' }, + 'bad1' : 'left', + 'bad2' : int, + 'bad3' : { 'type' : 'Current', 'key' : 'list2.index.type' }, + 'bad4' : { 'type' : 'Catalog' , 'num' : 2, 'col' : 'int1' }, + 'bad5' : { 'type' : 'Catalog' , 'num' : -1, 'col' : 'int1' }, } - test_yaml = True - try: - galsim.config.ProcessInput(config) - except ImportError: - # We don't require PyYAML as a dependency, so if this fails, just remove the YAML dict. - del config['input']['dict'][2] - galsim.config.ProcessInput(config) - test_yaml = False + galsim.config.ProcessInput(config) # Test direct values val1 = galsim.config.ParseValue(config,'val1',config, int)[0] @@ -609,15 +689,35 @@ def test_int_value(): dict = [] dict.append(galsim.config.ParseValue(config,'dict1',config, int)[0]) dict.append(galsim.config.ParseValue(config,'dict2',config, int)[0]) - if test_yaml: - dict.append(galsim.config.ParseValue(config,'dict3',config, int)[0]) - else: - dict.append(1) + dict.append(galsim.config.ParseValue(config,'dict3',config, int)[0]) np.testing.assert_array_equal(dict, [ 17, -23, 1 ]) sum1 = galsim.config.ParseValue(config,'sum1', config, int)[0] np.testing.assert_almost_equal(sum1, 72 + 2 + 17) + cur1 = galsim.config.ParseValue(config,'cur1', config, int)[0] + np.testing.assert_array_equal(cur1, 9) + cur2 = galsim.config.ParseValue(config,'cur2', config, int)[0] + np.testing.assert_array_equal(cur2, -3) + + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad1', config, int) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad2', config, int) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad3',config, int) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad4',config, int) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad5',config, int) + config = galsim.config.CleanConfig(config) + del config['input']['catalog'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'cat1',config, int) + del config['input'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'cat1',config, int) + @timer def test_bool_value(): @@ -656,17 +756,13 @@ def test_bool_value(): 'index' : { 'type' : 'Sequence', 'first' : 10, 'step' : -3 } }, 'dict1' : { 'type' : 'Dict', 'key' : 'b' }, 'dict2' : { 'type' : 'Dict', 'num' : 1, 'key' : 'b' }, - 'dict3' : { 'type' : 'Dict', 'num' : 2, 'key' : 'b' } + 'dict3' : { 'type' : 'Dict', 'num' : 2, 'key' : 'b' }, + 'bad1' : 'left', + 'bad2' : 'nope', + 'bad3' : { 'type' : 'RandomBinomial', 'N' : 2 }, } - test_yaml = True - try: - galsim.config.ProcessInput(config) - except ImportError: - # We don't require PyYAML as a dependency, so if this fails, just remove the YAML dict. - del config['input']['dict'][2] - galsim.config.ProcessInput(config) - test_yaml = False + galsim.config.ProcessInput(config) # Test direct values val1 = galsim.config.ParseValue(config,'val1',config, bool)[0] @@ -765,12 +861,17 @@ def test_bool_value(): dict = [] dict.append(galsim.config.ParseValue(config,'dict1',config, bool)[0]) dict.append(galsim.config.ParseValue(config,'dict2',config, bool)[0]) - if test_yaml: - dict.append(galsim.config.ParseValue(config,'dict3',config, bool)[0]) - else: - dict.append(False) + dict.append(galsim.config.ParseValue(config,'dict3',config, bool)[0]) np.testing.assert_array_equal(dict, [ True, False, False ]) + # Test bad values + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad1',config, bool) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad2',config, bool) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad3',config, bool) + @timer def test_str_value(): @@ -809,17 +910,15 @@ def test_str_value(): 'Goodbye cruel world.', ', said Pink.'] }, 'dict1' : { 'type' : 'Dict', 'key' : 's' }, 'dict2' : { 'type' : 'Dict', 'num' : 1, 'key' : 's' }, - 'dict3' : { 'type' : 'Dict', 'num' : 2, 'key' : 's' } + 'dict3' : { 'type' : 'Dict', 'num' : 2, 'key' : 's' }, + 'bad1' : { 'type' : 'FormattedStr', 'format' : 'realgal%02q.fits', 'items' : [4,5,6] }, + 'bad2' : { 'type' : 'FormattedStr', 'format' : 'realgal%02', 'items' : [4,5,6] }, + 'bad3' : { 'type' : 'FormattedStr', 'format' : 'realgal%02d_%d.fits', 'items' : [4,5,6] }, + 'bad4' : { 'type' : 'List', 'items' : 'Beautiful plumage! Ay?' }, + 'bad5' : { 'type' : 'List', 'items' : [ 'Beautiful', 'plumage!', 'Ay?' ], 'index' : 5 }, } - test_yaml = True - try: - galsim.config.ProcessInput(config) - except ImportError: - # We don't require PyYAML as a dependency, so if this fails, just remove the YAML dict. - del config['input']['dict'][2] - galsim.config.ProcessInput(config) - test_yaml = False + galsim.config.ProcessInput(config) # Test direct values val1 = galsim.config.ParseValue(config,'val1',config, str)[0] @@ -889,12 +988,19 @@ def test_str_value(): dict = [] dict.append(galsim.config.ParseValue(config,'dict1',config, str)[0]) dict.append(galsim.config.ParseValue(config,'dict2',config, str)[0]) - if test_yaml: - dict.append(galsim.config.ParseValue(config,'dict3',config, str)[0]) - else: - dict.append('Brian') + dict.append(galsim.config.ParseValue(config,'dict3',config, str)[0]) np.testing.assert_array_equal(dict, [ 'Life', 'of', 'Brian' ]) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad1',config, str) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad2',config, str) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad3',config, str) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad4',config, str) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad5',config, str) @timer def test_angle_value(): @@ -930,7 +1036,9 @@ def test_angle_value(): 'items' : [ 73 * galsim.arcmin, 8.9 * galsim.arcmin, 3.14 * galsim.arcmin ] }, - 'sum1' : { 'type' : 'Sum', 'items' : [ 72 * galsim.degrees, '2.33 degrees' ] } + 'sum1' : { 'type' : 'Sum', 'items' : [ 72 * galsim.degrees, '2.33 degrees' ] }, + 'bad1' : '1.9 * galsim.rradds', + 'bad2' : { 'type' : 'Sum', 'items' : 72 * galsim.degrees }, } galsim.config.ProcessInput(config) @@ -1024,6 +1132,11 @@ def test_angle_value(): sum1 = galsim.config.ParseValue(config,'sum1', config, galsim.Angle)[0] np.testing.assert_almost_equal(sum1 / galsim.degrees, 72 + 2.33) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad1', config, galsim.Angle) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad2', config, galsim.Angle) + @timer def test_shear_value(): @@ -1053,12 +1166,15 @@ def test_shear_value(): galsim.Shear(g1 = 0.1, g2 = 0.0) ] }, 'nfw' : { 'type' : 'NFWHaloShear' }, 'ps' : { 'type' : 'PowerSpectrumShear' }, + 'bad1' : { 'type' : 'G1G2', 'g1' : 0.5 }, + 'bad2' : { 'type' : 'G1G2' }, + 'bad3' : { 'type' : 'G1G2', 'g1' : 0.5, 'g2' : -0.1, 'g3' : 0.3 }, 'input' : { 'nfw_halo' : { 'mass' : halo_mass, 'conc' : halo_conc, 'redshift' : halo_z }, 'power_spectrum' : { 'e_power_function' : 'np.exp(-k**0.2)', 'grid_spacing' : 10, 'interpolant' : 'linear', 'ngrid' : 40, 'center' : '5,5' }, - } + }, } # Test direct values @@ -1134,11 +1250,11 @@ def test_shear_value(): galsim.config.ProcessInput(config) galsim.config.SetupInputsForImage(config, None) # Raise an error because no world_pos - with assert_raises(ValueError): + with assert_raises(galsim.GalSimConfigError): galsim.config.ParseValue(config, 'nfw', config, galsim.Shear) config['world_pos'] = galsim.PositionD(6,8) # Still raise an error because no redshift - with assert_raises(ValueError): + with assert_raises(galsim.GalSimConfigError): galsim.config.ParseValue(config, 'nfw', config, galsim.Shear) # With this, it should work. config['gal'] = { 'redshift' : gal_z } @@ -1182,8 +1298,7 @@ def test_shear_value(): ps.buildGrid(grid_spacing=10, ngrid=40, center=galsim.PositionD(5,5), interpolant='linear', rng=rng) print("strong lensing shear = ",ps.getShear((0.1,0.2))) - galsim.config.RemoveCurrent(config) - del config['input_objs'] + config = galsim.config.CleanConfig(config) config['input']['power_spectrum']['e_power_function'] = '500 * np.exp(-k**0.2)' galsim.config.SetupInputsForImage(config, None) ps2b = ps.getShear((0.1,0.2)) @@ -1204,6 +1319,19 @@ def test_shear_value(): assert "Warning: position (1000.000000,2000.000000) not within the bounds" in cl.output np.testing.assert_almost_equal((ps2c.g1, ps2c.g2), (0,0)) + # Error if no world_pos + del config['world_pos'] + galsim.config.RemoveCurrent(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config, 'ps', config, galsim.Shear) + + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad1',config, galsim.Shear) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad2',config, galsim.Shear) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad3',config, galsim.Shear) + @timer def test_pos_value(): @@ -1211,6 +1339,7 @@ def test_pos_value(): """ config = { 'val1' : galsim.PositionD(0.1,0.2), + 'val2' : '0.1, 0.2', 'xy1' : { 'type' : 'XY', 'x' : 1.3, 'y' : 2.4 }, 'ran1' : { 'type' : 'RandomCircle', 'radius' : 3 }, 'ran2' : { 'type' : 'RandomCircle', 'radius' : 1, 'center' : galsim.PositionD(3,7) }, @@ -1225,13 +1354,29 @@ def test_pos_value(): galsim.PositionD(-0.5, 0.2), galsim.PositionD(0.1, 0.0) ] }, 'radec' : { 'type' : 'RADec', 'ra' : 13.4 * galsim.hours, 'dec' : -0.3 * galsim.degrees }, + 'bad1' : '0.1, 0.2, 0.3', + 'bad2' : '0.1,', + 'bad3' : '0.1', + 'bad4' : 'red, blue', } + # Also use this to check CopyConfig and CleanConfig. Processing adds a lot to the + # config dict for efficiency. But CopyConfig should copy the current state, and + # CleanConfig should get it back to a clean state after processing is done. + # The one catch is that it needs to know what the top-level fields are, and we use non-standard + # ones here. So add them to top_level_fields. + galsim.config.top_level_fields += config.keys() + orig_config = galsim.config.CopyConfig(config) + assert orig_config == config # Test direct values val1 = galsim.config.ParseValue(config,'val1',config, galsim.PositionD)[0] np.testing.assert_almost_equal(val1.x, 0.1) np.testing.assert_almost_equal(val1.y, 0.2) + val2 = galsim.config.ParseValue(config,'val2',config, galsim.PositionD)[0] + np.testing.assert_almost_equal(val1.x, 0.1) + np.testing.assert_almost_equal(val1.y, 0.2) + xy1 = galsim.config.ParseValue(config,'xy1',config, galsim.PositionD)[0] np.testing.assert_almost_equal(xy1.x, 1.3) np.testing.assert_almost_equal(xy1.y, 2.4) @@ -1296,6 +1441,29 @@ def test_pos_value(): np.testing.assert_almost_equal(radec.ra / galsim.hours, 13.4) np.testing.assert_almost_equal(radec.dec / galsim.degrees, -0.3) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad1',config, galsim.PositionD) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad2',config, galsim.PositionD) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad3',config, galsim.PositionD) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad4',config, galsim.PositionD) + + clean_config = galsim.config.CleanConfig(config) + # Remove all current values + galsim.config.RemoveCurrent(clean_config) + # And a few extra things we added by hand. + for key in ['obj_num', 'index_key', 'rng', 'image_num']: + del clean_config[key] + # And one extra thing that gets set as a default, but CleanConfig doesn't remove + del clean_config['list1']['index'] + # Finally, this value got changed to a real Position, so it won't match the original + # unless we manually set it back to a string. + clean_config['val2'] = '0.1, 0.2' + print('orig_config = ',orig_config) + print('cleaned config = ',clean_config) + assert clean_config == orig_config @timer def test_eval(): @@ -1337,6 +1505,11 @@ def test_eval(): # A couple more to cover the other various letter prefixes. 'eval18' : { 'type' : 'Eval', 'str' : 'np.exp(-eval(half) * theta.rad**lit_two)' }, 'eval19' : { 'type' : 'Eval', 'str' : 'np.exp(-shear.g1 * pos.x * coord.ra.rad)' }, + 'bad1' : { 'type' : 'Eval', 'str' : 'npexp(-0.5)' }, + 'bad2' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * x**2)', 'x' : 1.8 }, + 'bad3' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * x**2)', 'qx' : 1.8 }, + 'bad4' : { 'type' : 'Eval', 'str' : 'np.exp(-0.5 * q**2)', 'fx' : 1.8 }, + 'bad5' : { 'type' : 'Eval', 'eval_str' : 'np.exp(-0.5 * x**2)', 'fx' : 1.8 }, # These would be set by config in real runs, but just add them here for the tests. 'image_pos' : galsim.PositionD(1.8,13), @@ -1357,6 +1530,24 @@ def test_eval(): print('i = ',i, 'val = ',test_val,true_val) np.testing.assert_almost_equal(test_val, true_val) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad1',config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad2',config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad3',config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad4',config, float) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'bad5',config, float) + config['eval_variables'] = 'itwo' + config = galsim.config.CleanConfig(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'eval3',config, float) + del config['eval_variables'] + with assert_raises(galsim.GalSimConfigError): + galsim.config.ParseValue(config,'eval3',config, float) + # Test the evaluation in RandomDistribution # Example config taken directly from Issue #776: config['shear'] = { diff --git a/tests/test_convolve.py b/tests/test_convolve.py index bcfb3e5564f..30f3d793f71 100644 --- a/tests/test_convolve.py +++ b/tests/test_convolve.py @@ -27,21 +27,6 @@ imgdir = os.path.join(".", "SBProfile_comparison_images") # Directory containing the reference # images. -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_convolve(): """Test the convolution of a Moffat and a Box profile against a known result. @@ -66,9 +51,7 @@ def test_convolve(): # Note: Since both of these have hard edges, GalSim wants to do this with real_space=True. # Here we are intentionally tesing the Fourier convolution, so we want to suppress the # warning that GalSim emits. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with assert_warns(galsim.GalSimWarning): # We'll do the real space convolution below conv = galsim.Convolve([psf,pixel],real_space=False) conv.drawImage(myImg,scale=dx, method="sb", use_true_center=False) @@ -107,11 +90,8 @@ def test_convolve(): check_basic(conv, "Moffat * Pixel") # Test photon shooting. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with assert_warns(galsim.GalSimWarning): do_shoot(conv,myImg,"Moffat * Pixel") - # Clear the warnings registry for later so we can test that appropriate warnings are raised. - galsim.Convolution.__init__.__globals__['__warningregistry__'].clear() # Convolution of just one argument should be equivalent to that argument. single = galsim.Convolve(psf) @@ -149,6 +129,16 @@ def test_convolve(): assert_raises(TypeError, galsim.Convolution, [psf, psf, myImg]) assert_raises(TypeError, galsim.Convolution, [psf, psf], realspace=False) + with assert_warns(galsim.GalSimWarning): + triple = galsim.Convolve(psf, psf, pixel) + assert_raises(galsim.GalSimError, triple.xValue, galsim.PositionD(0,0)) + assert_raises(galsim.GalSimError, triple.drawReal, myImg) + + deconv = galsim.Convolve(psf, galsim.Deconvolve(pixel)) + assert_raises(galsim.GalSimError, deconv.xValue, galsim.PositionD(0,0)) + assert_raises(galsim.GalSimError, deconv.drawReal, myImg) + assert_raises(galsim.GalSimError, deconv.drawPhot, myImg, n_photons=10) + @timer def test_convolve_flux_scaling(): @@ -257,12 +247,8 @@ def test_shearconvolve(): check_basic(conv, "sheared Gaussian * Pixel") # Test photon shooting. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with assert_warns(galsim.GalSimWarning): do_shoot(conv,myImg,"sheared Gaussian * Pixel") - # Clear the warnings registry for later so we can test that appropriate warnings are raised. - galsim.GSObject.drawImage.__globals__['__warningregistry__'].clear() @timer @@ -334,17 +320,18 @@ def test_realspace_convolve(): # Check some warnings that should be raised # More than 2 with only hard edges gives a warning either way. (Different warnings though.) - assert_warns(UserWarning, galsim.Convolve, [psf, psf, pixel]) - assert_warns(UserWarning, galsim.Convolve, [psf, psf, pixel], real_space=False) - assert_warns(UserWarning, galsim.Convolve, [psf, psf, pixel], real_space=True) + assert_warns(galsim.GalSimWarning, galsim.Convolve, [psf, psf, pixel]) + assert_warns(galsim.GalSimWarning, galsim.Convolve, [psf, psf, pixel], real_space=False) + assert_warns(galsim.GalSimWarning, galsim.Convolve, [psf, psf, pixel], real_space=True) # 2 with hard edges gives a warning if we ask it not to use real_space - assert_warns(UserWarning, galsim.Convolve, [psf, pixel], real_space=False) + assert_warns(galsim.GalSimWarning, galsim.Convolve, [psf, pixel], real_space=False) # >2 of any kind give a warning if we ask it to use real_space g = galsim.Gaussian(sigma=2) - assert_warns(UserWarning, galsim.Convolve, [g, g, g], real_space=True) + assert_warns(galsim.GalSimWarning, galsim.Convolve, [g, g, g], real_space=True) # non-analytic profiles cannot do real_space d = galsim.Deconvolve(galsim.Gaussian(sigma=2)) - assert_warns(UserWarning, galsim.Convolve, [g, d], real_space=True) + assert_warns(galsim.GalSimWarning, galsim.Convolve, [g, d], real_space=True) + assert_raises(TypeError, galsim.Convolve, [g, d], real_space='true') # Repeat some of the above for AutoConvolve and AutoCorrelate conv = galsim.AutoConvolve(psf,real_space=True) @@ -357,10 +344,12 @@ def test_realspace_convolve(): do_kvalue(conv,img,"AutoCorrelate Truncated Moffat") do_pickle(conv) - assert_warns(UserWarning, galsim.AutoConvolve, psf, real_space=False) - assert_warns(UserWarning, galsim.AutoConvolve, d, real_space=True) - assert_warns(UserWarning, galsim.AutoCorrelate, psf, real_space=False) - assert_warns(UserWarning, galsim.AutoCorrelate, d, real_space=True) + assert_warns(galsim.GalSimWarning, galsim.AutoConvolve, psf, real_space=False) + assert_warns(galsim.GalSimWarning, galsim.AutoConvolve, d, real_space=True) + assert_warns(galsim.GalSimWarning, galsim.AutoCorrelate, psf, real_space=False) + assert_warns(galsim.GalSimWarning, galsim.AutoCorrelate, d, real_space=True) + assert_raises(TypeError, galsim.AutoConvolve, d, real_space='true') + assert_raises(TypeError, galsim.AutoCorrelate, d, real_space='true') @timer @@ -557,6 +546,10 @@ def test_deconvolve(): assert_raises(TypeError, galsim.Deconvolution, psf, psf) assert_raises(TypeError, galsim.Deconvolution, psf, real_space=False) + assert_raises(NotImplementedError, inv_obj.xValue, galsim.PositionD(0,0)) + assert_raises(NotImplementedError, inv_obj.drawReal, myImg1) + assert_raises(NotImplementedError, inv_obj.shoot, 1) + @timer def test_autoconvolve(): @@ -778,9 +771,9 @@ def test_convolve_noise(): # to propagate noise properly. (It takes the input noise from the first one.) conv2 = galsim.Convolution(obj1, obj2) conv3 = galsim.Convolution(obj1, obj2, obj3) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): assert conv2.noise == obj1.noise.convolvedWith(obj2) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): assert conv3.noise == obj1.noise.convolvedWith(galsim.Convolve(obj2,obj3)) # Other types don't propagate noise and give a warning about it. @@ -788,13 +781,13 @@ def test_convolve_noise(): autoconv = galsim.AutoConvolution(obj2) autocorr = galsim.AutoCorrelation(obj2) four = galsim.FourierSqrt(obj2) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): assert deconv.noise is None - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): assert autoconv.noise is None - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): assert autocorr.noise is None - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): assert four.noise is None obj2.noise = None # Remove obj2 noise for the rest. @@ -813,7 +806,7 @@ def test_convolve_noise(): deconv = galsim.Deconvolution(obj2) autoconv = galsim.AutoConvolution(obj2) autocorr = galsim.AutoCorrelation(obj2) - four = galsim.AutoCorrelation(obj2) + four = galsim.FourierSqrt(obj2) assert deconv.noise is None assert autoconv.noise is None assert autocorr.noise is None diff --git a/tests/test_correlatednoise.py b/tests/test_correlatednoise.py index 1bae6b83994..8978b3d350c 100644 --- a/tests/test_correlatednoise.py +++ b/tests/test_correlatednoise.py @@ -100,6 +100,14 @@ def test_uncorrelated_noise_zero_lag(): do_pickle(ucn) do_pickle(cn) + assert_raises(TypeError, galsim.UncorrelatedNoise) + assert_raises(ValueError, galsim.UncorrelatedNoise, variance = -1.0) + assert_raises(TypeError, galsim.UncorrelatedNoise, 1, scale=1, wcs=galsim.PixelScale(3)) + assert_raises(TypeError, galsim.UncorrelatedNoise, 1, wcs=1) + assert_raises(ValueError, galsim.UncorrelatedNoise, 1, + wcs=galsim.FitsWCS('fits_files/tpv.fits')) + assert_raises(TypeError, galsim.UncorrelatedNoise, 1, rng=10) + @timer def test_uncorrelated_noise_nonzero_lag(): @@ -506,7 +514,18 @@ def test_output_generation_basic(): cn.drawImage(refim, scale=.18) # Generate a large image containing noise according to this function outimage = galsim.ImageD(xnoise_large.bounds, scale=0.18) + rng2 = ud.duplicate() outimage.addNoise(cn) + + # Can also use applyTo syntax. + outimage2 = galsim.ImageD(xnoise_large.bounds, scale=0.18) + cn.rng.reset(rng2) + cn.applyTo(outimage2) + np.testing.assert_equal(outimage2.array, outimage.array) + + assert_raises(TypeError, cn.applyTo, outimage2.array) + assert_raises(galsim.GalSimUndefinedBoundsError, cn.applyTo, galsim.Image()) + # Summed (average) CorrelatedNoises should be approximately equal to the input, so average # multiple CFs cn_2ndlevel = galsim.CorrelatedNoise(outimage, ud, correct_periodicity=False) @@ -525,6 +544,15 @@ def test_output_generation_basic(): testim.array, refim.array, decimal=2, err_msg="Generated noise field (basic) does not match input correlation properties.") + assert_raises(TypeError, galsim.CorrelatedNoise) + assert_raises(TypeError, galsim.CorrelatedNoise, outimage.array) + assert_raises(TypeError, galsim.CorrelatedNoise, outimage, scale=1, wcs=galsim.PixelScale(3)) + assert_raises(TypeError, galsim.CorrelatedNoise, outimage, wcs=1) + assert_raises(ValueError, galsim.CorrelatedNoise, outimage, + wcs=galsim.FitsWCS('fits_files/tpv.fits')) + assert_raises(TypeError, galsim.CorrelatedNoise, outimage, rng=10) + assert_raises(ValueError, galsim.CorrelatedNoise, outimage, x_interpolant='invalid') + @timer def test_output_generation_rotated(): @@ -682,6 +710,36 @@ def test_copy(): do_pickle(cn, drawNoise) do_pickle(cn) +@timer +def test_add(): + """Adding two correlated noise objects, just adds their profiles. + """ + rng = galsim.BaseDeviate(1234) + cosmos_scale = 0.03 + ccn = galsim.getCOSMOSNoise(rng=rng) + print('ccn.variance = ',ccn.getVariance()) + ucn1 = galsim.UncorrelatedNoise(variance=5.e-6, scale=cosmos_scale) + print('ucn1.variance = ',ucn1.getVariance()) + ucn2 = galsim.UncorrelatedNoise(variance=5.e-6, scale=1.) + print('ucn2.variance = ',ucn2.getVariance()) + + sum = ccn + ucn1 + print('sum.variance = ',sum.getVariance()) + np.testing.assert_allclose(sum.getVariance(), ccn.getVariance() + ucn1.getVariance()) + + with assert_warns(galsim.GalSimWarning): + sum = ccn + ucn2 + print('sum.variance = ',sum.getVariance()) + np.testing.assert_allclose(sum.getVariance(), ccn.getVariance() + ucn2.getVariance()) + + diff = ccn - ucn1 + print('diff.variance = ',diff.getVariance()) + np.testing.assert_allclose(diff.getVariance(), ccn.getVariance() - ucn1.getVariance()) + + with assert_warns(galsim.GalSimWarning): + diff = ccn - ucn2 + print('diff.variance = ',diff.getVariance()) + np.testing.assert_allclose(diff.getVariance(), ccn.getVariance() - ucn2.getVariance()) @timer def test_cosmos_and_whitening(): @@ -697,7 +755,7 @@ def test_cosmos_and_whitening(): outimage = galsim.ImageD(3 * largeim_size + 11, 3 * largeim_size, scale=cosmos_scale) outimage.addNoise(ccn) # Add the COSMOS noise # Then estimate correlation function from generated noise - cntest_correlated = galsim.CorrelatedNoise(outimage, ccn.rng) + cntest_correlated = galsim.CorrelatedNoise(outimage, ccn.rng, scale=cosmos_scale) # Check basic correlation function values of the 3x3 pixel region around (0,0) pos = galsim.PositionD(0., 0.) cf00 = ccn._profile.xValue(pos) @@ -766,11 +824,12 @@ def test_cosmos_and_whitening(): ccn_convolved = ccn_transformed.convolvedWith(galsim.Convolve([psf_ground, pix_ground])) # Reset the outimage, and set its pixel scale to now be the ground-based resolution # Also, check both odd-size and non-square here. Both should be ok. - outimage = galsim.ImageD(3 * largeim_size + 1, 3 * largeim_size + 43, scale=scale) + outimage = galsim.ImageD(3 * largeim_size + 1, 3 * largeim_size + 43) # Add correlated noise outimage.addNoise(ccn_convolved) # Then whiten - #wht_variance = ccn_convolved.whitenImage(outimage) + # Note: Use alternate syntax here. Equivalent to + # wht_variance = ccn_convolved.whitenImage(outimage) wht_variance = outimage.whitenNoise(ccn_convolved) # Then test cntest_whitened = galsim.CorrelatedNoise(outimage, ccn.rng) # Get the correlation function @@ -790,6 +849,9 @@ def test_cosmos_and_whitening(): err_msg="Noise field generated by whitening rotated, sheared, magnified, convolved "+ "COSMOS CorrelatedNoise does not have approximately zero interpixel covariances") + assert_raises(TypeError, ccn.whitenImage, outimage.array) + assert_raises(galsim.GalSimUndefinedBoundsError, ccn.whitenImage, galsim.Image()) + @timer def test_symmetrizing(): @@ -813,8 +875,7 @@ def test_symmetrizing(): outimage = galsim.ImageD(symm_size_mult * largeim_size, symm_size_mult * largeim_size, scale=cosmos_scale) outimage.addNoise(ccn) # Add the COSMOS noise - # Then estimate correlation function from generated noise - cntest_correlated = galsim.CorrelatedNoise(outimage, ccn.rng) + outimage2 = outimage.copy() # Now apply 4-fold symmetry to the noise field, and check that its variance and covariances are # as expected (non-zero distance correlations should be symmetric) symmetrized_variance = ccn.symmetrizeImage(outimage, order=4) @@ -839,6 +900,11 @@ def test_symmetrizing(): err_msg="Noise field generated by symmetrizing COSMOS CorrelatedNoise does not have "+ "approximate 4-fold symmetry") + # If outimage doesn't have a scale set, then it uses the ccn scale. + outimage2.wcs = None + symmetrized_variance2 = ccn.symmetrizeImage(outimage2, order=4) + np.testing.assert_almost_equal(symmetrized_variance2, symmetrized_variance) + # Now test symmetrizing, but having first expanded and sheared the COSMOS noise correlation. # Also we'll make the output image odd-sized, so as to ensure we test that option. ccn_transformed = ccn.shear(g1=-0.05, g2=0.11).rotate(313. * galsim.degrees).expand(2.1) @@ -904,6 +970,12 @@ def test_symmetrizing(): err_msg="Noise field generated by symmetrizing rotated, sheared, magnified, convolved "+ "COSMOS CorrelatedNoise does not have approximate 4-fold symmetry") + assert_raises(TypeError, ccn.symmetrizeImage) + assert_raises(TypeError, ccn.symmetrizeImage, outimage.array) + assert_raises(ValueError, ccn.symmetrizeImage, galsim.Image(24,20)) + assert_raises(ValueError, ccn.symmetrizeImage, outimage, order=2) + assert_raises(ValueError, ccn.symmetrizeImage, outimage, order=5) + assert_raises(galsim.GalSimUndefinedBoundsError, ccn.symmetrizeImage, galsim.Image()) @timer def test_convolve_cosmos(): @@ -1085,7 +1157,7 @@ def test_uncorrelated_noise_tracking(): # Convolving two objects with noise works fine, but accessing the resulting noise attribute # leads to a warning. conv_obj = galsim.Convolve(int_im, int_im) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): noise = conv_obj.noise # The noise should be correlated, not just the original UncorrelatedNoise assert isinstance(noise, galsim.correlatednoise._BaseCorrelatedNoise) @@ -1115,6 +1187,14 @@ def test_variance_changes(): np.testing.assert_equal(cn.getVariance(), new_var, err_msg='Failure to reset and then get variance for CorrelatedNoise') + # Also check some errors here + assert_raises(ValueError, cn.withVariance, -1.0) + assert_raises(OSError, galsim.getCOSMOSNoise, file_name='not_a_file') + assert_raises(OSError, galsim.getCOSMOSNoise, file_name='config_input/catalog.fits') + assert_raises(TypeError, galsim.getCOSMOSNoise, rng='invalid') + assert_raises(ValueError, galsim.getCOSMOSNoise, variance = -1.0) + assert_raises(ValueError, galsim.getCOSMOSNoise, x_interpolant='invalid') + @timer def test_cosmos_wcs(): @@ -1171,7 +1251,7 @@ def test_cosmos_wcs(): test_im.setZero() test_im.view(wcs=cn_orig.wcs).addNoise(cn_orig) cn_test = galsim.CorrelatedNoise(test_im) - cn_raw = galsim.CorrelatedNoise(test_im.view(scale=cosmos_scale)) + cn_raw = galsim.CorrelatedNoise(test_im, wcs=galsim.PixelScale(cosmos_scale)) # This time it is the raw cf values that should match. for xpos, ypos in zip((0., cosmos_scale, 0., cosmos_scale, cosmos_scale), @@ -1231,6 +1311,7 @@ def test_covariance_spectrum(): test_output_generation_rotated() test_output_generation_magnified() test_copy() + test_add() test_cosmos_and_whitening() test_symmetrizing() test_convolve_cosmos() diff --git a/tests/test_deltafunction.py b/tests/test_deltafunction.py index ba81248faaa..fb3eb63c194 100644 --- a/tests/test_deltafunction.py +++ b/tests/test_deltafunction.py @@ -25,21 +25,6 @@ from galsim_test_helpers import * -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_deltaFunction(): """Test the generation of a Delta function profile diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 200fadcda57..7265abcf519 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -28,17 +28,9 @@ def check_dep(f, *args, **kwargs): """Check that some function raises a GalSimDeprecationWarning as a warning, but not an error. """ - import warnings - # Cause all warnings to always be triggered. - # Important in case we want to trigger the same one twice in the test suite. - warnings.simplefilter("always") - # Check that f() raises a warning, but not an error. - with warnings.catch_warnings(record=True) as w: + with assert_warns(galsim.GalSimDeprecationWarning): res = f(*args, **kwargs) - assert len(w) >= 1, "Calling %s did not raise a warning"%str(f) - #print([ str(wk.message) for wk in w ]) - assert issubclass(w[0].category, galsim.GalSimDeprecationWarning) return res if __name__ == "__main__": diff --git a/tests/test_des.py b/tests/test_des.py index 4c16fab9cb0..24a9c1baa82 100644 --- a/tests/test_des.py +++ b/tests/test_des.py @@ -57,10 +57,9 @@ def test_meds(): psf13 = galsim.Image(box_size, box_size, init_value=143) dudx = 11.1; dudy = 11.2; dvdx = 11.3; dvdy = 11.4; x0 = 11.5; y0 = 11.6; wcs11 = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, galsim.PositionD(x0, y0)) - dudx = 12.1; dudy = 12.2; dvdx = 12.3; dvdy = 12.4; x0 = 12.5; y0 = 12.6; - wcs12 = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, galsim.PositionD(x0, y0)) - dudx = 13.1; dudy = 13.2; dvdx = 13.3; dvdy = 13.4; x0 = 13.5; y0 = 13.6; - wcs13 = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, galsim.PositionD(x0, y0)) + dudx = 12.1; dudy = 12.2; dvdx = 12.3; dvdy = 12.4; + wcs12 = galsim.JacobianWCS(dudx, dudy, dvdx, dvdy) + wcs13 = galsim.PixelScale(13) # create lists @@ -90,10 +89,9 @@ def test_meds(): dudx = 21.1; dudy = 21.2; dvdx = 21.3; dvdy = 21.4; x0 = 21.5; y0 = 21.6; wcs21 = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, galsim.PositionD(x0, y0)) - dudx = 22.1; dudy = 22.2; dvdx = 22.3; dvdy = 22.4; x0 = 22.5; y0 = 22.6; - wcs22 = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, galsim.PositionD(x0, y0)) - dudx = 23.1; dudy = 23.2; dvdx = 23.3; dvdy = 23.4; x0 = 23.5; y0 = 23.6; - wcs23 = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, galsim.PositionD(x0, y0)) + dudx = 22.1; dudy = 22.2; dvdx = 22.3; dvdy = 22.4; + wcs22 = galsim.JacobianWCS(dudx, dudy, dvdx, dvdy) + wcs23 = galsim.PixelScale(23) # create lists images = [img21, img22, img23] @@ -109,6 +107,8 @@ def test_meds(): img23.wcs = wcs23 obj2 = galsim.des.MultiExposureObject(images=images, weight=weight, seg=seg, psf=psf, id=2) + obj3 = galsim.des.MultiExposureObject(images=images, id=3) + # create an object list objlist = [obj1, obj2] @@ -116,6 +116,43 @@ def test_meds(): filename_meds = 'output/test_meds.fits' galsim.des.WriteMEDS(objlist, filename_meds, clobber=True) + bad1 = galsim.Image(32, 48, init_value=0) + bad2 = galsim.Image(35, 35, init_value=0) + bad3 = galsim.Image(48, 48, init_value=0) + + with assert_raises(TypeError): + galsim.des.MultiExposureObject(images=img11) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[]) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[bad1]) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[bad2]) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[img11,bad3]) + with assert_raises(TypeError): + galsim.des.MultiExposureObject(images=images, weight=wth11) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=images, weight=[]) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[img11], weight=[bad3]) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[img11], psf=[bad1]) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[img11], psf=[bad2]) + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[img11, img12], psf=[bad2, psf12]) + with assert_raises(TypeError): + galsim.des.MultiExposureObject(images=images, wcs=wcs11) + celestial_wcs = galsim.FitsWCS("DECam_00154912_12_header.fits", dir='des_data') + with assert_raises(galsim.GalSimValueError): + galsim.des.MultiExposureObject(images=[img11], wcs=[celestial_wcs]) + + # Check the one with no psf, weight, etc. + filename_meds2 = 'output/test_meds_image_only.fits' + galsim.des.WriteMEDS([obj3], filename_meds2, clobber=True) + + # Note that while there are no tests prior to this, the above still checks for # syntax errors in the meds creation software, so it's still worth running as part # of the normal unit tests. @@ -123,19 +160,16 @@ def test_meds(): # stays in sync with any changes there. try: import meds - except ImportError: - print('Failed to import meds. Unable to do tests of meds file.') - return - try: # Meds will import this, so check for this too. import fitsio except ImportError: - print('Failed to import fitsio. Unable to do tests of meds file.') + print('Failed to import either meds or fitsio. Unable to do tests of meds file.') return # Run meds module's validate function try: meds.util.validate_meds(filename_meds) + meds.util.validate_meds(filename_meds2) except AttributeError: print('Seems to be the wrong meds package. Unable to do tests of meds file.') return @@ -288,14 +322,27 @@ def get_offset(obj_num): import logging logging.basicConfig(format="%(message)s", level=logging.WARN, stream=sys.stdout) logger = logging.getLogger('test_meds_config') - galsim.config.Process(config, logger=logger) + galsim.config.BuildFile(config, logger=logger) # Add in badpix and offset so we run both with and without options. + config = galsim.config.CleanConfig(config) config['image']['offset'] = { 'type' : 'XY' , 'x' : offset_x, 'y' : offset_y } config['output']['badpix'] = {} - galsim.config.Process(config, logger=logger) + galsim.config.BuildFile(config, logger=logger) + + # Scattered image is invalid with MEDS output + config = galsim.config.CleanConfig(config) + config['image'] = { + 'type' : 'Scattered', + 'nobjects' : 20, + 'pixel_scale' : pixel_scale, + 'size' : stamp_size , + } + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildFile(config, logger=logger) # Now repeat, making a separate file for each + config = galsim.config.CleanConfig(config) config['gal']['half_light_radius'] = { 'type' : 'Sequence', 'first' : 0.7, 'step' : 0.1, 'index_key' : 'file_num' } config['output'] = { 'type' : 'Fits', @@ -310,7 +357,7 @@ def get_offset(obj_num): 'nx_tiles' : 1, 'ny_tiles' : n_per_obj, 'pixel_scale' : pixel_scale, - 'offset' : config['image']['offset'], + 'offset' : { 'type' : 'XY' , 'x' : offset_x, 'y' : offset_y }, 'stamp_size' : stamp_size, 'random_seed' : seed } @@ -594,6 +641,19 @@ def test_psf(): numpy.testing.assert_almost_equal(meas.observed_shape.g2/2, ref_shape.g2/2, decimal=2, err_msg="no-wcs PSFEx shape.g2 doesn't match") + with assert_raises(TypeError): + # file_name must be a string. + galsim.des.DES_PSFEx(psf, wcs=wcs_file, dir=data_dir) + with assert_raises(galsim.GalSimError): + # Cannot provide both image_file_name and wcs + galsim.des.DES_PSFEx(psfex_file, image_file_name=wcs_file, wcs=wcs_file, dir=data_dir) + with assert_raises((IOError, OSError)): + # This one doesn't exist. + galsim.des.DES_PSFEx('nonexistant.psf', wcs=wcs_file, dir=data_dir) + with assert_raises(OSError): + # This one exists, but has invalid header parameters. + galsim.des.DES_PSFEx('invalid_psfcat.psf', wcs=wcs_file, dir=data_dir) + # Now the shapelet PSF model. This model is already in sky coordinates, so no wcs_file needed. fitpsf = galsim.des.DES_Shapelet(os.path.join(data_dir,fitpsf_file)) psf = fitpsf.getPSF(image_pos) @@ -609,6 +669,9 @@ def test_psf(): numpy.testing.assert_almost_equal(meas.observed_shape.g2/2, ref_shape.g2/2, decimal=2, err_msg="Shapelet PSF shape.g2 doesn't match") + with assert_raises(galsim.GalSimBoundsError): + fitpsf.getPSF(image_pos = galsim.PositionD(4000, 5000)) + @timer def test_psf_config(): @@ -637,6 +700,7 @@ def test_psf_config(): 'gsparams' : { 'folding_threshold' : 1.e-4 } }, 'psf5' : { 'type' : 'DES_PSFEx', 'image_pos' : galsim.PositionD(789,567), 'flux' : 388, 'gsparams' : { 'folding_threshold' : 1.e-4 } }, + 'bad1' : { 'type' : 'DES_Shapelet', 'image_pos' : galsim.PositionD(5670,789) }, # This would normally be set by the config processing. Set it manually here. 'image_pos' : image_pos, @@ -666,13 +730,22 @@ def test_psf_config(): # Insert a wcs for thes last one. config['wcs'] = galsim.FitsWCS(os.path.join(data_dir,wcs_file)) - del config['input_objs'] + config = galsim.config.CleanConfig(config) galsim.config.ProcessInput(config) psfex2 = galsim.des.DES_PSFEx(psfex_file, dir=data_dir, wcs=config['wcs']) psf5a = galsim.config.BuildGSObject(config, 'psf5')[0] psf5b = psfex2.getPSF(galsim.PositionD(789,567),gsparams=gsparams).withFlux(388) gsobject_compare(psf5a, psf5b) + del config['image_pos'] + galsim.config.RemoveCurrent(config) + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'psf1')[0] + with assert_raises(galsim.GalSimConfigError): + galsim.config.BuildGSObject(config, 'psf2')[0] + with assert_raises(galsim.config.gsobject.SkipThisObject): + galsim.config.BuildGSObject(config, 'bad1')[0] + if __name__ == "__main__": test_meds() diff --git a/tests/test_detectors.py b/tests/test_detectors.py index 25f27377652..45c3fb4f20e 100644 --- a/tests/test_detectors.py +++ b/tests/test_detectors.py @@ -38,8 +38,8 @@ def test_nonlinearity_basic(): im_save = im.copy() # Basic - exceptions / bad usage (invalid function, does not return NumPy array). - with assert_raises(ValueError): - im.applyNonlinearity(lambda x : 1.0) + assert_raises(ValueError, im.applyNonlinearity, lambda x : 1.0) + assert_raises(ValueError, im.applyNonlinearity, lambda x : np.array([1,2,3])) # Check for constant function as NLfunc im_new = im.copy() @@ -177,8 +177,9 @@ def test_recipfail_basic(): im_save = im.copy() # Basic - exceptions / bad usage. - with assert_raises(ValueError): - im.addReciprocityFailure(-1.0, 200, 1.0) + assert_raises(ValueError, im.addReciprocityFailure, -1.0, 200, 1.0) + assert_raises(ValueError, im.addReciprocityFailure, 1.0, -200, 1.0) + assert_raises(ValueError, im.addReciprocityFailure, 1.0, 200, -1.0) # Preservation of data type / scale / bounds im_new = im.copy() @@ -255,6 +256,11 @@ def test_recipfail_basic(): im_new.array,im.array*(1+alpha*np.log10(im.array/(exp_time*base_flux))),6, err_msg='Difference between power law and log behavior') + # If input image has negative values, then raise a warning. + im_new.setValue(30, 30, -100) + with assert_warns(galsim.GalSimWarning): + im_new.addReciprocityFailure(alpha=alpha, exp_time=exp_time, base_flux=base_flux) + @timer def test_quantize(): @@ -322,6 +328,11 @@ def test_IPC_basic(): im_new.array, im.array, err_msg="Image is altered for no IPC with edge_treatment = 'crop'" ) + assert_raises(ValueError, im_new.applyIPC, galsim.Image(2,2,init_value=1)) + assert_raises(ValueError, im_new.applyIPC, galsim.Image(3,3,init_value=-1)) + assert_raises(ValueError, im_new.applyIPC, ipc_kernel * -1) + assert_raises(ValueError, im_new.applyIPC, ipc_kernel, edge_treatment='invalid') + # Test with a scalar fill_value fill_value = np.pi # a non-trivial one im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop',fill_value=fill_value) @@ -347,19 +358,25 @@ def test_IPC_basic(): # Testing for flux conservation np.random.seed(1234) ipc_kernel = galsim.Image(abs(np.random.randn(3,3))) # a random kernel - ipc_kernel /= ipc_kernel.array.sum() # but make it normalized so we do not get warnings im_new = im.copy() # Set edges to zero since flux is not conserved at the edges otherwise im_new.array[0,:] = 0.0 im_new.array[-1,:] = 0.0 im_new.array[:,0] = 0.0 im_new.array[:,-1] = 0.0 - im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='extend', kernel_normalization=True) + with assert_warns(galsim.GalSimWarning): # warn about the sum not being 1 + im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='extend') np.testing.assert_almost_equal(im_new.array.sum(), im.array[1:-1,1:-1].sum(), 4, err_msg="Normalized IPC kernel does not conserve the total flux for 'extend' option.") + # With kernel_normalization = False, it won't warn, but it also won't conserve flux. + im_new = im.copy() + im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='extend', kernel_normalization=False) + assert np.abs(im_new.array.sum() - im.array[1:-1,1:-1].sum()) > 1.e-8 + im_new = im.copy() - im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='wrap', kernel_normalization=True) + ipc_kernel /= ipc_kernel.array.sum() # Explicitly normalizing also avoids warning. + im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='wrap') np.testing.assert_almost_equal(im_new.array.sum(), im.array.sum(), 4, err_msg="Normalized IPC kernel does not conserve the total flux for 'wrap' option.") @@ -369,7 +386,7 @@ def test_IPC_basic(): ipc_kernel.setValue(2,3,0.125) # This kernel should correspond to each pixel getting contribution from the pixel above it. im1 = im.copy() - im1.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop',kernel_normalization=False) + im1.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop') np.testing.assert_array_almost_equal(0.875*im.array[1:-1,1:-1]+0.125*im.array[2:,1:-1], im1.array[1:-1,1:-1], 7, err_msg="Difference in directionality for up kernel in applyIPC") # Checking for one pixel in the central bulk @@ -381,7 +398,7 @@ def test_IPC_basic(): ipc_kernel.setValue(1,2,0.125) # This kernel should correspond to each pixel getting contribution from the pixel to its left. im1 = im.copy() - im1.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop',kernel_normalization=False) + im1.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop') np.testing.assert_array_almost_equal(im1.array[1:-1,1:-1], im1.array[1:-1,1:-1], 7, err_msg="Difference in directionality for left kernel in applyIPC") # Checking for one pixel in the central bulk @@ -494,7 +511,7 @@ def test_Persistence_basic(): # Test for different lengths of imgs and coeffs im_new = im.copy() - with assert_raises(TypeError): + with assert_raises(ValueError): im_new.applyPersistence(im_prev, [0.2, 0.3]) # Test for a single image and coeffs as a float diff --git a/tests/test_draw.py b/tests/test_draw.py index 884c5fd5a9c..87e26261a83 100644 --- a/tests/test_draw.py +++ b/tests/test_draw.py @@ -291,13 +291,6 @@ def test_drawImage(): np.testing.assert_almost_equal( mom['My'], (ny+1.+1.)/2., 4, "obj.drawImage(nx,ny) (odd) did not center in y correctly") - # Test if we provide nx, ny, scale, and an existing image. It should: - # - raise a ValueError - im10 = galsim.ImageF() - kwargs = {'nx':nx, 'ny':ny, 'scale':scale, 'image':im10} - with assert_raises(ValueError): - obj.drawImage(**kwargs) - # Test if we provide bounds and scale. It should: # - create a new image with the right size # - set the scale @@ -332,12 +325,39 @@ def test_drawImage(): np.testing.assert_almost_equal(mom['My'], (ny+1.+1.)/2., 4, "obj.drawImage(bounds) did not center in y correctly") - # Test if we provide bounds, scale, and an existing image. It should: - # - raise a ValueError - bounds = galsim.BoundsI(1,nx,1,ny) - kwargs = {'bounds':bounds, 'scale':scale, 'image':im10} - with assert_raises(ValueError): - obj.drawImage(**kwargs) + # Combinations that raise errors: + assert_raises(TypeError, obj.drawImage, image=im10, bounds=bounds) + assert_raises(TypeError, obj.drawImage, image=im10, dtype=int) + assert_raises(TypeError, obj.drawImage, nx=3, ny=4, image=im10, scale=scale) + assert_raises(TypeError, obj.drawImage, nx=3, ny=4, image=im10) + assert_raises(TypeError, obj.drawImage, nx=3, ny=4, bounds=bounds) + assert_raises(TypeError, obj.drawImage, nx=3, ny=4, add_to_image=True) + assert_raises(TypeError, obj.drawImage, bounds=bounds, add_to_image=True) + assert_raises(TypeError, obj.drawImage, image=galsim.Image(), add_to_image=True) + assert_raises(TypeError, obj.drawImage, nx=3) + assert_raises(TypeError, obj.drawImage, ny=3) + assert_raises(TypeError, obj.drawImage, nx=3, ny=3, invalid=True) + assert_raises(TypeError, obj.drawImage, bounds=bounds, scale=scale, wcs=galsim.PixelScale(3)) + assert_raises(TypeError, obj.drawImage, bounds=bounds, wcs=scale) + assert_raises(TypeError, obj.drawImage, image=im10.array) + assert_raises(TypeError, obj.drawImage, wcs=galsim.FitsWCS('fits_files/tpv.fits')) + + assert_raises(ValueError, obj.drawImage, bounds=galsim.BoundsI()) + assert_raises(ValueError, obj.drawImage, image=im10, gain=0.) + assert_raises(ValueError, obj.drawImage, image=im10, gain=-1.) + assert_raises(ValueError, obj.drawImage, image=im10, area=0.) + assert_raises(ValueError, obj.drawImage, image=im10, area=-1.) + assert_raises(ValueError, obj.drawImage, image=im10, exptime=0.) + assert_raises(ValueError, obj.drawImage, image=im10, exptime=-1.) + assert_raises(ValueError, obj.drawImage, image=im10, method='invalid') + + # These options are invalid unless metho=phot + assert_raises(TypeError, obj.drawImage, image=im10, n_photons=3) + assert_raises(TypeError, obj.drawImage, rng=galsim.BaseDeviate(234)) + assert_raises(TypeError, obj.drawImage, max_extra_noise=23) + assert_raises(TypeError, obj.drawImage, poisson_flux=True) + assert_raises(TypeError, obj.drawImage, surface_ops=('dummy')) + assert_raises(TypeError, obj.drawImage, save_photons=True) @timer @@ -433,9 +453,7 @@ def test_draw_methods(): "obj.drawImage(real_space) differs from obj.drawImage") # fft should be similar, but not precisely equal. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with assert_warns(galsim.GalSimWarning): # This emits a warning about convolving two things with hard edges. im3 = obj.drawImage(image=im1.copy(), method='fft') print('im1, im3 max diff = ',abs(im1.array - im3.array).max()) @@ -714,12 +732,52 @@ def test_drawKImage(): np.testing.assert_almost_equal( im6.scale, scale, 9, "obj.drawKImage(image,recenter=False) produced image with wrong scale") - np.testing.assert_equal( - im6.array.shape, (ny//4+1, nx//3+1), - "obj.drawKImage(image,recenter=False) produced image with wrong shape") np.testing.assert_almost_equal( im6.array, im4[bounds6].array, 9, - "obj.drawKImage(image,rcenter=False) produced different values than recenter=True") + "obj.drawKImage(image,recenter=False) produced different values than recenter=True") + + # Can add to image if recenter is False + im6.setZero() + obj.drawKImage(im6, recenter=False, add_to_image=True) + np.testing.assert_almost_equal( + im6.scale, scale, 9, + "obj.drawKImage(image,add_to_image=True) produced image with wrong scale") + np.testing.assert_almost_equal( + im6.array, im4[bounds6].array, 9, + "obj.drawKImage(image,add_to_image=True) produced different values than recenter=True") + + # .. or if image is centered. + im7 = im4.copy() + im7.setZero() + im7.setCenter(0,0) + obj.drawKImage(im7, add_to_image=True) + np.testing.assert_almost_equal( + im7.scale, scale, 9, + "obj.drawKImage(image,add_to_image=True) produced image with wrong scale") + np.testing.assert_almost_equal( + im7.array, im4.array, 9, + "obj.drawKImage(image,add_to_image=True) produced different values than recenter=True") + + # .. but otherwise not. + with assert_raises(galsim.GalSimIncompatibleValuesError): + obj.drawKImage(image=im6, add_to_image=True) + + # Other error combinations: + assert_raises(TypeError, obj.drawKImage, image=im6, bounds=bounds) + assert_raises(TypeError, obj.drawKImage, image=im6, dtype=int) + assert_raises(TypeError, obj.drawKImage, nx=3, ny=4, image=im6, scale=scale) + assert_raises(TypeError, obj.drawKImage, nx=3, ny=4, image=im6) + assert_raises(TypeError, obj.drawKImage, nx=3, ny=4, add_to_image=True) + assert_raises(TypeError, obj.drawKImage, nx=3, ny=4, bounds=bounds) + assert_raises(TypeError, obj.drawKImage, bounds=bounds, add_to_image=True) + assert_raises(TypeError, obj.drawKImage, image=galsim.Image(dtype=complex), add_to_image=True) + assert_raises(TypeError, obj.drawKImage, nx=3) + assert_raises(TypeError, obj.drawKImage, ny=3) + assert_raises(TypeError, obj.drawKImage, nx=3, ny=3, invalid=True) + assert_raises(TypeError, obj.drawKImage, bounds=bounds, wcs=galsim.PixelScale(3)) + assert_raises(TypeError, obj.drawKImage, image=im6.array) + assert_raises(ValueError, obj.drawKImage, image=galsim.ImageF(3,4)) + assert_raises(ValueError, obj.drawKImage, bounds=galsim.BoundsI()) @timer @@ -1005,6 +1063,16 @@ def test_shoot(): image4 = (obj*0).drawImage(method='phot') np.testing.assert_equal(image4.array, 0) + # Warns if flux is 1 and n_photons not given. + with assert_warns(galsim.GalSimWarning): + psf = galsim.Gaussian(sigma=3) + psf.drawImage(method='phot') + + # Also if flux << 1, n_photons will end up 0. + with assert_warns(galsim.GalSimWarning): + psf = galsim.Gaussian(sigma=3, flux=1.e-5) + psf.drawImage(method='phot') + @timer def test_drawImage_area_exptime(): @@ -1047,13 +1115,11 @@ def test_drawImage_area_exptime(): # Shooting with flux=1 raises a warning. obj1 = obj.withFlux(1) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): obj1.drawImage(method='phot') # But not if we explicitly tell it to shoot 1 photon - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("error") - obj1.drawImage(method='phot', n_photons=1) + with assert_raises(AssertionError): + assert_warns(galsim.GalSimWarning, obj1.drawImage, method='phot', n_photons=1) @timer @@ -1170,6 +1236,25 @@ def test_fft(): im2_real.array, im2_alt_real.array, 9, "inverse_fft produce a different array than obj2.drawImage(nx,ny,method='sb')") + # wcs must be a PixelScale + xim.wcs = galsim.JacobianWCS(1.1,0.1,0.1,1) + with assert_raises(galsim.GalSimError): + xim.calculate_fft() + with assert_raises(galsim.GalSimError): + xim.calculate_inverse_fft() + xim.wcs = None + with assert_raises(galsim.GalSimError): + xim.calculate_fft() + with assert_raises(galsim.GalSimError): + xim.calculate_inverse_fft() + + # inverse needs image with 0,0 + xim.scale=1 + xim.setOrigin(1,1) + with assert_raises(galsim.GalSimBoundsError): + xim.calculate_inverse_fft() + + @timer def test_np_fft(): """Test the equivalence between np.fft functions and the galsim versions diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 00000000000..96a5f1c1735 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,319 @@ +# Copyright (c) 2012-2018 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. +# + +from __future__ import print_function + +import galsim +from galsim_test_helpers import * + +@timer +def test_galsim_error(): + """Test basic usage of GalSimError + """ + err = galsim.GalSimError("Test") + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test" + assert isinstance(err, RuntimeError) + do_pickle(err) + + +@timer +def test_galsim_value_error(): + """Test basic usage of GalSimValueError + """ + value = 2.3 + err = galsim.GalSimValueError("Test", value) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Value 2.3" + assert err.value == value + assert err.allowed_values == None + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, ValueError) + do_pickle(err) + + err = galsim.GalSimValueError("Test", value, (0,1,2)) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Value 2.3 not in (0, 1, 2)" + assert err.value == value + assert err.allowed_values == (0,1,2) + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, ValueError) + do_pickle(err) + + +@timer +def test_galsim_key_error(): + """Test basic usage of GalSimKeyError + """ + key = 'foo' + err = galsim.GalSimKeyError("Test", key) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Key foo" + assert err.key == key + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, KeyError) + do_pickle(err) + + +@timer +def test_galsim_index_error(): + """Test basic usage of GalSimIndexError + """ + index = 3 + err = galsim.GalSimIndexError("Test", index) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Index 3" + assert err.index == index + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, IndexError) + do_pickle(err) + + +@timer +def test_galsim_range_error(): + """Test basic usage of GalSimRangeError + """ + value = 2.3 + err = galsim.GalSimRangeError("Test", value, 0, 1) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Value 2.3 not in range [0, 1]" + assert err.value == value + assert err.min == 0 + assert err.max == 1 + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, ValueError) + do_pickle(err) + + err = galsim.GalSimRangeError("Test", value, 10) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Value 2.3 not in range [10, None]" + assert err.value == value + assert err.min == 10 + assert err.max == None + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, ValueError) + do_pickle(err) + + +@timer +def test_galsim_bounds_error(): + """Test basic usage of GalSimBoundsError + """ + pos = galsim.PositionI(0,0) + bounds = galsim.BoundsI(1,10,1,10) + err = galsim.GalSimBoundsError("Test", pos, bounds) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test galsim.PositionI(0,0) not in galsim.BoundsI(1,10,1,10)" + assert err.pos == pos + assert err.bounds == bounds + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, ValueError) + do_pickle(err) + + +@timer +def test_galsim_undefined_bounds_error(): + """Test basic usage of GalSimUndefinedBoundsError + """ + err = galsim.GalSimUndefinedBoundsError("Test") + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test" + assert isinstance(err, galsim.GalSimError) + do_pickle(err) + + +@timer +def test_galsim_immutable_error(): + """Test basic usage of GalSimImmutableError + """ + im = galsim.ImageD(np.array([[0]]), make_const=True) + err = galsim.GalSimImmutableError("Test", im) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Image: galsim.Image(bounds=galsim.BoundsI(1,1,1,1), wcs=None, dtype=numpy.float64)" + assert err.image == im + assert isinstance(err, galsim.GalSimError) + do_pickle(err) + + +@timer +def test_galsim_incompatible_values_error(): + """Test basic usage of GalSimIncompatibleValuesError + """ + err = galsim.GalSimIncompatibleValuesError("Test", a=1, b=2) + print('str = ',str(err)) + print('repr = ',repr(err)) + # This isn't completely deterministic across python versions. + str_possibilities = ["Test Values {'a': 1, 'b': 2}", + "Test Values {'b': 2, 'a': 1}"] + assert str(err) in str_possibilities + assert err.values == dict(a=1, b=2) + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, ValueError) + assert isinstance(err, TypeError) + do_pickle(err) + + +@timer +def test_galsim_sed_error(): + """Test basic usage of GalSimSEDError + """ + sed = galsim.SED('1', wave_type='nm', flux_type='fphotons') + err = galsim.GalSimSEDError("Test", sed) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test SED: galsim.SED('1', redshift=0.0)" + assert err.sed == sed + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, TypeError) + do_pickle(err) + + +@timer +def test_galsim_hsm_error(): + """Test basic usage of GalSimHSMError + """ + err = galsim.GalSimHSMError("Test") + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test" + assert isinstance(err, galsim.GalSimError) + do_pickle(err) + + +@timer +def test_galsim_fft_size_error(): + """Test basic usage of GalSimFFTSizeError + """ + err = galsim.GalSimFFTSizeError("Test FFT is too big.", 10240) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == ("Test FFT is too big.\nThe required FFT size would be 10240 x 10240, " + "which requires 2.34 GB of memory.\nIf you can handle " + "the large FFT, you may update gsparams.maximum_fft_size.") + assert err.size == 10240 + np.testing.assert_almost_equal(err.mem, 2.34375) + assert isinstance(err, galsim.GalSimError) + do_pickle(err) + + +@timer +def test_galsim_config_error(): + """Test basic usage of GalSimConfigError + """ + err = galsim.GalSimConfigError("Test") + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test" + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, ValueError) + do_pickle(err) + + +@timer +def test_galsim_config_value_error(): + """Test basic usage of GalSimConfigValueError + """ + value = 2.3 + err = galsim.GalSimConfigValueError("Test", value) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Value 2.3" + assert err.value == value + assert err.allowed_values == None + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, galsim.GalSimConfigError) + assert isinstance(err, ValueError) + do_pickle(err) + + err = galsim.GalSimConfigValueError("Test", value, (0,1,2)) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test Value 2.3 not in (0, 1, 2)" + assert err.value == value + assert err.allowed_values == (0,1,2) + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, galsim.GalSimConfigError) + assert isinstance(err, ValueError) + do_pickle(err) + + +@timer +def test_galsim_notimplemented_error(): + """Test basic usage of GalSimNotImplementedError + """ + err = galsim.GalSimNotImplementedError("Test") + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test" + assert isinstance(err, galsim.GalSimError) + assert isinstance(err, NotImplementedError) + do_pickle(err) + + +@timer +def test_galsim_warning(): + """Test basic usage of GalSimWarning + """ + err = galsim.GalSimWarning("Test") + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test" + assert isinstance(err, UserWarning) + do_pickle(err) + + +@timer +def test_galsim_deprecation_warning(): + """Test basic usage of GalSimDeprecationWarning + """ + err = galsim.GalSimDeprecationWarning("Test") + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err) == "Test" + assert isinstance(err, UserWarning) + do_pickle(err) + + +if __name__ == "__main__": + test_galsim_error() + test_galsim_value_error() + test_galsim_key_error() + test_galsim_index_error() + test_galsim_range_error() + test_galsim_bounds_error() + test_galsim_undefined_bounds_error() + test_galsim_immutable_error() + test_galsim_incompatible_values_error() + test_galsim_sed_error() + test_galsim_hsm_error() + test_galsim_fft_size_error() + test_galsim_config_error() + test_galsim_config_value_error() + test_galsim_notimplemented_error() + test_galsim_warning() + test_galsim_deprecation_warning() diff --git a/tests/test_exponential.py b/tests/test_exponential.py index 118fb151605..9032d47cd42 100644 --- a/tests/test_exponential.py +++ b/tests/test_exponential.py @@ -28,21 +28,6 @@ imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference # images. -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_exponential(): """Test the generation of a specific exp profile against a known result. @@ -93,6 +78,8 @@ def test_exponential(): # Should raise an exception if both scale_radius and half_light_radius are provided. assert_raises(TypeError, galsim.Exponential, scale_radius=3, half_light_radius=1) + # Or neither. + assert_raises(TypeError, galsim.Exponential) @timer diff --git a/tests/test_fitsheader.py b/tests/test_fitsheader.py index cb95ff2a3ea..5798863a4f4 100644 --- a/tests/test_fitsheader.py +++ b/tests/test_fitsheader.py @@ -26,16 +26,13 @@ # Get whatever version of pyfits or astropy we are using -from galsim._pyfits import pyfits, pyfits_version +from galsim._pyfits import pyfits @timer def test_read(): """Test reading a FitsHeader from an existing FITS file """ - # Older pyfits versions treat the blank rows differently, so it comes out as 213. - # I don't know exactly when it switched, but for < 3.1, I'll just update this to - # whatever the initial value is. tpv_len = 215 def check_tpv(header): @@ -58,8 +55,6 @@ def check_tpv(header): dir = 'fits_files' # First option: give a file_name header = galsim.FitsHeader(file_name=os.path.join(dir,file_name)) - if pyfits_version < '3.1': - tpv_len = len(header) check_tpv(header) do_pickle(header) # Let the FitsHeader init handle the dir @@ -100,6 +95,11 @@ def check_tpv(header): check_tpv(header) do_pickle(header) + assert_raises(TypeError, galsim.FitsHeader, file_name=file_name, header=header) + with pyfits.open(os.path.join(dir,file_name)) as hdu_list: + assert_raises(TypeError, galsim.FitsHeader, file_name=file_name, hdu_list=hdu_list) + assert_raises(TypeError, galsim.FitsHeader, header=header, hdu_list=hdu_list) + # Remove an item from the header # Start with file_name constructor, to test that the repr is changed by the edit. orig_header = header @@ -107,8 +107,7 @@ def check_tpv(header): assert header == orig_header del header['AIRMASS'] assert 'AIRMASS' not in header - if pyfits_version >= '3.1': - assert len(header) == tpv_len-1 + assert len(header) == tpv_len-1 assert header != orig_header do_pickle(header) @@ -116,8 +115,7 @@ def check_tpv(header): assert header.get('AIRMASS', 2.0) == 2.0 # key should still not be in the header assert 'AIRMASS' not in header - if pyfits_version >= '3.1': - assert len(header) == tpv_len-1 + assert len(header) == tpv_len-1 assert header != orig_header # Add items to a header diff --git a/tests/test_fouriersqrt.py b/tests/test_fouriersqrt.py index 24caec43420..fac7cd6b5cb 100644 --- a/tests/test_fouriersqrt.py +++ b/tests/test_fouriersqrt.py @@ -28,21 +28,6 @@ # images. -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_fourier_sqrt(): """Test that the FourierSqrt operator is the inverse of auto-convolution. @@ -113,5 +98,9 @@ def test_fourier_sqrt(): assert_raises(TypeError, galsim.FourierSqrtProfile, psf, psf) assert_raises(TypeError, galsim.FourierSqrtProfile, psf, real_space=False) + assert_raises(NotImplementedError, sqrt1.xValue, galsim.PositionD(0,0)) + assert_raises(NotImplementedError, sqrt1.drawReal, myImg1) + assert_raises(NotImplementedError, sqrt1.shoot, 1) + if __name__ == "__main__": test_fourier_sqrt() diff --git a/tests/test_gaussian.py b/tests/test_gaussian.py index d9020353b47..e51b2a6c7b1 100644 --- a/tests/test_gaussian.py +++ b/tests/test_gaussian.py @@ -29,22 +29,6 @@ # images. -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - - @timer def test_gaussian(): """Test the generation of a specific Gaussian profile against a known result. @@ -121,12 +105,17 @@ def test_gaussian(): assert_raises(TypeError, galsim.Gaussian, half_light_radius=1, fwhm=2) assert_raises(TypeError, galsim.Gaussian, sigma=3, fwhm=2) assert_raises(TypeError, galsim.Gaussian, sigma=3, half_light_radius=1) + # Or none. + assert_raises(TypeError, galsim.Gaussian) # Finally, test the noise property for things that don't have any noise set. assert gauss.noise is None # And accessing the attribute from the class should indicate that it is a lazyproperty assert 'lazy_property' in str(galsim.GSObject._noise) + # And check that trying to use GSObject directly is an error. + assert_raises(NotImplementedError, galsim.GSObject) + @timer def test_gaussian_properties(): @@ -152,6 +141,22 @@ def test_gaussian_properties(): outFlux = gauss.flux np.testing.assert_almost_equal(outFlux, inFlux) + # Check some valid and invalid ways to pass arguments to xValue + # Same code applies to kValue and others, so just do this one. + assert gauss.xValue(cen.x, cen.y) == gauss.xValue(cen) + assert gauss.xValue(x=cen.x, y=cen.y) == gauss.xValue(cen) + assert gauss.xValue( (cen.x, cen.y) ) == gauss.xValue(cen) + assert_raises(TypeError, gauss.xValue, cen.x) + assert_raises(TypeError, gauss.xValue, x=cen.x) + assert_raises(TypeError, gauss.xValue, cen.x, y=cen.y) + assert_raises(TypeError, gauss.xValue, dx=cen.x, dy=cen.y) + assert_raises(TypeError, gauss.xValue, dx=cen.x, y=cen.y) + assert_raises(TypeError, gauss.xValue, x=cen.x, dy=cen.y) + assert_raises(TypeError, gauss.xValue, cen.x, cen.y, cen.y) + assert_raises(TypeError, gauss.xValue, cen.x, cen.y, invalid=True) + assert_raises(TypeError, gauss.xValue, pos=cen) + + @timer def test_gaussian_radii(): diff --git a/tests/test_hsm.py b/tests/test_hsm.py index 3e4c4212ee9..2a067c99a26 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -314,11 +314,11 @@ def test_masks(): assert_raises(ValueError, galsim.hsm.EstimateShear, im, p_im, weight_im) ## excludes all pixels weight_im = galsim.ImageI(imsize, imsize) - assert_raises(RuntimeError, galsim.hsm.FindAdaptiveMom, im, weight_im) - assert_raises(RuntimeError, galsim.hsm.EstimateShear, im, p_im, weight_im) + assert_raises(galsim.GalSimError, galsim.hsm.FindAdaptiveMom, im, weight_im) + assert_raises(galsim.GalSimError, galsim.hsm.EstimateShear, im, p_im, weight_im) badpix_im = galsim.ImageI(imsize, imsize, init_value = -1) - assert_raises(RuntimeError, galsim.hsm.FindAdaptiveMom, im, good_weight_im, badpix_im) - assert_raises(RuntimeError, galsim.hsm.EstimateShear, im, p_im, good_weight_im, badpix_im) + assert_raises(galsim.GalSimError, galsim.hsm.FindAdaptiveMom, im, good_weight_im, badpix_im) + assert_raises(galsim.GalSimError, galsim.hsm.EstimateShear, im, p_im, good_weight_im, badpix_im) # check moments, shear without mask resm = im.FindAdaptiveMom() @@ -565,11 +565,14 @@ def test_hsmparams(): # Then check failure modes: force it to fail by changing HSMParams. new_params_niter = galsim.hsm.HSMParams(max_mom2_iter = res.moments_n_iter-1) new_params_size = galsim.hsm.HSMParams(max_amoment = 0.3*res.moments_sigma**2) - assert_raises(RuntimeError, galsim.hsm.FindAdaptiveMom, tot_gal_image, + assert_raises(galsim.GalSimError, galsim.hsm.FindAdaptiveMom, tot_gal_image, hsmparams=new_params_niter) - assert_raises(RuntimeError, galsim.hsm.EstimateShear, tot_gal_image, tot_psf_image, + assert_raises(galsim.GalSimError, galsim.hsm.EstimateShear, tot_gal_image, tot_psf_image, hsmparams=new_params_size) + assert_raises(TypeError, galsim.hsm.EstimateShear, tot_gal_image, tot_psf_image, + hsmparams='hsmparams') + @timer def test_hsmparams_nodefault(): @@ -628,10 +631,10 @@ def test_hsmparams_nodefault(): assert(res.moments_amp > res2.moments_amp),'Amplitudes do not change as expected' # Check that max_amoment, max_ashift work as expected - assert_raises(RuntimeError, + assert_raises(galsim.GalSimError, galsim.hsm.EstimateShear, tot_gal_image, tot_psf_image, hsmparams=galsim.hsm.HSMParams(max_amoment = 10.)) - assert_raises(RuntimeError, + assert_raises(galsim.GalSimError, galsim.hsm.EstimateShear, tot_gal_image, tot_psf_image, guess_centroid=galsim.PositionD(47., tot_gal_image.true_center.y), hsmparams=galsim.hsm.HSMParams(max_ashift=0.1)) @@ -668,11 +671,11 @@ def test_strict(): # Check that measuring moments with strict = True results in the expected exception, and that # it is the same one as is stored when running with strict = False. - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimError): galsim.hsm.FindAdaptiveMom(im) try: res2 = im.FindAdaptiveMom() - except RuntimeError as err: + except galsim.GalSimError as err: if str(err) != res.error_message: raise AssertionError("Error messages do not match when running identical tests!") @@ -680,11 +683,11 @@ def test_strict(): res = galsim.hsm.EstimateShear(im, im, strict = False) if res.error_message == '': raise AssertionError("Should have error message stored in case of EstimateShear failure!") - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimError): galsim.hsm.EstimateShear(im, im) try: res2 = galsim.hsm.EstimateShear(im, im) - except RuntimeError as err: + except galsim.GalSimError as err: if str(err) != res.error_message: raise AssertionError("Error messages do not match when running identical tests!") @@ -734,7 +737,7 @@ def test_bounds_centroid(): # Check that we can take a weird/asymmetric sub-image, and it fails because of centroid shift. sub_im = im[galsim.BoundsI(b2.xmin, b2.xmax-100, b2.ymin+27, b2.ymax)] - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimError): galsim.hsm.FindAdaptiveMom(sub_im) # ... and that it passes if we hand in a good centroid guess. Note that this test is a bit less diff --git a/tests/test_image.py b/tests/test_image.py index a830def9168..0084fa32e0f 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -108,6 +108,7 @@ def test_Image_basic(): assert im1.array.dtype.type == np_array_type assert im1.array.flags.writeable == True assert im1.array.flags.c_contiguous == True + assert im1.dtype == np_array_type im1.fill(23) np.testing.assert_array_equal(im1.array, 23.) @@ -154,6 +155,7 @@ def test_Image_basic(): assert im2_view.ymax == nrow assert im2_view.bounds == bounds assert im2_view.array.dtype.type == np_array_type + assert im2_view.dtype == np_array_type assert im2_cview.xmin == 1 assert im2_cview.xmax == ncol @@ -161,6 +163,7 @@ def test_Image_basic(): assert im2_cview.ymax == nrow assert im2_cview.bounds == bounds assert im2_cview.array.dtype.type == np_array_type + assert im2_cview.dtype == np_array_type assert im1.real.bounds == bounds assert im1.imag.bounds == bounds @@ -171,14 +174,14 @@ def test_Image_basic(): assert im2_cview.real.bounds == bounds assert im2_cview.imag.bounds == bounds if tchar[i] == 'CF': - assert im1.real.array.dtype.type == np.float32 - assert im1.imag.array.dtype.type == np.float32 + assert im1.real.dtype == np.float32 + assert im1.imag.dtype == np.float32 elif tchar[i] == 'CD': - assert im1.real.array.dtype.type == np.float64 - assert im1.imag.array.dtype.type == np.float64 + assert im1.real.dtype == np.float64 + assert im1.imag.dtype == np.float64 else: - assert im1.real.array.dtype.type == np_array_type - assert im1.imag.array.dtype.type == np_array_type + assert im1.real.dtype == np_array_type + assert im1.imag.dtype == np_array_type # Check various ways to set and get values for y in range(1,nrow+1): @@ -246,36 +249,40 @@ def test_Image_basic(): assert im2_cview[x,y] == value3 # Setting or getting the value outside the bounds should throw an exception. - assert_raises(RuntimeError,im1.setValue,0,0,1) - assert_raises(RuntimeError,im1.__call__,0,0) - assert_raises(RuntimeError,im1.__getitem__,0,0) - assert_raises(RuntimeError,im1.__setitem__,0,0,1) - assert_raises(RuntimeError,im1.view().setValue,0,0,1) - assert_raises(RuntimeError,im1.view().__call__,0,0) - assert_raises(RuntimeError,im1.view().__getitem__,0,0) - assert_raises(RuntimeError,im1.view().__setitem__,0,0,1) - - assert_raises(RuntimeError,im1.setValue,ncol+1,0,1) - assert_raises(RuntimeError,im1.__call__,ncol+1,0) - assert_raises(RuntimeError,im1.view().setValue,ncol+1,0,1) - assert_raises(RuntimeError,im1.view().__call__,ncol+1,0) - - assert_raises(RuntimeError,im1.setValue,0,nrow+1,1) - assert_raises(RuntimeError,im1.__call__,0,nrow+1) - assert_raises(RuntimeError,im1.view().setValue,0,nrow+1,1) - assert_raises(RuntimeError,im1.view().__call__,0,nrow+1) - - assert_raises(RuntimeError,im1.setValue,ncol+1,nrow+1,1) - assert_raises(RuntimeError,im1.__call__,ncol+1,nrow+1) - assert_raises(RuntimeError,im1.view().setValue,ncol+1,nrow+1,1) - assert_raises(RuntimeError,im1.view().__call__,ncol+1,nrow+1) + assert_raises(galsim.GalSimBoundsError,im1.setValue,0,0,1) + assert_raises(galsim.GalSimBoundsError,im1.addValue,0,0,1) + assert_raises(galsim.GalSimBoundsError,im1.__call__,0,0) + assert_raises(galsim.GalSimBoundsError,im1.__getitem__,0,0) + assert_raises(galsim.GalSimBoundsError,im1.__setitem__,0,0,1) + assert_raises(galsim.GalSimBoundsError,im1.view().setValue,0,0,1) + assert_raises(galsim.GalSimBoundsError,im1.view().__call__,0,0) + assert_raises(galsim.GalSimBoundsError,im1.view().__getitem__,0,0) + assert_raises(galsim.GalSimBoundsError,im1.view().__setitem__,0,0,1) + + assert_raises(galsim.GalSimBoundsError,im1.setValue,ncol+1,0,1) + assert_raises(galsim.GalSimBoundsError,im1.addValue,ncol+1,0,1) + assert_raises(galsim.GalSimBoundsError,im1.__call__,ncol+1,0) + assert_raises(galsim.GalSimBoundsError,im1.view().setValue,ncol+1,0,1) + assert_raises(galsim.GalSimBoundsError,im1.view().__call__,ncol+1,0) + + assert_raises(galsim.GalSimBoundsError,im1.setValue,0,nrow+1,1) + assert_raises(galsim.GalSimBoundsError,im1.addValue,0,nrow+1,1) + assert_raises(galsim.GalSimBoundsError,im1.__call__,0,nrow+1) + assert_raises(galsim.GalSimBoundsError,im1.view().setValue,0,nrow+1,1) + assert_raises(galsim.GalSimBoundsError,im1.view().__call__,0,nrow+1) + + assert_raises(galsim.GalSimBoundsError,im1.setValue,ncol+1,nrow+1,1) + assert_raises(galsim.GalSimBoundsError,im1.addValue,ncol+1,nrow+1,1) + assert_raises(galsim.GalSimBoundsError,im1.__call__,ncol+1,nrow+1) + assert_raises(galsim.GalSimBoundsError,im1.view().setValue,ncol+1,nrow+1,1) + assert_raises(galsim.GalSimBoundsError,im1.view().__call__,ncol+1,nrow+1) # Also, setting values in something that should be const - assert_raises(ValueError,im1.view(make_const=True).setValue,1,1,1) - assert_raises(ValueError,im1.view(make_const=True).real.setValue,1,1,1) - assert_raises(ValueError,im1.view(make_const=True).imag.setValue,1,1,1) + assert_raises(galsim.GalSimImmutableError,im1.view(make_const=True).setValue,1,1,1) + assert_raises(galsim.GalSimImmutableError,im1.view(make_const=True).real.setValue,1,1,1) + assert_raises(galsim.GalSimImmutableError,im1.view(make_const=True).imag.setValue,1,1,1) if tchar[i][0] != 'C': - assert_raises(ValueError,im1.imag.setValue,1,1,1) + assert_raises(galsim.GalSimImmutableError,im1.imag.setValue,1,1,1) # Finally check for the wrong number of arguments in get/setitem assert_raises(TypeError,im1.__getitem__,1) @@ -304,8 +311,8 @@ def test_Image_basic(): dx = 31 dy = 16 im1.shift(dx,dy) - im2_view.setOrigin( 1+dx , 1+dy ) - im3_view.setCenter( (ncol+1)/2+dx , (nrow+1)/2+dy ) + im2_view.setOrigin(1+dx , 1+dy) + im3_view.setCenter((ncol+1)/2+dx , (nrow+1)/2+dy) shifted_bounds = galsim.BoundsI(1+dx, ncol+dx, 1+dy, nrow+dy) assert im1.bounds == shifted_bounds @@ -323,6 +330,13 @@ def test_Image_basic(): assert im2_view(x+dx,y+dy) == value3 assert im3_view(x+dx,y+dy) == value3 + assert_raises(TypeError, im1.shift, dx) + assert_raises(TypeError, im1.shift, dx=dx) + assert_raises(TypeError, im1.shift, x=dx, y=dy) + assert_raises(TypeError, im1.shift, dx, dy=dy) + assert_raises(TypeError, im1.shift, dx, dy, dy) + assert_raises(TypeError, im1.shift, dx, dy, invalid=True) + # Check picklability do_pickle(im1) do_pickle(im1_view) @@ -402,10 +416,17 @@ def test_undefined_image(): assert im11.array.shape == (1,1) assert im11 == im1 - assert_raises(RuntimeError,im1.setValue,0,0,1) - assert_raises(RuntimeError,im1.__call__,0,0) - assert_raises(RuntimeError,im1.view().setValue,0,0,1) - assert_raises(RuntimeError,im1.view().__call__,0,0) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.setValue, 0, 0, 1) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.__call__, 0, 0) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.view().setValue, 0, 0, 1) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.view().__call__, 0, 0) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.view().addValue, 0, 0, 1) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.fill, 3) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.view().fill, 3) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.invertSelf) + im1.scale = 1. + assert_raises(galsim.GalSimUndefinedBoundsError,im1.calculate_fft) + assert_raises(galsim.GalSimUndefinedBoundsError,im1.calculate_inverse_fft) do_pickle(im1.bounds) do_pickle(im1) @@ -479,6 +500,19 @@ def test_Image_FITS_IO(): np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, err_msg="Image"+tchar[i]+" read failed reading from filename input.") + assert_raises(ValueError, galsim.fits.read, test_file, compression='invalid') + assert_raises(ValueError, ref_image.write, test_file, compression='invalid') + assert_raises(OSError, galsim.fits.read, test_file, compression='rice') + assert_raises(OSError, galsim.fits.read, 'invalid.fits') + + assert_raises(TypeError, galsim.fits.read) + assert_raises(TypeError, galsim.fits.read, test_file, hdu_list=hdu) + assert_raises(TypeError, ref_image.write) + assert_raises(TypeError, ref_image.write, file_name=test_file, hdu_list=hdu) + + # If clobbert = False, then trying to overwrite will raise an OSError + assert_raises(OSError, ref_image.write, test_file, clobber=False) + # # Test various compression schemes # @@ -489,6 +523,8 @@ def test_Image_FITS_IO(): if i > 0 and __name__ != "__main__": continue + test_file0 = test_file # Save the name of the uncompressed file. + # Test full-file gzip test_file = os.path.join(datadir, "test"+tchar[i]+".fits.gz") test_image = galsim.fits.read(test_file, compression='gzip') @@ -510,6 +546,13 @@ def test_Image_FITS_IO(): np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, err_msg="Image"+tchar[i]+" write failed for auto full-file gzip") + # With compression = None or 'none', astropy automatically figures it out anyway. + test_image = galsim.fits.read(test_file, compression=None) + np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, + err_msg="Image"+tchar[i]+" write failed for auto full-file gzip") + + assert_raises(OSError, galsim.fits.read, test_file0, compression='gzip') + # Test full-file bzip2 test_file = os.path.join(datadir, "test"+tchar[i]+".fits.bz2") test_image = galsim.fits.read(test_file, compression='bzip2') @@ -531,7 +574,14 @@ def test_Image_FITS_IO(): np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, err_msg="Image"+tchar[i]+" write failed for auto full-file bzip2") - # Test ric + # With compression = None or 'none', astropy automatically figures it out anyway. + test_image = galsim.fits.read(test_file, compression=None) + np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, + err_msg="Image"+tchar[i]+" write failed for auto full-file gzip") + + assert_raises(OSError, galsim.fits.read, test_file0, compression='bzip2') + + # Test rice test_file = os.path.join(datadir, "test"+tchar[i]+".fits.fz") test_image = galsim.fits.read(test_file, compression='rice') np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, @@ -552,6 +602,9 @@ def test_Image_FITS_IO(): np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, err_msg="Image"+tchar[i]+" write failed for auto rice") + assert_raises(OSError, galsim.fits.read, test_file0, compression='rice') + assert_raises(OSError, galsim.fits.read, test_file, compression='none') + # Test gzip_tile test_file = os.path.join(datadir, "test"+tchar[i]+"_internal.fits.gzt") ref_image.write(test_file, compression='gzip_tile') @@ -559,6 +612,9 @@ def test_Image_FITS_IO(): np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, err_msg="Image"+tchar[i]+" write failed for gzip_tile") + assert_raises(OSError, galsim.fits.read, test_file0, compression='gzip_tile') + assert_raises(OSError, galsim.fits.read, test_file, compression='none') + # Test hcompress test_file = os.path.join(datadir, "test"+tchar[i]+"_internal.fits.hc") ref_image.write(test_file, compression='hcompress') @@ -566,6 +622,9 @@ def test_Image_FITS_IO(): np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, err_msg="Image"+tchar[i]+" write failed for hcompress") + assert_raises(OSError, galsim.fits.read, test_file0, compression='hcompress') + assert_raises(OSError, galsim.fits.read, test_file, compression='none') + # Test plio (only valid on positive integer values) if tchar[i] in ['S', 'I']: test_file = os.path.join(datadir, "test"+tchar[i]+"_internal.fits.plio") @@ -574,6 +633,9 @@ def test_Image_FITS_IO(): np.testing.assert_array_equal(ref_array.astype(types[i]), test_image.array, err_msg="Image"+tchar[i]+" write failed for plio") + assert_raises(OSError, galsim.fits.read, test_file0, compression='plio') + assert_raises(OSError, galsim.fits.read, test_file, compression='none') + @timer def test_Image_MultiFITS_IO(): @@ -690,6 +752,45 @@ def test_Image_MultiFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" readMulti failed after using writeFile") + assert_raises(ValueError, galsim.fits.readMulti, test_multi_file, compression='invalid') + assert_raises(ValueError, galsim.fits.writeMulti, image_list, test_multi_file, + compression='invalid') + assert_raises(ValueError, galsim.fits.writeFile, image_list, test_multi_file, + compression='invalid') + assert_raises(OSError, galsim.fits.readMulti, test_multi_file, compression='rice') + assert_raises(OSError, galsim.fits.readFile, test_multi_file, compression='rice') + assert_raises(OSError, galsim.fits.readMulti, hdu_list=pyfits.HDUList()) + assert_raises(OSError, galsim.fits.readMulti, hdu_list=pyfits.HDUList(), compression='rice') + assert_raises(OSError, galsim.fits.readMulti, 'invalid.fits') + assert_raises(OSError, galsim.fits.readFile, 'invalid.fits') + + assert_raises(TypeError, galsim.fits.readMulti) + assert_raises(TypeError, galsim.fits.readMulti, test_multi_file, hdu_list=hdu) + assert_raises(TypeError, galsim.fits.readMulti, hdu_list=test_multi_file) + assert_raises(TypeError, galsim.fits.writeMulti) + assert_raises(TypeError, galsim.fits.writeMulti, image_list) + assert_raises(TypeError, galsim.fits.writeMulti, image_list, + file_name=test_multi_file, hdu_list=hdu) + + assert_raises(OSError, galsim.fits.writeMulti, image_list, test_multi_file, clobber=False) + + assert_raises(TypeError, galsim.fits.writeFile) + assert_raises(TypeError, galsim.fits.writeFile, image_list) + assert_raises(ValueError, galsim.fits.writeFile, test_multi_file, image_list, + compression='invalid') + assert_raises(ValueError, galsim.fits.writeFile, test_multi_file, image_list, + compression='rice') + assert_raises(ValueError, galsim.fits.writeFile, test_multi_file, image_list, + compression='gzip_tile') + assert_raises(ValueError, galsim.fits.writeFile, test_multi_file, image_list, + compression='hcompress') + assert_raises(ValueError, galsim.fits.writeFile, test_multi_file, image_list, + compression='plio') + + galsim.fits.writeFile(test_multi_file, hdu_list) + assert_raises(OSError, galsim.fits.writeFile, test_multi_file, image_list, clobber=False) + + # # Test various compression schemes # @@ -700,6 +801,8 @@ def test_Image_MultiFITS_IO(): if i > 0 and __name__ != "__main__": continue + test_multi_file0 = test_multi_file + # Test full-file gzip test_multi_file = os.path.join(datadir, "test_multi"+tchar[i]+".fits.gz") test_image_list = galsim.fits.readMulti(test_multi_file, compression='gzip') @@ -729,6 +832,15 @@ def test_Image_MultiFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeMulti failed for auto full-file gzip") + # With compression = None or 'none', astropy automatically figures it out anyway. + test_image_list = galsim.fits.readMulti(test_multi_file, compression=None) + for k in range(nimages): + np.testing.assert_array_equal((ref_array+k).astype(types[i]), + test_image_list[k].array, + err_msg="Image"+tchar[i]+" writeMulti failed for auto full-file gzip") + + assert_raises(OSError, galsim.fits.readMulti, test_multi_file0, compression='gzip') + # Test full-file bzip2 test_multi_file = os.path.join(datadir, "test_multi"+tchar[i]+".fits.bz2") test_image_list = galsim.fits.readMulti(test_multi_file, compression='bzip2') @@ -758,6 +870,15 @@ def test_Image_MultiFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeMulti failed for auto full-file bzip2") + # With compression = None or 'none', astropy automatically figures it out anyway. + test_image_list = galsim.fits.readMulti(test_multi_file, compression=None) + for k in range(nimages): + np.testing.assert_array_equal((ref_array+k).astype(types[i]), + test_image_list[k].array, + err_msg="Image"+tchar[i]+" writeMulti failed for auto full-file gzip") + + assert_raises(OSError, galsim.fits.readMulti, test_multi_file0, compression='bzip2') + # Test rice test_multi_file = os.path.join(datadir, "test_multi"+tchar[i]+".fits.fz") test_image_list = galsim.fits.readMulti(test_multi_file, compression='rice') @@ -787,6 +908,9 @@ def test_Image_MultiFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeMulti failed for auto rice") + assert_raises(OSError, galsim.fits.readMulti, test_multi_file0, compression='rice') + assert_raises(OSError, galsim.fits.readMulti, test_multi_file, compression='none') + # Test gzip_tile test_multi_file = os.path.join(datadir, "test_multi"+tchar[i]+"_internal.fits.gzt") galsim.fits.writeMulti(image_list,test_multi_file, compression='gzip_tile') @@ -796,6 +920,9 @@ def test_Image_MultiFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeMulti failed for gzip_tile") + assert_raises(OSError, galsim.fits.readMulti, test_multi_file0, compression='gzip_tile') + assert_raises(OSError, galsim.fits.readMulti, test_multi_file, compression='none') + # Test hcompress test_multi_file = os.path.join(datadir, "test_multi"+tchar[i]+"_internal.fits.hc") galsim.fits.writeMulti(image_list,test_multi_file, compression='hcompress') @@ -805,6 +932,9 @@ def test_Image_MultiFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeMulti failed for hcompress") + assert_raises(OSError, galsim.fits.readMulti, test_multi_file0, compression='hcompress') + assert_raises(OSError, galsim.fits.readMulti, test_multi_file, compression='none') + # Test plio (only valid on positive integer values) if tchar[i] in ['S', 'I']: test_multi_file = os.path.join(datadir, "test_multi"+tchar[i]+"_internal.fits.plio") @@ -815,6 +945,9 @@ def test_Image_MultiFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeMulti failed for plio") + assert_raises(OSError, galsim.fits.readMulti, test_multi_file0, compression='plio') + assert_raises(OSError, galsim.fits.readMulti, test_multi_file, compression='none') + @timer def test_Image_CubeFITS_IO(): @@ -957,6 +1090,29 @@ def test_Image_CubeFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" readCube failed after using writeFile") + assert_raises(ValueError, galsim.fits.readCube, test_cube_file, compression='invalid') + assert_raises(ValueError, galsim.fits.writeCube, image_list, test_cube_file, + compression='invalid') + assert_raises(ValueError, galsim.fits.writeFile, image_list, test_cube_file, + compression='invalid') + assert_raises(OSError, galsim.fits.readCube, test_cube_file, compression='rice') + assert_raises(OSError, galsim.fits.readCube, 'invalid.fits') + + assert_raises(TypeError, galsim.fits.readCube) + assert_raises(TypeError, galsim.fits.readCube, test_cube_file, hdu_list=hdu) + assert_raises(TypeError, galsim.fits.readCube, hdu_list=test_cube_file) + assert_raises(TypeError, galsim.fits.writeCube) + assert_raises(TypeError, galsim.fits.writeCube, image_list) + assert_raises(TypeError, galsim.fits.writeCube, image_list, + file_name=test_cube_file, hdu_list=hdu_list) + + assert_raises(OSError, galsim.fits.writeCube, image_list, test_cube_file, clobber=False) + + assert_raises(ValueError, galsim.fits.writeCube, image_list[:0], test_cube_file) + assert_raises(ValueError, galsim.fits.writeCube, + [image_list[0], image_list[1].subImage(galsim.BoundsI(0,4,0,4))], + test_cube_file) + # # Test various compression schemes # @@ -967,6 +1123,8 @@ def test_Image_CubeFITS_IO(): if i > 0 and __name__ != "__main__": continue + test_cube_file0 = test_cube_file + # Test full-file gzip test_cube_file = os.path.join(datadir, "test_cube"+tchar[i]+".fits.gz") test_image_list = galsim.fits.readCube(test_cube_file, compression='gzip') @@ -996,6 +1154,15 @@ def test_Image_CubeFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeCube failed for auto full-file gzip") + # With compression = None or 'none', astropy automatically figures it out anyway. + test_image_list = galsim.fits.readCube(test_cube_file, compression=None) + for k in range(nimages): + np.testing.assert_array_equal((ref_array+k).astype(types[i]), + test_image_list[k].array, + err_msg="Image"+tchar[i]+" writeCube failed for auto full-file gzip") + + assert_raises(OSError, galsim.fits.readCube, test_cube_file0, compression='gzip') + # Test full-file bzip2 test_cube_file = os.path.join(datadir, "test_cube"+tchar[i]+".fits.bz2") test_image_list = galsim.fits.readCube(test_cube_file, compression='bzip2') @@ -1025,6 +1192,15 @@ def test_Image_CubeFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeCube failed for auto full-file bzip2") + # With compression = None or 'none', astropy automatically figures it out anyway. + test_image_list = galsim.fits.readCube(test_cube_file, compression=None) + for k in range(nimages): + np.testing.assert_array_equal((ref_array+k).astype(types[i]), + test_image_list[k].array, + err_msg="Image"+tchar[i]+" writeCube failed for auto full-file gzip") + + assert_raises(OSError, galsim.fits.readCube, test_cube_file0, compression='bzip2') + # Test rice test_cube_file = os.path.join(datadir, "test_cube"+tchar[i]+".fits.fz") test_image_list = galsim.fits.readCube(test_cube_file, compression='rice') @@ -1054,6 +1230,10 @@ def test_Image_CubeFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeCube failed for auto rice") + assert_raises(OSError, galsim.fits.readCube, test_cube_file0, compression='rice') + assert_raises(OSError, galsim.fits.readCube, test_cube_file, compression='none') + assert_raises(OSError, galsim.fits.readCube, test_cube_file, hdu=1, compression='none') + # Test gzip_tile test_cube_file = os.path.join(datadir, "test_cube"+tchar[i]+"_internal.fits.gzt") galsim.fits.writeCube(image_list,test_cube_file, compression='gzip_tile') @@ -1063,7 +1243,20 @@ def test_Image_CubeFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeCube failed for gzip_tile") - # Note: hcompress is invalid for data cubes + assert_raises(OSError, galsim.fits.readCube, test_cube_file0, compression='gzip_tile') + assert_raises(OSError, galsim.fits.readCube, test_cube_file, compression='none') + + # Test hcompress + test_cube_file = os.path.join(datadir, "test_cube"+tchar[i]+"_internal.fits.hc") + galsim.fits.writeCube(image_list,test_cube_file, compression='hcompress') + test_image_list = galsim.fits.readCube(test_cube_file, compression='hcompress') + for k in range(nimages): + np.testing.assert_array_equal((ref_array+k).astype(types[i]), + test_image_list[k].array, + err_msg="Image"+tchar[i]+" writeCube failed for hcompress") + + assert_raises(OSError, galsim.fits.readCube, test_cube_file0, compression='hcompress') + assert_raises(OSError, galsim.fits.readCube, test_cube_file, compression='none') # Test plio (only valid on positive integer values) if tchar[i] in ['S', 'I']: @@ -1075,6 +1268,9 @@ def test_Image_CubeFITS_IO(): test_image_list[k].array, err_msg="Image"+tchar[i]+" writeCube failed for plio") + assert_raises(OSError, galsim.fits.readCube, test_cube_file0, compression='plio') + assert_raises(OSError, galsim.fits.readCube, test_cube_file, compression='none') + @timer def test_Image_array_view(): @@ -1131,12 +1327,8 @@ def test_Image_binary_add(): # shape. Note that this test is only included here (not in the unit tests for all # other operations) because all operations have the same error-checking code, so it should # only be necessary to check once. - image1 = galsim.Image(ref_array.astype(types[i])) - image2 = image1.subImage(galsim.BoundsI(image1.xmin, image1.xmax-1, - image1.ymin+1, image1.ymax)) with assert_raises(ValueError): - image1.__add__(image2) - + image1 + image1.subImage(galsim.BoundsI(0,4,0,4)) @timer def test_Image_binary_subtract(): @@ -1170,6 +1362,9 @@ def test_Image_binary_subtract(): err_msg="Inplace add in Image class does not match reference for dtypes = " +str(types[i])+" and "+str(types[j])) + with assert_raises(ValueError): + image1 - image1.subImage(galsim.BoundsI(0,4,0,4)) + @timer def test_Image_binary_multiply(): @@ -1210,6 +1405,9 @@ def test_Image_binary_multiply(): err_msg="Inplace add in Image class does not match reference for dtypes = " +str(types[i])+" and "+str(types[j])) + with assert_raises(ValueError): + image1 * image1.subImage(galsim.BoundsI(0,4,0,4)) + @timer def test_Image_binary_divide(): @@ -1253,6 +1451,9 @@ def test_Image_binary_divide(): err_msg="Inplace divide in Image class does not match reference for dtypes = " +str(types[i])+" and "+str(types[j])) + with assert_raises(ValueError): + image1 / image1.subImage(galsim.BoundsI(0,4,0,4)) + @timer def test_Image_binary_scalar_add(): @@ -1402,6 +1603,9 @@ def test_Image_binary_scalar_pow(): err_msg="Binary pow scalar in Image class (dictionary call) does" +" not match reference for dtype = "+str(types[i])) + with assert_raises(TypeError): + image1 ** image2 + @timer def test_Image_inplace_add(): @@ -1436,6 +1640,9 @@ def test_Image_inplace_add(): err_msg="Inplace add in Image class does not match reference for dtypes = " +str(types[i])+" and "+str(types[j])) + with assert_raises(ValueError): + image1 += image1.subImage(galsim.BoundsI(0,4,0,4)) + @timer def test_Image_inplace_subtract(): @@ -1470,6 +1677,9 @@ def test_Image_inplace_subtract(): err_msg="Inplace subtract in Image class does not match reference for dtypes = " +str(types[i])+" and "+str(types[j])) + with assert_raises(ValueError): + image1 -= image1.subImage(galsim.BoundsI(0,4,0,4)) + @timer def test_Image_inplace_multiply(): @@ -1504,6 +1714,9 @@ def test_Image_inplace_multiply(): err_msg="Inplace multiply in Image class does not match reference for dtypes = " +str(types[i])+" and "+str(types[j])) + with assert_raises(ValueError): + image1 *= image1.subImage(galsim.BoundsI(0,4,0,4)) + @timer def test_Image_inplace_divide(): @@ -1556,6 +1769,9 @@ def test_Image_inplace_divide(): err_msg="Inplace divide in Image class does not match reference for dtypes = " +str(types[i])+" and "+str(types[j])) + with assert_raises(ValueError): + image1 /= image1.subImage(galsim.BoundsI(0,4,0,4)) + @timer def test_Image_inplace_scalar_add(): @@ -1677,6 +1893,8 @@ def test_Image_inplace_scalar_pow(): err_msg="Inplace scalar pow in Image class (dictionary " +"call) does not match reference for dtype = "+str(types[i])) + with assert_raises(TypeError): + image1 **= image2 @timer def test_Image_subImage(): @@ -1770,6 +1988,10 @@ def test_Image_subImage(): do_pickle(image) + assert_raises(TypeError, image.subImage, bounds=None) + assert_raises(TypeError, image.subImage, bounds=galsim.BoundsD(0,4,0,4)) + + def make_subImage(file_name, bounds): """Helper function for test_subImage_persistence """ @@ -1880,17 +2102,17 @@ def test_Image_resize(): do_pickle(im2) do_pickle(im3) + assert_raises(TypeError, im1.resize, bounds=None) + assert_raises(TypeError, im1.resize, bounds=galsim.BoundsD(0,5,0,5)) + @timer def test_ConstImage_array_constness(): """Test that Image instances with make_const=True cannot be modified via their .array - attributes, and that if this is attempted a RuntimeError is raised. + attributes, and that if this is attempted a GalSimImmutableError is raised. """ for i in range(ntypes): image = galsim.Image(ref_array.astype(types[i]), make_const=True) - try: - image.array[1, 2] = 666 - assert False, "Setting values in a const image.array should have raised an error." # Apparently older numpy versions might raise a RuntimeError, a ValueError, or a TypeError # when trying to write to arrays that have writeable=False. # From the numpy 1.7.0 release notes: @@ -1898,45 +2120,28 @@ def test_ConstImage_array_constness(): # ``arr.flags.writeable`` set to ``False``) used to raise either a # RuntimeError, ValueError, or TypeError inconsistently, depending on # which code path was taken. It now consistently raises a ValueError. - except (RuntimeError, ValueError, TypeError): - pass - except: - assert False, "Unexpected error: "+str(sys.exc_info()[0]) + with assert_raises((RuntimeError, ValueError, TypeError)): + image.array[1, 2] = 666 - # Native image operations that are invalid just raise ValueError - try: + # Native image operations that are invalid just raise GalSimImmutableError + with assert_raises(galsim.GalSimImmutableError): image[1, 2] = 666 - assert False, "Setting values in a const image should have raised an error." - except ValueError: - pass - except: - assert False, "Unexpected error: "+str(sys.exc_info()[0]) - try: + with assert_raises(galsim.GalSimImmutableError): image.setValue(1,2,666) - assert False, "Calling setValue on a const image should have raised an error." - except ValueError: - pass - except: - assert False, "Unexpected error: "+str(sys.exc_info()[0]) - try: + with assert_raises(galsim.GalSimImmutableError): image[image.bounds] = image - assert False, "Setting subImage of a const image should have raised an error." - except ValueError: - pass - except: - assert False, "Unexpected error: "+str(sys.exc_info()[0]) # The rest are functions, so just use assert_raises. - assert_raises(ValueError, image.setValue, 1, 2, 666) - assert_raises(ValueError, image.setSubImage, image.bounds, image) - assert_raises(ValueError, image.addValue, 1, 2, 666) - assert_raises(ValueError, image.copyFrom, image) - assert_raises(ValueError, image.resize, image.bounds) - assert_raises(ValueError, image.fill, 666) - assert_raises(ValueError, image.setZero) - assert_raises(ValueError, image.invertSelf) + assert_raises(galsim.GalSimImmutableError, image.setValue, 1, 2, 666) + assert_raises(galsim.GalSimImmutableError, image.setSubImage, image.bounds, image) + assert_raises(galsim.GalSimImmutableError, image.addValue, 1, 2, 666) + assert_raises(galsim.GalSimImmutableError, image.copyFrom, image) + assert_raises(galsim.GalSimImmutableError, image.resize, image.bounds) + assert_raises(galsim.GalSimImmutableError, image.fill, 666) + assert_raises(galsim.GalSimImmutableError, image.setZero) + assert_raises(galsim.GalSimImmutableError, image.invertSelf) do_pickle(image) @@ -1968,9 +2173,9 @@ def test_BoundsI_init_with_non_pure_ints(): assert ref_bounds == galsim.BoundsI(*bound_arr_flt), \ "Cannot initialize a BoundI with float array elements" - # Using non-integers should raise a ValueError - assert_raises(ValueError, galsim.BoundsI, *bound_arr_flt_nonint) - assert_raises(ValueError, galsim.BoundsI, + # Using non-integers should raise a TypeError + assert_raises(TypeError, galsim.BoundsI, *bound_arr_flt_nonint) + assert_raises(TypeError, galsim.BoundsI, xmin=bound_arr_flt_nonint[0], xmax=bound_arr_flt_nonint[1], ymin=bound_arr_flt_nonint[2], ymax=bound_arr_flt_nonint[3]) @@ -2108,6 +2313,8 @@ def test_Image_view(): imv2.setCenter(0,0) assert imv.bounds == imv2.bounds assert imv.wcs == imv2.wcs + with assert_raises(galsim.GalSimError): + imv.scale # scale is invalid if wcs is not a PixelScale do_pickle(imv) do_pickle(imv2) @@ -2120,6 +2327,8 @@ def test_Image_view(): assert imv(11,19) == 50 assert im(11,19) == 50 imv2 = im.view() + with assert_raises(galsim.GalSimError): + imv2.scale = 0.17 # Invalid if wcs is not PixelScale imv2.wcs = None imv2.scale = 0.17 assert imv.bounds == imv2.bounds @@ -2147,6 +2356,11 @@ def test_Image_view(): assert im.array.min() == 17 assert im.array.max() == 17 + assert_raises(TypeError, im.view, origin=(0,0), center=(0,0)) + assert_raises(TypeError, im.view, scale=0.3, wcs=galsim.JacobianWCS(1.1, 0.1, 0.1, 1.)) + assert_raises(TypeError, im.view, scale=galsim.PixelScale(0.3)) + assert_raises(TypeError, im.view, wcs=0.3) + @timer def test_Image_writeheader(): @@ -2170,6 +2384,9 @@ def test_Image_writeheader(): assert key_name.upper() in new_header.keys() assert new_header['CD1_1'] == 0.0 + # If clobbert = False, then trying to overwrite will raise an OSError + assert_raises(OSError, im_test.write, test_file, clobber=False) + @timer def test_ne(): @@ -2299,6 +2516,10 @@ def test_copy(): im8[3,8] = 15 assert im5(3,8) == 11. + assert_raises(TypeError, im5.copyFrom, im8.array) + im9 = galsim.Image(5,5,init_value=3) + assert_raises(ValueError, im5.copyFrom, im9) + @timer def test_complex_image(): @@ -2820,6 +3041,69 @@ def test_int_image_arith(): np.testing.assert_array_equal(test.array, 0, err_msg="//= failed for Images with dtype = %s."%types[i]) + with assert_raises(ValueError): + full & full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full | full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full ^ full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full // full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full % full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full &= full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full |= full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full ^= full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full //= full.subImage(galsim.BoundsI(0,4,0,4)) + with assert_raises(ValueError): + full %= full.subImage(galsim.BoundsI(0,4,0,4)) + + imd = galsim.ImageD(ref_array) + with assert_raises(ValueError): + imd & full + with assert_raises(ValueError): + imd | full + with assert_raises(ValueError): + imd ^ full + with assert_raises(ValueError): + imd // full + with assert_raises(ValueError): + imd % full + with assert_raises(ValueError): + imd &= full + with assert_raises(ValueError): + imd |= full + with assert_raises(ValueError): + imd ^= full + with assert_raises(ValueError): + imd //= full + with assert_raises(ValueError): + imd %= full + + with assert_raises(ValueError): + full & imd + with assert_raises(ValueError): + full | imd + with assert_raises(ValueError): + full ^ imd + with assert_raises(ValueError): + full // imd + with assert_raises(ValueError): + full % imd + with assert_raises(ValueError): + full &= imd + with assert_raises(ValueError): + full |= imd + with assert_raises(ValueError): + full ^= imd + with assert_raises(ValueError): + full //= imd + with assert_raises(ValueError): + full %= imd @timer @@ -2946,13 +3230,26 @@ def test_wrap(): np.testing.assert_equal(im3_wrap.bounds, b3, "image.wrap(%s) does not have the correct bounds") + b = galsim.BoundsI(-K+1,K,-L+1,L) + b2 = galsim.BoundsI(-K+1,K,0,L) + b3 = galsim.BoundsI(0,K,-L+1,L) + assert_raises(TypeError, im.wrap, bounds=None) + assert_raises(ValueError, im3.wrap, b, hermitian='x') + assert_raises(ValueError, im3.wrap, b2, hermitian='x') + assert_raises(ValueError, im.wrap, b3, hermitian='x') + assert_raises(ValueError, im2.wrap, b, hermitian='y') + assert_raises(ValueError, im2.wrap, b3, hermitian='y') + assert_raises(ValueError, im.wrap, b2, hermitian='y') + assert_raises(ValueError, im.wrap, b, hermitian='invalid') + assert_raises(ValueError, im2.wrap, b2, hermitian='invalid') + assert_raises(ValueError, im3.wrap, b3, hermitian='invalid') + + @timer def test_FITS_bad_type(): """Test that reading FITS files with an invalid data type succeeds by converting the type to float64. """ - import warnings - # We check this by monkey patching the Image.valid_types list to not include int16 # and see if it reads properly and raises the appropriate warning. orig_dtypes = galsim.Image.valid_dtypes @@ -2961,11 +3258,11 @@ def test_FITS_bad_type(): testS_file = os.path.join(datadir, "testS.fits") testMultiS_file = os.path.join(datadir, "test_multiS.fits") testCubeS_file = os.path.join(datadir, "test_cubeS.fits") - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): testS_image = galsim.fits.read(testS_file) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): testMultiS_image_list = galsim.fits.readMulti(testMultiS_file) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): testCubeS_image_list = galsim.fits.readCube(testCubeS_file) np.testing.assert_equal(np.float64, testS_image.array.dtype.type) diff --git a/tests/test_inclined.py b/tests/test_inclined.py index 18e9ffd0149..14ef7dd2e54 100644 --- a/tests/test_inclined.py +++ b/tests/test_inclined.py @@ -335,7 +335,7 @@ def test_edge_on(): for inclination in inclinations: # Set up the profile prof = get_prof(mode, inclination * galsim.radians, scale_radius=scale_radius, - scale_h_over_r=0.1, n=n, gsparams=galsim.GSParams(maximum_fft_size=5132)) + scale_h_over_r=0.1, n=n) check_basic(prof, "Edge-on " + mode) @@ -644,6 +644,12 @@ def test_exceptions(): get_prof("InclinedSersic", inclination = 0.*galsim.degrees, scale_radius = 1., trunc = -4.5) + # trunc can't be too small in InclinedSersic + with assert_raises(ValueError): + get_prof("InclinedSersic", inclination = 0.*galsim.degrees, + half_light_radius = 1., trunc = 1.4) + + @timer def test_value_retrieval(): """ Tests to make sure that if a parameter is passed to a profile, we get back the same diff --git a/tests/test_integ.py b/tests/test_integ.py index 2789a9c9b48..ebb735f4b13 100644 --- a/tests/test_integ.py +++ b/tests/test_integ.py @@ -186,7 +186,7 @@ def test_func(x): return x**-2 test_integral, true_result, decimal=test_decimal, verbose=True, err_msg="x^(-2) integral failed across interval [1, inf].") - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimError): galsim.integ.int1d(test_func, 0., 1., test_rel_err, test_abs_err) @@ -234,6 +234,10 @@ def test_trapz_basic(): result/expected_val, 1.0, decimal=6, verbose=True, err_msg='Test of trapzRule() with points failed for f(x)=x^2 from 0 to 1') + assert_raises(ValueError, galsim.integ.trapz, func, 0, 1, points=np.linspace(0, 1.1, 100)) + assert_raises(ValueError, galsim.integ.trapz, func, 0.1, 1, points=np.linspace(0, 1, 100)) + assert_raises(TypeError, galsim.integ.trapz, func, 0.1, 1, points=2.3) + if __name__ == "__main__": test_gaussian_finite_limits() diff --git a/tests/test_interpolatedimage.py b/tests/test_interpolatedimage.py index 11b00eb7d7f..d0af7241dd7 100644 --- a/tests/test_interpolatedimage.py +++ b/tests/test_interpolatedimage.py @@ -163,6 +163,7 @@ def test_roundtrip(): assert_raises(ValueError, galsim.Interpolant.from_name, 'lanczos3A') assert_raises(ValueError, galsim.Interpolant.from_name, 'lanczosF') assert_raises(ValueError, galsim.Interpolant.from_name, 'lanzos') + assert_raises(NotImplementedError, galsim.Interpolant) @timer def test_fluxnorm(): @@ -229,36 +230,49 @@ def test_fluxnorm(): def test_exceptions(): """Test failure modes for InterpolatedImage class. """ - # What if it receives as input something that is not an Image? Give it a GSObject to check. - g = galsim.Gaussian(sigma=1.) - with assert_raises((ValueError, AttributeError)): - galsim.InterpolatedImage(g) - # What if Image does not have a scale set, but scale keyword is not specified? - im = galsim.ImageF(5, 5) - with assert_raises(ValueError): - galsim.InterpolatedImage(im) - # Image must have bounds defined - im = galsim.ImageF() - im.scale = 1. - with assert_raises(ValueError): - galsim.InterpolatedImage(im) - # Weird flux normalization - im = galsim.ImageF(5, 5, scale=1.) - with assert_raises(ValueError): - galsim.InterpolatedImage(im, normalization = 'foo') - # scale and WCS - with assert_raises(TypeError): - galsim.InterpolatedImage(im, wcs = galsim.PixelScale(1.), scale=1.) - # weird WCS - with assert_raises(TypeError): - galsim.InterpolatedImage(im, wcs = 1.) - # Weird interpolant - give it something random like a GSObject - with assert_raises(Exception): - galsim.InterpolatedImage(im, x_interpolant = g) - # Image has wrong type - im = galsim.ImageI(5, 5) - with assert_raises(ValueError): - galsim.InterpolatedImage(im) + # Check that provided image has valid bounds + with assert_raises(galsim.GalSimUndefinedBoundsError): + galsim.InterpolatedImage(image=galsim.ImageF(scale=1.)) + + # Scale must be set + with assert_raises(galsim.GalSimIncompatibleValuesError): + galsim.InterpolatedImage(image=galsim.ImageF(5, 5)) + + # Image must be real type (F or D) + with assert_raises(galsim.GalSimValueError): + galsim.InterpolatedImage(image=galsim.ImageI(5, 5, scale=1)) + + # Image must have non-zero flux + with assert_raises(galsim.GalSimValueError): + galsim.InterpolatedImage(image=galsim.ImageF(5, 5, scale=1, init_value=0.)) + + # Can't shoot II with SincInterpolant + ii = galsim.InterpolatedImage(image=galsim.ImageF(5, 5, scale=1, init_value=1.), + x_interpolant='sinc') + with assert_raises(galsim.GalSimError): + ii.drawImage(method='phot') + with assert_raises(galsim.GalSimError): + ii.shoot(n_photons=3) + + # Check types of inputs + im = galsim.ImageF(5, 5, scale=1., init_value=10.) + assert_raises(TypeError, galsim.InterpolatedImage, image=im.array) + assert_raises(TypeError, galsim.InterpolatedImage, im, wcs=galsim.PixelScale(1.), scale=1.) + assert_raises(TypeError, galsim.InterpolatedImage, im, wcs=1.) + assert_raises(TypeError, galsim.InterpolatedImage, im, pad_image=im.array) + assert_raises(TypeError, galsim.InterpolatedImage, im, noise_pad_size=33) + assert_raises(TypeError, galsim.InterpolatedImage, im, noise_pad=33) + + # Other invalid values: + assert_raises(ValueError, galsim.InterpolatedImage, im, normalization='invalid') + assert_raises(ValueError, galsim.InterpolatedImage, im, x_interpolant='invalid') + assert_raises(ValueError, galsim.InterpolatedImage, im, k_interpolant='invalid') + assert_raises(ValueError, galsim.InterpolatedImage, im, pad_image=galsim.ImageI(25,25)) + assert_raises(ValueError, galsim.InterpolatedImage, im, pad_factor=0.) + assert_raises(ValueError, galsim.InterpolatedImage, im, pad_factor=-1.) + assert_raises(ValueError, galsim.InterpolatedImage, im, noise_pad_size=33, noise_pad=im.wcs) + assert_raises(ValueError, galsim.InterpolatedImage, im, noise_pad_size=33, noise_pad=-1.) + assert_raises(ValueError, galsim.InterpolatedImage, im, noise_pad_size=-33, noise_pad=1.) @timer @@ -1096,8 +1110,6 @@ def test_stepk_maxk(): def test_kroundtrip(): """ Test that GSObjects `a` and `b` are the same when b = InterpolatedKImage(a.drawKImage) """ - import warnings - a = final kim_a = a.drawKImage() b = galsim.InterpolatedKImage(kim_a) @@ -1164,7 +1176,7 @@ def test_kroundtrip(): np.testing.assert_almost_equal(b.maxk, c.maxk) # Smaller stepk is overridden. - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): d = galsim.InterpolatedKImage(kim_a, stepk=0.5*b.stepk) np.testing.assert_almost_equal(b.stepk, d.stepk) np.testing.assert_almost_equal(b.maxk, d.maxk) @@ -1188,6 +1200,41 @@ def test_kroundtrip(): np.testing.assert_array_almost_equal(a_conv_c_img.array, b_conv_c_img.array, 5, "Convolution of InterpolatedKImage drawn incorrectly.") + +@timer +def test_kexceptions(): + """Test failure modes for InterpolatedKImage class. + """ + # Check that provided image has valid bounds + with assert_raises(galsim.GalSimUndefinedBoundsError): + galsim.InterpolatedKImage(kimage=galsim.ImageCD(scale=1.)) + + # Image must be complex type (CF or CD) + with assert_raises(galsim.GalSimValueError): + galsim.InterpolatedKImage(kimage=galsim.ImageD(5, 5, scale=1)) + + # Check types of inputs + im = galsim.ImageCD(5, 5, scale=1., init_value=10.) + assert_raises(TypeError, galsim.InterpolatedKImage) + assert_raises(TypeError, galsim.InterpolatedKImage, kimage=im.array) + assert_raises(TypeError, galsim.InterpolatedKImage, real_kimage=im.real, imag_kimage=4) + assert_raises(TypeError, galsim.InterpolatedKImage, real_kimage=3, imag_kimage=im.imag) + assert_raises(TypeError, galsim.InterpolatedKImage, kimage=im, + real_kimage=im.real, imag_kimage=im.imag) + + # Other invalid values: + assert_raises(ValueError, galsim.InterpolatedKImage, im, k_interpolant='invalid') + assert_raises(ValueError, galsim.InterpolatedKImage, real_kimage=im.real) + assert_raises(ValueError, galsim.InterpolatedKImage, imag_kimage=im.imag) + assert_raises(ValueError, galsim.InterpolatedKImage, real_kimage=im, imag_kimage=im) + assert_raises(ValueError, galsim.InterpolatedKImage, real_kimage=im.real, + imag_kimage=galsim.ImageD(4,4,scale=1.)) + assert_raises(ValueError, galsim.InterpolatedKImage, real_kimage=im.real, + imag_kimage=galsim.ImageD(5,5,scale=2.)) + assert_raises(ValueError, galsim.InterpolatedKImage, + kimage=galsim.ImageCD(5, 5, wcs=galsim.JacobianWCS(2.1, 0.3, -0.4, 2.3))) + + @timer def test_multihdu_readin(): """Test the ability to read in from a file with multiple FITS extensions. diff --git a/tests/test_kolmogorov.py b/tests/test_kolmogorov.py index 2307347cebc..46a74d361e7 100644 --- a/tests/test_kolmogorov.py +++ b/tests/test_kolmogorov.py @@ -28,22 +28,6 @@ imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference # images. - -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_kolmogorov(): """Test the generation of a specific Kolmogorov profile against a known result. @@ -133,6 +117,7 @@ def test_kolmogorov(): assert_raises(TypeError, galsim.Kolmogorov, half_light_radius=1, r0=1) assert_raises(TypeError, galsim.Kolmogorov, lam=3) assert_raises(TypeError, galsim.Kolmogorov, r0=1) + assert_raises(TypeError, galsim.Kolmogorov) @timer def test_kolmogorov_properties(): diff --git a/tests/test_lensing.py b/tests/test_lensing.py index 0b84c523287..05fad8a75f7 100644 --- a/tests/test_lensing.py +++ b/tests/test_lensing.py @@ -21,7 +21,6 @@ import math import os import sys -import warnings import galsim from galsim_test_helpers import * @@ -212,6 +211,12 @@ def test_cosmology(): do_pickle(halo2) assert halo == halo2 + assert_raises(ValueError, cosmo.Da, -0.1) + assert_raises(ValueError, cosmo.Da, 2.1, 2.3) + assert_raises(TypeError, galsim.NFWHalo, 1e15, 4, 1, cosmo=5) + assert_raises(TypeError, galsim.NFWHalo, 1e15, 4, 1, cosmo=cosmo, omega_m=wm) + assert_raises(TypeError, galsim.NFWHalo, 1e15, 4, 1, cosmo=cosmo, omega_lam=wl) + @timer def test_shear_variance(): @@ -237,7 +242,7 @@ def test_shear_variance(): test_ps = galsim.PowerSpectrum(e_power_function=pk_flat_lim, b_power_function=pk_flat_lim) # get shears on 500x500 grid with spacing 0.1 degree rng2 = rng.duplicate() - assert_raises(RuntimeError, test_ps.nRandCallsForBuildGrid) + assert_raises(galsim.GalSimError, test_ps.nRandCallsForBuildGrid) g1, g2 = test_ps.buildGrid(grid_spacing=grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) @@ -506,11 +511,11 @@ def test_shear_variance(): "Incorrect shear variance post-interpolation" # Warn for accessing values outside of valid bounds (and not periodic) - assert_warns(UserWarning, test_ps.getShear, pos=(max*2, 0), units='deg') - assert_warns(UserWarning, test_ps.getShear, pos=(max*2, 0), reduced=False, units='deg') - assert_warns(UserWarning, test_ps.getConvergence, pos=(max*2, 0), units='deg') - assert_warns(UserWarning, test_ps.getMagnification, pos=(max*2, 0), units='deg') - assert_warns(UserWarning, test_ps.getLensing, pos=(max*2, 0), units='deg') + assert_warns(galsim.GalSimWarning, test_ps.getShear, pos=(max*2, 0), units='deg') + assert_warns(galsim.GalSimWarning, test_ps.getShear, pos=(max*2, 0), reduced=False, units='deg') + assert_warns(galsim.GalSimWarning, test_ps.getConvergence, pos=(max*2, 0), units='deg') + assert_warns(galsim.GalSimWarning, test_ps.getMagnification, pos=(max*2, 0), units='deg') + assert_warns(galsim.GalSimWarning, test_ps.getLensing, pos=(max*2, 0), units='deg') @timer @@ -631,6 +636,13 @@ def test_shear_get(): # build the grid grid_spacing = 17. ngrid = 100 + + # Before calling buildGrid, these are invalid + assert_raises(galsim.GalSimError, my_ps.getShear, galsim.PositionD(0,0)) + assert_raises(galsim.GalSimError, my_ps.getConvergence, galsim.PositionD(0,0)) + assert_raises(galsim.GalSimError, my_ps.getMagnification, galsim.PositionD(0,0)) + assert_raises(galsim.GalSimError, my_ps.getLensing, galsim.PositionD(0,0)) + g1, g2, kappa = my_ps.buildGrid(grid_spacing = grid_spacing, ngrid = ngrid, get_convergence = True) min = (-ngrid/2 + 0.5) * grid_spacing @@ -676,8 +688,7 @@ def test_shear_get(): (g1_r[0,0], g2_r[0,0], mu[0,0])) # Test outside of bounds - with warnings.catch_warnings(): - warnings.filterwarnings("ignore") + with assert_warns(galsim.GalSimWarning): np.testing.assert_almost_equal(my_ps.getShear((5000,5000)), (0,0)) np.testing.assert_almost_equal(my_ps.getShear((5000,5000), reduced=False), (0,0)) np.testing.assert_almost_equal(my_ps.getConvergence((5000,5000)), 0) @@ -842,17 +853,24 @@ def test_tabulated(): assert_raises(ValueError, galsim.LookupTable, (0.,1.,2.), (0.,1.,2.), f_log=True) assert_raises(ValueError, galsim.LookupTable, (0.,1.,2.), (0.,1.,2.), x_log=True, f_log=True) + # Negative power is invalid. + neg_power = galsim.LookupTable(k_arr, np.cos(k_arr)) + print('neg_power = ',neg_power) + with assert_raises(galsim.GalSimError): + negps = galsim.PowerSpectrum(neg_power) + negps.buildGrid(grid_spacing=1.7, ngrid=10) + # Check some invalid PowerSpectrum parameters - assert_raises(AttributeError, galsim.PowerSpectrum) - assert_raises(AttributeError, galsim.PowerSpectrum, delta2=True) - assert_raises(AttributeError, galsim.PowerSpectrum, delta2=True, units='radians') + assert_raises(ValueError, galsim.PowerSpectrum) + assert_raises(ValueError, galsim.PowerSpectrum, delta2=True) + assert_raises(ValueError, galsim.PowerSpectrum, delta2=True, units='radians') assert_raises(ValueError, galsim.PowerSpectrum, e_power_function=tab, units='inches') assert_raises(ValueError, galsim.PowerSpectrum, e_power_function=tab, units=True) assert_raises(ValueError, galsim.PowerSpectrum, e_power_function='not_a_file') assert_raises(ValueError, galsim.PowerSpectrum, b_power_function='not_a_file') - assert_raises(ValueError, ps_tab.buildGrid) - assert_raises(ValueError, ps_tab.buildGrid, grid_spacing=1.7) - assert_raises(ValueError, ps_tab.buildGrid, ngrid=10) + assert_raises(TypeError, ps_tab.buildGrid) + assert_raises(TypeError, ps_tab.buildGrid, grid_spacing=1.7) + assert_raises(TypeError, ps_tab.buildGrid, ngrid=10) assert_raises(ValueError, ps_tab.buildGrid, grid_spacing=1.7, ngrid=10.5) assert_raises(ValueError, ps_tab.buildGrid, grid_spacing=1.7, ngrid=10, kmin_factor=2.5) assert_raises(ValueError, ps_tab.buildGrid, grid_spacing=1.7, ngrid=10, kmax_factor=1.5) @@ -860,6 +878,13 @@ def test_tabulated(): assert_raises(ValueError, ps_tab.buildGrid, grid_spacing=1.7, ngrid=10, units='inches') assert_raises(ValueError, ps_tab.buildGrid, grid_spacing=1.7, ngrid=10, units=True) assert_raises(ValueError, ps_tab.buildGrid, grid_spacing=1.7, ngrid=10, bandlimit='none') + assert_raises(TypeError, ps_tab.getShear) + assert_raises(TypeError, ps_tab.getShear, pos=()) + assert_raises(TypeError, ps_tab.getShear, pos=3) + assert_raises(TypeError, ps_tab.getShear, pos=(3,)) + assert_raises(TypeError, ps_tab.getShear, pos=(3,4,5)) + assert_raises(ValueError, ps_tab.getShear, pos=(3,4), units='invalid') + assert_raises(ValueError, ps_tab.getShear, pos=(3,4), units=17) # check that when calling LookupTable, you can provide a scalar, list, tuple or array tab = galsim.LookupTable(k_arr, p_arr) @@ -950,6 +975,11 @@ def test_kappa_gauss(): err_msg="Reconstructed kappaE is non-zero at greater than 3 decimal places for rotated "+ "shear field.") + assert_raises(TypeError, galsim.lensing_ps.kappaKaiserSquires, g1=0.3, g2=0.1) + assert_raises(ValueError, galsim.lensing_ps.kappaKaiserSquires, g1=g1, g2=g2[:50,:50]) + assert_raises(NotImplementedError, galsim.lensing_ps.kappaKaiserSquires, + g1=g1[:,:50], g2=g2[:,:50]) + @timer def test_power_spectrum_with_kappa(): @@ -1286,6 +1316,13 @@ def test_periodic(): np.testing.assert_almost_equal(np.var(mu_shift), np.var(mu), decimal=8, err_msg='Magnification variance altered by periodic interpolation') + # If image is too small, can't use periodic boundaries. + ps.buildGrid(ngrid=5, grid_spacing=0.1, units=galsim.degrees, + rng=galsim.UniformDeviate(314159), interpolant='nearest', + kmin_factor=3., kmax_factor=1., get_convergence=True) + with assert_raises(galsim.GalSimError): + ps.getShear(pos=(x.flatten(),y.flatten()), units=galsim.degrees, + reduced=False, periodic=True) @timer def test_bandlimit(): @@ -1336,6 +1373,9 @@ def test_psr(): galsim.lensing_ps.PowerSpectrumRealizer(100, 0.005, pb, pe)] all_obj_diff(diff_psr_list) + with assert_raises(TypeError): + psr(gd=galsim.BaseDeviate(1234)) + @timer def test_normalization(): diff --git a/tests/test_moffat.py b/tests/test_moffat.py index f074c1812ce..981fc7ed219 100644 --- a/tests/test_moffat.py +++ b/tests/test_moffat.py @@ -28,22 +28,6 @@ imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference # images. - -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_moffat(): """Test the generation of a specific Moffat profile against a known result. @@ -111,11 +95,22 @@ def test_moffat(): np.testing.assert_almost_equal(moffat.xValue(cen), moffat.max_sb) # Should raise an exception if >=2 radii are provided. - assert_raises(TypeError, galsim.Moffat, beta=1, scale_radius=3, half_light_radius=1, fwhm=2) - assert_raises(TypeError, galsim.Moffat, beta=1, half_light_radius=1, fwhm=2) - assert_raises(TypeError, galsim.Moffat, beta=1, scale_radius=3, fwhm=2) - assert_raises(TypeError, galsim.Moffat, beta=1, scale_radius=3, half_light_radius=1) - + assert_raises(TypeError, galsim.Moffat, beta=3, scale_radius=3, half_light_radius=1, fwhm=2) + assert_raises(TypeError, galsim.Moffat, beta=3, half_light_radius=1, fwhm=2) + assert_raises(TypeError, galsim.Moffat, beta=3, scale_radius=3, fwhm=2) + assert_raises(TypeError, galsim.Moffat, beta=3, scale_radius=3, half_light_radius=1) + assert_raises(TypeError, galsim.Moffat, beta=3) + + # beta <= 1.1 needs to be truncated. + assert_raises(ValueError, galsim.Moffat, beta=1.1, scale_radius=3) + assert_raises(ValueError, galsim.Moffat, beta=0.9, scale_radius=3) + + # trunc must be > sqrt(2) * hlr + assert_raises(ValueError, galsim.Moffat, beta=3, half_light_radius=1, trunc=1.4) + + # Other errors + assert_raises(TypeError, galsim.Moffat, scale_radius=3) + assert_raises(ValueError, galsim.Moffat, beta=3, scale_radius=3, trunc=-1) @timer def test_moffat_properties(): diff --git a/tests/test_noise.py b/tests/test_noise.py index 9fe74808e3d..f261cd85711 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -91,9 +91,9 @@ def test_deviate_noise(): assert_raises(NotImplementedError, galsim.BaseNoise().withScaledVariance, 23) assert_raises(TypeError, noise.applyTo, 23) assert_raises(NotImplementedError, galsim.BaseNoise().applyTo, testimage) - assert_raises(RuntimeError, noise.getVariance) - assert_raises(RuntimeError, noise.withVariance, 23) - assert_raises(RuntimeError, noise.withScaledVariance, 23) + assert_raises(galsim.GalSimError, noise.getVariance) + assert_raises(galsim.GalSimError, noise.withVariance, 23) + assert_raises(galsim.GalSimError, noise.withScaledVariance, 23) @timer @@ -367,9 +367,9 @@ def test_variable_gaussian_noise(): assert_raises(TypeError, vgn.applyTo, 23) assert_raises(ValueError, vgn.applyTo, galsim.ImageF(3,3)) - assert_raises(RuntimeError, vgn.getVariance) - assert_raises(RuntimeError, vgn.withVariance, 23) - assert_raises(RuntimeError, vgn.withScaledVariance, 23) + assert_raises(galsim.GalSimError, vgn.getVariance) + assert_raises(galsim.GalSimError, vgn.withVariance, 23) + assert_raises(galsim.GalSimError, vgn.withScaledVariance, 23) @timer diff --git a/tests/test_optics.py b/tests/test_optics.py index 76264750d5d..9056abeea97 100644 --- a/tests/test_optics.py +++ b/tests/test_optics.py @@ -297,7 +297,7 @@ def test_OpticalPSF_aberrations_kwargs(): with assert_raises(ValueError): galsim.OpticalPSF(lod, aberrations=[0.0]*2) # The first element must be 0. (Just a warning!) - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): galsim.OpticalPSF(lod, aberrations=[0.3]*8) # Cannot provide both aberrations and specific ones by name. with assert_raises(TypeError): @@ -408,7 +408,7 @@ def test_OpticalPSF_pupil_plane(): .format(pp_file)) im = galsim.Image(ref_psf._psf.aper.illuminated.astype(float)) im.scale = ref_psf._psf.aper.pupil_plane_scale - print('pupil_plane image has scale = ',pp_scale) + print('pupil_plane image has scale = ',im.scale) im.write(os.path.join(imgdir, pp_file)) pp_scale = im.scale print('pupil_plane image has scale = ',pp_scale) @@ -417,8 +417,8 @@ def test_OpticalPSF_pupil_plane(): # need it, and it is invalid to give lam_over_diam (rather than lam, diam separately) when # there is a specific scale for the pupil plane image. But see the last test below where # we do use lam, diam separately with the input image. - im.scale = None - # This implies that the lam_over_diam value + im.wcs = None + # This implies that the lam_over_diam value is valid. test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, oversampling=pp_oversampling, pupil_plane_im=im, pad_factor=pp_pad_factor) @@ -441,6 +441,23 @@ def test_OpticalPSF_pupil_plane(): do_pickle(test_psf, lambda x: x.drawImage(nx=20, ny=20, scale=0.07, method='no_pixel')) do_pickle(test_psf) + # Make a smaller pupil plane image to test the pickling of this, even without slow tests. + with assert_warns(galsim.GalSimWarning): + alt_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, + oversampling=1., pupil_plane_im=im.bin(4,4), + pad_factor=1.) + do_pickle(alt_psf) + + assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, pupil_plane_im='pp_file') + assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, pupil_plane_im=im, + pupil_plane_scale=pp_scale) + assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, + pupil_plane_im=im.view(scale=pp_scale)) + assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, + pupil_plane_im=galsim.Image(im.array[:-2,:])) + assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, + pupil_plane_im=galsim.Image(im.array[:-1,:-1])) + # It is supposed to be able to figure this out even if we *don't* tell it the pad factor. So # make sure that it still works even if we don't tell it that value. test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, @@ -563,21 +580,18 @@ def test_OpticalPSF_pupil_plane(): # Supply the pupil plane at higher resolution, and make sure that the routine figures out the # sampling and gets the right image scale etc. - gsp = galsim.GSParams(maximum_fft_size=8192) rescale_fac = 0.77 ref_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, nstruts=nstruts, strut_angle=strut_angle, oversampling=pp_oversampling, - pad_factor=pp_pad_factor/rescale_fac, - gsparams=gsp) + pad_factor=pp_pad_factor/rescale_fac) # Make higher resolution pupil plane image via interpolation int_im = galsim.InterpolatedImage(galsim.Image(im, scale=1.0, dtype=np.float32), calculate_maxk=False, calculate_stepk=False, x_interpolant='linear') new_im = int_im.drawImage(scale=rescale_fac, method='no_pixel') - new_im.scale = None # Let OpticalPSF figure out the scale automatically. + new_im.wcs = None # Let OpticalPSF figure out the scale automatically. test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, - pupil_plane_im=new_im, oversampling=pp_oversampling, - gsparams=gsp) + pupil_plane_im=new_im, oversampling=pp_oversampling) im_ref_psf = ref_psf.drawImage(scale=scale) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) @@ -613,7 +627,7 @@ def test_OpticalPSF_pupil_plane(): big_im[im.bounds] = im test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=big_im, oversampling=pp_oversampling, - pad_factor=pp_pad_factor, gsparams=gsp) + pad_factor=pp_pad_factor) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) test_moments = im_test_psf.FindAdaptiveMom() diff --git a/tests/test_phase_psf.py b/tests/test_phase_psf.py index 7d8f021feb4..e11834c02be 100644 --- a/tests/test_phase_psf.py +++ b/tests/test_phase_psf.py @@ -34,20 +34,61 @@ def test_aperture(): """Test various ways to construct Apertures.""" # Simple tests for constructing and pickling Apertures. - aper1 = galsim.Aperture(diam=1.0) + aper1 = galsim.Aperture(diam=1.7) im = galsim.fits.read(os.path.join(imgdir, pp_file)) - aper2 = galsim.Aperture(diam=1.0, pupil_plane_im=im) - aper3 = galsim.Aperture(diam=1.0, nstruts=4) + aper2 = galsim.Aperture(diam=1.7, pupil_plane_im=im) + aper3 = galsim.Aperture(diam=1.7, nstruts=4, gsparams=galsim.GSParams(maximum_fft_size=4096)) do_pickle(aper1) do_pickle(aper2) do_pickle(aper3) # Automatically created Aperture should match one created via OpticalScreen - aper1 = galsim.Aperture(diam=1.0) - aper2 = galsim.Aperture(diam=1.0, lam=500, screen_list=[galsim.OpticalScreen(diam=1.0)]) + aper1 = galsim.Aperture(diam=1.7) + aper2 = galsim.Aperture(diam=1.7, lam=500, screen_list=[galsim.OpticalScreen(diam=1.7)]) err_str = ("Aperture created implicitly using Airy does not match Aperture created using " "OpticalScreen.") assert aper1 == aper2, err_str + assert_raises(ValueError, galsim.Aperture, 1.7, obscuration=-0.3) + assert_raises(ValueError, galsim.Aperture, 1.7, obscuration=1.1) + assert_raises(ValueError, galsim.Aperture, -1.7) + assert_raises(ValueError, galsim.Aperture, 0) + + assert_raises(ValueError, galsim.Aperture, 1.7, pupil_plane_im=im, circular_pupil=False) + assert_raises(ValueError, galsim.Aperture, 1.7, pupil_plane_im=im, nstruts=2) + assert_raises(ValueError, galsim.Aperture, 1.7, pupil_plane_im=im, strut_thick=0.01) + assert_raises(ValueError, galsim.Aperture, 1.7, pupil_plane_im=im, strut_angle=5*galsim.degrees) + assert_raises(ValueError, galsim.Aperture, 1.7, pupil_plane_im=im, strut_angle=5*galsim.degrees) + assert_raises(ValueError, galsim.Aperture, 1.7, screen_list=[galsim.OpticalScreen(diam=1)]) + + # rho is a convenience property that can be useful when debugging, but isn't used in the + # main code base. + np.testing.assert_almost_equal(aper1.rho, aper1.u * 2./1.7 + 1j * aper1.v * 2./1.7) + + # Some other functions that aren't used by anything anymore, but were useful in development. + for lam in [300, 550, 1200]: + stepk = aper1._getStepK(lam=lam) + maxk = aper1._getMaxK(lam=lam) + scale = aper1._sky_scale(lam=lam) + size = aper1._sky_size(lam=lam) + np.testing.assert_almost_equal(stepk, 2.*np.pi/size) + np.testing.assert_almost_equal(maxk, np.pi/scale) + + # If the constructed pupil plane would be too large, raise an error + assert_raises(galsim.GalSimFFTSizeError, galsim.Aperture, 1.7, pupil_plane_scale=1.e-4) + + # Similar if the given image is too large. + # Here, we change gsparams.maximum_fft_size, rather than build a really large image to load. + with assert_raises(galsim.GalSimFFTSizeError): + galsim.Aperture(1.7, pupil_plane_im=im, gsparams=galsim.GSParams(maximum_fft_size=64)) + + # Other choices just give warnings + with assert_warns(galsim.GalSimWarning): + galsim.Aperture(diam=1.7, pupil_plane_size=3, pupil_plane_scale=0.03) + + im.wcs = None # Otherwise get an error. + with assert_warns(galsim.GalSimWarning): + galsim.Aperture(diam=1.7, pupil_plane_im=im, pupil_plane_scale=0.03) + @timer def test_atm_screen_size(): @@ -166,7 +207,7 @@ def test_phase_screen_list(): do_pickle(atm, func=lambda x:np.sum(x.wavefront_gradient(aper.u, aper.v, 0.0))) # testing append, extend, __getitem__, __setitem__, __delitem__, __eq__, __ne__ - atm2 = galsim.PhaseScreenList(atm[:-1]) # Refers to first n-1 screens + atm2 = atm[:-1] # Refers to first n-1 screens assert atm != atm2 # Append a different screen to the end of atm2 atm2.append(ar2) @@ -176,6 +217,11 @@ def test_phase_screen_list(): atm2.append(atm[-1]) assert atm == atm2 + with assert_raises(TypeError): + atm['invalid'] + with assert_raises(IndexError): + atm[3] + # Test building from empty PhaseScreenList atm3 = galsim.PhaseScreenList() atm3.extend(atm2) @@ -252,6 +298,19 @@ def test_phase_screen_list(): np.testing.assert_array_equal(psf, psf3, "PhaseScreenPSFs are inconsistent") np.testing.assert_array_equal(psf, psf4, "PhaseScreenPSFs are inconsistent") + # Check errors in u,v,t shapes. + assert_raises(ValueError, ar1.wavefront, aper.u, aper.v[:-1,:-1]) + assert_raises(ValueError, ar1.wavefront, aper.u[:-1,:-1], aper.v) + assert_raises(ValueError, ar1.wavefront, aper.u, aper.v, 0.1 * aper.u[:-1,:-1]) + assert_raises(ValueError, ar1.wavefront_gradient, aper.u, aper.v[:-1,:-1]) + assert_raises(ValueError, ar1.wavefront_gradient, aper.u[:-1,:-1], aper.v) + assert_raises(ValueError, ar1.wavefront_gradient, aper.u, aper.v, 0.1 * aper.u[:-1,:-1]) + + assert_raises(ValueError, ar3.wavefront, aper.u, aper.v[:-1,:-1]) + assert_raises(ValueError, ar3.wavefront, aper.u[:-1,:-1], aper.v) + assert_raises(ValueError, ar3.wavefront_gradient, aper.u, aper.v[:-1,:-1]) + assert_raises(ValueError, ar3.wavefront_gradient, aper.u[:-1,:-1], aper.v) + @timer def test_frozen_flow(): @@ -264,9 +323,7 @@ def test_frozen_flow(): alt = x/1000 # -> 0.00005 km; silly example, but yields exact results... screen = galsim.AtmosphericScreen(1.0, dx, alt, vx=vx, rng=rng) - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with assert_warns(galsim.GalSimWarning): aper = galsim.Aperture(diam=1, pupil_plane_size=20., pupil_plane_scale=20./dx) wf0 = screen.wavefront(aper.u, aper.v, None, theta0) dwdu0, dwdv0 = screen.wavefront_gradient(aper.u, aper.v, t=screen._time) @@ -391,6 +448,14 @@ def test_scale_unit(): opt_psf2 = galsim.OpticalPSF(lam=500.0, diam=1.0, scale_unit='arcsec') assert opt_psf1 == opt_psf2, "scale unit did not parse as string" + assert_raises(ValueError, galsim.OpticalPSF, lam=500.0, diam=1.0, scale_unit='invalid') + assert_raises(ValueError, galsim.PhaseScreenPSF, atm, 500.0, aper=aper, scale_unit='invalid') + # Check a few other construction errors now too. + assert_raises(ValueError, galsim.PhaseScreenPSF, atm, 500.0, scale_unit='arcmin') + assert_raises(TypeError, galsim.PhaseScreenPSF, atm, 500.0, aper=aper, theta=34.*galsim.degrees) + assert_raises(TypeError, galsim.PhaseScreenPSF, atm, 500.0, aper=aper, theta=(34, 5)) + assert_raises(ValueError, galsim.PhaseScreenPSF, atm, 500.0, aper=aper, exptime=-1) + @timer def test_stepk_maxk(): @@ -459,6 +524,19 @@ def test_stepk_maxk(): psf3.centroid psf3.max_sb + # If we force stepk very low, it will trigger a warning when we try to draw it. + psf4 = galsim.PhaseScreenPSF(atm, 500.0, aper=aper, scale_unit=galsim.arcsec, + _force_stepk=stepk2/3.5) + with assert_warns(galsim.GalSimWarning): + psf4._prepareDraw() + + # Can suppress this warning if desired. + psf5 = galsim.PhaseScreenPSF(atm, 500.0, aper=aper, scale_unit=galsim.arcsec, + _force_stepk=stepk2/3.5, suppress_warning=True) + with assert_raises(AssertionError): + with assert_warns(galsim.GalSimWarning): + psf5._prepareDraw() + @timer def test_ne(): @@ -802,9 +880,6 @@ def test_speedup(): significant speedup. """ import time - #import cProfile, pstats - #pr = cProfile.Profile() - #pr.enable() atm = galsim.Atmosphere(screen_size=10.0, altitude=[0,1,2,3], r0_500=0.2) # Should be ~seconds if _prepareDraw() gets executed, ~0.01s otherwise. psf = atm.makePSF(lam=500.0, diam=1.0, exptime=15.0, time_step=0.025) @@ -817,6 +892,23 @@ def test_speedup(): print("Time for geometric approximation draw: {:6.4f}s".format(t1-t0)) assert (t1-t0) < 0.1, "Photon-shooting took too long ({0} s).".format(t1-t0) +@timer +def test_instantiation_check(): + """Check that after instantiating, drawing with the other method will emit a warning. + """ + atm1 = galsim.Atmosphere(screen_size=10.0, altitude=10, r0_500=0.2) + psf1 = atm1.makePSF(lam=500.0, diam=1.0) + psf1.drawImage() + with assert_warns(galsim.GalSimWarning): + psf1.drawImage(method='phot', n_photons=10) + + atm2 = galsim.Atmosphere(screen_size=10.0, altitude=10, r0_500=0.2) + psf2 = atm2.makePSF(lam=500.0, diam=1.0) # exptime = 0, so reasonable to draw w/ FFT + psf2.drawImage(method='phot', n_photons=10) + with assert_warns(galsim.GalSimWarning): + psf2.drawImage() + + @timer def test_gc(): """Make sure that pending psfs don't leak memory. @@ -882,4 +974,5 @@ def test_gc(): test_input() test_r0_weights() test_speedup() + test_instantiation_check() test_gc() diff --git a/tests/test_photon_array.py b/tests/test_photon_array.py index 8e5e43a001b..fc034c26c9e 100644 --- a/tests/test_photon_array.py +++ b/tests/test_photon_array.py @@ -181,6 +181,40 @@ def test_photon_array(): np.testing.assert_almost_equal(pa2.dydz[50:], pa1.dydz) np.testing.assert_almost_equal(pa2.wavelength[50:], pa1.wavelength) + # Error if it doesn't fit. + assert_raises(ValueError, pa2.assignAt, 90, pa1) + + # Test some trivial usage of makeFromImage + zero = galsim.Image(4,4,init_value=0) + photons = galsim.PhotonArray.makeFromImage(zero) + print('photons = ',photons) + assert len(photons) == 16 + np.testing.assert_array_equal(photons.flux, 0.) + + ones = galsim.Image(4,4,init_value=1) + photons = galsim.PhotonArray.makeFromImage(ones) + print('photons = ',photons) + assert len(photons) == 16 + np.testing.assert_almost_equal(photons.flux, 1.) + + tens = galsim.Image(4,4,init_value=8) + photons = galsim.PhotonArray.makeFromImage(tens, max_flux=5.) + print('photons = ',photons) + assert len(photons) == 32 + np.testing.assert_almost_equal(photons.flux, 4.) + + assert_raises(ValueError, galsim.PhotonArray.makeFromImage, zero, max_flux=0.) + assert_raises(ValueError, galsim.PhotonArray.makeFromImage, zero, max_flux=-2) + + # Check some other errors + undef = galsim.Image() + assert_raises(galsim.GalSimUndefinedBoundsError, pa2.addTo, undef) + + # This shouldn't be able to happen in regular photon-shooting usage, so check here. + # TODO: Would be nice to have some real tests of the convolve functionality here, + # rather than just implicitly in the shooting tests. + assert_raises(galsim.GalSimError, pa2.convolve, pa1) + # Check picklability again with non-zero values for everything do_pickle(photon_array) @@ -329,6 +363,9 @@ def test_photon_io(): photons = image.photons assert photons.size() == len(photons) == nphotons + with assert_raises(galsim.GalSimIncompatibleValuesError): + obj.drawImage(method='phot', n_photons=nphotons, save_photons=True, maxN=1.e5) + file_name = 'output/photons1.dat' photons.write(file_name) @@ -499,23 +536,47 @@ def test_dcr(): err_msg="PhotonDCR with alpha=0 didn't match") # Also check invalid parameters + zenith_coord = galsim.CelestialCoord(13.54 * galsim.hours, lsst_lat) assert_raises(TypeError, galsim.PhotonDCR, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle) # base_wavelength is required assert_raises(TypeError, galsim.PhotonDCR, base_wavelength=500, parallactic_angle=parallactic_angle) # zenith_angle (somehow) is required - assert_raises(TypeError, galsim.PhotonDCR, - base_wavelength=500, + assert_raises(TypeError, galsim.PhotonDCR, 500, + zenith_angle=34.4, + parallactic_angle=parallactic_angle) # zenith_angle must be Angle + assert_raises(TypeError, galsim.PhotonDCR, 500, + zenith_angle=zenith_angle, + parallactic_angle=34.5) # parallactic_angle must be Angle + assert_raises(TypeError, galsim.PhotonDCR, 500, + obj_coord=obj_coord, + latitude=lsst_lat) # Missing HA + assert_raises(TypeError, galsim.PhotonDCR, 500, + obj_coord=obj_coord, + HA=local_sidereal_time-obj_coord.ra) # Missing latitude + assert_raises(TypeError, galsim.PhotonDCR, 500, + obj_coord=obj_coord) # Need either zenith_coord, or (HA,lat) + assert_raises(TypeError, galsim.PhotonDCR, 500, + obj_coord=obj_coord, + zenith_coord=zenith_coord, + HA=local_sidereal_time-obj_coord.ra) # Can't have both HA and zenith_coord + assert_raises(TypeError, galsim.PhotonDCR, 500, + obj_coord=obj_coord, + zenith_coord=zenith_coord, + latitude=lsst_lat) # Can't have both lat and zenith_coord + assert_raises(TypeError, galsim.PhotonDCR, 500, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle, H20_pressure=1.) # invalid (misspelled) - assert_raises(ValueError, galsim.PhotonDCR, - base_wavelength=500, + assert_raises(ValueError, galsim.PhotonDCR, 500, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle, scale_unit='inches') # invalid scale_unit + # Invalid to use dcr without some way of setting wavelengths. + assert_raises(galsim.GalSimError, achrom.drawImage, im2, method='phot', surface_ops=[dcr]) + @unittest.skipIf(no_astroplan, 'Unable to import astroplan') @timer def test_dcr_angles(): diff --git a/tests/test_pse.py b/tests/test_pse.py index a532df8e3b6..e5e6825b6c5 100644 --- a/tests/test_pse.py +++ b/tests/test_pse.py @@ -172,8 +172,19 @@ def test_PSE_weight(): np.testing.assert_allclose(P_eb3[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='Weighted PSE found EB cross-power') - assert_raises(ValueError, pse.estimate, g1, g2, weight_EE=8) - assert_raises(ValueError, pse.estimate, g1, g2, weight_BB='yes') + assert_raises(TypeError, pse.estimate, g1, g2, weight_EE=8) + assert_raises(TypeError, pse.estimate, g1, g2, weight_BB='yes') + + # If N is fairly small, then can get zeros in the counts, which raises an error + array_size = 5 + g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, + rng=galsim.BaseDeviate(rand_seed)) + pse = galsim.pse.PowerSpectrumEstimator(N=array_size, + sky_size_deg=array_size*grid_spacing, + nbin=n_ell) + with assert_raises(galsim.GalSimError): + pse.estimate(g1,g2) + if __name__ == "__main__": diff --git a/tests/test_random.py b/tests/test_random.py index dc207a27f63..84e68aff7e1 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -243,6 +243,9 @@ def test_uniform(): assert_raises(TypeError, galsim.UniformDeviate, list()) assert_raises(TypeError, galsim.UniformDeviate, set()) + assert_raises(TypeError, u.seed, '123') + assert_raises(TypeError, u.seed, 12.3) + @timer def test_gaussian(): @@ -389,6 +392,8 @@ def test_gaussian(): assert_raises(TypeError, galsim.GaussianDeviate, list()) assert_raises(TypeError, galsim.GaussianDeviate, set()) + assert_raises(ValueError, galsim.GaussianDeviate, testseed, mean=1, sigma=-1) + @timer def test_binomial(): @@ -1491,13 +1496,6 @@ def test_multiprocess(): """Test that the same random numbers are generated in single-process and multi-process modes. """ from multiprocessing import Process, Queue, current_process - # Workaround for a bug in python 2.6. The bug is that sys.stdin can be double closed if - # multiprocessing is used within something that already uses multiprocessing. - # Specifically, if we are using nosetests with multiple processes. - # See http://bugs.python.org/issue5313 for more info. - if sys.version_info < (2,7): - sys.stdin.close() - sys.stdin = open(os.devnull) def generate_list(seed): """Given a particular seed value, generate a list of random numbers. diff --git a/tests/test_real.py b/tests/test_real.py index 852ba1342de..8f99061a893 100644 --- a/tests/test_real.py +++ b/tests/test_real.py @@ -49,6 +49,104 @@ def moments_to_ellip(mxx, myy, mxy): sig = (mxx*myy - mxy**2)**(0.25) return e1, e2, sig +@timer +def test_real_galaxy_catalog(): + """Test basic operations of RealGalaxyCatalog""" + + # Start with the test RGC that we will use throughout this test file. + rgc = galsim.RealGalaxyCatalog(file_name=catalog_file, dir=image_dir) + + assert len(rgc) == rgc.nobjects == rgc.getNObjects() == 2 + assert rgc.file_name == os.path.join(image_dir, catalog_file) + assert rgc.image_dir == image_dir + + print('sample = ',rgc.sample) + print('ident = ',rgc.ident) + assert rgc.sample == None + assert len(rgc.ident) == 2 + + gal1 = rgc.getGalImage(0) + assert isinstance(gal1, galsim.Image) + psf1 = rgc.getPSFImage(0) + assert isinstance(psf1, galsim.Image) + noise, scale, var = rgc.getNoiseProperties(0) + assert noise is None # No noise images for the test catalog. + print('noise info = ',noise, scale, var) + np.testing.assert_almost_equal(scale, 0.03) + assert var < 1.e-5 + + assert rgc.getIndexForID(100533) == 0 + + # With _nobjects_only=True, it doesn't finish loadin + rgc2 = galsim.RealGalaxyCatalog(file_name=catalog_file, dir=image_dir, _nobjects_only=True) + assert len(rgc2) == rgc2.nobjects == rgc2.getNObjects() == 2 + assert rgc2.file_name == os.path.join(image_dir, catalog_file) + assert rgc2.image_dir == image_dir + assert rgc2.sample == None + with assert_raises(AttributeError): + rgc2.ident + with assert_raises(AttributeError): + rgc2.getGalImage(0) + with assert_raises(AttributeError): + rgc2.getPSFImage(0) + + assert_raises(TypeError, galsim.RealGalaxyCatalog, catalog_file, dir=image_dir, sample='25.2') + assert_raises(ValueError, galsim.RealGalaxyCatalog, sample='23.2') + assert_raises(ValueError, galsim.RealGalaxyCatalog, sample='23.2') + assert_raises(OSError, galsim.RealGalaxyCatalog, file_name='invalid.fits') + assert_raises(ValueError, rgc.getIndexForID, 1234) + assert_raises(IndexError, rgc.getGalImage, 5) + assert_raises(IndexError, rgc.getPSFImage, 5) + assert_raises(IndexError, rgc.getNoiseProperties, 5) + + # The test catalog doesn't have noise information, so we use this hack to test the + # behavior of another IndexError that would be raised in the usual case. + rgc.noise_file_name = [ 'none' for i in rgc.ident ] + assert_raises(IndexError, rgc.getNoiseProperties, 5) + + assert_raises(OSError, galsim.RealGalaxyCatalog, dir=image_dir) + assert_raises(OSError, galsim.RealGalaxyCatalog, file_name='25.2.fits', dir=image_dir) + assert_raises(OSError, galsim.RealGalaxyCatalog, file_name='23.5.fits', dir='invalid') + + # Test the catalog used by a few demos. + rgc = galsim.RealGalaxyCatalog(sample='23.5_example', dir='../examples/data') + assert(rgc.sample == '23.5_example') + assert(len(rgc.ident) == 100) + + # Now test out the real ones. But if they aren't installed, abort gracefully. + try: + rgc = galsim.RealGalaxyCatalog(sample='25.2') + except OSError: + print('Skipping tests of 25.2 sample, since not downloaded.') + else: + print('sample = ',rgc.sample) + print('len(ident) = ',len(rgc.ident)) + assert(rgc.sample == '25.2') + assert(len(rgc.ident) == 87798) + + try: + rgc = galsim.RealGalaxyCatalog(sample='23.5') + except OSError: + print('Skipping tests of 25.2 sample, since not downloaded.') + else: + print('sample = ',rgc.sample) + print('len(ident) = ',len(rgc.ident)) + assert(rgc.sample == '23.5') + assert(len(rgc.ident) == 56062) + + # Check error message if COSMOS galaxies aren't in share_dir. Do this by temporarily + # changing share_dir value. + save = galsim.meta_data.share_dir + galsim.meta_data.share_dir = image_dir + try: + rgc = galsim.RealGalaxyCatalog(sample='23.5') + except OSError as err: + assert 'Run the program galsim_download_cosmos -s 23.5' in str(err) + else: + assert False, "Automatic sample=23.5 should have failed with share_dir = " + image_dir + galsim.meta_data.share_dir = save + + @timer def test_real_galaxy_ideal(): """Test accuracy of various calculations with fake Gaussian RealGalaxy vs. ideal expectations""" @@ -72,11 +170,15 @@ def test_real_galaxy_ideal(): # or when trying to specify the galaxy too many ways rg_1 = galsim.RealGalaxy(rgc, index = ind_fake, rng = galsim.BaseDeviate(1234)) rg_2 = galsim.RealGalaxy(rgc, random=True) + assert_raises(TypeError, galsim.RealGalaxy, rgc, index=ind_fake, rng='foo') - assert_raises(AttributeError, galsim.RealGalaxy, rgc, index=ind_fake, id=0) - assert_raises(AttributeError, galsim.RealGalaxy, rgc, index=ind_fake, random=True) - assert_raises(AttributeError, galsim.RealGalaxy, rgc, id=0, random=True) - assert_raises(AttributeError, galsim.RealGalaxy, rgc) + assert_raises(TypeError, galsim.RealGalaxy, rgc) + assert_raises(TypeError, galsim.RealGalaxy, rgc, index=ind_fake, flux=12, flux_rescale=2) + + assert_raises(ValueError, galsim.RealGalaxy, rgc, index=ind_fake, id=0) + assert_raises(ValueError, galsim.RealGalaxy, rgc, index=ind_fake, random=True) + assert_raises(ValueError, galsim.RealGalaxy, rgc, id=0, random=True) + # Different RNGs give different random galaxies. rg_3 = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(12345)) rg_4 = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(67890)) @@ -343,6 +445,14 @@ def test_crg_roundtrip(): im_f814w_mom.observed_shape.g2, rtol=0, atol=1e-4) + # Check some errors + cats = [f606w_cat, f814w_cat] + assert_raises(TypeError, galsim.ChromaticRealGalaxy, real_galaxy_catalogs=cats) + assert_raises(TypeError, galsim.ChromaticRealGalaxy, cats, index=3, id=4) + assert_raises(TypeError, galsim.ChromaticRealGalaxy, cats, index=3, random=True) + assert_raises(TypeError, galsim.ChromaticRealGalaxy, cats, id=4, random=True) + assert_raises(TypeError, galsim.ChromaticRealGalaxy, cats, random=True, rng='foo') + @timer def test_crg_roundtrip_larger_target_psf(): @@ -687,6 +797,9 @@ def check_crg_noise(n_sed, n_im, n_trial, tol): print("Convolving by output PSF") objs = [galsim.Convolve(crg, out_PSF) for crg in crgs] + with assert_raises(galsim.GalSimError): + noise = objs[0].noise # Invalid before drawImage is called + print("Drawing through output filter") out_imgs = [obj.drawImage(visband, nx=30, ny=30, scale=0.1) for obj in objs] @@ -761,6 +874,7 @@ def test_crg_noise_pad(): if __name__ == "__main__": + test_real_galaxy_catalog() test_real_galaxy_ideal() test_real_galaxy_saved() test_real_galaxy_makeFromImage() diff --git a/tests/test_scene.py b/tests/test_scene.py index 8aafbd6adea..90580db7c2b 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -41,52 +41,105 @@ def test_cosmos_basic(): # Initialize one that doesn't exclude failures. It should be >= the previous one in length. cat2 = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', dir=datapath, exclusion_level='none') - assert cat2.nobjects>=cat.nobjects + assert cat2.nobjects >= cat.nobjects + assert len(cat2) == cat2.nobjects == cat2.getNTot() == 100 + assert len(cat) == cat.nobjects < cat.getNTot() + + # Check other exclusion levels: + cat3 = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', + dir=datapath, exclusion_level='bad_stamp') + assert len(cat3) == 97 + cat4 = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', + dir=datapath, exclusion_level='bad_fits') + assert len(cat4) == 100 # no bad fits in the example file as it happens. + cat5 = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', + dir=datapath, exclusion_level='marginal') + assert len(cat5) == 96 # this is actually the default, so == cat + assert cat == cat5 + + # Check the 25.2 exclusions. We don't have a 25.2 catalog available in Travis runs, so + # mock up the example catalog as though it were 25.2 + # Also check the min/max hlr and flux options. + cat2.use_sample = '25.2' + hlr = cat2.param_cat['hlr'][:,0] + flux = cat2.param_cat['flux'][:,0] + print("Full range of hlr = ", np.min(hlr), np.max(hlr)) + print("Full range of flux = ", np.min(flux), np.max(flux)) + cat2._apply_exclusion('marginal', min_hlr=0.2, max_hlr=2, min_flux=50, max_flux=5000) + print("Size of catalog with hlr and flux exclusions == ",len(cat2)) + assert len(cat2) == 47 # Check for reasonable exceptions when initializing. # Can't find data (wrong directory). - with assert_raises(IOError): + with assert_raises(OSError): galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits') # Try making galaxies - gal_real = cat2.makeGalaxy(index=0,gal_type='real',chromatic=False) - if not isinstance(gal_real, galsim.RealGalaxy): - raise TypeError("COSMOS Catalog makeGalaxy routine does not return an instance of " - "'galsim.RealGalaxy'") + gal_real = cat.makeGalaxy(index=0,gal_type='real',chromatic=False) + assert isinstance(gal_real, galsim.RealGalaxy) gal_param = cat.makeGalaxy(index=10,gal_type='parametric',chromatic=True) - if not isinstance(gal_param, galsim.ChromaticObject): - raise TypeError("COSMOS Catalog makeGalaxy routine does not return an instance of " - "'galsim.ChromaticObject' for parametric galaxies") + assert isinstance(gal_param, galsim.ChromaticObject) + + # Second time through, don't make the bandpass. + bp = cat._bandpass + sed = cat._sed + assert bp is not None + gal_param2 = cat.makeGalaxy(index=13, gal_type='parametric', chromatic=True) + assert isinstance(gal_param2, galsim.ChromaticObject) + assert gal_param != gal_param2 + assert cat._bandpass is bp # Not just ==. "is" means the same object. + assert cat._sed is sed + + # So far, we've made a bulge+disk and a disky Sersic. + # Do two more to run through two more paths in the code. + gal_param3 = cat.makeGalaxy(index=50, gal_type='parametric', chromatic=True) + gal_param4 = cat.makeGalaxy(index=67, gal_type='parametric', chromatic=True) gal_real_list = cat.makeGalaxy(index=[3,6],gal_type='real',chromatic=False) for gal_real in gal_real_list: - if not isinstance(gal_real, galsim.RealGalaxy): - raise TypeError("COSMOS Catalog makeGalaxy routine does not return a list of instances " - "of 'galsim.RealGalaxy'") + assert isinstance(gal_real, galsim.RealGalaxy) gal_param_list = cat.makeGalaxy(index=[4,7],gal_type='parametric',chromatic=False) for gal_param in gal_param_list: - if not isinstance(gal_param, galsim.GSObject): - raise TypeError("COSMOS Catalog makeGalaxy routine does not return a list of instances " - "of 'galsim.GSObect'") + assert isinstance(gal_param, galsim.GSObject) # Check for parametric catalog - cat_param = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example_fits.fits', + # Can give either the regular name or the _fits name. + cat_param = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', dir=datapath, use_real=False) + cat_param2 = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example_fits.fits', + dir=datapath, use_real=False) + assert cat_param2 == cat_param # Try making galaxies gal = cat_param.makeGalaxy(index=1) - if not isinstance(gal, galsim.GSObject): - raise TypeError("COSMOS Catalog makeGalaxy routine does not return an instance of " - "'galsim.GSObject when loaded from a fits file.") + assert isinstance(gal, galsim.GSObject) gal_list = cat_param.makeGalaxy(index=[2,3]) for gal in gal_list: - if not isinstance(gal, galsim.GSObject): - raise TypeError("COSMOS Catalog makeGalaxy routine does not return a list of instances " - "of 'galsim.GSObject when loaded from a fits file.") + assert isinstance(gal, galsim.GSObject) + # Check sersic_prec option. + sersic0 = cat_param.makeGalaxy(index=59, sersic_prec=0) + np.testing.assert_almost_equal(sersic0.original.n, 1.14494567108) + sersic1 = cat_param.makeGalaxy(index=59, sersic_prec=0.05) # The default. + np.testing.assert_almost_equal(sersic1.original.n, 1.15) + sersic2 = cat_param.makeGalaxy(index=59, sersic_prec=0.1) + np.testing.assert_almost_equal(sersic2.original.n, 1.1) + sersic3 = cat_param.makeGalaxy(index=59, sersic_prec=0.5) + np.testing.assert_almost_equal(sersic3.original.n, 1.0) + + assert_raises(TypeError, galsim.COSMOSCatalog, 'real_galaxy_catalog_23.5_example.fits', + dir=datapath, sample='23.5') + assert_raises(ValueError, galsim.COSMOSCatalog, sample='invalid') + + assert_raises(ValueError, cat_param.makeGalaxy, gal_type='real') + assert_raises(ValueError, cat_param.makeGalaxy, gal_type='invalid') + assert_raises(ValueError, cat.makeGalaxy, gal_type='invalid') + assert_raises(TypeError, cat_param.makeGalaxy, rng='invalid') + + assert_raises(NotImplementedError, cat.makeGalaxy, gal_type='real', chromatic=True) @timer def test_cosmos_fluxnorm(): @@ -134,6 +187,9 @@ def test_cosmos_fluxnorm(): assert hasattr(gal1_param.shear(g1=0.05).original, 'index'), \ 'Bulge+disk galaxy does not retain index information after transformation' + assert_raises(ValueError, galsim.COSMOSCatalog, 'real_galaxy_catalog_23.5_example.fits', + dir=datapath, exclusion_level='invalid') + @timer def test_cosmos_random(): """Check the random object functionality of the COSMOS catalog.""" @@ -148,8 +204,8 @@ def test_cosmos_random(): dir=datapath) cat_param = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', dir=datapath, use_real=False) - assert hasattr(cat, 'real_cat') - assert not hasattr(cat_param, 'real_cat') + assert cat_param.real_cat is None + assert cat.real_cat is not None # Check for exception handling if bad inputs given for the random functionality. assert_raises(ValueError, cat.selectRandomIndex, 0) @@ -171,99 +227,152 @@ def test_cosmos_random(): # to probabilistically select galaxies. np.testing.assert_almost_equal(np.mean(wtrand), wavg_weight_val,3, err_msg='Average weight for random sample is wrong') - # From here on we need to suppress some warnings that come from calling cat_param. It doesn't - # have weights, so it does unweighted selection, which emits a warning. We know about this and - # don't want to spit out the warning each time, so suppress it. - import warnings - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + + # The example catalog doesn't have weights, so it does unweighted selection, which emits a + # warning. We know about this and want to ignore it here. + with assert_warns(galsim.GalSimWarning): randind = cat_param.selectRandomIndex(30000, rng=galsim.BaseDeviate(1234)) - wtrand = cat.real_cat.weight[cat.orig_index[randind]] / \ - np.max(cat.real_cat.weight[cat.orig_index]) - # The average value of wtrand should be avg_weight_val, since we did not do a weighted - # selection. - np.testing.assert_almost_equal(np.mean(wtrand), avg_weight_val,3, - err_msg='Average weight for random sample is wrong') - - # Check for consistency of randoms with same random seed. Do this both for the weighted and the - # unweighted calculation. - # Check for inconsistency of randoms with different random seed, or same seed but without/with - # weighting. - rng1 = galsim.BaseDeviate(1234) - rng2 = galsim.BaseDeviate(1234) - ind1 = cat.selectRandomIndex(10, rng=rng1) - ind2 = cat.selectRandomIndex(10, rng=rng2) - np.testing.assert_array_equal(ind1,ind2, - err_msg='Different random indices selected with same seed!') + wtrand = cat.real_cat.weight[cat.orig_index[randind]] / \ + np.max(cat.real_cat.weight[cat.orig_index]) + # The average value of wtrand should be avg_weight_val, since we did not do a weighted + # selection. + np.testing.assert_almost_equal(np.mean(wtrand), avg_weight_val,3, + err_msg='Average weight for random sample is wrong') + + # Check for consistency of randoms with same random seed. Do this both for the weighted and the + # unweighted calculation. + # Check for inconsistency of randoms with different random seed, or same seed but without/with + # weighting. + rng1 = galsim.BaseDeviate(1234) + rng2 = galsim.BaseDeviate(1234) + ind1 = cat.selectRandomIndex(10, rng=rng1) + ind2 = cat.selectRandomIndex(10, rng=rng2) + np.testing.assert_array_equal(ind1,ind2, + err_msg='Different random indices selected with same seed!') + with assert_warns(galsim.GalSimWarning): ind1p = cat_param.selectRandomIndex(10, rng=rng1) + with assert_warns(galsim.GalSimWarning): ind2p = cat_param.selectRandomIndex(10, rng=rng2) - np.testing.assert_array_equal(ind1p,ind2p, - err_msg='Different random indices selected with same seed!') - rng3 = galsim.BaseDeviate(5678) - ind3 = cat.selectRandomIndex(10, rng=rng3) + np.testing.assert_array_equal(ind1p,ind2p, + err_msg='Different random indices selected with same seed!') + rng3 = galsim.BaseDeviate(5678) + ind3 = cat.selectRandomIndex(10, rng=rng3) + with assert_warns(galsim.GalSimWarning): ind3p = cat_param.selectRandomIndex(10) # initialize RNG based on time - assert_raises(AssertionError, np.testing.assert_array_equal, ind1, ind1p) - assert_raises(AssertionError, np.testing.assert_array_equal, ind1, ind3) - assert_raises(AssertionError, np.testing.assert_array_equal, ind1p, ind3p) - - # Finally, make sure that directly calling selectRandomIndex() gives the same random ones as - # makeGalaxy(). We'll do one real object because they are slower, and multiple parametric (just - # to make sure that the multi-object selection works consistently). - use_seed = 567 - obj = cat.makeGalaxy(rng=galsim.BaseDeviate(use_seed)) - ind = cat.selectRandomIndex(1, rng=galsim.BaseDeviate(use_seed)) - obj_2 = cat.makeGalaxy(ind) - # Note: for real galaxies we cannot require that obj==obj_2, just that obj.index==obj_2.index. - # That's because we want to make sure the same galaxy is being randomly selected, but we cannot - # require that noise padding be the same, given the inconsistency in how the BaseDeviates are - # used in the above cases. - assert obj.index==obj_2.index,'makeGalaxy selects random objects inconsistently' - - n_random = 3 + assert_raises(AssertionError, np.testing.assert_array_equal, ind1, ind1p) + assert_raises(AssertionError, np.testing.assert_array_equal, ind1, ind3) + assert_raises(AssertionError, np.testing.assert_array_equal, ind1p, ind3p) + + # Finally, make sure that directly calling selectRandomIndex() gives the same random ones as + # makeGalaxy(). We'll do one real object because they are slower, and multiple parametric (just + # to make sure that the multi-object selection works consistently). + use_seed = 567 + obj = cat.makeGalaxy(rng=galsim.BaseDeviate(use_seed)) + ind = cat.selectRandomIndex(1, rng=galsim.BaseDeviate(use_seed)) + obj_2 = cat.makeGalaxy(ind) + # Note: for real galaxies we cannot require that obj==obj_2, just that obj.index==obj_2.index. + # That's because we want to make sure the same galaxy is being randomly selected, but we cannot + # require that noise padding be the same, given the inconsistency in how the BaseDeviates are + # used in the above cases. + assert obj.index==obj_2.index,'makeGalaxy selects random objects inconsistently' + + n_random = 3 + with assert_warns(galsim.GalSimWarning): + # Warns because we aren't using weights objs = cat_param.makeGalaxy(rng=galsim.BaseDeviate(use_seed), n_random=n_random) + with assert_warns(galsim.GalSimWarning): inds = cat_param.selectRandomIndex(n_random, rng=galsim.BaseDeviate(use_seed)) - objs_2 = cat_param.makeGalaxy(inds) - for i in range(n_random): - # With parametric objects there is no noise padding, so we can require completely identical - # GSObjects (not just equal indices). - assert objs[i]==objs_2[i],'makeGalaxy selects random objects inconsistently' - - # Finally, check for consistency with random object selected from RealGalaxyCatalog. For this - # case, we need to make another COSMOSCatalog that does not flag the bad objects. - use_seed=31415 - cat = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', - dir=datapath, exclusion_level='none') - rgc = galsim.RealGalaxyCatalog(file_name='real_galaxy_catalog_23.5_example.fits', - dir=datapath) - ind_cc = cat.selectRandomIndex(1, rng=galsim.BaseDeviate(use_seed)) - foo = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(use_seed)) - ind_rgc = foo.index - assert ind_cc==ind_rgc,\ - 'Different weighted random index selected from COSMOSCatalog and RealGalaxyCatalog' - # Also check for the unweighted case. Just remove that info from the catalogs and redo the - # test. - del cat.real_cat - del rgc.weight + objs_2 = cat_param.makeGalaxy(inds) + for i in range(n_random): + # With parametric objects there is no noise padding, so we can require completely identical + # GSObjects (not just equal indices). + assert objs[i]==objs_2[i],'makeGalaxy selects random objects inconsistently' + + # Finally, check for consistency with random object selected from RealGalaxyCatalog. For this + # case, we need to make another COSMOSCatalog that does not flag the bad objects. + use_seed=31415 + cat = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', + dir=datapath, exclusion_level='none') + rgc = galsim.RealGalaxyCatalog(file_name='real_galaxy_catalog_23.5_example.fits', + dir=datapath) + ind_cc = cat.selectRandomIndex(1, rng=galsim.BaseDeviate(use_seed)) + foo = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(use_seed)) + ind_rgc = foo.index + assert ind_cc==ind_rgc,\ + 'Different weighted random index selected from COSMOSCatalog and RealGalaxyCatalog' + + # Also check for the unweighted case. Just remove that info from the catalogs and redo the + # test. + cat.real_cat = None + del rgc.weight + with assert_warns(galsim.GalSimWarning): ind_cc = cat.selectRandomIndex(1, rng=galsim.BaseDeviate(use_seed)) - foo = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(use_seed)) - ind_rgc = foo.index - assert ind_cc==ind_rgc,\ - 'Different unweighted random index selected from COSMOSCatalog and RealGalaxyCatalog' - - # Check that setting _n_rng_calls properly tracks the RNG calls for n_random=1 and >1. - test_seed = 123456 - ud = galsim.UniformDeviate(test_seed) + foo = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(use_seed)) + ind_rgc = foo.index + assert ind_cc==ind_rgc,\ + 'Different unweighted random index selected from COSMOSCatalog and RealGalaxyCatalog' + + # Check that setting _n_rng_calls properly tracks the RNG calls for n_random=1 and >1. + test_seed = 123456 + ud = galsim.UniformDeviate(test_seed) + with assert_warns(galsim.GalSimWarning): obj, n_rng_calls = cat.selectRandomIndex(1, rng=ud, _n_rng_calls=True) - ud2 = galsim.UniformDeviate(test_seed) - ud2.discard(n_rng_calls) - assert ud()==ud2(), '_n_rng_calls kwarg did not give proper tracking of RNG calls' - ud = galsim.UniformDeviate(test_seed) + ud2 = galsim.UniformDeviate(test_seed) + ud2.discard(n_rng_calls) + assert ud()==ud2(), '_n_rng_calls kwarg did not give proper tracking of RNG calls' + ud = galsim.UniformDeviate(test_seed) + with assert_warns(galsim.GalSimWarning): obj, n_rng_calls = cat.selectRandomIndex(17, rng=ud, _n_rng_calls=True) - ud2 = galsim.UniformDeviate(test_seed) - ud2.discard(n_rng_calls) - assert ud()==ud2(), '_n_rng_calls kwarg did not give proper tracking of RNG calls' + ud2 = galsim.UniformDeviate(test_seed) + ud2.discard(n_rng_calls) + assert ud()==ud2(), '_n_rng_calls kwarg did not give proper tracking of RNG calls' + + # Invalid to both privide index and ask for random selection + with assert_raises(galsim.GalSimIncompatibleValuesError): + cat_param.makeGalaxy(index=(11,13,17), n_random=3) + +@timer +def test_cosmos_deep(): + """Test the deep option of makeGalaxy""" + + cat = galsim.COSMOSCatalog(file_name='real_galaxy_catalog_23.5_example.fits', dir=datapath) + + # Pick a random galaxy + # Turn off noise padding to make the comparisons more reliable. + gal_shallow = cat.makeGalaxy(index=17, gal_type='real', noise_pad_size=0) + print('gal_shallow = ',gal_shallow) + shallow_flux = gal_shallow.flux + shallow_hlr = gal_shallow.calculateHLR() + print('flux = ',shallow_flux) + print('hlr = ',shallow_hlr) + + gal_deep = cat.makeGalaxy(index=17, gal_type='real', deep=True, noise_pad_size=0) + print('gal_deep = ',gal_deep) + deep_flux = gal_deep.flux + deep_hlr = gal_deep.calculateHLR() + print('flux = ',deep_flux) + print('hlr = ',deep_hlr) + + # Deep galaxy is fainter and smaller. + np.testing.assert_almost_equal(deep_flux / shallow_flux, 10.**(-0.6)) + np.testing.assert_almost_equal(deep_hlr / shallow_hlr, 0.6) + + # With samples other than 23.5, it raises a warning and doesn't do any scaling. + cat.use_sample = '25.2' + with assert_warns(galsim.GalSimWarning): + gal_not_deep = cat.makeGalaxy(index=17, gal_type='real', deep=True, noise_pad_size=0) + assert gal_not_deep.flux == shallow_flux + assert gal_not_deep.calculateHLR() == shallow_hlr + + cat.use_sample = 'user_defined' + with assert_warns(galsim.GalSimWarning): + gal_not_deep = cat.makeGalaxy(index=17, gal_type='real', deep=True, noise_pad_size=0) + assert gal_not_deep.flux == shallow_flux + assert gal_not_deep.calculateHLR() == shallow_hlr + if __name__ == "__main__": test_cosmos_basic() test_cosmos_fluxnorm() test_cosmos_random() + test_cosmos_deep() diff --git a/tests/test_second_kick.py b/tests/test_second_kick.py index 8fc9b6d961e..dd4370e1637 100644 --- a/tests/test_second_kick.py +++ b/tests/test_second_kick.py @@ -29,7 +29,6 @@ def test_init(): """Test generation of SecondKick profiles """ obscuration = 0.5 - bigGSP = galsim.GSParams(maximum_fft_size=8192) if __name__ == '__main__': lams = [300.0, 500.0, 1100.0] @@ -46,7 +45,6 @@ def test_init(): t0 = time.time() kwargs = {'lam':lam, 'r0':r0, 'kcrit':kcrit, 'diam':4.0} print(kwargs) - kwargs['gsparams'] = bigGSP sk = galsim.SecondKick(flux=2.2, **kwargs) t1 = time.time() @@ -119,11 +117,27 @@ def test_limiting_cases(): rtol=1e-3, atol=1e-4) + # Normally, one wouldn't use SecondKick.xValue, since it does a real-space convolution, + # so it's slow. But we do allow it, so test it here. + import time + t0 = time.time() + xv_2k = sk.xValue(0,0) + print("xValue(0,0) = ",xv_2k) + t1 = time.time() + # The VonKarman * Airy xValue is much slower still, so don't do that. + # Instead compare it to the 'sb' image. + xv_image = limiting_case.drawImage(nx=1,ny=1,method='sb',scale=0.1)(1,1) + print('from image ',xv_image) + t2 = time.time() + print('t = ',t1-t0, t2-t1) + np.testing.assert_almost_equal(xv_2k, xv_image, decimal=3) + # kcrit=inf sk = galsim.SecondKick(lam, r0, diam, obscuration, kcrit=np.inf) limiting_case = galsim.Airy(lam=lam, diam=diam, obscuration=obscuration) for k in [0.0, 0.1, 0.3, 1.0, 3.0, 10.0, 20.0]: + print(sk.kValue(0, k).real, limiting_case.kValue(0, k).real) np.testing.assert_allclose( sk.kValue(0, k).real, limiting_case.kValue(0, k).real, diff --git a/tests/test_sed.py b/tests/test_sed.py index 990900ad176..77cc2d588ab 100644 --- a/tests/test_sed.py +++ b/tests/test_sed.py @@ -184,10 +184,27 @@ def test_SED_add(): err_msg="Wrong sum in SED.__add__") np.testing.assert_almost_equal(c.redshift, a.redshift, 10, err_msg="Wrong redshift in SED sum") + # Adding together two SEDs with different redshifts should fail. d = b.atRedshift(0.1) - with assert_raises(ValueError): - b.__add__(d) + with assert_raises(galsim.GalSimIncompatibleValuesError): + b + d + with assert_raises(galsim.GalSimIncompatibleValuesError): + d + b + + # Can't add incompatible spectral types + a = a.atRedshift(0) + b = a.atRedshift(0) + c = galsim.SED(2.0, 'nm', '1') + with assert_raises(galsim.GalSimIncompatibleValuesError): + a + c + with assert_raises(galsim.GalSimIncompatibleValuesError): + c + a + with assert_raises(galsim.GalSimIncompatibleValuesError): + b + c + with assert_raises(galsim.GalSimIncompatibleValuesError): + c + b + @timer @@ -326,6 +343,16 @@ def test_SED_div(): np.testing.assert_almost_equal(d(x), a(x)/4.2/2/e(x), 10, err_msg="Found wrong value in SED.__div__") + # Can't divide by spectral SED + with assert_raises(galsim.GalSimSEDError): + a0_lt / a0_fn + with assert_raises(galsim.GalSimSEDError): + a0_fn / a0_lt + with assert_raises(galsim.GalSimSEDError): + e / a0_lt + with assert_raises(galsim.GalSimSEDError): + e / a0_fn + @timer def test_SED_atRedshift(): @@ -378,9 +405,19 @@ def __init__(self, wave_list): np.testing.assert_equal(wave_list, c.wave_list) np.testing.assert_equal(blue_limit, c.blue_limit) np.testing.assert_equal(red_limit, c.red_limit) - with assert_raises(RuntimeError): + with assert_raises(galsim.GalSimError): galsim.utilities.combine_wave_list(a, d) + # Degenerate case works. + sed = galsim.SED('CWW_Scd_ext.sed', wave_type='nm', flux_type='flambda') + wave_list, blue_limit, red_limit = galsim.utilities.combine_wave_list(sed) + np.testing.assert_equal(wave_list, sed.wave_list) + np.testing.assert_equal(blue_limit, sed.blue_limit) + np.testing.assert_equal(red_limit, sed.red_limit) + + # Doesn't know about our A class though. + assert_raises(TypeError, galsim.utilities.combine_wave_list, a) + @timer def test_SED_roundoff_guard(): @@ -414,7 +451,7 @@ def test_SED_init(): assert_raises(TypeError, galsim.SED, spec=lambda w:1.0, flux_type='bar') assert_raises(TypeError, galsim.SED, spec=lambda w:1.0) assert_raises(ValueError, galsim.SED, spec='wave', wave_type=units.Hz, flux_type='2') - assert_raises(ValueError, galsim.SED, 1.0, 'nm', 'fphotons') + assert_raises(galsim.GalSimSEDError, galsim.SED, 1.0, 'nm', 'fphotons') # These should succeed. galsim.SED(spec='wave', wave_type='nm', flux_type='flambda') galsim.SED(spec='wave/wave', wave_type='nm', flux_type='flambda') @@ -435,7 +472,7 @@ def test_SED_init(): sed = galsim.SED(galsim.LookupTable(foo,foo), wave_type=units.Hz, flux_type='flambda') assert_raises(ValueError, sed, 0.5) assert_raises(ValueError, sed, 12.0) - assert_raises(TypeError, galsim.SED, '1', 'nm', units.erg/units.s) + assert_raises(ValueError, galsim.SED, '1', 'nm', units.erg/units.s) assert_raises(ValueError, galsim.SED, '1', 'nm', '2') # Check a few valid calls for when fast=False @@ -466,25 +503,40 @@ def test_SED_withFlux(): for fast in [True, False]: a = galsim.SED(os.path.join(sedpath, 'CWW_E_ext.sed'), wave_type='ang', flux_type='flambda', fast=fast) + b = galsim.SED('wave', wave_type='nm', flux_type='fphotons') if z != 0: a = a.atRedshift(z) + b = b.atRedshift(z) a = a.withFlux(1.0, rband) + b = b.withFlux(1.0, rband) np.testing.assert_array_almost_equal(a.calculateFlux(rband), 1.0, 5, "Setting SED flux failed.") + np.testing.assert_array_almost_equal(b.calculateFlux(rband), 1.0, 5, + "Setting SED flux failed.") # Should be equivalent to multiplying an SED * Bandpass and computing the # "bolometric" flux. ab = a * rband + bb = b * rband bolo_bp = galsim.Bandpass('1', blue_limit=ab.blue_limit, red_limit=ab.red_limit, wave_type='nm') np.testing.assert_array_almost_equal(ab.calculateFlux(bolo_bp), 1.0, 5, "Calculating SED flux from sed * bp failed.") + np.testing.assert_array_almost_equal(bb.calculateFlux(bolo_bp), 1.0, 5, + "Calculating SED flux from sed * bp failed.") # Multiplying in the other order also works. ba = rband * a np.testing.assert_array_almost_equal(ba.calculateFlux(bolo_bp), 1.0, 5, "Calculating SED flux from sed * bp failed.") + # Invalid for dimensionless SED + flat = galsim.SED(2.0, 'nm', '1') + with assert_raises(galsim.GalSimSEDError): + flat.withFlux(1.0, rband) + with assert_raises(galsim.GalSimSEDError): + flat.calculateFlux(rband) + @timer def test_SED_withFluxDensity(): @@ -505,6 +557,11 @@ def test_SED_withFluxDensity(): np.testing.assert_array_almost_equal( a(500), 3.0, 5, "Setting SED flux density failed.") + # Invalid for dimensionless SED + flat = galsim.SED(2.0, 'nm', '1') + with assert_raises(galsim.GalSimSEDError): + flat.withFluxDensity(1.0, 500) + @timer def test_SED_calculateMagnitude(): @@ -566,6 +623,18 @@ def test_SED_calculateMagnitude(): thresh = 0.3 if filter_name == 'u' else 0.1 assert (abs((AB_mag - vega_mag) - conversion) < thresh) + # Invalid for dimensionless SED + flat = galsim.SED(2.0, 'nm', '1') + with assert_raises(galsim.GalSimSEDError): + flat.withMagnitude(24.0, bandpass) + + # Zeropoint needs to be set. + bp = galsim.Bandpass(galsim.LookupTable([1,2,3,4,5], [1,2,3,4,5]), 'nm') + with assert_raises(galsim.GalSimError): + sed.withMagnitude(24.0, bp) + with assert_raises(galsim.GalSimError): + sed.calculateMagnitude(bp) + @timer def test_redshift_calculateFlux(): @@ -582,6 +651,16 @@ def test_redshift_calculateFlux(): else: print('z = {} flux = {}'.format(z, sedz.calculateFlux(bp))) + # All analytic has easy to check answers + sed = galsim.SED('(wave/500)**2', wave_type='nm', flux_type='fphotons') + bp = galsim.Bandpass('1', blue_limit=500, red_limit=1000, wave_type='nm') + + for z in [0, 0.19, 0.2, 0.21, 2.5, 2.99, 3, 3.01, 4]: + sedz = sed.atRedshift(z) + f = sedz.calculateFlux(bp) + print('z = {} flux = {}'.format(z, f)) + np.testing.assert_almost_equal(f, 7./3. * 500 / (1.+z)**2) + @timer def test_SED_calculateDCRMomentShifts(): @@ -670,6 +749,10 @@ def test_SED_calculateSeeingMomentRatio(): den = np.trapz(sed(waves), waves) np.testing.assert_almost_equal(relative_size, num/den, 4) + # Invalid for dimensionless SED + flat = galsim.SED(2.0, 'nm', '1') + with assert_raises(galsim.GalSimSEDError): + flat.calculateSeeingMomentRatio(bandpass) @timer def test_SED_sampleWavelength(): @@ -855,6 +938,41 @@ def test_thin(): print("realized error = ",(flux-thin_flux)/flux) assert np.abs(thin_err) < err, "Thinned SED failed accuracy goal, w/ range shrinkage." + assert_raises(ValueError, s.thin, rel_err=-0.5) + assert_raises(ValueError, s.thin, rel_err=1.5) + # These errors aren't accessible from the SED or Bandpass calls. + assert_raises(ValueError, galsim.utilities.thin_tabulated_values, + s.wave_list[3:], s._spec.getVals()) + assert_raises(ValueError, galsim.utilities.thin_tabulated_values, + s.wave_list[-1::-1], s._spec.getVals()) + + # Check some pathalogical spectra to stress the thinning algorithm + s = galsim.SED(galsim.LookupTable(range(6), [0,0,1,1,0,0]),'nm','1').thin() + print('s = ',s) + np.testing.assert_equal(s.wave_list, range(1,5)) + + s = galsim.SED(galsim.LookupTable(range(6), [0,0,1,1,0,0]),'nm','1').thin(trim_zeros=False) + print('s = ',s) + np.testing.assert_equal(s.wave_list, range(6)) + + s = galsim.SED(galsim.LookupTable(range(8), [1.e-8,1.e-6,1,1,1,1.e-6,1.e-10,1.e-100]), + 'nm','1').thin(preserve_range=False) + print('s = ',s) + np.testing.assert_equal(s.wave_list, range(1,6)) + + s = galsim.SED(galsim.LookupTable(range(8), np.zeros(8)),'nm','1').thin() + print('s = ',s) + np.testing.assert_equal(s.wave_list, [0,7]) + + s = galsim.SED(galsim.LookupTable(range(2), [1,1], interpolant='linear'),'nm','1').thin() + print('s = ',s) + np.testing.assert_equal(s.wave_list, [0,1]) + + s = galsim.SED(galsim.LookupTable(range(3), [1, 1.e-20, 0], interpolant='linear'), + 'nm','1').thin(preserve_range=False) + print('s = ',s) + np.testing.assert_equal(s.wave_list, [0,1]) + if __name__ == "__main__": test_SED_basic() diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 5b7e4fdb08c..3f6da9ce366 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -292,11 +292,20 @@ def test_silicon(): do_pickle(s1) do_pickle(s7) - assert_raises(IOError, galsim.SiliconSensor, name='junk') - assert_raises(IOError, galsim.SiliconSensor, name='output') + assert_raises(OSError, galsim.SiliconSensor, name='junk') + assert_raises(OSError, galsim.SiliconSensor, name='output') assert_raises(TypeError, galsim.SiliconSensor, rng=3.4) assert_raises(TypeError, galsim.SiliconSensor, 'lsst_itl_8', rng1) + # Invalid to accumulate onto undefined image. + photons = galsim.PhotonArray(3) + image = galsim.ImageD() + with assert_raises(galsim.GalSimUndefinedBoundsError): + simple.accumulate(photons, image) + with assert_raises(galsim.GalSimUndefinedBoundsError): + silicon.accumulate(photons, image) + + @timer def test_silicon_fft(): """Test that drawing with method='fft' also works for SiliconSensor. @@ -740,6 +749,10 @@ def test_treerings(): np.testing.assert_almost_equal(ref_mom['My'] + treering_amplitude * center[1] / 1000, mom['My'], decimal=1) + assert_raises(TypeError, galsim.SiliconSensor, treering_func=lambda x:np.cos(x)) + assert_raises(TypeError, galsim.SiliconSensor, treering_func=tr7, treering_center=(3,4)) + + @timer def test_resume(): """Test that the resume option for accumulate works properly. @@ -976,7 +989,6 @@ def test_flat(): np.testing.assert_allclose(cov20 / counts_total, 0., atol=2*toler) np.testing.assert_allclose(cov02 / counts_total, 0., atol=2*toler) - if __name__ == "__main__": test_simple() test_silicon() diff --git a/tests/test_sersic.py b/tests/test_sersic.py index 5b097c10e82..8810331e517 100644 --- a/tests/test_sersic.py +++ b/tests/test_sersic.py @@ -28,22 +28,6 @@ imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference # images. - -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_sersic(): """Test the generation of a specific Sersic profile against a known result. @@ -153,7 +137,22 @@ def test_sersic(): # Should raise an exception if both scale_radius and half_light_radius are provided. assert_raises(TypeError, galsim.Sersic, n=1.2, scale_radius=3, half_light_radius=1) + assert_raises(TypeError, galsim.Sersic, n=1.2) assert_raises(TypeError, galsim.DeVaucouleurs, scale_radius=3, half_light_radius=1) + assert_raises(TypeError, galsim.DeVaucouleurs) + + # Allowed range is [0.3, 6.2] + assert_raises(ValueError, galsim.Sersic, n=0.2, scale_radius=3) + assert_raises(ValueError, galsim.Sersic, n=6.3, scale_radius=3) + + # trunc must be > sqrt(2) * hlr + assert_raises(ValueError, galsim.Sersic, n=3, half_light_radius=1, trunc=1.4) + assert_raises(ValueError, galsim.DeVaucouleurs, half_light_radius=1, trunc=1.4) + + # Other errors + assert_raises(TypeError, galsim.Sersic, scale_radius=3) + assert_raises(ValueError, galsim.Sersic, n=3, scale_radius=3, trunc=-1) + assert_raises(ValueError, galsim.DeVaucouleurs, scale_radius=3, trunc=-1) @timer diff --git a/tests/test_shapelet.py b/tests/test_shapelet.py index d60a4439431..b2b192a8710 100644 --- a/tests/test_shapelet.py +++ b/tests/test_shapelet.py @@ -24,10 +24,9 @@ import galsim from galsim_test_helpers import * -imgdir = os.path.join(".", "SBProfile_comparison_images") # Directory containing the reference - # images. - -# define a series of tests +path, filename = os.path.split(__file__) +imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference + # images. @timer def test_shapelet_gaussian(): @@ -172,6 +171,11 @@ def test_shapelet_properties(): # Check picklability do_pickle(shapelet) + assert_raises(TypeError, galsim.Shapelet, sigma=sigma) + assert_raises(TypeError, galsim.Shapelet, sigma=sigma, bvec=bvec) + assert_raises(TypeError, galsim.Shapelet, order=order, bvec=bvec) + assert_raises(ValueError, galsim.Shapelet, sigma=sigma, order=5, bvec=bvec) + @timer def test_shapelet_fit(): @@ -237,6 +241,13 @@ def test_shapelet_fit(): np.testing.assert_almost_equal(shapelet.bvec, shapelet2.bvec, 6, err_msg="Second fitted shapelet coefficients do not match original") + assert_raises(ValueError, galsim.Shapelet.fit, sigma, 10, im1, normalization='invalid') + + # Haven't gotten around to implementing this yet... + im2.wcs = galsim.JacobianWCS(0.2,0.01,0.01,0.2) + with assert_raises(NotImplementedError): + galsim.Shapelet.fit(sigma, 10, im2) + @timer def test_shapelet_adjustments(): diff --git a/tests/test_shear.py b/tests/test_shear.py index 40344f4403a..bb3d3a82049 100644 --- a/tests/test_shear.py +++ b/tests/test_shear.py @@ -177,17 +177,17 @@ def test_shear_initialization(): assert_raises(TypeError,galsim.Shear,g1=0.3,e2=0.2) assert_raises(TypeError,galsim.Shear,eta1=0.3,beta=0.*galsim.degrees) assert_raises(TypeError,galsim.Shear,q=0.3) - assert_raises(ValueError,galsim.Shear,q=1.3,beta=0.*galsim.degrees) - assert_raises(ValueError,galsim.Shear,g1=0.9,g2=0.6) - assert_raises(ValueError,galsim.Shear,e=-1.3,beta=0.*galsim.radians) - assert_raises(ValueError,galsim.Shear,e=1.3,beta=0.*galsim.radians) - assert_raises(ValueError,galsim.Shear,e1=0.7,e2=0.9) + assert_raises(galsim.GalSimRangeError,galsim.Shear,q=1.3,beta=0.*galsim.degrees) + assert_raises(galsim.GalSimRangeError,galsim.Shear,g1=0.9,g2=0.6) + assert_raises(galsim.GalSimRangeError,galsim.Shear,e=-1.3,beta=0.*galsim.radians) + assert_raises(galsim.GalSimRangeError,galsim.Shear,e=1.3,beta=0.*galsim.radians) + assert_raises(galsim.GalSimRangeError,galsim.Shear,e1=0.7,e2=0.9) assert_raises(TypeError,galsim.Shear,g=0.5) assert_raises(TypeError,galsim.Shear,e=0.5) assert_raises(TypeError,galsim.Shear,eta=0.5) - assert_raises(ValueError,galsim.Shear,eta=-0.5,beta=0.*galsim.radians) - assert_raises(ValueError,galsim.Shear,g=1.3,beta=0.*galsim.radians) - assert_raises(ValueError,galsim.Shear,g=-0.3,beta=0.*galsim.radians) + assert_raises(galsim.GalSimRangeError,galsim.Shear,eta=-0.5,beta=0.*galsim.radians) + assert_raises(galsim.GalSimRangeError,galsim.Shear,g=1.3,beta=0.*galsim.radians) + assert_raises(galsim.GalSimRangeError,galsim.Shear,g=-0.3,beta=0.*galsim.radians) assert_raises(TypeError,galsim.Shear,e=0.3,beta=0.) assert_raises(TypeError,galsim.Shear,eta=0.3,beta=0.) assert_raises(TypeError,galsim.Shear,randomkwarg=0.1) diff --git a/tests/test_spergel.py b/tests/test_spergel.py index 2bda867293f..f3162a4d55f 100644 --- a/tests/test_spergel.py +++ b/tests/test_spergel.py @@ -28,22 +28,6 @@ imgdir = os.path.join(path, "SBProfile_comparison_images") # Directory containing the reference # images. - -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_spergel(): """Test the generation of a specific Spergel profile against a known result. @@ -106,6 +90,12 @@ def test_spergel(): # Should raise an exception if both scale_radius and half_light_radius are provided. assert_raises(TypeError, galsim.Spergel, nu=0, scale_radius=3, half_light_radius=1) + assert_raises(TypeError, galsim.Spergel, nu=0) + assert_raises(TypeError, galsim.Spergel, scale_radius=3) + + # Allowed range = [-0.85, 4.0] + assert_raises(ValueError, galsim.Spergel, nu=-0.9) + assert_raises(ValueError, galsim.Spergel, nu=4.1) @timer diff --git a/tests/test_sum.py b/tests/test_sum.py index 7e93312a882..f755181d434 100644 --- a/tests/test_sum.py +++ b/tests/test_sum.py @@ -27,22 +27,6 @@ imgdir = os.path.join(".", "SBProfile_comparison_images") # Directory containing the reference # images. - -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - @timer def test_add(): """Test the addition of two rescaled Gaussian profiles against a known double Gaussian result. @@ -318,7 +302,7 @@ def test_sum_noise(): obj4 = galsim.Gaussian(sigma=3.3) obj4.noise = galsim.UncorrelatedNoise(variance=0.3, scale=0.8) try: - np.testing.assert_warns(UserWarning, galsim.Sum, [obj1, obj4]) + np.testing.assert_warns(galsim.GalSimWarning, galsim.Sum, [obj1, obj4]) except: pass diff --git a/tests/test_table.py b/tests/test_table.py index 17c6c6a8f8d..21187a7bcbb 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -121,6 +121,15 @@ def test_table(): do_pickle(table1) do_pickle(table2) + assert_raises(ValueError, galsim.LookupTable, x=args1, f=vals1, interpolant='invalid') + assert_raises(ValueError, galsim.LookupTable, x=[1], f=[1], interpolant='linear') + assert_raises(ValueError, galsim.LookupTable, x=[1,2], f=[1,2], interpolant='spline') + assert_raises(ValueError, galsim.LookupTable, x=[1,1,1], f=[1,2,3]) + assert_raises(ValueError, galsim.LookupTable, x=[0,1,2], f=[1,2,3], x_log=True) + assert_raises(ValueError, galsim.LookupTable, x=[-1,0,1], f=[1,2,3], x_log=True) + assert_raises(ValueError, galsim.LookupTable, x=[0,1,2], f=[0,1,2], f_log=True) + assert_raises(ValueError, galsim.LookupTable, x=[0,1,2], f=[2,-1,2], f_log=True) + @timer def test_init(): @@ -171,6 +180,15 @@ def test_log(): result_4, result_1, decimal=3, err_msg='Disagreement when interpolating in log(f)') + with assert_raises(galsim.GalSimRangeError): + tab_2(-1) + with assert_raises(galsim.GalSimRangeError): + tab_3(-1) + with assert_raises(galsim.GalSimRangeError): + tab_2(x_neg) + with assert_raises(galsim.GalSimRangeError): + tab_3(x_neg) + # Check picklability do_pickle(tab_1) do_pickle(tab_2) @@ -341,11 +359,13 @@ def f(x_, y_): # Test edge exception with assert_raises(ValueError): tab2d(1e6, 1e6) + with assert_raises(ValueError): + tab2d.gradient(1e6, 1e6) # Test edge wrapping # Check that can't construct table with edge-wrapping if edges don't match with assert_raises(ValueError): - galsim.LookupTable((x, y, z), dict(edge_mode='wrap')) + galsim.LookupTable2D(x, y, z, edge_mode='wrap') # Extend edges and make vals match x = np.append(x, x[-1] + (x[-1]-x[-2])) @@ -366,7 +386,8 @@ def f(x_, y_): # Test floor/ceil/nearest interpolant - x = y = np.arange(5) + x = np.arange(5) + y = np.arange(5) z = x + y[:, np.newaxis] tab2d = galsim.LookupTable2D(x, y, z, interpolant='ceil') assert tab2d(2.4, 3.6) == 3+4, "Ceil interpolant failed." @@ -375,6 +396,10 @@ def f(x_, y_): tab2d = galsim.LookupTable2D(x, y, z, interpolant='nearest') assert tab2d(2.4, 3.6) == 2+4, "Nearest interpolant failed." + assert_raises(ValueError, galsim.LookupTable2D, x, y, z, interpolant='invalid') + assert_raises(ValueError, galsim.LookupTable2D, x, y, z, edge_mode='invalid') + assert_raises(ValueError, galsim.LookupTable2D, x, y, z[:-1,:-1]) + # Test that x,y arrays need to be strictly increasing. x[0] = x[1] assert_raises(ValueError, galsim.LookupTable2D, x, y, z) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index b7114f0546b..b90205358e9 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -27,25 +27,9 @@ imgdir = os.path.join(".", "SBProfile_comparison_images") # Directory containing the reference # images. - # for flux normalization tests test_flux = 1.8 -# These are the default GSParams used when unspecified. We'll check that specifying -# these explicitly produces the same results. -default_params = galsim.GSParams( - minimum_fft_size = 128, - maximum_fft_size = 4096, - folding_threshold = 5.e-3, - maxk_threshold = 1.e-3, - kvalue_accuracy = 1.e-5, - xvalue_accuracy = 1.e-5, - shoot_accuracy = 1.e-5, - realspace_relerr = 1.e-4, - realspace_abserr = 1.e-6, - integration_relerr = 1.e-6, - integration_abserr = 1.e-8) - # Some parameters used in the two unit tests test_integer_shift_fft and test_integer_shift_photon: test_sigma = 1.8 test_hlr = 1.8 @@ -108,6 +92,12 @@ def test_smallshear(): # Check really small shear (This mostly tests a branch in the str function.) do_pickle(galsim.Gaussian(sigma=2.3).shear(g1=1.e-13,g2=0)) + assert_raises(TypeError, gauss.shear) + assert_raises(TypeError, gauss.shear, 0.3) + assert_raises(TypeError, gauss.shear, 0.1, 0.3) + assert_raises(TypeError, gauss.shear, g1=0.1, g2=0.1, invalid=0.3) + assert_raises(TypeError, gauss.shear, myShear, invalid=0.3) + @timer def test_largeshear(): """Test the application of a large shear to a Sersic profile against a known result. @@ -213,6 +203,8 @@ def test_rotate(): do_pickle(gal, lambda x: x.drawImage()) do_pickle(gal) + assert_raises(TypeError, gal.rotate) + assert_raises(TypeError, gal.rotate, 34) @timer def test_mag(): @@ -641,8 +633,7 @@ def test_flip(): gsparams=galsim.GSParams(realspace_relerr=1.e-6)), # Without being convolved by anything with a reasonable k cutoff, this needs # a very large fft. - galsim.DeVaucouleurs(half_light_radius=0.17, flux=1.7, - gsparams=galsim.GSParams(maximum_fft_size=8000)), + galsim.DeVaucouleurs(half_light_radius=0.17, flux=1.7), # I don't really understand why this needs a lower maxk_threshold to work, but # without it, the k-space tests fail. galsim.Exponential(scale_radius=0.17, flux=1.7, @@ -883,6 +874,7 @@ def test_ne(): objs = [galsim.Transform(gal1), galsim.Transform(gal2), galsim.Transform(gal1, jac=(1, 0.5, 0.5, 1)), + galsim.Transform(gal1, jac=(1, 1, 1, 1)), galsim.Transform(gal1, jac=jac), galsim.Transform(gal1, offset=galsim.PositionD(2, 2)), galsim.Transform(gal1, offset=offset), @@ -891,6 +883,14 @@ def test_ne(): galsim.Transform(gal1, gsparams=gsp)] all_obj_diff(objs) + # The degenerate jacobian will build fine, but will raise an exception when used. + degen = galsim.Transform(gal1, jac=(1, 1, 1, 1)) + with assert_raises(galsim.GalSimError): + sbp = degen._sbp + + assert_raises(TypeError, galsim.Transform, jac) + + @timer def test_compound(): """Check that transformations of transformations work the same whether they are compounded diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 99701d28f6f..f73f1c17255 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -71,18 +71,26 @@ def test_pos(): assert isinstance(pd5.x, float) assert isinstance(pd5.y, float) + assert_raises(TypeError, galsim.PositionI, 11) assert_raises(TypeError, galsim.PositionI, 11, 23, 9) assert_raises(TypeError, galsim.PositionI, x=11, z=23) assert_raises(TypeError, galsim.PositionI, x=11) - assert_raises(TypeError, galsim.PositionI, 11) - assert_raises(ValueError, galsim.PositionI, 11, 23.5) + assert_raises(TypeError, galsim.PositionD, x=11, y=23, z=17) + assert_raises(TypeError, galsim.PositionI, 11, 23, x=13, z=21) + assert_raises(TypeError, galsim.PositionI, 11, 23.5) + assert_raises(TypeError, galsim.PositionD, 11) assert_raises(TypeError, galsim.PositionD, 11, 23, 9) assert_raises(TypeError, galsim.PositionD, x=11, z=23) assert_raises(TypeError, galsim.PositionD, x=11) - assert_raises(TypeError, galsim.PositionD, 11) + assert_raises(TypeError, galsim.PositionD, x=11, y=23, z=17) + assert_raises(TypeError, galsim.PositionD, 11, 23, x=13, z=21) assert_raises(ValueError, galsim.PositionD, 11, "blue") + # Can't use base class directly. + assert_raises(TypeError, galsim.Position, 11, 23) + assert_raises(NotImplementedError, galsim.Position) + # Check arithmetic for p1 in [pi1, pd1]: @@ -125,12 +133,17 @@ def test_pos(): assert pd9 == 0*pd1 assert isinstance(pd9, galsim.PositionD) - assert_raises(ValueError, pd1.__mul__, "11") - assert_raises(ValueError, pd1.__mul__, None) - assert_raises(ValueError, pd1.__div__, "11e") - assert_raises(ValueError, pi1.__mul__, "11e") - assert_raises(ValueError, pi1.__mul__, None) - assert_raises(ValueError, pi1.__div__, 11.5) + assert_raises(TypeError, pd1.__add__, 11) + assert_raises(TypeError, pd1.__sub__, 11) + assert_raises(TypeError, pd1.__mul__, "11") + assert_raises(TypeError, pd1.__mul__, None) + assert_raises(TypeError, pd1.__div__, "11e") + + assert_raises(TypeError, pi1.__add__, 11) + assert_raises(TypeError, pi1.__sub__, 11) + assert_raises(TypeError, pi1.__mul__, "11e") + assert_raises(TypeError, pi1.__mul__, None) + assert_raises(TypeError, pi1.__div__, 11.5) do_pickle(pi1) do_pickle(pd1) @@ -161,7 +174,10 @@ def test_bounds(): bi11 = galsim.BoundsI(galsim.BoundsD(11.,23.,17.,50.)) bi12 = galsim.BoundsI(xmin=11,ymin=17,xmax=23,ymax=50) bi13 = galsim._BoundsI(11,23,17,50) - for b in [bi1, bi2, bi3, bi4, bi5, bi6, bi7, bi8, bi9, bi10, bi11, bi12, bi13]: + bi14 = galsim.BoundsI() + bi14 += galsim.PositionI(11,17) + bi14 += galsim.PositionI(23,50) + for b in [bi1, bi2, bi3, bi4, bi5, bi6, bi7, bi8, bi9, bi10, bi11, bi12, bi13, bi14]: assert b.isDefined() assert b == bi1 assert isinstance(b.xmin, int) @@ -194,7 +210,10 @@ def test_bounds(): bd11 = galsim.BoundsD(galsim.BoundsI(11,23,17,50)) bd12 = galsim.BoundsD(xmin=11.0,ymin=17.0,xmax=23.0,ymax=50.0) bd13 = galsim._BoundsD(11,23,17,50) - for b in [bd1, bd2, bd3, bd4, bd5, bd6, bd7, bd8, bd9, bd10, bd11, bd12, bd13]: + bd14 = galsim.BoundsD() + bd14 += galsim.PositionD(11.,17.) + bd14 += galsim.PositionD(23,50) + for b in [bd1, bd2, bd3, bd4, bd5, bd6, bd7, bd8, bd9, bd10, bd11, bd12, bd13, bd14]: assert b.isDefined() assert b == bd1 assert isinstance(b.xmin, float) @@ -205,30 +224,55 @@ def test_bounds(): assert b.center == galsim.PositionD(17, 33.5) assert b.true_center == galsim.PositionD(17, 33.5) + assert_raises(TypeError, galsim.BoundsI, 11) + assert_raises(TypeError, galsim.BoundsI, 11, 23) assert_raises(TypeError, galsim.BoundsI, 11, 23, 9) assert_raises(TypeError, galsim.BoundsI, 11, 23, 9, 12, 59) assert_raises(TypeError, galsim.BoundsI, xmin=11, xmax=23, ymin=17, ymax=50, z=23) assert_raises(TypeError, galsim.BoundsI, xmin=11, xmax=50) - assert_raises(TypeError, galsim.BoundsI, 11) - assert_raises(ValueError, galsim.BoundsI, 11, 23.5, 17, 50.9) + assert_raises(TypeError, galsim.BoundsI, 11, 23.5, 17, 50.9) + assert_raises(TypeError, galsim.BoundsI, 11, 23, 9, 12, xmin=19, xmax=2) + with assert_raises(TypeError): + bi1 += (11,23) + assert_raises(TypeError, galsim.BoundsD, 11) + assert_raises(TypeError, galsim.BoundsD, 11, 23) assert_raises(TypeError, galsim.BoundsD, 11, 23, 9) assert_raises(TypeError, galsim.BoundsD, 11, 23, 9, 12, 59) assert_raises(TypeError, galsim.BoundsD, xmin=11, xmax=23, ymin=17, ymax=50, z=23) assert_raises(TypeError, galsim.BoundsD, xmin=11, xmax=50) - assert_raises(TypeError, galsim.BoundsD, 11) assert_raises(ValueError, galsim.BoundsD, 11, 23, 17, "blue") + assert_raises(TypeError, galsim.BoundsD, 11, 23, 9, 12, xmin=19, xmax=2) + with assert_raises(TypeError): + bd1 += (11,23) + + # Can't use base class directly. + assert_raises(TypeError, galsim.Bounds, 11, 23, 9, 12) + assert_raises(NotImplementedError, galsim.Bounds) # Check intersection assert bi1 == galsim.BoundsI(0,100,0,100) & bi1 assert bi1 == bi1 & galsim.BoundsI(0,100,0,100) assert bi1 == galsim.BoundsI(0,23,0,50) & galsim.BoundsI(11,100,17,100) assert bi1 == galsim.BoundsI(0,23,17,100) & galsim.BoundsI(11,100,0,50) + assert not (bi1 & galsim.BoundsI()).isDefined() + assert not (galsim.BoundsI() & bi1).isDefined() assert bd1 == galsim.BoundsD(0,100,0,100) & bd1 assert bd1 == bd1 & galsim.BoundsD(0,100,0,100) assert bd1 == galsim.BoundsD(0,23,0,50) & galsim.BoundsD(11,100,17,100) assert bd1 == galsim.BoundsD(0,23,17,100) & galsim.BoundsD(11,100,0,50) + assert not (bd1 & galsim.BoundsD()).isDefined() + assert not (galsim.BoundsD() & bd1).isDefined() + + with assert_raises(TypeError): + bi1 & galsim.PositionI(1,2) + with assert_raises(TypeError): + bi1 & galsim.PositionD(1,2) + with assert_raises(TypeError): + bd1 & galsim.PositionI(1,2) + with assert_raises(TypeError): + bd1 & galsim.PositionD(1,2) # Check withBorder assert bi1.withBorder(4) == galsim.BoundsI(7,27,13,54) @@ -237,13 +281,13 @@ def test_bounds(): assert bd1.withBorder(4.1) == galsim.BoundsD(6.9,27.1,12.9,54.1) assert bd1.withBorder(0) == galsim.BoundsD(11,23,17,50) assert bd1.withBorder(-1) == galsim.BoundsD(12,22,18,49) - assert_raises(ValueError, bi1.withBorder, 'blue') - assert_raises(ValueError, bi1.withBorder, 4.1) - assert_raises(ValueError, bi1.withBorder, '4') - assert_raises(ValueError, bi1.withBorder, None) - assert_raises(ValueError, bd1.withBorder, 'blue') - assert_raises(ValueError, bd1.withBorder, '4.1') - assert_raises(ValueError, bd1.withBorder, None) + assert_raises(TypeError, bi1.withBorder, 'blue') + assert_raises(TypeError, bi1.withBorder, 4.1) + assert_raises(TypeError, bi1.withBorder, '4') + assert_raises(TypeError, bi1.withBorder, None) + assert_raises(TypeError, bd1.withBorder, 'blue') + assert_raises(TypeError, bd1.withBorder, '4.1') + assert_raises(TypeError, bd1.withBorder, None) # Check expand assert bi1.expand(2) == galsim.BoundsI(5,29,0,67) @@ -306,10 +350,10 @@ def test_bounds(): assert galsim.BoundsD(23, 11, 17, 50) == galsim.BoundsD() assert galsim.BoundsD(11, 23, 50, 17) == galsim.BoundsD() - assert_raises(ValueError, getattr, galsim.BoundsI(), 'center') - assert_raises(ValueError, getattr, galsim.BoundsD(), 'center') - assert_raises(ValueError, getattr, galsim.BoundsI(), 'true_center') - assert_raises(ValueError, getattr, galsim.BoundsD(), 'true_center') + assert_raises(galsim.GalSimUndefinedBoundsError, getattr, galsim.BoundsI(), 'center') + assert_raises(galsim.GalSimUndefinedBoundsError, getattr, galsim.BoundsD(), 'center') + assert_raises(galsim.GalSimUndefinedBoundsError, getattr, galsim.BoundsI(), 'true_center') + assert_raises(galsim.GalSimUndefinedBoundsError, getattr, galsim.BoundsD(), 'true_center') do_pickle(bi1) do_pickle(bd1) @@ -429,13 +473,15 @@ def test_check_all_contiguous(): @timer def test_deInterleaveImage(): + from galsim.utilities import deInterleaveImage, interleaveImages + np.random.seed(84) # for generating the same random instances # 1) Check compatability with interleaveImages img = galsim.Image(np.random.randn(64,64),scale=0.25) img.setOrigin(galsim.PositionI(5,7)) ## for non-trivial bounds - im_list, offsets = galsim.utilities.deInterleaveImage(img,8) - img1 = galsim.utilities.interleaveImages(im_list,8,offsets) + im_list, offsets = deInterleaveImage(img,8) + img1 = interleaveImages(im_list,8,offsets) np.testing.assert_array_equal(img1.array,img.array, err_msg = "interleaveImages cannot reproduce the input to deInterleaveImage for square " "images") @@ -445,8 +491,8 @@ def test_deInterleaveImage(): img = galsim.Image(abs(np.random.randn(16*5,16*2)),scale=0.5) img.setCenter(0,0) ## for non-trivial bounds - im_list, offsets = galsim.utilities.deInterleaveImage(img,(2,5)) - img1 = galsim.utilities.interleaveImages(im_list,(2,5),offsets) + im_list, offsets = deInterleaveImage(img,(2,5)) + img1 = interleaveImages(im_list,(2,5),offsets) np.testing.assert_array_equal(img1.array,img.array, err_msg = "interleaveImages cannot reproduce the input to deInterleaveImage for " "rectangular images") @@ -456,7 +502,7 @@ def test_deInterleaveImage(): # 2) Checking for offsets img = galsim.Image(np.random.randn(32,32),scale=2.0) - im_list, offsets = galsim.utilities.deInterleaveImage(img,(4,2)) + im_list, offsets = deInterleaveImage(img,(4,2)) ## Checking if offsets are centered around zero assert np.sum([offset.x for offset in offsets]) == 0. @@ -473,7 +519,7 @@ def test_deInterleaveImage(): img0 = galsim.Image(32,32) g0.drawImage(image=img0,method='no_pixel',scale=0.25) - im_list0, offsets0 = galsim.utilities.deInterleaveImage(img0,2,conserve_flux=True) + im_list0, offsets0 = deInterleaveImage(img0,2,conserve_flux=True) for n in range(len(im_list0)): im = galsim.Image(16,16) @@ -496,8 +542,8 @@ def test_deInterleaveImage(): g1.drawImage(image=img1,scale=0.5/n1,method='no_pixel') g2.drawImage(image=img2,scale=0.5/n2,method='no_pixel') - im_list1, offsets1 = galsim.utilities.deInterleaveImage(img1,(n1**2,1),conserve_flux=True) - im_list2, offsets2 = galsim.utilities.deInterleaveImage(img2,[1,n2**2],conserve_flux=False) + im_list1, offsets1 = deInterleaveImage(img1,(n1**2,1),conserve_flux=True) + im_list2, offsets2 = deInterleaveImage(img2,[1,n2**2],conserve_flux=False) for n in range(n1**2): im, offset = im_list1[n], offsets1[n] @@ -514,9 +560,26 @@ def test_deInterleaveImage(): "horizontal direction") # im is scaled to account for flux not being conserved + assert_raises(TypeError, deInterleaveImage, image=img0.array, N=2) + assert_raises(TypeError, deInterleaveImage, image=img0, N=2.0) + assert_raises(TypeError, deInterleaveImage, image=img0, N=(2.0, 2.0)) + assert_raises(TypeError, deInterleaveImage, image=img0, N=(2,2,3)) + assert_raises(ValueError, deInterleaveImage, image=img0, N=7) + assert_raises(ValueError, deInterleaveImage, image=img0, N=(2,7)) + assert_raises(ValueError, deInterleaveImage, image=img0, N=(7,2)) + + # It is legal to have the input image with wcs=None, but it emits a warning + img0.wcs = None + with assert_warns(galsim.GalSimWarning): + deInterleaveImage(img0, N=2) + # Unless suppress_warnings is True + deInterleaveImage(img0, N=2, suppress_warnings=True) + @timer def test_interleaveImages(): + from galsim.utilities import interleaveImages, deInterleaveImage + # 1a) With galsim Gaussian g = galsim.Gaussian(sigma=3.7,flux=1000.) gal = galsim.Convolve([g,galsim.Pixel(1.0)]) @@ -534,7 +597,7 @@ def test_interleaveImages(): scale = im.scale # Input to N as an int - img = galsim.utilities.interleaveImages(im_list,n,offsets=offset_list) + img = interleaveImages(im_list,n,offsets=offset_list) im = galsim.Image(16*n*n,16*n*n) g = galsim.Gaussian(sigma=3.7,flux=1000.*n*n) gal = galsim.Convolve([g,galsim.Pixel(1.0)]) @@ -560,7 +623,7 @@ def test_interleaveImages(): im_list_randperm = [im_list[idx] for idx in rand_idx] offset_list_randperm = [offset_list[idx] for idx in rand_idx] # Input to N as a tuple - img_randperm = galsim.utilities.interleaveImages(im_list_randperm,(n,n),offsets=offset_list_randperm) + img_randperm = interleaveImages(im_list_randperm,(n,n),offsets=offset_list_randperm) np.testing.assert_array_equal(img_randperm.array,img.array, err_msg="Interleaved images do not match when 'offsets' is supplied") @@ -571,7 +634,7 @@ def test_interleaveImages(): im_list = [] n = 5 # Generate approximate offsets - DX = np.array([-0.67,-0.33,0.,0.33,0.67]) + DX = np.array([-0.47,-0.23,0.,0.23,0.47]) DY = DX for dy in DY: for dx in DX: @@ -583,7 +646,9 @@ def test_interleaveImages(): N = (n,n) with assert_raises(ValueError): - galsim.utilities.interleaveImages(im_list,N,offset_list) + interleaveImages(im_list,N,offset_list) + # Can turn off the checks and just use these as they are with catch_offset_errors=False + interleaveImages(im_list,N,offset_list, catch_offset_errors=False) offset_list = [] im_list = [] @@ -600,7 +665,8 @@ def test_interleaveImages(): N = (n,n) with assert_raises(ValueError): - galsim.utilities.interleaveImages(im_list, N, offset_list) + interleaveImages(im_list, N, offset_list) + interleaveImages(im_list, N, offset_list, catch_offset_errors=False) # 2a) Increase resolution along one direction - square to rectangular images n = 2 @@ -619,8 +685,8 @@ def test_interleaveImages(): gal1.drawImage(im,offset=offset,method='no_pixel',scale=2.0) im_list.append(im) - img = galsim.utilities.interleaveImages(im_list, N=[1,n**2], offsets=offset_list, - add_flux=False, suppress_warnings=True) + img = interleaveImages(im_list, N=[1,n**2], offsets=offset_list, + add_flux=False, suppress_warnings=True) im = galsim.Image(16,16*n*n) # The interleaved image has the total flux averaged out since `add_flux = False' gal = galsim.Gaussian(sigma=3.7*n,flux=100.) @@ -647,7 +713,7 @@ def test_interleaveImages(): gal2.drawImage(im,offset=offset,method='no_pixel',scale=3.0) im_list.append(im) - img = galsim.utilities.interleaveImages(im_list, N=np.array([n**2,1]), offsets=offset_list, + img = interleaveImages(im_list, N=np.array([n**2,1]), offsets=offset_list, suppress_warnings=True) im = galsim.Image(16*n*n,16*n*n) gal = galsim.Gaussian(sigma=3.7,flux=100.*n*n) @@ -676,8 +742,8 @@ def test_interleaveImages(): im.setOrigin(3,3) # for non-trivial bounds im_list.append(im) - img = galsim.utilities.interleaveImages(im_list,N=n,offsets=offset_list) - im_list_1, offset_list_1 = galsim.utilities.deInterleaveImage(img, N=n) + img = interleaveImages(im_list,N=n,offsets=offset_list) + im_list_1, offset_list_1 = deInterleaveImage(img, N=n) for k in range(n**2): assert offset_list_1[k] == offset_list[k] @@ -688,14 +754,45 @@ def test_interleaveImages(): assert im_list[k].bounds == im_list_1[k].bounds # Checking for non-default flux option - img = galsim.utilities.interleaveImages(im_list,N=n,offsets=offset_list,add_flux=False) - im_list_2, offset_list_2 = galsim.utilities.deInterleaveImage(img,N=n,conserve_flux=True) + img = interleaveImages(im_list,N=n,offsets=offset_list,add_flux=False) + im_list_2, offset_list_2 = deInterleaveImage(img,N=n,conserve_flux=True) for k in range(n**2): assert offset_list_2[k] == offset_list[k] np.testing.assert_array_equal(im_list_2[k].array, im_list[k].array) assert im_list_2[k].wcs == im_list[k].wcs + assert_raises(TypeError, interleaveImages, im_list=img, N=n, offsets=offset_list) + assert_raises(ValueError, interleaveImages, [img], N=1, offsets=offset_list) + assert_raises(ValueError, interleaveImages, im_list, n, offset_list[:-1]) + assert_raises(TypeError, interleaveImages, [im.array for im in im_list], n, offset_list) + assert_raises(TypeError, interleaveImages, + [im_list[0]] + [im.array for im in im_list[1:]], + n, offset_list) + assert_raises(TypeError, interleaveImages, + [galsim.Image(16+i,16+j,scale=1) for i in range(n) for j in range(n)], + n, offset_list) + assert_raises(TypeError, interleaveImages, + [galsim.Image(16,16,scale=i) for i in range(n) for j in range(n)], + n, offset_list) + assert_raises(TypeError, interleaveImages, im_list, N=3.0, offsets=offset_list) + assert_raises(TypeError, interleaveImages, im_list, N=(3.0, 3.0), offsets=offset_list) + assert_raises(TypeError, interleaveImages, im_list, N=(3,3,3), offsets=offset_list) + assert_raises(ValueError, interleaveImages, im_list, N=7, offsets=offset_list) + assert_raises(ValueError, interleaveImages, im_list, N=(2,7), offsets=offset_list) + assert_raises(ValueError, interleaveImages, im_list, N=(7,2), offsets=offset_list) + assert_raises(TypeError, interleaveImages, im_list, N=n) + assert_raises(TypeError, interleaveImages, im_list, N=n, offsets=offset_list[0]) + assert_raises(TypeError, interleaveImages, im_list, N=n, offsets=range(n*n)) + + # It is legal to have the input images with wcs=None, but it emits a warning + for im in im_list: + im.wcs = None + with assert_warns(galsim.GalSimWarning): + interleaveImages(im_list, N=n, offsets=offset_list) + # Unless suppress_warnings is True + interleaveImages(im_list, N=n, offsets=offset_list, suppress_warnings=True) + @timer def test_python_LRU_Cache(): @@ -730,6 +827,13 @@ def test_python_LRU_Cache(): assert cache(i) == f(i) assert (1,) not in cache.cache + # "Resize" to same size does nothing. + cache.resize(newsize) + assert len(cache.cache) == 20 + assert (1,) not in cache.cache + for i in range(2, newsize+2): + assert (i,) in cache.cache + # Test mostly non-destructive cache contraction. # Already bumped (0,) and (1,), so (2,) should be the first to get bumped for i in range(newsize-1, size, -1): @@ -737,56 +841,85 @@ def test_python_LRU_Cache(): cache.resize(i) assert (newsize - (i - 1),) not in cache.cache + assert_raises(ValueError, cache.resize, 0) + assert_raises(ValueError, cache.resize, -20) + + @timer def test_rand_with_replacement(): """Test routine to select random indices with replacement.""" # Most aspects of this routine get tested when it's used by COSMOSCatalog. We just check some # of the exception-handling here. + + # Invalid rng + with assert_raises(TypeError): + galsim.utilities.rand_with_replacement( + n=2, n_choices=10, rng='foo') + + # Invalid n with assert_raises(ValueError): galsim.utilities.rand_with_replacement( n=1.5, n_choices=10, rng=galsim.BaseDeviate(1234)) - with assert_raises(TypeError): + with assert_raises(ValueError): galsim.utilities.rand_with_replacement( - n=2, n_choices=10, rng='foo') + n=0, n_choices=10, rng=galsim.BaseDeviate(1234)) + with assert_raises(ValueError): + galsim.utilities.rand_with_replacement( + n=-2, n_choices=10, rng=galsim.BaseDeviate(1234)) + + # Invalid n_choices with assert_raises(ValueError): galsim.utilities.rand_with_replacement( n=2, n_choices=10.5, rng=galsim.BaseDeviate(1234)) with assert_raises(ValueError): galsim.utilities.rand_with_replacement( - n=2, n_choices=-11, rng=galsim.BaseDeviate(1234)) + n=2, n_choices=0, rng=galsim.BaseDeviate(1234)) with assert_raises(ValueError): galsim.utilities.rand_with_replacement( - n=-2, n_choices=11, rng=galsim.BaseDeviate(1234)) + n=2, n_choices=-11, rng=galsim.BaseDeviate(1234)) + # Negative weights tmp_weights = np.arange(10).astype(float)-3 with assert_raises(ValueError): galsim.utilities.rand_with_replacement( n=2, n_choices=10, rng=galsim.BaseDeviate(1234), weight=tmp_weights) + # NaN weights tmp_weights[0] = np.nan with assert_raises(ValueError): galsim.utilities.rand_with_replacement( n=2, n_choices=10, rng=galsim.BaseDeviate(1234), weight=tmp_weights) + # inf weights tmp_weights[0] = np.inf with assert_raises(ValueError): galsim.utilities.rand_with_replacement( n=2, n_choices=10, rng=galsim.BaseDeviate(1234), weight=tmp_weights) + # Wrong length for weights + with assert_raises(ValueError): + galsim.utilities.rand_with_replacement( + n=2, n_choices=10, rng=galsim.BaseDeviate(1234), weight=tmp_weights[:4]) + # Make sure results come out the same whether we use _n_rng_calls or not. - result_1 = galsim.utilities.rand_with_replacement(n=10, n_choices=100, - rng=galsim.BaseDeviate(314159)) - result_2, _ = galsim.utilities.rand_with_replacement(n=10, n_choices=100, - rng=galsim.BaseDeviate(314159), - _n_rng_calls=True) + rng1 = galsim.BaseDeviate(314159) + rng2 = galsim.BaseDeviate(314159) + rng3 = galsim.BaseDeviate(314159) + result_1 = galsim.utilities.rand_with_replacement(n=10, n_choices=100, rng=rng1) + result_2, n_rng = galsim.utilities.rand_with_replacement(n=10, n_choices=100, rng=rng2, + _n_rng_calls=True) assert np.all(result_1==result_2),"Using _n_rng_calls results in different random numbers" + rng3.discard(n_rng) + assert rng1.raw() == rng2.raw() == rng3.raw() + + # Repeat with weights weight = np.zeros(100) galsim.UniformDeviate(1234).generate(weight) - result_1 = galsim.utilities.rand_with_replacement( - n=10, n_choices=100, rng=galsim.BaseDeviate(314159), weight=weight) + result_1 = galsim.utilities.rand_with_replacement(10, 100, rng1, weight=weight) assert not np.all(result_1==result_2),"Weights did not have an effect" - result_2, _ = galsim.utilities.rand_with_replacement( - n=10, n_choices=100, rng=galsim.BaseDeviate(314159), - weight=weight, _n_rng_calls=True) + result_2, n_rng = galsim.utilities.rand_with_replacement(10, 100, rng2, weight=weight, + _n_rng_calls=True) assert np.all(result_1==result_2),"Using _n_rng_calls results in different random numbers" + rng3.discard(n_rng) + assert rng1.raw() == rng2.raw() == rng3.raw() @timer def test_position_type_promotion(): diff --git a/tests/test_vonkarman.py b/tests/test_vonkarman.py index c716605b866..83391c6da2e 100644 --- a/tests/test_vonkarman.py +++ b/tests/test_vonkarman.py @@ -21,20 +21,14 @@ import os import sys +import galsim from galsim_test_helpers import * -try: - import galsim -except ImportError: - sys.path.append(os.path.abspath(os.path.join(path, ".."))) - import galsim - @timer def test_vk(slow=False): """Test the generation of VonKarman profiles """ - gsp = galsim.GSParams(maximum_fft_size=8192) if slow: lams = [300.0, 500.0, 1100.0] r0_500s = [0.05, 0.15, 0.3] @@ -57,7 +51,7 @@ def test_vk(slow=False): print("Skip this combination, since delta > maxk_threshold") continue - vk = galsim.VonKarman(flux=2.2, gsparams=gsp, **kwargs) + vk = galsim.VonKarman(flux=2.2, **kwargs) np.testing.assert_almost_equal(vk.flux, 2.2) check_basic(vk, "VonKarman") @@ -74,7 +68,7 @@ def test_vk_delta(): """Test a VonKarman with a significant delta-function amplitude""" kwargs = {'lam':1100.0, 'r0':0.8, 'L0':5.0, 'flux':2.2} # Try to see if we can catch the warning first - with assert_warns(UserWarning): + with assert_warns(galsim.GalSimWarning): vk = galsim.VonKarman(**kwargs) kwargs['suppress_warning'] = True diff --git a/tests/test_wcs.py b/tests/test_wcs.py index ca793586581..4b70d89d0d4 100644 --- a/tests/test_wcs.py +++ b/tests/test_wcs.py @@ -177,26 +177,41 @@ def do_wcs_pos(wcs, ufunc, vfunc, name, x0=0, y0=0, color=None): image_pos = galsim.PositionD(x+x0,y+y0) world_pos = galsim.PositionD(u,v) world_pos2 = wcs.toWorld(image_pos, color=color) + world_pos3 = wcs.posToWorld(image_pos, color=color) np.testing.assert_almost_equal( world_pos.x, world_pos2.x, digits2, 'wcs.toWorld returned wrong world position for '+name) np.testing.assert_almost_equal( world_pos.y, world_pos2.y, digits2, 'wcs.toWorld returned wrong world position for '+name) + np.testing.assert_almost_equal( + world_pos.x, world_pos3.x, digits2, + 'wcs.postoWorld returned wrong world position for '+name) + np.testing.assert_almost_equal( + world_pos.y, world_pos3.y, digits2, + 'wcs.postoWorld returned wrong world position for '+name) scale = wcs.maxLinearScale(image_pos, color=color) try: # The reverse transformation is not guaranteed to be implemented, # so guard against NotImplementedError being raised: image_pos2 = wcs.toImage(world_pos, color=color) + image_pos3 = wcs.posToImage(world_pos, color=color) np.testing.assert_almost_equal( image_pos.x*scale, image_pos2.x*scale, digits2, 'wcs.toImage returned wrong image position for '+name) np.testing.assert_almost_equal( image_pos.y*scale, image_pos2.y*scale, digits2, 'wcs.toImage returned wrong image position for '+name) + np.testing.assert_almost_equal( + image_pos.x*scale, image_pos3.x*scale, digits2, + 'wcs.posToImage returned wrong image position for '+name) + np.testing.assert_almost_equal( + image_pos.y*scale, image_pos3.y*scale, digits2, + 'wcs.posToImage returned wrong image position for '+name) except NotImplementedError: - pass + assert_raises(NotImplementedError, wcs._x, world_pos.x, world_pos.y, color=color) + assert_raises(NotImplementedError, wcs._y, world_pos.x, world_pos.y, color=color) if x0 == 0 and y0 == 0: # The last item in list should also work as a PositionI @@ -207,6 +222,21 @@ def do_wcs_pos(wcs, ufunc, vfunc, name, x0=0, y0=0, color=None): np.testing.assert_almost_equal( world_pos.y, wcs.toWorld(image_pos, color=color).y, digits2, 'wcs.toWorld gave different value with PositionI image_pos for '+name) + assert_raises(TypeError, wcs.posToWorld, (3,4)) + assert_raises(TypeError, wcs.toWorld, (3,4)) + assert_raises(TypeError, wcs.toWorld, galsim.CelestialCoord(0*galsim.degrees,0*galsim.degrees)) + assert_raises(TypeError, wcs.posToWorld, + galsim.CelestialCoord(0*galsim.degrees,0*galsim.degrees)) + assert_raises(TypeError, wcs.toImage, (3,4)) + assert_raises(TypeError, wcs.posToImage, (3,4)) + if wcs.isCelestial(): + assert_raises(TypeError, wcs.toImage, galsim.PositionD(3,4)) + assert_raises(TypeError, wcs.posToImage, galsim.PositionD(3,4)) + else: + assert_raises(TypeError, wcs.toImage, + galsim.CelestialCoord(0*galsim.degrees,0*galsim.degrees)) + assert_raises(TypeError, wcs.posToImage, + galsim.CelestialCoord(0*galsim.degrees,0*galsim.degrees)) def check_world(pos1, pos2, digits, err_msg): @@ -271,9 +301,7 @@ def do_wcs_image(wcs, name, approx=False): if wcs.isUniform(): # Test that the regular CD, CRPIX, CRVAL items that are written to the header # describe an equivalent WCS as this one. - hdu, hdu_list, fin = galsim.fits.readFile(test_name, dir=dir) - affine = galsim.AffineTransform._readHeader(hdu.header) - galsim.fits.closeHDUList(hdu_list, fin) + affine = galsim.FitsWCS(test_name, dir=dir) check_world(affine.toWorld(im.origin), world1, digits2, "World position of origin is wrong after write/read.") check_world(affine.toWorld(im.center), world2, digits2, @@ -567,17 +595,15 @@ def do_nonlocal_wcs(wcs, ufunc, vfunc, name, test_pickle=True, color=None): np.testing.assert_almost_equal( world_pos2.y, world_pos1.y, digits, 'withOrigin(new_origin) returned wrong world position') - if not wcs.isCelestial(): - new_world_origin = galsim.PositionD(5352.7, 9234.3) - wcs5 = wcs.withOrigin(new_origin, new_world_origin, color=color) - world_pos3 = wcs5.toWorld(new_origin, color=color) - np.testing.assert_almost_equal( - world_pos3.x, new_world_origin.x, digits, - 'withOrigin(new_origin, new_world_origin) returned wrong position') - np.testing.assert_almost_equal( - world_pos3.y, new_world_origin.y, digits, - 'withOrigin(new_origin, new_world_origin) returned wrong position') - + new_world_origin = galsim.PositionD(5352.7, 9234.3) + wcs5 = wcs.withOrigin(new_origin, new_world_origin, color=color) + world_pos3 = wcs5.toWorld(new_origin, color=color) + np.testing.assert_almost_equal( + world_pos3.x, new_world_origin.x, digits, + 'withOrigin(new_origin, new_world_origin) returned wrong position') + np.testing.assert_almost_equal( + world_pos3.y, new_world_origin.y, digits, + 'withOrigin(new_origin, new_world_origin) returned wrong position') # Check that (x,y) -> (u,v) and converse work correctly # These tests work regardless of whether the WCS is local or not. @@ -665,6 +691,27 @@ def do_nonlocal_wcs(wcs, ufunc, vfunc, name, test_pickle=True, color=None): im1.array, im2.array, digits, 'world_profile with given wcs and image_profile differed when drawn for '+name) + # Check some properties that should be the same for the wcs and its local jacobian. + np.testing.assert_allclose( + wcs.minLinearScale(image_pos=image_pos, color=color), + wcs.jacobian(image_pos=image_pos, color=color).minLinearScale(color=color)) + np.testing.assert_allclose( + wcs.maxLinearScale(image_pos=image_pos, color=color), + wcs.jacobian(image_pos=image_pos, color=color).maxLinearScale(color=color)) + np.testing.assert_allclose( + wcs.pixelArea(image_pos=image_pos, color=color), + wcs.jacobian(image_pos=image_pos, color=color).pixelArea(color=color)) + + if not wcs.isUniform(): + assert_raises(TypeError, wcs.local) + assert_raises(TypeError, wcs.local, image_pos=image_pos, world_pos=world_pos, color=color) + assert_raises(TypeError, wcs.local, image_pos=(3,4), color=color) + assert_raises(TypeError, wcs.local, world_pos=(3,4), color=color) + + assert_raises(TypeError, wcs.withOrigin) + assert_raises(TypeError, wcs.withOrigin, origin=(3,4), color=color) + assert_raises(TypeError, wcs.withOrigin, origin=image_pos, world_origin=(3,4), color=color) + def do_celestial_wcs(wcs, name, test_pickle=True): # It's a bit harder to test WCS functions that return a CelestialCoord, since @@ -778,6 +825,16 @@ def do_celestial_wcs(wcs, name, test_pickle=True): except NotImplementedError: pass + assert_raises(TypeError, wcs.local) + assert_raises(TypeError, wcs.local, image_pos=image_pos, world_pos=world_pos) + assert_raises(TypeError, wcs.local, image_pos=(3,4)) + assert_raises(TypeError, wcs.local, world_pos=(3,4)) + + assert_raises(TypeError, wcs.withOrigin) + assert_raises(TypeError, wcs.withOrigin, origin=(3,4)) + assert_raises(TypeError, wcs.withOrigin, world_origin=(3,4)) + assert_raises(TypeError, wcs.withOrigin, origin=image_pos, world_origin=world_pos) + @timer def test_pixelscale(): @@ -791,6 +848,14 @@ def test_pixelscale(): assert wcs == wcs2, 'PixelScale copy is not == the original' wcs3 = galsim.PixelScale(scale + 0.1234) assert wcs != wcs3, 'PixelScale is not != a different one' + assert wcs.scale == scale + assert wcs.origin == galsim.PositionD(0,0) + assert wcs.world_origin == galsim.PositionD(0,0) + + assert_raises(TypeError, galsim.PixelScale) + assert_raises(TypeError, galsim.PixelScale, scale=galsim.PixelScale(scale)) + assert_raises(TypeError, galsim.PixelScale, scale=scale, origin=galsim.PositionD(0,0)) + assert_raises(TypeError, galsim.PixelScale, scale=scale, world_origin=galsim.PositionD(0,0)) ufunc = lambda x,y: x*scale vfunc = lambda x,y: y*scale @@ -818,10 +883,25 @@ def test_pixelscale(): # Add an image origin offset x0 = 1 y0 = 1 - origin = galsim.PositionD(x0,y0) + origin = galsim.PositionI(x0,y0) wcs = galsim.OffsetWCS(scale, origin) wcs2 = galsim.PixelScale(scale).withOrigin(origin) assert wcs == wcs2, 'OffsetWCS is not == PixelScale.withOrigin(origin)' + assert wcs.origin == origin + assert wcs.scale == scale + + # Default origin is (0,0) + wcs3 = galsim.OffsetWCS(scale) + assert wcs3.origin == galsim.PositionD(0,0) + assert wcs3.world_origin == galsim.PositionD(0,0) + + assert_raises(TypeError, galsim.OffsetWCS) + assert_raises(TypeError, galsim.OffsetWCS, scale=galsim.PixelScale(scale)) + assert_raises(TypeError, galsim.OffsetWCS, scale=scale, origin=5) + assert_raises(TypeError, galsim.OffsetWCS, scale=scale, + origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) + assert_raises(TypeError, galsim.OffsetWCS, scale=scale, + world_origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) # Check basic copy and == , != for OffsetWCS: wcs2 = wcs.copy() @@ -871,6 +951,15 @@ def test_shearwcs(): g2 = -0.37 shear = galsim.Shear(g1=g1,g2=g2) wcs = galsim.ShearWCS(scale, shear) + assert wcs.shear == shear + assert wcs.origin == galsim.PositionD(0,0) + assert wcs.world_origin == galsim.PositionD(0,0) + + assert_raises(TypeError, galsim.ShearWCS) + assert_raises(TypeError, galsim.ShearWCS, shear=0.3) + assert_raises(TypeError, galsim.ShearWCS, shear=shear, origin=galsim.PositionD(0,0)) + assert_raises(TypeError, galsim.ShearWCS, shear=shear, world_origin=galsim.PositionD(0,0)) + assert_raises(TypeError, galsim.ShearWCS, g1=g1, g2=g2) # Check basic copy and == , !=: wcs2 = wcs.copy() @@ -911,6 +1000,21 @@ def test_shearwcs(): wcs = galsim.OffsetShearWCS(scale, shear, origin) wcs2 = galsim.ShearWCS(scale, shear).withOrigin(origin) assert wcs == wcs2, 'OffsetShearWCS is not == ShearWCS.withOrigin(origin)' + assert wcs.shear == shear + assert wcs.origin == origin + assert wcs.world_origin == galsim.PositionD(0,0) + + wcs3 = galsim.OffsetShearWCS(scale, shear) + assert wcs3.origin == galsim.PositionD(0,0) + assert wcs3.world_origin == galsim.PositionD(0,0) + + assert_raises(TypeError, galsim.OffsetShearWCS) + assert_raises(TypeError, galsim.OffsetShearWCS, shear=0.3) + assert_raises(TypeError, galsim.OffsetShearWCS, shear=shear, origin=5) + assert_raises(TypeError, galsim.OffsetShearWCS, shear=shear, + origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) + assert_raises(TypeError, galsim.OffsetShearWCS, shear=shear, + world_origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) # Check basic copy and == , != for OffsetShearWCS: wcs2 = wcs.copy() @@ -965,6 +1069,18 @@ def test_affinetransform(): wcs = galsim.JacobianWCS(dudx, dudy, dvdx, dvdy) + assert wcs.dudx == dudx + assert wcs.dudy == dudy + assert wcs.dvdx == dvdx + assert wcs.dvdy == dvdy + + assert_raises(TypeError, galsim.JacobianWCS) + assert_raises(TypeError, galsim.JacobianWCS, dudx, dudy, dvdx) + assert_raises(TypeError, galsim.JacobianWCS, dudx, dudy, dvdx, dvdy, + origin=galsim.PositionD(0,0)) + assert_raises(TypeError, galsim.JacobianWCS, dudx, dudy, dvdx, dvdy, + world_origin=galsim.PositionD(0,0)) + # Check basic copy and == , !=: wcs2 = wcs.copy() assert wcs == wcs2, 'JacobianWCS copy is not == the original' @@ -992,6 +1108,14 @@ def test_affinetransform(): wcs2 = galsim.JacobianWCS(dudx, dudy, dvdx, dvdy).withOrigin(origin) assert wcs == wcs2, 'AffineTransform is not == JacobianWCS.withOrigin(origin)' + assert_raises(TypeError, galsim.AffineTransform) + assert_raises(TypeError, galsim.AffineTransform, dudx, dudy, dvdx) + assert_raises(TypeError, galsim.AffineTransform, dudx, dudy, dvdx, dvdy, origin=3) + assert_raises(TypeError, galsim.AffineTransform, dudx, dudy, dvdx, dvdy, + origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) + assert_raises(TypeError, galsim.AffineTransform, dudx, dudy, dvdx, dvdy, + world_origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) + # Check basic copy and == , != for AffineTransform: wcs2 = wcs.copy() assert wcs == wcs2, 'AffineTransform copy is not == the original' @@ -1066,6 +1190,18 @@ def test_affinetransform(): # Check that using a wcs in the context of an image works correctly do_wcs_image(wcs, 'AffineTransform') + # Degenerate transformation should raise some errors + degen_wcs = galsim.JacobianWCS(0.2, 0.1, 0.2, 0.1) + assert_raises(galsim.GalSimError, degen_wcs.getDecomposition) + + image_pos = galsim.PositionD(0,0) + world_pos = degen_wcs.toWorld(image_pos) # This direction is ok. + assert_raises(galsim.GalSimError, degen_wcs.toImage, world_pos) # This is not. + assert_raises(galsim.GalSimError, degen_wcs._x, 0, 0) + assert_raises(galsim.GalSimError, degen_wcs._y, 0, 0) + assert_raises(galsim.GalSimError, degen_wcs.inverse) + assert_raises(galsim.GalSimError, degen_wcs.toImage, galsim.Gaussian(sigma=2)) + def radial_u(x, y): """A cubic radial function used for a u(x,y) function """ @@ -1120,12 +1256,29 @@ def test_uvfunction(): vfunc = lambda x,y: y * scale wcs = galsim.UVFunction(ufunc, vfunc) do_nonlocal_wcs(wcs, ufunc, vfunc, 'UVFunction like PixelScale', test_pickle=False) + assert wcs.ufunc(2.9, 3.7) == ufunc(2.9, 3.7) + assert wcs.vfunc(2.9, 3.7) == vfunc(2.9, 3.7) + assert wcs.xfunc is None + assert wcs.yfunc is None # Also check with inverse functions. xfunc = lambda u,v: u / scale yfunc = lambda u,v: v / scale wcs = galsim.UVFunction(ufunc, vfunc, xfunc, yfunc) do_nonlocal_wcs(wcs, ufunc, vfunc, 'UVFunction like PixelScale with inverse', test_pickle=False) + assert wcs.ufunc(2.9, 3.7) == ufunc(2.9, 3.7) + assert wcs.vfunc(2.9, 3.7) == vfunc(2.9, 3.7) + assert wcs.xfunc(2.9, 3.7) == xfunc(2.9, 3.7) + assert wcs.yfunc(2.9, 3.7) == yfunc(2.9, 3.7) + + assert_raises(TypeError, galsim.UVFunction) + assert_raises(TypeError, galsim.UVFunction, ufunc=ufunc) + assert_raises(TypeError, galsim.UVFunction, vfunc=vfunc) + assert_raises(TypeError, galsim.UVFunction, ufunc, vfunc, origin=5) + assert_raises(TypeError, galsim.UVFunction, ufunc, vfunc, + origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) + assert_raises(TypeError, galsim.UVFunction, ufunc, vfunc, + world_origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) # 2. Like ShearWCS scale = 0.23 @@ -1279,6 +1432,19 @@ def test_uvfunction(): wcs = galsim.UVFunction(ufunc, vfunc, xfunc, yfunc) do_nonlocal_wcs(wcs, ufunc, vfunc, 'UVFunction from demo9', test_pickle=False) + # Check that passing really long strings works correctly. + ufuncs = "0.05 * x * (1. + 2.e-6 * (x**2 + y**2))" + vfuncs = "0.05 * y * (1. + 2.e-6 * (x**2 + y**2))" + xfuncs = ("(lambda w: ( 0. if w==0. else " + " 100.*u/w*(( 5*math.sqrt(w**2+5.e3/27.)+5*w )**(1./3.) - " + " ( 5*math.sqrt(w**2+5.e3/27.)-5*w )**(1./3.))) )(math.sqrt(u**2+v**2))") + yfuncs = ("(lambda w: ( 0. if w==0. else " + " 100.*v/w*(( 5*math.sqrt(w**2+5.e3/27.)+5*w )**(1./3.) - " + " ( 5*math.sqrt(w**2+5.e3/27.)-5*w )**(1./3.))) )(math.sqrt(u**2+v**2))") + wcs = galsim.UVFunction(ufuncs, vfuncs, xfuncs, yfuncs) + do_nonlocal_wcs(wcs, ufunc, vfunc, 'UVFunction from demo9, string', test_pickle=True) + do_wcs_image(wcs, 'UVFunction from demo9, string') + # This version doesn't work with numpy arrays because of the math functions. # This provides a test of that branch of the makeSkyImage function. ufunc = lambda x,y : 0.17 * x * (1. + 1.e-5 * math.sqrt(x**2 + y**2)) @@ -1296,7 +1462,7 @@ def test_uvfunction(): do_nonlocal_wcs(wcs, lambda x,y: ufunc(x,y,-0.3), lambda x,y: vfunc(x,y,-0.3), 'UVFunction with color-dependence', test_pickle=False, color=-0.3) - # Check that passing functions as strings works correctly. + # Also, check this one as a string wcs = galsim.UVFunction(ufunc='(%r+0.1*c)*x + %r*y'%(dudx,dudy), vfunc='%r*x + (%r-0.2*c)*y'%(dvdx,dvdy), xfunc='((%r-0.2*c)*u - %r*v)/((%r+0.1*c)*(%r-0.2*c)-%r)'%( @@ -1401,6 +1567,10 @@ def test_radecfunction(): dec_str = '%r.deproject(x*galsim.arcsec,y*galsim.arcsec,projection="lambert").dec.rad'%center wcs5 = galsim.RaDecFunction(ra_str, dec_str, origin=galsim.PositionD(-9.,-8.)) + wcs6 = wcs2.copy() + assert wcs2 == wcs6, 'RaDecFunction copy is not == the original' + assert wcs6.radec_func(3,4) == radec_func(3,4) + # Check that distance, jacobian for some x,y positions match the UV values. for x,y in zip(far_x_list, far_y_list): @@ -1458,16 +1628,16 @@ def test_radecfunction(): # match pretty well. np.testing.assert_almost_equal( jac2.minLinearScale(), jac1.minLinearScale(), digits, - 'RaDecFunction '+name+' minScale() does not match expected value.') + 'RaDecFunction '+name+' minLinearScale() does not match expected value.') np.testing.assert_almost_equal( test_wcs.minLinearScale(image_pos), jac1.minLinearScale(), digits, - 'RaDecFunction '+name+' minScale(pos) does not match expected value.') + 'RaDecFunction '+name+' minLinearScale(pos) does not match expected value.') np.testing.assert_almost_equal( jac2.maxLinearScale(), jac1.maxLinearScale(), digits, - 'RaDecFunction '+name+' maxScale() does not match expected value.') + 'RaDecFunction '+name+' maxLinearScale() does not match expected value.') np.testing.assert_almost_equal( test_wcs.maxLinearScale(image_pos), jac1.maxLinearScale(), digits, - 'RaDecFunction '+name+' maxScale(pos) does not match expected value.') + 'RaDecFunction '+name+' maxLinearScale(pos) does not match expected value.') # The main discrepancy between the jacobians is a rotation term. # The pixels in the projected coordinates do not necessarily point north, @@ -1520,6 +1690,12 @@ def test_radecfunction(): do_celestial_wcs(wcs5, 'RaDecFunc 4 centered at '+str(center.ra/galsim.degrees)+ ', '+str(center.dec/galsim.degrees), test_pickle=True) + assert_raises(TypeError, galsim.RaDecFunction) + assert_raises(TypeError, galsim.RaDecFunction, radec_func, origin=5) + assert_raises(TypeError, galsim.RaDecFunction, radec_func, + origin=galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees)) + assert_raises(TypeError, galsim.RaDecFunction, radec_func, world_origin=galsim.PositionD(0,0)) + # Check that using a wcs in the context of an image works correctly # (Uses the last wcs2, wcs3 set in the above loops.) do_wcs_image(wcs2, 'RaDecFunction') @@ -1577,7 +1753,7 @@ def test_astropywcs(): import astropy.wcs import scipy # AstropyWCS constructor will do this, so check now. except ImportError as e: - print('Unable to import astropy.wcs. Skipping AstropyWCS tests.') + print('Unable to import astropy.wcs or scipy. Skipping AstropyWCS tests.') print('Caught ',e) return @@ -1586,20 +1762,67 @@ def test_astropywcs(): if __name__ == "__main__": test_tags = [ 'HPX', 'TAN', 'TSC', 'STG', 'ZEA', 'ARC', 'ZPN', 'SIP', 'TAN-FLIP', 'REGION' ] else: - test_tags = [ 'SIP' ] + test_tags = [ 'TAN', 'SIP' ] dir = 'fits_files' for tag in test_tags: file_name, ref_list = references[tag] print(tag,' file_name = ',file_name) - wcs = galsim.AstropyWCS(file_name, dir=dir) + if tag == 'TAN': + wcs = galsim.AstropyWCS(file_name, dir=dir, compression='none', hdu=0) + else: + wcs = galsim.AstropyWCS(file_name, dir=dir) do_ref(wcs, ref_list, 'AstropyWCS '+tag) + if tag == 'TAN': + # Also check origin. (Now that reference checks are done.) + wcs = galsim.AstropyWCS(file_name, dir=dir, compression='none', hdu=0, + origin=galsim.PositionD(3,4)) + do_celestial_wcs(wcs, 'Astropy file '+file_name) do_wcs_image(wcs, 'AstropyWCS_'+tag) + # Can also use an existing astropy.wcs.WCS instance to construct. + # This is probably a rare use case, but could aid efficiency if you already build the + # astropy WCS for other purposes. + astropy_wcs = wcs.wcs # Just steal such an object from the last wcs above. + assert isinstance(astropy_wcs, astropy.wcs.WCS) + wcs1 = galsim.AstropyWCS(wcs=astropy_wcs) + do_celestial_wcs(wcs1, 'AstropyWCS from wcs', test_pickle=False) + repr(wcs1) + + # Can also use a header. Again steal it from the wcs above. + wcs2 = galsim.AstropyWCS(header=wcs.header) + do_celestial_wcs(wcs2, 'AstropyWCS from header', test_pickle=True) + + # Astropy gives an error when trying to read this one. + with assert_raises(OSError): + wcs = galsim.AstropyWCS(references['TAN-PV'][0], dir=dir) + + # Doesn't support LINEAR WCS types. + with assert_raises(galsim.GalSimError): + galsim.AstropyWCS('SBProfile_comparison_images/kolmogorov.fits') + + # This file does not have any WCS information in it. + with assert_raises(galsim.GalSimError): + galsim.AstropyWCS('fits_files/blankimg.fits') + + assert_raises(TypeError, galsim.AstropyWCS) + assert_raises(TypeError, galsim.AstropyWCS, file_name, header='dummy') + assert_raises(TypeError, galsim.AstropyWCS, file_name, wcs=wcs) + assert_raises(TypeError, galsim.AstropyWCS, wcs=wcs, header='dummy') + + # Astropy thinks it can handle ZPX files, but as of version 2.0.4, they don't work right. + # It reads it in ok, and even works with it fine. But it doesn't round trip through + # its own write and read. Even worse, it natively gives a fairly obscure error, which + # we convert into an OSError by hand. + # This test will let us know when they finally fix it. If it fails, we can remove this + # test and add 'ZPX' to the list of working astropy.wcs types above. + with assert_raises(OSError): + wcs = galsim.AstropyWCS(references['ZPX'][0], dir=dir) + do_wcs_image(wcs, 'AstropyWCS_ZPX') @timer def test_pyastwcs(): @@ -1623,13 +1846,21 @@ def test_pyastwcs(): for tag in test_tags: file_name, ref_list = references[tag] print(tag,' file_name = ',file_name) - wcs = galsim.PyAstWCS(file_name, dir=dir) + if tag == 'TAN': + wcs = galsim.PyAstWCS(file_name, dir=dir, compression='none', hdu=0) + else: + wcs = galsim.PyAstWCS(file_name, dir=dir) # The PyAst implementation of the SIP type only gets the inverse transformation # approximately correct. So we need to be a bit looser in that check. approx = tag in [ 'SIP' ] do_ref(wcs, ref_list, 'PyAstWCS '+tag, approx) + if tag == 'TAN': + # Also check origin. (Now that reference checks are done.) + wcs = galsim.PyAstWCS(file_name, dir=dir, compression='none', hdu=0, + origin=galsim.PositionD(3,4)) + do_celestial_wcs(wcs, 'PyAst file '+file_name) # TAN-FLIP has an error of 4mas after write and read here, which I don't really understand. @@ -1637,6 +1868,34 @@ def test_pyastwcs(): approx = tag in [ 'ZPX', 'TAN-FLIP' ] do_wcs_image(wcs, 'PyAstWCS_'+tag, approx) + # Can also use an existing startlink.Ast.FrameSet instance to construct. + # This is probably a rare use case, but could aid efficiency if you already open the + # fits file with starlink for other purposes. + wcs = galsim.PyAstWCS(references['TAN'][0], dir=dir) + wcsinfo = wcs.wcsinfo + assert isinstance(wcsinfo, starlink.Ast.FrameSet) + wcs1 = galsim.PyAstWCS(wcsinfo=wcsinfo) + do_celestial_wcs(wcs1, 'PyAstWCS from wcsinfo', test_pickle=False) + repr(wcs1) + + # Can also use a header. Again steal it from the wcs above. + wcs2 = galsim.PyAstWCS(header=wcs.header) + do_celestial_wcs(wcs2, 'PyAstWCS from header', test_pickle=True) + + # Doesn't support LINEAR WCS types. + with assert_raises(galsim.GalSimError): + galsim.PyAstWCS('SBProfile_comparison_images/kolmogorov.fits') + + # This file does not have any WCS information in it. + with assert_raises(OSError): + galsim.PyAstWCS('fits_files/blankimg.fits') + + assert_raises(TypeError, galsim.PyAstWCS) + assert_raises(TypeError, galsim.PyAstWCS, file_name, header='dummy') + assert_raises(TypeError, galsim.PyAstWCS, file_name, wcsinfo=wcsinfo) + assert_raises(TypeError, galsim.PyAstWCS, wcsinfo=wcsinfo, header='dummy') + + @timer def test_wcstools(): @@ -1680,6 +1939,22 @@ def test_wcstools(): do_wcs_image(wcs, 'WcsToolsWCS_'+tag) + # HPX is one of the ones that WcsToolsWCS doesn't support. + with assert_raises(galsim.GalSimError): + galsim.WcsToolsWCS(references['HPX'][0], dir=dir) + + # This file does not have any WCS information in it. + with assert_raises(OSError): + galsim.WcsToolsWCS('fits_files/blankimg.fits') + + # Doesn't support LINEAR WCS types. + with assert_raises(galsim.GalSimError): + galsim.WcsToolsWCS('SBProfile_comparison_images/kolmogorov.fits') + + assert_raises(TypeError, galsim.WcsToolsWCS) + assert_raises(TypeError, galsim.WcsToolsWCS, file_name, header='dummy') + + @timer def test_gsfitswcs(): @@ -1695,14 +1970,43 @@ def test_gsfitswcs(): for tag in test_tags: file_name, ref_list = references[tag] print(tag,' file_name = ',file_name) - wcs = galsim.GSFitsWCS(file_name, dir=dir) + if tag == 'TAN': + # For this one, check compression and hdu options. + wcs = galsim.GSFitsWCS(file_name, dir=dir, compression='none', hdu=0) + else: + wcs = galsim.GSFitsWCS(file_name, dir=dir) do_ref(wcs, ref_list, 'GSFitsWCS '+tag) + if tag == 'TAN': + # Also check origin. (Now that reference checks are done.) + wcs = galsim.GSFitsWCS(file_name, dir=dir, compression='none', hdu=0, + origin=galsim.PositionD(3,4)) + do_celestial_wcs(wcs, 'GSFitsWCS '+file_name) do_wcs_image(wcs, 'GSFitsWCS_'+tag) + # TSC is one of the ones that GSFitsWCS doesn't support. + with assert_raises(galsim.GalSimValueError): + galsim.GSFitsWCS(references['TSC'][0], dir=dir) + + # Doesn't support LINEAR WCS types. + with assert_raises(galsim.GalSimError): + galsim.GSFitsWCS('SBProfile_comparison_images/kolmogorov.fits') + + # This file does not have any WCS information in it. + with assert_raises(galsim.GalSimError): + galsim.GSFitsWCS('fits_files/blankimg.fits') + + assert_raises(TypeError, galsim.GSFitsWCS) + assert_raises(TypeError, galsim.GSFitsWCS, file_name, header='dummy') + +@timer +def test_tanwcs(): + """Test the TanWCS function, which returns a GSFitsWCS instance. + """ + # Use TanWCS function to create TAN GSFitsWCS objects from scratch. # First a slight tweak on a simple scale factor dudx = 0.2342 @@ -1773,7 +2077,10 @@ def test_fitswcs(): for tag in test_tags: file_name, ref_list = references[tag] print(tag,' file_name = ',file_name) - wcs = galsim.FitsWCS(file_name, dir=dir, suppress_warning=True) + if tag == 'TAN': + wcs = galsim.FitsWCS(file_name, dir=dir, compression='none', hdu=0) + else: + wcs = galsim.FitsWCS(file_name, dir=dir, suppress_warning=True) print('FitsWCS is really ',type(wcs)) if isinstance(wcs, galsim.AffineTransform): @@ -1797,6 +2104,18 @@ def test_fitswcs(): affine = galsim.AffineTransform._readHeader(hdu.header) galsim.fits.closeHDUList(hdu_list, fin) + # This does support LINEAR WCS types. + linear = galsim.FitsWCS('SBProfile_comparison_images/kolmogorov.fits') + assert isinstance(linear, galsim.OffsetWCS) + + # This file does not have any WCS information in it. + pixel = galsim.FitsWCS('fits_files/blankimg.fits') + assert pixel == galsim.PixelScale(1.0) + + assert_raises(TypeError, galsim.FitsWCS) + assert_raises(TypeError, galsim.FitsWCS, file_name, header='dummy') + + @timer def test_scamp(): @@ -1989,6 +2308,7 @@ def test_coadd(): test_pyastwcs() test_wcstools() test_gsfitswcs() + test_tanwcs() test_fitswcs() test_scamp() test_compateq() diff --git a/tests/test_wfirst.py b/tests/test_wfirst.py index e65b93a1281..d2ab92c4bbc 100644 --- a/tests/test_wfirst.py +++ b/tests/test_wfirst.py @@ -157,9 +157,12 @@ def test_wfirst_wcs(): pa = test_data[4,:] chris_sca = test_data[5,:] n_test = len(ra_cen) + if __name__ != "__main__": + n_test = 3 # None of the first 3 fail, so the nfail test is ok. (Only 2 fail in all 100.) n_fail = 0 for i_test in range(n_test): + print('i_test = ',i_test) # Make the WCS for this test. world_pos = galsim.CelestialCoord(ra_cen[i_test]*galsim.degrees, dec_cen[i_test]*galsim.degrees) @@ -177,9 +180,43 @@ def test_wfirst_wcs(): galsim.CelestialCoord(ra[i_test]*galsim.degrees, dec[i_test]*galsim.degrees)) if found_sca is None: found_sca=0 - if found_sca != chris_sca[i_test]: n_fail += 1 + if found_sca != chris_sca[i_test]: + n_fail += 1 + print('Failed to find SCA: ',found_sca, chris_sca[i_test]) + + # Just cycle through the SCAs for the next bits. + sca_test = i_test % 18 + 1 + gs_wcs = gs_wcs_dict[sca_test] + + # Check center position: + im_cent_pos = galsim.PositionD(galsim.wfirst.n_pix/2., galsim.wfirst.n_pix/2) + gs_cent_pos = gs_wcs.toWorld(im_cent_pos) + + # Check pixel area + pix_area = gs_wcs.pixelArea(image_pos=im_cent_pos) + print('pix_area = ',pix_area) + np.testing.assert_allclose(pix_area, 0.012, atol=0.001) + + if i_test == 0: + # For just one of our tests cases, we'll do some additional tests. These will target + # the findSCA() functionality. First, check that the center is found in that SCA. + found_sca = galsim.wfirst.findSCA(gs_wcs_dict, gs_cent_pos) + np.testing.assert_equal(found_sca, sca_test, + err_msg='Did not find SCA center position to be on that SCA!') + + # Then, we go to a place that should be off the side by a tiny bit, and check that it is + # NOT on an SCA if we exclude borders, but IS on the SCA if we include borders. + im_off_edge_pos = galsim.PositionD(-2., galsim.wfirst.n_pix/2.) + world_off_edge_pos = gs_wcs.toWorld(im_off_edge_pos) + found_sca = galsim.wfirst.findSCA(gs_wcs_dict, world_off_edge_pos) + assert found_sca is None + found_sca = galsim.wfirst.findSCA(gs_wcs_dict, world_off_edge_pos, include_border=True) + np.testing.assert_equal(found_sca, sca_test, + err_msg='Did not find slightly off-edge position on the SCA' + ' when including borders!') # There were few-arcsec offsets in our WCS, so allow some fraction of failures. + print('n_fail = ',n_fail) assert n_fail < 0.05*n_test, 'Failed in SCA-matching against reference' # Check whether we're allowed to look at certain positions on certain dates. @@ -206,6 +243,30 @@ def test_wfirst_wcs(): pa = galsim.wfirst.bestPA(pos, test_date) np.testing.assert_almost_equal(pa.rad, -np.pi/2, decimal=3) + sun_pos= galsim.CelestialCoord(0*galsim.degrees, 0*galsim.degrees) + sun_pa = galsim.wfirst.bestPA(sun_pos, test_date) + assert sun_pa is None + + with assert_raises(TypeError): + galsim.wfirst.getWCS(world_pos=galsim.PositionD(300,400)) + with assert_raises(galsim.GalSimError): + galsim.wfirst.getWCS(world_pos=sun_pos, date=test_date) + with assert_raises(TypeError): + galsim.wfirst.getWCS(world_pos=pos, PA=33.) + + # Check the rather bizarre convention that LONPOLE is always 180 EXCEPT (!!) when + # observing directly at the south pole. Apparently, this convention comes from the WFIRST + # project office's use of the LONPOLE keyword. So we keep it, even though it's stupid. + # cf. https://github.com/GalSim-developers/GalSim/pull/651#discussion-diff-26277673 + assert gs_wcs_dict[1].header['LONPOLE'] == 180. + south_pole = galsim.CelestialCoord(0*galsim.degrees, -90*galsim.degrees) + wcs = galsim.wfirst.getWCS(world_pos=south_pole, SCAs=1) + assert wcs[1].header['LONPOLE'] == 0 + + with assert_raises(TypeError): + galsim.wfirst.findSCA(wcs_dict=None, world_pos=pos) + with assert_raises(TypeError): + galsim.wfirst.findSCA(wcs_dict=wcs, world_pos=galsim.PositionD(300,400)) @timer def test_wfirst_backgrounds(): @@ -227,6 +288,19 @@ def test_wfirst_backgrounds(): bp, world_pos=galsim.CelestialCoord(180.*galsim.degrees, 5.*galsim.degrees), date=datetime.date(2025,9,15)) + # world_pos must be a CelestialCoord. + with assert_raises(TypeError): + galsim.wfirst.getSkyLevel(bp, world_pos=galsim.PositionD(300,400)) + + # No world_pos works. Produces sky level for some plausible generic location. + sky_level = galsim.wfirst.getSkyLevel(bp) + print('sky_level = ',sky_level) + np.testing.assert_allclose(sky_level, 6233.47369567) # regression test relative to v1.6 + + # But not with a non-wfirst bandpass + with assert_raises(galsim.GalSimError): + galsim.wfirst.getSkyLevel(galsim.Bandpass('wave', 'nm', 400, 550)) + # The routine should have some obvious symmetry, for example, ecliptic latitude above vs. below # plane and ecliptic longitude positive vs. negative (or vs. 360 degrees - original value). # Because of how equatorial and ecliptic coordinates are related on the adopted date, we can do @@ -263,8 +337,7 @@ def test_wfirst_bandpass(): for filter_name, filter_ in bp.items(): mag = AB_sed.calculateMagnitude(bandpass=filter_) np.testing.assert_almost_equal(mag,0.0,decimal=6, - err_msg="Zeropoint not set accurately enough for bandpass filter \ - {0}".format(filter_name)) + err_msg="Zeropoint not set accurately enough for bandpass filter "+filter_name) # Do a slightly less trivial check of bandpass-related calculations: # Jeff Kruk (at Goddard) took an SED template from the Castelli-Kurucz library, normalized it to @@ -329,10 +402,38 @@ def test_wfirst_bandpass(): for key in ref_zp.keys(): galsim_zp = bp[key].zeropoint + delta_zp # They use slightly different versions of the bandpasses, so we only require agreement to - # 0.1 mag. - np.testing.assert_almost_equal(galsim_zp, ref_zp[key], decimal=1, - err_msg="Zeropoint not as expected for bandpass " - "{0}".format(key)) + # 0.05 mag. + print('zp for %s: '%key, galsim_zp, ref_zp[key]) + np.testing.assert_allclose(galsim_zp, ref_zp[key], atol=0.05, + err_msg="Wrong zeropoint for bandpass "+key) + + # Note: the difference is not due to our default thinning. This isn't any better. + nothin_bp = galsim.wfirst.getBandpasses(AB_zeropoint=True, default_thin_trunc=False) + for key in ref_zp.keys(): + galsim_zp = nothin_bp[key].zeropoint + delta_zp + print('nothin zp for %s: '%key, galsim_zp, ref_zp[key]) + np.testing.assert_allclose(galsim_zp, ref_zp[key], atol=0.05, + err_msg="Wrong zeropoint for bandpass "+key) + + # Even with fairly extreme thinning, the error is still only 0.07 mag. + verythin_bp = galsim.wfirst.getBandpasses(AB_zeropoint=True, default_thin_trunc=False, + relative_throughput=0.05, rel_err=0.1) + for key in ref_zp.keys(): + galsim_zp = verythin_bp[key].zeropoint + delta_zp + print('verythin zp for %s: '%key, galsim_zp, ref_zp[key]) + np.testing.assert_allclose(galsim_zp, ref_zp[key], atol=0.07, + err_msg="Wrong zeropoint for bandpass "+key) + + with assert_raises(TypeError): + galsim.wfirst.getBandpasses(default_thin_trunc=False, rel_tp=0.05) + with assert_warns(galsim.GalSimWarning): + galsim.wfirst.getBandpasses(relative_throughput=0.05, rel_err=0.1) + + # Can also not bother to set the zeropoint. + nozp_bp = galsim.wfirst.getBandpasses(AB_zeropoint=False) + for key in nozp_bp: + assert nozp_bp[key].zeropoint is None + @timer def test_wfirst_detectors(): @@ -398,6 +499,7 @@ def test_wfirst_detectors(): np.testing.assert_array_equal( im_2.array, im_1.array, err_msg='Persistence results depend on function used.') + assert_raises(TypeError, galsim.wfirst.applyPersistence, im_2, im0) # Then we do IPC: im_1 = im.copy() @@ -412,6 +514,17 @@ def test_wfirst_detectors(): im_2.array, im_1.array, err_msg='IPC results depend on function used.') + # Finally, just check that this runs. + # (Accuracy of component functionality is all tested elsewhere.) + npersist = len(galsim.wfirst.persistence_coefficients) + print('ncoeff for persistence: ', npersist) + ntest = npersist + 3 # Just need a few more to test that we keep npersist. + past_images = [] + for i in range(ntest): + im = obj.drawImage(scale=galsim.wfirst.pixel_scale) + past_images = galsim.wfirst.allDetectorEffects(im, past_images, rng=rng) + assert len(past_images) == npersist + @timer def test_wfirst_psfs(): @@ -428,7 +541,7 @@ def test_wfirst_psfs(): # - achromatic PSFs without loading the pupil plane image. # First test: check that if we don't specify SCAs, then we get all the expected ones. - wfirst_psfs = galsim.wfirst.getPSF(approximate_struts=True) + wfirst_psfs = galsim.wfirst.getPSF() got_scas = np.array(list(wfirst_psfs.keys())) expected_scas = np.arange(1, galsim.wfirst.n_sca+1, 1) np.testing.assert_array_equal( @@ -437,21 +550,27 @@ def test_wfirst_psfs(): # Check that if we specify SCAs, then we get the ones we specified. expected_scas = [5, 7, 14] - wfirst_psfs = galsim.wfirst.getPSF(SCAs=expected_scas, - approximate_struts=True) + wfirst_psfs = galsim.wfirst.getPSF(SCAs=expected_scas) got_scas = list(wfirst_psfs.keys()) # Have to sort it in numerical order for this comparison. got_scas.sort() got_scas = np.array(got_scas) np.testing.assert_array_equal( got_scas, expected_scas, err_msg='List of SCAs was not as requested') + for sca in got_scas: + assert isinstance(wfirst_psfs[sca], galsim.ChromaticObject) + + # Providing a wavelength returns achromatic PSFs + psfs_5 = galsim.wfirst.getPSF(SCAs=5, wavelength=1950.) + assert isinstance(psfs_5[5], galsim.GSObject) # Check that if we specify a particular wavelength, the PSF that is drawn is the same as if we # had gotten chromatic PSFs and then used evaluateAtWavelength. Note that this nominally seems # like a test of the chromatic functionality, but there are ways that getPSF() could mess up # inputs such that there is a disagreement. That's why this unit test belongs here. use_sca = 5 - use_lam = 900. # nm + bp = galsim.wfirst.getBandpasses() + use_lam = bp['Y106'].effective_wavelength wfirst_psfs_chrom = galsim.wfirst.getPSF(SCAs=use_sca, approximate_struts=True) psf_chrom = wfirst_psfs_chrom[use_sca] @@ -470,13 +589,16 @@ def test_wfirst_psfs(): np.testing.assert_array_almost_equal( im_chrom.array, im_achrom.array, decimal=8, err_msg='PSF at a given wavelength and chromatic one evaluated at that wavelength disagree.') + wfirst_psfs_achrom2 = galsim.wfirst.getPSF(SCAs=use_sca, approximate_struts=True, + wavelength=bp['Y106']) # This is equivalent. + psf_achrom2 = wfirst_psfs_achrom[use_sca] + assert psf_achrom2 == psf_achrom # Make a very limited check that interpolation works: just 2 wavelengths, 1 SCA. # use the blue and red limits for Y106: - bp = galsim.wfirst.getBandpasses() blue_limit = bp['Y106'].blue_limit red_limit = bp['Y106'].red_limit - n_waves = 2 + n_waves = 3 other_sca = 12 wfirst_psfs_int = galsim.wfirst.getPSF(SCAs=[use_sca, other_sca], approximate_struts=True, n_waves=n_waves, @@ -484,7 +606,7 @@ def test_wfirst_psfs(): psf_int = wfirst_psfs_int[use_sca] # Check that evaluation at a single wavelength is consistent with previous results. im_int = im_achrom.copy() - obj_int = psf_int.evaluateAtWavelength(blue_limit) + obj_int = psf_int.evaluateAtWavelength(use_lam) im_int = obj_int.drawImage(image=im_int, scale=galsim.wfirst.pixel_scale) # These images should agree well, but not perfectly. One of them comes from drawing an image # from an object directly, whereas the other comes from drawing an image of that object, making @@ -498,48 +620,79 @@ def test_wfirst_psfs(): err_msg='PSF at a given wavelength and interpolated chromatic one evaluated at that ' 'wavelength disagree.') + # Check some invalid inputs. + # Note, this is a total cheat for getting test coverage of the high_accuracy branches + # in getPSF. The actual test of this functionality comes below, but it is only run for + # __name__==__main__ runs (i.e. run_all_tests). + with assert_raises(galsim.GalSimIncompatibleValuesError): + galsim.wfirst.getPSF(SCAs=use_sca, n_waves=2, + approximate_struts=False, high_accuracy=True, + wavelength_limits=(red_limit, blue_limit)) + with assert_raises(TypeError): + galsim.wfirst.getPSF(SCAs=use_sca, n_waves=2, + approximate_struts=True, high_accuracy=True, + wavelength_limits=red_limit) + with assert_raises(TypeError): + galsim.wfirst.getPSF(SCAs=use_sca, + approximate_struts=False, high_accuracy=False, + wavelength='Y106') + # This is a little slow, but we do want to run this as part of normal unit testing # to cover the storePSFImage and loadPSFImages functions. - if True: - #if __name__ == '__main__': - # Check that if we store and reload, what we get back is consistent with what we put in. - test_file = 'tmp_store.fits' - # Make sure we clear out any old versions - import os - if os.path.exists(test_file): - os.remove(test_file) - full_bp_list = galsim.wfirst.getBandpasses() - bp_list = ['Y106'] - galsim.wfirst.storePSFImages(bandpass_list=bp_list, PSF_dict=wfirst_psfs_int, - filename=test_file) - new_dict = galsim.wfirst.loadPSFImages(test_file) - # Check that it contains the right list of bandpasses. - np.testing.assert_array_equal( - list(new_dict.keys()), bp_list, err_msg='Wrong list of bandpasses in stored file') - # Check that when we take the dict for that bandpass, we get the right list of SCAs. - np.testing.assert_array_equal( - list(new_dict[bp_list[0]].keys()), list(wfirst_psfs_int.keys()), - err_msg='Wrong list of SCAs in stored file') - # Now draw an image from the stored object. - img_stored = new_dict[bp_list[0]][other_sca].drawImage(scale=1.3*galsim.wfirst.pixel_scale) - # Make a comparable image from the original interpolated object. This requires convolving with - # a star that has a flat SED. - star = galsim.Gaussian(sigma=1.e-8, flux=1.) - star_sed = galsim.SED(lambda x:1, - wave_type='nanometers', - flux_type='flambda').withFlux(1, full_bp_list[bp_list[0]]) - obj = galsim.Convolve(wfirst_psfs_int[other_sca], star*star_sed) - test_im = img_stored.copy() - test_im = obj.drawImage(full_bp_list[bp_list[0]], - image=test_im, scale=1.3*galsim.wfirst.pixel_scale) - # We have made some approximations here, so we cannot expect it to be great. - # Request 1% accuracy. - np.testing.assert_array_almost_equal( - img_stored.array, test_im.array, decimal=2, - err_msg='PSF from stored file and actual PSF object disagree.') - - # Delete test files when done. - os.remove(test_file) + + # Check that if we store and reload, what we get back is consistent with what we put in. + test_file = 'tmp_store.fits' + with open(test_file, 'wb'): pass # Just make it exist to test clobber feature. + full_bp_list = galsim.wfirst.getBandpasses() + bp_list = ['Y106'] + galsim.wfirst.storePSFImages(bandpass_list=bp_list, PSF_dict=wfirst_psfs_int, + filename=test_file, clobber=True) + new_dict = galsim.wfirst.loadPSFImages(test_file) + # Check that it contains the right list of bandpasses. + np.testing.assert_array_equal( + list(new_dict.keys()), bp_list, err_msg='Wrong list of bandpasses in stored file') + # Check that when we take the dict for that bandpass, we get the right list of SCAs. + np.testing.assert_array_equal( + list(new_dict[bp_list[0]].keys()), list(wfirst_psfs_int.keys()), + err_msg='Wrong list of SCAs in stored file') + # Now draw an image from the stored object. + img_stored = new_dict[bp_list[0]][other_sca].drawImage(scale=1.3*galsim.wfirst.pixel_scale) + # Make a comparable image from the original interpolated object. This requires convolving with + # a star that has a flat SED. + star = galsim.Gaussian(sigma=1.e-8, flux=1.) + star_sed = galsim.SED(lambda x:1, + wave_type='nanometers', + flux_type='flambda').withFlux(1, full_bp_list[bp_list[0]]) + obj = galsim.Convolve(wfirst_psfs_int[other_sca], star*star_sed) + test_im = img_stored.copy() + test_im = obj.drawImage(full_bp_list[bp_list[0]], + image=test_im, scale=1.3*galsim.wfirst.pixel_scale) + # We have made some approximations here, so we cannot expect it to be great. + # Request 1% accuracy. + np.testing.assert_array_almost_equal( + img_stored.array, test_im.array, decimal=2, + err_msg='PSF from stored file and actual PSF object disagree.') + + # Delete test files when done. + os.remove(test_file) + + # Now can write to that without clobber. + galsim.wfirst.storePSFImages(wfirst_psfs_int, test_file, bp_list) + assert os.path.isfile(test_file) + # Then without clobber, raises error, since file exists. + with assert_raises(OSError): + galsim.wfirst.storePSFImages(wfirst_psfs_int, test_file, bp_list) + + with assert_raises(galsim.GalSimError): + galsim.wfirst.storePSFImages({}, test_file, bp_list, clobber=True) + with assert_raises(TypeError): + galsim.wfirst.storePSFImages(wfirst_psfs_int, test_file, [1,2,3], clobber=True) + with assert_raises(galsim.GalSimValueError): + galsim.wfirst.storePSFImages(wfirst_psfs_int, test_file, ['g','r','i','z'], clobber=True) + # Note: another coverage cheat. Leaving out bandpass_list does all bands. + # It takes a long time to write them all out for real though, so we don't do so here. + with assert_raises(galsim.GalSimValueError): + galsim.wfirst.storePSFImages(wfirst_psfs_achrom, test_file, clobber=True) # Test the construction of PSFs with high_accuracy and/or not approximate_struts # But only if we're running from the command line. @@ -550,8 +703,7 @@ def test_wfirst_psfs(): { 'approximate_struts':False, 'high_accuracy':False }, # This last test works, but it takes ~10 min to run. So even in the slow tests, # this is a bit too extreme. - #{ 'approximate_struts':False, 'high_accuracy':True, - # 'gsparams':galsim.GSParams(maximum_fft_size=8192) } + #{ 'approximate_struts':False, 'high_accuracy':True, } ]: psf = galsim.wfirst.getPSF(SCAs=use_sca, **kwargs)[use_sca] @@ -600,69 +752,54 @@ def test_wfirst_basic_numbers(): ref_pupil_plane_file = os.path.join(galsim.meta_data.share_dir, "WFIRST-AFTA_Pupil_Mask_C5_20141010_PLT.fits.gz") ref_stray_light_fraction = 0.1 - ref_ipc_kernel = np.array([ [0.001269938, 0.015399776, 0.001199862], \ - [0.013800177, 1.0, 0.015600367], \ - [0.001270391, 0.016129619, 0.001200137] ]) + ref_ipc_kernel = np.array([ [0.001269938, 0.015399776, 0.001199862], + [0.013800177, 1.0, 0.015600367], + [0.001270391, 0.016129619, 0.001200137] ]) ref_ipc_kernel /= np.sum(ref_ipc_kernel) ref_ipc_kernel = galsim.Image(ref_ipc_kernel) - ref_persistence_coefficients = \ - np.array([0.045707683,0.014959818,0.009115737,0.00656769,0.005135571, - 0.004217028,0.003577534,0.003106601])/100. + ref_persistence_coefficients = np.array( + [0.045707683,0.014959818,0.009115737,0.00656769,0.005135571, + 0.004217028,0.003577534,0.003106601])/100. ref_n_sca = 18 ref_n_pix_tot = 4096 ref_n_pix = 4088 ref_jitter_rms = 0.014 ref_charge_diffusion = 0.1 - assert galsim.wfirst.gain==ref_gain, \ - 'WFIRST gain disagrees with expected value' - assert galsim.wfirst.pixel_scale==ref_pixel_scale, \ - 'WFIRST pixel scale disagrees with expected value' - assert galsim.wfirst.diameter==ref_diameter, \ - 'WFIRST diameter disagrees with expected value' - assert galsim.wfirst.obscuration==ref_obscuration, \ - 'WFIRST obscuration disagrees with expected value' - assert galsim.wfirst.exptime==ref_exptime, \ - 'WFIRST exptime disagrees with expected value' - assert galsim.wfirst.dark_current==ref_dark_current, \ - 'WFIRST dark current disagrees with expected value' - assert galsim.wfirst.nonlinearity_beta==ref_nonlinearity_beta, \ - 'WFIRST nonlinearity disagrees with expected value' - assert galsim.wfirst.reciprocity_alpha==ref_reciprocity_alpha, \ - 'WFIRST reciprocity alpha disagrees with expected value' - assert galsim.wfirst.read_noise==ref_read_noise, \ - 'WFIRST read noise disagrees with expected value' - assert galsim.wfirst.n_dithers==ref_n_dithers, \ - 'WFIRST n_dithers disagrees with expected value' - assert galsim.wfirst.thermal_backgrounds.keys()==ref_thermal_backgrounds.keys(),\ - 'WFIRST thermal background list of filters disagrees with reference' + assert galsim.wfirst.gain==ref_gain + assert galsim.wfirst.pixel_scale==ref_pixel_scale + assert galsim.wfirst.diameter==ref_diameter + assert galsim.wfirst.obscuration==ref_obscuration + assert galsim.wfirst.exptime==ref_exptime + assert galsim.wfirst.dark_current==ref_dark_current + assert galsim.wfirst.nonlinearity_beta==ref_nonlinearity_beta + assert galsim.wfirst.reciprocity_alpha==ref_reciprocity_alpha + assert galsim.wfirst.read_noise==ref_read_noise + assert galsim.wfirst.n_dithers==ref_n_dithers + assert galsim.wfirst.thermal_backgrounds.keys()==ref_thermal_backgrounds.keys() for key in ref_thermal_backgrounds.keys(): - assert galsim.wfirst.thermal_backgrounds[key]==ref_thermal_backgrounds[key],\ - 'WFIRST thermal background for %s disagrees with expected value'%key - assert galsim.wfirst.pupil_plane_file==ref_pupil_plane_file, \ - 'WFIRST pupil plane filename disagrees with reference' - assert galsim.wfirst.stray_light_fraction==ref_stray_light_fraction, \ - 'WFIRST stray_light_fraction disagrees with expected value' - np.testing.assert_array_equal(ref_ipc_kernel, galsim.wfirst.ipc_kernel, - 'WFIRST IPC kernel disagrees with expected value') - np.testing.assert_array_equal( - ref_persistence_coefficients, galsim.wfirst.persistence_coefficients, - 'WFIRST persistence coefficients disagree with expected value') - assert galsim.wfirst.n_sca==ref_n_sca, \ - 'WFIRST n_sca disagrees with expected value' - assert galsim.wfirst.n_pix_tot==ref_n_pix_tot, \ - 'WFIRST n_pix_tot disagrees with expected value' - assert galsim.wfirst.n_pix==ref_n_pix, \ - 'WFIRST n_pix disagrees with expected value' - assert galsim.wfirst.jitter_rms==ref_jitter_rms, \ - 'WFIRST jitter_rms disagrees with expected value' - assert galsim.wfirst.charge_diffusion==ref_charge_diffusion, \ - 'WFIRST charge_diffusion disagrees with expected value' + assert galsim.wfirst.thermal_backgrounds[key]==ref_thermal_backgrounds[key] + assert galsim.wfirst.pupil_plane_file==ref_pupil_plane_file + assert galsim.wfirst.stray_light_fraction==ref_stray_light_fraction + np.testing.assert_array_equal(ref_ipc_kernel, galsim.wfirst.ipc_kernel) + np.testing.assert_array_equal(ref_persistence_coefficients, + galsim.wfirst.persistence_coefficients) + assert galsim.wfirst.n_sca==ref_n_sca + assert galsim.wfirst.n_pix_tot==ref_n_pix_tot + assert galsim.wfirst.n_pix==ref_n_pix + assert galsim.wfirst.jitter_rms==ref_jitter_rms + assert galsim.wfirst.charge_diffusion==ref_charge_diffusion if __name__ == "__main__": + #import cProfile, pstats + #pr = cProfile.Profile() + #pr.enable() test_wfirst_wcs() test_wfirst_backgrounds() test_wfirst_bandpass() test_wfirst_detectors() test_wfirst_psfs() test_wfirst_basic_numbers() + #pr.disable() + #ps = pstats.Stats(pr).sort_stats('tottime') + #ps.print_stats(30) diff --git a/tests/test_zernike.py b/tests/test_zernike.py index b81e013235a..3ef5c809123 100644 --- a/tests/test_zernike.py +++ b/tests/test_zernike.py @@ -21,15 +21,9 @@ import os import sys +import galsim from galsim_test_helpers import * -try: - import galsim -except ImportError: - path, filename = os.path.split(__file__) - sys.path.append(os.path.abspath(os.path.join(path, ".."))) - import galsim - @timer def test_Zernike_orthonormality(): @@ -100,6 +94,10 @@ def test_Zernike_orthonormality(): do_pickle(Z1) do_pickle(Z1, lambda z: tuple(z.evalCartesian(x, y))) + with assert_raises(ValueError): + Z1 = galsim.zernike.Zernike([0]*4 + [0.1]*7, R_outer=R_inner, R_inner=R_outer) + val1 = Z1.evalCartesian(x, y) + @timer def test_annular_Zernike_limit(): diff --git a/tests/time_noise_pad.py b/tests/time_noise_pad.py index 38a80faa059..aa27c1460d4 100644 --- a/tests/time_noise_pad.py +++ b/tests/time_noise_pad.py @@ -26,12 +26,7 @@ n_iter = 50 -try: - import galsim -except ImportError: - path, filename = os.path.split(__file__) - sys.path.append(os.path.abspath(os.path.join(path, ".."))) - import galsim +import galsim def funcname(): import inspect diff --git a/tests/time_zip.py b/tests/time_zip.py index c2d931eb705..a3ad29f7fd3 100644 --- a/tests/time_zip.py +++ b/tests/time_zip.py @@ -28,12 +28,7 @@ n_iter = 20 -try: - import galsim -except ImportError: - path, filename = os.path.split(__file__) - sys.path.append(os.path.abspath(os.path.join(path, ".."))) - import galsim +import galsim big_im = galsim.Image(5000, 5000) big_im_file = 'big_im_file.fits'