diff --git a/galsim/errors.py b/galsim/errors.py index 01b1be4082..2302b4b2a4 100644 --- a/galsim/errors.py +++ b/galsim/errors.py @@ -25,8 +25,8 @@ 'GalSimSEDError', 'GalSimHSMError', 'GalSimFFTSizeError', 'GalSimConfigError', 'GalSimConfigValueError', 'GalSimNotImplementedError', - 'GalSimWarning', 'GalSimDeprecationWarning', - 'convert_cpp_errors', 'galsim_warn', ] + 'GalSimWarning', 'GalSimDeprecationWarning', 'GalSimFFTSizeWarning', + 'convert_cpp_errors', 'galsim_warn', 'galsim_warn_fft' ] import warnings from contextlib import contextmanager @@ -125,10 +125,6 @@ # 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, @@ -345,6 +341,9 @@ class GalSimFFTSizeError(GalSimError): mem: The estimated memory that would be required (in GB) for the FFT. """ def __init__(self, message, size): + from .deprecated import depr + depr(GalSimFFTSizeError, 2.7, '', + "Cases that used to raise GalSimFFTSizeError now emit a GalSimFFTSizeWarning instead.") self.message = message self.size = size self.mem = size * size * 24. / 1024**3 @@ -412,6 +411,31 @@ class GalSimDeprecationWarning(GalSimWarning): """ def __repr__(self): return 'galsim.GalSimDeprecationWarning(%r)'%(str(self)) +class GalSimFFTSizeWarning(GalSimWarning): + """A GalSim-specific warning 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 and want to suppress this warning,\n" + message += "you may update gsparams.maximum_fft_size." + super(GalSimFFTSizeWarning, self).__init__(message) + def __repr__(self): + return 'galsim.GalSimFFTSizeWarning(%r,%r)'%(self.message, self.size) + def __reduce__(self): + return GalSimFFTSizeWarning, (self.message, self.size) + +def galsim_warn_fft(message, size): + warnings.warn(GalSimFFTSizeWarning(message, size)) + @contextmanager def convert_cpp_errors(error_type=GalSimError): try: diff --git a/galsim/gsobject.py b/galsim/gsobject.py index 4be5872097..d998d743fb 100644 --- a/galsim/gsobject.py +++ b/galsim/gsobject.py @@ -27,7 +27,8 @@ from .position import _PositionD, _PositionI, Position, parse_pos_args from ._utilities import lazy_property from .errors import GalSimError, GalSimRangeError, GalSimValueError, GalSimIncompatibleValuesError -from .errors import GalSimFFTSizeError, GalSimNotImplementedError, convert_cpp_errors, galsim_warn +from .errors import GalSimNotImplementedError, convert_cpp_errors +from .errors import galsim_warn, galsim_warn_fft from .image import Image, ImageD, ImageF, ImageCD, ImageCF from .shear import Shear, _Shear from .angle import Angle @@ -162,17 +163,11 @@ class GSObject: >>> conv = galsim.Convolve([gal,psf]) >>> 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 1666, in drawImage - added_photons = prof.drawFFT(draw_image, add) - File "galsim/gsobject.py", line 1877, in drawFFT - kimage, wrap_size = self.drawFFT_makeKImage(image) - 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. + galsim/errors.py:437: GalSimFFTSizeWarning: drawFFT requires a very large FFT. 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. + If you can handle the large FFT and want to suppress this warning, + you may update gsparams.maximum_fft_size. + warnings.warn(GalSimFFTSizeWarning(message, size)) >>> 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!) @@ -1968,7 +1963,7 @@ def drawFFT_makeKImage(self, image): Nk = int(np.ceil(maxk/dk)) * 2 if Nk > self.gsparams.maximum_fft_size: - raise GalSimFFTSizeError("drawFFT requires an FFT that is too large.", Nk) + galsim_warn_fft("drawFFT requires a very large FFT.", Nk) bounds = _BoundsI(0,Nk//2,-Nk//2,Nk//2) if image.dtype in (np.complex128, np.float64, np.int32, np.uint32): diff --git a/galsim/gsparams.py b/galsim/gsparams.py index f032083652..64f10868ee 100644 --- a/galsim/gsparams.py +++ b/galsim/gsparams.py @@ -43,14 +43,16 @@ class GSParams: Parameters: minimum_fft_size: The minimum size of any FFT that may need to be performed. [default: 128] - maximum_fft_size: The maximum allowed size of an image for performing an FFT. This - is more about memory use than accuracy. We have this maximum - value to help prevent the user from accidentally trying to perform - an extremely large FFT that crashes the program. Instead, GalSim - 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: 8192] + maximum_fft_size: The maximum allowed size of an image for performing an FFT without + warning. This is more about memory use than accuracy. We have this + maximum value to inform a user who accidentally performs an extremely + large FFT why they just crashed the program. GalSim used to + raise an exception indicating that the image is too large, + which is often a sign of an error in the user's code. However, we + now just emit a warning about the large FFT, so if the code crashes + you have some indication of why. If you have the memory to handle it, + you can raise this limit to allow the calculation to happen without + seeing the warning. [default: 8192] 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 diff --git a/galsim/phase_psf.py b/galsim/phase_psf.py index 003881ba45..e1181f8bf8 100644 --- a/galsim/phase_psf.py +++ b/galsim/phase_psf.py @@ -34,7 +34,7 @@ from .interpolatedimage import InterpolatedImage from .utilities import doc_inherit, OrderedWeakRef, rotate_xy, lazy_property, basestring from .errors import GalSimValueError, GalSimRangeError, GalSimIncompatibleValuesError -from .errors import GalSimFFTSizeError, galsim_warn +from .errors import galsim_warn, galsim_warn_fft from .photon_array import TimeSampler, PhotonArray from .airy import Airy from .second_kick import SecondKick @@ -328,7 +328,7 @@ def _generate_pupil_plane(self): # Check FFT size if self._npix > self.gsparams.maximum_fft_size: - raise GalSimFFTSizeError("Created pupil plane array that is too large.",self._npix) + galsim_warn_fft("Created pupil plane array that will need a very large fft.",self._npix) # Shrink scale such that size = scale * npix exactly. self._pupil_plane_scale = self._pupil_plane_size / self._npix @@ -383,7 +383,7 @@ def _load_pupil_plane(self): # Check FFT size if self._npix > self.gsparams.maximum_fft_size: - raise GalSimFFTSizeError("Loaded pupil plane array that is too large.", self._npix) + galsim_warn_fft("Loaded pupil plane array that will need a very large fft.",self._npix) # Sanity checks if self._pupil_plane_im.array.shape[0] != self._pupil_plane_im.array.shape[1]: diff --git a/tests/test_chromatic.py b/tests/test_chromatic.py index 4eccb9a4a6..4c1603e080 100644 --- a/tests/test_chromatic.py +++ b/tests/test_chromatic.py @@ -1493,7 +1493,7 @@ def test_gsparams(): # 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(galsim.GalSimFFTSizeError): + with assert_warns(galsim.GalSimFFTSizeWarning): gal.drawImage(bandpass) assert (galsim.Gaussian(fwhm=1) * bulge_SED) != gal assert (galsim.Gaussian(fwhm=1) * bulge_SED).withGSParams(gsparams) == gal @@ -1503,7 +1503,7 @@ def test_gsparams(): gal = galsim.Gaussian(fwhm=1) * bulge_SED psf = galsim.Gaussian(sigma=0.4) final = galsim.Convolve([gal, psf], gsparams=gsparams) - with assert_raises(galsim.GalSimFFTSizeError): + with assert_warns(galsim.GalSimFFTSizeWarning): final.drawImage(bandpass) # Use a restrictive one this time, so we test the "most restrictive gsparams" feature diff --git a/tests/test_config_gsobject.py b/tests/test_config_gsobject.py index 6eba44ee8a..bec54a181b 100644 --- a/tests/test_config_gsobject.py +++ b/tests/test_config_gsobject.py @@ -669,7 +669,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(galsim.GalSimFFTSizeError): + with assert_warns(galsim.GalSimFFTSizeWarning): gsobject_compare(gal6a, gal6b, conv=galsim.Gaussian(sigma=1)) gal7a = galsim.config.BuildGSObject(config, 'gal7')[0] diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index ea7141914a..6f921a948d 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -17,6 +17,7 @@ # import os +import warnings import numpy as np import galsim @@ -968,6 +969,27 @@ def test_save_photons(): assert np.allclose(np.sum(image.photons.flux), flux, rtol=0.1) repr(obj) +@timer +def test_galsim_fft_size_error(): + """Test basic usage of GalSimFFTSizeError + """ + # This feela a little gratuitous, since almost certainly no one used GalSimFFTSizeError + # for anything directly. Even catching it seems unlikely. But it was technically part + # of our API, so just deprecate it and make sure it still works appropriately. + err = check_dep(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) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore",category=galsim.GalSimDeprecationWarning) + check_pickle(err) + + if __name__ == "__main__": runtests(__file__) diff --git a/tests/test_errors.py b/tests/test_errors.py index f407857aed..7d75f0e4f8 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -203,21 +203,6 @@ def test_galsim_hsm_error(): check_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) - check_pickle(err) - @timer def test_galsim_config_error(): @@ -296,6 +281,17 @@ def test_galsim_deprecation_warning(): assert isinstance(err, UserWarning) check_pickle(err) +@timer +def test_galsim_fftsize_warning(): + """Test basic usage of GalSimFFTSizeWarning + """ + err = galsim.GalSimFFTSizeWarning("Test", 10240) + print('str = ',str(err)) + print('repr = ',repr(err)) + assert str(err).startswith("Test") + assert isinstance(err, UserWarning) + check_pickle(err) + if __name__ == "__main__": runtests(__file__) diff --git a/tests/test_phase_psf.py b/tests/test_phase_psf.py index 78bac176ca..658feb2e92 100644 --- a/tests/test_phase_psf.py +++ b/tests/test_phase_psf.py @@ -79,18 +79,20 @@ def test_aperture(): 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 - with assert_raises(galsim.GalSimFFTSizeError): - ap = galsim.Aperture(1.7, pupil_plane_scale=1.e-4) + # If the constructed pupil plane would be too large, emit a warning + # For testing this and the next one, we change gsparams.maximum_fft_size, rather than try + # to build or load a really large image. + with assert_warns(galsim.GalSimFFTSizeWarning): + ap = galsim.Aperture(1.7, pupil_plane_scale=0.01, + gsparams=galsim.GSParams(maximum_fft_size=64)) ap._illuminated # Only triggers once we force it to build the illuminated array # 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): + with assert_warns(galsim.GalSimFFTSizeWarning): ap = galsim.Aperture(1.7, pupil_plane_im=im, gsparams=galsim.GSParams(maximum_fft_size=64)) ap._illuminated - # Other choices just give warnings about pupil scale or size being inappropriate + # Other choices give warnings about pupil scale or size being inappropriate with assert_warns(galsim.GalSimWarning): ap = galsim.Aperture(diam=1.7, pupil_plane_size=3, pupil_plane_scale=0.03) ap._illuminated @@ -1537,8 +1539,8 @@ def test_t_persistence(): nphot = 1_000_000 photons = psf.drawImage(save_photons=True, method='phot', n_photons=nphot).photons assert photons.hasAllocatedTimes() - assert np.min(photons.time) > 10.0 - assert np.max(photons.time) < 25.0 + assert np.min(photons.time) >= 10.0 + assert np.max(photons.time) <= 25.0 + 1.e-10 # slight slop to allow for numerical imprecision @timer