@@ -84,18 +84,21 @@ class ChromaticObject(object):
84
84
# profile at a particular wavelength.
85
85
# 2) Define a `withScaledFlux` method, which scales the flux at all wavelengths by a fixed
86
86
# multiplier.
87
- # 3) Initialize a `separable` attribute. This marks whether (`separable = True`) or not
87
+ # 3) Potentially define their own `__repr__` and `__str__` methods. Note that the default
88
+ # assumes that `.obj` is the only attribute of significance, but this isn't always
89
+ # appropriate, (e.g. ChromaticSum, ChromaticConvolution).
90
+ # 4) Initialize a `separable` attribute. This marks whether (`separable = True`) or not
88
91
# (`separable = False`) the given chromatic profile can be factored into a spatial profile
89
92
# and a spectral profile. Separable profiles can be drawn quickly by evaluating at a single
90
93
# wavelength and adjusting the flux via a (fast) 1D integral over the spectral component.
91
94
# Inseparable profiles, on the other hand, need to be evaluated at multiple wavelengths
92
95
# in order to draw (slow).
93
- # 4 ) Separable objects must initialize an `SED` attribute, which is a callable object (often a
96
+ # 5 ) Separable objects must initialize an `SED` attribute, which is a callable object (often a
94
97
# `galsim.SED` instance) that returns the _relative_ flux of the profile at a given
95
98
# wavelength. (The _absolute_ flux is controlled by both the `SED` and the `.flux` attribute
96
99
# of the underlying chromaticized GSObject(s). See `galsim.Chromatic` docstring for details
97
100
# concerning normalization.)
98
- # 5 ) Initialize a `wave_list` attribute, which specifies wavelengths at which the profile (or
101
+ # 6 ) Initialize a `wave_list` attribute, which specifies wavelengths at which the profile (or
99
102
# the SED in the case of separable profiles) will be evaluated when drawing a
100
103
# ChromaticObject. The type of `wave_list` should be a numpy array, and may be empty, in
101
104
# which case either the Bandpass object being drawn against, or the integrator being used
@@ -105,6 +108,7 @@ class ChromaticObject(object):
105
108
# attribute representing a manipulated `GSObject` or `ChromaticObject`, or an `objlist`
106
109
# attribute in the case of compound classes like `ChromaticSum` and `ChromaticConvolution`.
107
110
111
+
108
112
def __init__ (self , obj ):
109
113
self .obj = obj
110
114
if isinstance (obj , galsim .GSObject ):
@@ -119,6 +123,25 @@ def __init__(self, obj):
119
123
raise TypeError ("Can only directly instantiate ChromaticObject with a GSObject " +
120
124
"or ChromaticObject argument." )
121
125
126
+ @staticmethod
127
+ def _get_multiplier (sed , bandpass , wave_list ):
128
+ wave_list = np .array (wave_list )
129
+ if len (wave_list ) > 0 :
130
+ multiplier = np .trapz (sed (wave_list ) * bandpass (wave_list ), wave_list )
131
+ else :
132
+ multiplier = galsim .integ .int1d (lambda w : sed (w ) * bandpass (w ),
133
+ bandpass .blue_limit , bandpass .red_limit )
134
+ return multiplier
135
+
136
+ @staticmethod
137
+ def resize_multiplier_cache (maxsize ):
138
+ """ Resize the cache (default size=10) containing the integral over the product of an SED
139
+ and a Bandpass, which is used by ChromaticObject.drawImage().
140
+
141
+ @param maxsize The new number of products to cache.
142
+ """
143
+ ChromaticObject ._multiplier_cache .resize (maxsize )
144
+
122
145
def __repr__ (self ):
123
146
return 'galsim.ChromaticObject(%r)' % self .obj
124
147
@@ -225,12 +248,19 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs):
225
248
226
249
By default, the above two integrators will use the trapezoidal rule for integration. The
227
250
midpoint rule for integration can be specified instead by passing an integrator that has
228
- been initialized with the `rule=galsim.integ.midpt` argument. Finally, when creating a
251
+ been initialized with the `rule=galsim.integ.midpt` argument. When creating a
229
252
ContinuousIntegrator, the number of samples `N` is also an argument. For example:
230
253
231
254
>>> integrator = galsim.ContinuousIntegrator(rule=galsim.integ.midpt, N=100)
232
255
>>> image = chromatic_obj.drawImage(bandpass, integrator=integrator)
233
256
257
+ Finally, this method uses a cache to avoid recomputing the integral over the product of
258
+ the bandpass and object SED when possible (i.e., for separable profiles). Because the
259
+ cache size is finite, users may find that it is more efficient when drawing many images
260
+ to group images using the same SEDs and bandpasses together in order to hit the cache more
261
+ often. The default cache size is 10, but may be resized using the
262
+ `ChromaticObject.resize_multiplier_cache()` method.
263
+
234
264
@param bandpass A Bandpass object representing the filter against which to
235
265
integrate.
236
266
@param image Optionally, the Image to draw onto. (See GSObject.drawImage()
@@ -265,11 +295,7 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', **kwargs):
265
295
wave_list = self ._getCombinedWaveList (bandpass )
266
296
267
297
if self .separable :
268
- if len (wave_list ) > 0 :
269
- multiplier = np .trapz (self .SED (wave_list ) * bandpass (wave_list ), wave_list )
270
- else :
271
- multiplier = galsim .integ .int1d (lambda w : self .SED (w ) * bandpass (w ),
272
- bandpass .blue_limit , bandpass .red_limit )
298
+ multiplier = ChromaticObject ._multiplier_cache (self .SED , bandpass , tuple (wave_list ))
273
299
prof0 *= multiplier / self .SED (bandpass .effective_wavelength )
274
300
image = prof0 .drawImage (image = image , ** kwargs )
275
301
return image
@@ -712,6 +738,9 @@ def offset_func(w):
712
738
713
739
return galsim .Transform (self , offset = offset )
714
740
741
+ ChromaticObject ._multiplier_cache = galsim .utilities .LRU_Cache (
742
+ ChromaticObject ._get_multiplier , maxsize = 10 )
743
+
715
744
716
745
class InterpolatedChromaticObject (ChromaticObject ):
717
746
"""A ChromaticObject that uses interpolation of predrawn images to speed up subsequent
@@ -788,8 +817,8 @@ def __init__(self, obj, waves, oversample_fac=1.0):
788
817
789
818
# Finally, now that we have an image scale and size, draw all the images. Note that
790
819
# `no_pixel` is used (we want the object on its own, without a pixel response).
791
- self .ims = [ obj .drawImage (scale = scale , nx = im_size , ny = im_size , method = 'no_pixel' ) \
792
- for obj in objs ]
820
+ self .ims = [ obj .drawImage (scale = scale , nx = im_size , ny = im_size , method = 'no_pixel' )
821
+ for obj in objs ]
793
822
794
823
def __repr__ (self ):
795
824
s = 'galsim.InterpolatedChromaticObject(%r,%r' % (self .original , self .waves )
@@ -1648,6 +1677,48 @@ def __init__(self, *args, **kwargs):
1648
1677
for obj in self .objlist :
1649
1678
self .wave_list = np .union1d (self .wave_list , obj .wave_list )
1650
1679
1680
+ @staticmethod
1681
+ def _get_effective_prof (sep_SED , insep_profs , bandpass ,
1682
+ iimult , wave_list , wmult , integrator ,
1683
+ gsparams ):
1684
+ # Collapse inseparable profiles into one effective profile
1685
+ SED = lambda w : reduce (lambda x ,y :x * y , [s (w ) for s in sep_SED ], 1 )
1686
+ insep_obj = galsim .Convolve (insep_profs , gsparams = gsparams )
1687
+ # Find scale at which to draw effective profile
1688
+ iiscale = insep_obj .evaluateAtWavelength (bandpass .effective_wavelength ).nyquistScale ()
1689
+ if iimult is not None :
1690
+ iiscale /= iimult
1691
+ # Create the effective bandpass.
1692
+ wave_list = np .union1d (wave_list , bandpass .wave_list )
1693
+ wave_list = wave_list [wave_list >= bandpass .blue_limit ]
1694
+ wave_list = wave_list [wave_list <= bandpass .red_limit ]
1695
+ effective_bandpass = galsim .Bandpass (
1696
+ galsim .LookupTable (wave_list , bandpass (wave_list ) * SED (wave_list ),
1697
+ interpolant = 'linear' ))
1698
+ # If there's only one inseparable profile, let it draw itself.
1699
+ if len (insep_profs ) == 1 :
1700
+ effective_prof_image = insep_profs [0 ].drawImage (
1701
+ effective_bandpass , wmult = wmult , scale = iiscale , integrator = integrator ,
1702
+ method = 'no_pixel' )
1703
+ # Otherwise, use superclass ChromaticObject to draw convolution of inseparable profiles.
1704
+ else :
1705
+ effective_prof_image = ChromaticObject .drawImage (
1706
+ insep_obj , effective_bandpass , wmult = wmult , scale = iiscale ,
1707
+ integrator = integrator , method = 'no_pixel' )
1708
+
1709
+ effective_prof = galsim .InterpolatedImage (effective_prof_image , gsparams = gsparams )
1710
+ return effective_prof
1711
+
1712
+ @staticmethod
1713
+ def resize_effective_prof_cache (maxsize ):
1714
+ """ Resize the cache containing effective profiles, (i.e., wavelength-integrated products
1715
+ of separable profile SEDs, inseparable profiles, and Bandpasses), which are used by
1716
+ ChromaticConvolution.drawImage().
1717
+
1718
+ @param maxsize The new number of effective profiles to cache.
1719
+ """
1720
+ ChromaticConvolution ._effective_prof_cache .resize (maxsize )
1721
+
1651
1722
def _findSED (self ):
1652
1723
# pull out the non-trivial seds
1653
1724
sedlist = [ obj .SED for obj in self .objlist if obj .SED != galsim .SED ('1' ) ]
@@ -1692,6 +1763,14 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', iimult=None,
1692
1763
Works by finding sums of profiles which include separable portions, which can then be
1693
1764
integrated before doing any convolutions, which are pushed to the end.
1694
1765
1766
+ This method uses a cache to avoid recomputing 'effective' profiles, which are the
1767
+ wavelength-integrated products of inseparable profiles, the spectral components of
1768
+ separable profiles, and the bandpass. Because the cache size is finite, users may find
1769
+ that it is more efficient when drawing many images to group images using the same
1770
+ SEDs, bandpasses, and inseparable profiles (generally PSFs) together in order to hit the
1771
+ cache more often. The default cache size is 10, but may be resized using the
1772
+ `ChromaticConvolution.resize_effective_prof_cache()` method.
1773
+
1695
1774
@param bandpass A Bandpass object representing the filter against which to
1696
1775
integrate.
1697
1776
@param image Optionally, the Image to draw onto. (See GSObject.drawImage()
@@ -1810,42 +1889,20 @@ def drawImage(self, bandpass, image=None, integrator='trapezoidal', iimult=None,
1810
1889
# insep_profs should never be empty, since separable cases were farmed out to
1811
1890
# ChromaticObject.drawImage() above.
1812
1891
1813
- # Collapse inseparable profiles into one effective profile
1814
- SED = lambda w : reduce (lambda x ,y :x * y , [s (w ) for s in sep_SED ], 1 )
1815
- insep_obj = galsim .Convolve (insep_profs , gsparams = self .gsparams )
1816
- # Find scale at which to draw effective profile
1817
- iiscale = insep_obj .evaluateAtWavelength (bandpass .effective_wavelength ).nyquistScale ()
1818
- if iimult is not None :
1819
- iiscale /= iimult
1820
- # Create the effective bandpass.
1821
- wave_list = np .union1d (wave_list , bandpass .wave_list )
1822
- wave_list = wave_list [wave_list >= bandpass .blue_limit ]
1823
- wave_list = wave_list [wave_list <= bandpass .red_limit ]
1824
- effective_bandpass = galsim .Bandpass (
1825
- galsim .LookupTable (wave_list , bandpass (wave_list ) * SED (wave_list ),
1826
- interpolant = 'linear' ))
1827
- # If there's only one inseparable profile, let it draw itself.
1828
1892
wmult = kwargs .get ('wmult' , 1 )
1829
- if len (insep_profs ) == 1 :
1830
- effective_prof_image = insep_profs [0 ].drawImage (
1831
- effective_bandpass , wmult = wmult , scale = iiscale , integrator = integrator ,
1832
- method = 'no_pixel' )
1833
- # Otherwise, use superclass ChromaticObject to draw convolution of inseparable profiles.
1834
- else :
1835
- effective_prof_image = ChromaticObject .drawImage (
1836
- insep_obj , effective_bandpass , wmult = wmult , scale = iiscale ,
1837
- integrator = integrator , method = 'no_pixel' )
1838
-
1839
- # Image -> InterpolatedImage
1840
- # It could be useful to cache this result if drawing more than one object with the same
1841
- # PSF+SED combination. This naturally happens in a ring test or when fitting the
1842
- # parameters of a galaxy profile to an image when the PSF is constant.
1843
- effective_prof = galsim .InterpolatedImage (effective_prof_image , gsparams = self .gsparams )
1893
+ # Collapse inseparable profiles into one effective profile
1894
+ effective_prof = ChromaticConvolution ._effective_prof_cache (
1895
+ tuple (sep_SED ), tuple (insep_profs ), bandpass , iimult , tuple (wave_list ),
1896
+ wmult , integrator , self .gsparams )
1897
+
1844
1898
# append effective profile to separable profiles (which should all be GSObjects)
1845
1899
sep_profs .append (effective_prof )
1846
1900
# finally, convolve and draw.
1847
1901
final_prof = galsim .Convolve (sep_profs , gsparams = self .gsparams )
1848
- return final_prof .drawImage (image = image ,** kwargs )
1902
+ return final_prof .drawImage (image = image , ** kwargs )
1903
+
1904
+ ChromaticConvolution ._effective_prof_cache = galsim .utilities .LRU_Cache (
1905
+ ChromaticConvolution ._get_effective_prof , maxsize = 10 )
1849
1906
1850
1907
1851
1908
class ChromaticDeconvolution (ChromaticObject ):
0 commit comments