From 5919469341136d5799e3c4cf7f487aabea848b08 Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Tue, 9 Dec 2025 12:24:13 -0500 Subject: [PATCH 1/8] Emit warnings for large FFT rather than raising GalSimFFTSizeError (#1332) --- galsim/errors.py | 31 +++++++++++++++++++++++++++++-- galsim/gsobject.py | 5 +++-- galsim/phase_psf.py | 6 +++--- tests/test_chromatic.py | 4 ++-- tests/test_config_gsobject.py | 2 +- tests/test_phase_psf.py | 14 ++++++++------ 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/galsim/errors.py b/galsim/errors.py index 01b1be4082..40de46693b 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 @@ -412,6 +412,33 @@ 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, " + 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..fcffa4f3ed 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 @@ -1968,7 +1969,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/phase_psf.py b/galsim/phase_psf.py index 003881ba45..5e2099d37e 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 is too large.",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 is too large.", 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_phase_psf.py b/tests/test_phase_psf.py index 78bac176ca..1cebc38eb2 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 From 4743f5695a584c6c1caef5538f6337b104af0f2a Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Tue, 9 Dec 2025 12:31:42 -0500 Subject: [PATCH 2/8] Deprecate GalSimFFTSizeError (#1332) --- galsim/errors.py | 9 +++------ tests/test_deprecated.py | 18 ++++++++++++++++++ tests/test_errors.py | 15 --------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/galsim/errors.py b/galsim/errors.py index 40de46693b..a772536ecb 100644 --- a/galsim/errors.py +++ b/galsim/errors.py @@ -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 @@ -434,11 +433,9 @@ def __repr__(self): 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/tests/test_deprecated.py b/tests/test_deprecated.py index ea7141914a..b814e69c3c 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -968,6 +968,24 @@ 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) + + if __name__ == "__main__": runtests(__file__) diff --git a/tests/test_errors.py b/tests/test_errors.py index f407857aed..1270cea323 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(): From 17c35c4f1d3f223b490aa200edcedde7bb24f6c9 Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Tue, 9 Dec 2025 12:46:37 -0500 Subject: [PATCH 3/8] Update some docs for new behavior (#1332) --- galsim/errors.py | 2 +- galsim/gsobject.py | 14 ++++---------- galsim/gsparams.py | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/galsim/errors.py b/galsim/errors.py index a772536ecb..2302b4b2a4 100644 --- a/galsim/errors.py +++ b/galsim/errors.py @@ -425,7 +425,7 @@ def __init__(self, message, 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, " + 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): diff --git a/galsim/gsobject.py b/galsim/gsobject.py index fcffa4f3ed..d998d743fb 100644 --- a/galsim/gsobject.py +++ b/galsim/gsobject.py @@ -163,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!) 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 From 79835d78c088975be8d81510dc091896164ce788 Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Tue, 9 Dec 2025 15:34:29 -0500 Subject: [PATCH 4/8] coverage (#1332) --- tests/test_deprecated.py | 1 - tests/test_errors.py | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index b814e69c3c..f0aa5c0dbc 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -986,6 +986,5 @@ def test_galsim_fft_size_error(): assert isinstance(err, galsim.GalSimError) - if __name__ == "__main__": runtests(__file__) diff --git a/tests/test_errors.py b/tests/test_errors.py index 1270cea323..c315dfd4de 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -281,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 GalSimDeprecationWarning + """ + 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__) From 805957e71a7f5ea47a28bec70f5191793f81b6d9 Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Tue, 9 Dec 2025 20:40:14 -0500 Subject: [PATCH 5/8] Add some slight slop on time sampler test to allow for rare values at edge of range. (#1332) --- tests/test_phase_psf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_phase_psf.py b/tests/test_phase_psf.py index 1cebc38eb2..658feb2e92 100644 --- a/tests/test_phase_psf.py +++ b/tests/test_phase_psf.py @@ -1539,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 From 5adcca66939409e5bf31d2a7961b84e65e7ba456 Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Tue, 9 Dec 2025 21:59:00 -0500 Subject: [PATCH 6/8] coverage (#1332) --- tests/test_deprecated.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index f0aa5c0dbc..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 @@ -984,6 +985,10 @@ def test_galsim_fft_size_error(): 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__": From a3a11320ee793c7c84de8d0d11f6185d64bac018 Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Wed, 17 Dec 2025 10:10:58 -0500 Subject: [PATCH 7/8] Not 'too' large anymore (#1332) --- galsim/phase_psf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/galsim/phase_psf.py b/galsim/phase_psf.py index 5e2099d37e..e1181f8bf8 100644 --- a/galsim/phase_psf.py +++ b/galsim/phase_psf.py @@ -328,7 +328,7 @@ def _generate_pupil_plane(self): # Check FFT size if self._npix > self.gsparams.maximum_fft_size: - galsim_warn_fft("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: - galsim_warn_fft("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]: From 52ee181931c3970374f60d73977378655bfabdc9 Mon Sep 17 00:00:00 2001 From: Mike Jarvis Date: Wed, 17 Dec 2025 10:11:35 -0500 Subject: [PATCH 8/8] typo (#1332) --- tests/test_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_errors.py b/tests/test_errors.py index c315dfd4de..7d75f0e4f8 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -283,7 +283,7 @@ def test_galsim_deprecation_warning(): @timer def test_galsim_fftsize_warning(): - """Test basic usage of GalSimDeprecationWarning + """Test basic usage of GalSimFFTSizeWarning """ err = galsim.GalSimFFTSizeWarning("Test", 10240) print('str = ',str(err))