Skip to content

Commit fea23ea

Browse files
committed
Merge branch 'master' into #670
2 parents 55a48df + 86f5682 commit fea23ea

File tree

10 files changed

+933
-574
lines changed

10 files changed

+933
-574
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ API Changes
1818
- Removed (from the python layer) Interpolant2d and InterpolantXY, since we
1919
we have only actually implemented 1-d interpolants. (#218)
2020
- Removed the MultipleImage way of constructing an SBInterpolatedImage, since
21-
we do not use it anywhere, nor are there unit tests for it. The C++ layer
22-
still has the implementation of it if we ever find a need for it. (#218)
21+
we do not use it anywhere, nor are there unit tests for it. (#218, #642)
2322
- Made the default tolerance for all Interpolants equal to 1.e-4. It already
2423
was for Cubic, Quintic, and Lanczos, which are the ones we normally use,
2524
so this just changes the default for the others. (#218)
@@ -87,7 +86,8 @@ New Features
8786
- Added TopHat class implementing a circular tophat profile. (#639)
8887
- Added ability of Noise objects to take a new random number generator (a
8988
BaseDeviate instance) when being copied. (#643)
90-
89+
- Added InterpolatedKImage GSObject for constructing a surface brightness
90+
profile out of samples of its Fourier transform. (#642)
9191

9292
Bug Fixes and Improvements
9393
--------------------------

galsim/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
from shapelet import Shapelet, ShapeletSize, FitShapelet
131131
from interpolatedimage import Interpolant
132132
from interpolatedimage import Nearest, Linear, Cubic, Quintic, Lanczos, SincInterpolant, Delta
133-
from interpolatedimage import InterpolatedImage
133+
from interpolatedimage import InterpolatedImage, InterpolatedKImage
134134
from compound import Add, Sum, Convolve, Convolution, Deconvolve, Deconvolution
135135
from compound import AutoConvolve, AutoConvolution, AutoCorrelate, AutoCorrelation
136136
from transform import Transform, Transformation

galsim/interpolatedimage.py

Lines changed: 175 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from . import _galsim
2626
from ._galsim import Interpolant
2727
from ._galsim import Nearest, Linear, Cubic, Quintic, Lanczos, SincInterpolant, Delta
28+
import numpy as np
2829

2930
class InterpolatedImage(GSObject):
3031
"""A class describing non-parametric profiles specified using an Image, which can be
@@ -258,8 +259,6 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization=
258259
depr('dx', 1.1, 'scale')
259260
scale = dx
260261

261-
import numpy
262-
263262
# first try to read the image as a file. If it's not either a string or a valid
264263
# pyfits hdu or hdulist, then an exception will be raised, which we ignore and move on.
265264
try:
@@ -270,8 +269,8 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization=
270269
# make sure image is really an image and has a float type
271270
if not isinstance(image, galsim.Image):
272271
raise ValueError("Supplied image is not an Image instance")
273-
if image.dtype != numpy.float32 and image.dtype != numpy.float64:
274-
raise ValueError("Supplied image is not an image of floats or doubles!")
272+
if image.dtype != np.float32 and image.dtype != np.float64:
273+
raise ValueError("Supplied image does not have dtype of float32 or float64!")
275274

276275
# it must have well-defined bounds, otherwise seg fault in SBInterpolatedImage constructor
277276
if not image.bounds.isDefined():
@@ -320,12 +319,11 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization=
320319

321320
# Check that given pad_image is valid:
322321
if pad_image:
323-
import numpy
324322
if isinstance(pad_image, str):
325323
pad_image = galsim.fits.read(pad_image)
326324
if not isinstance(pad_image, galsim.Image):
327325
raise ValueError("Supplied pad_image is not an Image!")
328-
if pad_image.dtype != numpy.float32 and pad_image.dtype != numpy.float64:
326+
if pad_image.dtype != np.float32 and pad_image.dtype != np.float64:
329327
raise ValueError("Supplied pad_image is not one of the allowed types!")
330328

331329
# Check that the given noise_pad is valid:
@@ -489,8 +487,6 @@ def __init__(self, image, x_interpolant=None, k_interpolant=None, normalization=
489487
def buildNoisePadImage(self, noise_pad_size, noise_pad, rng):
490488
"""A helper function that builds the `pad_image` from the given `noise_pad` specification.
491489
"""
492-
import numpy as np
493-
494490
# Make it with the same dtype as the image
495491
pad_image = galsim.Image(noise_pad_size, noise_pad_size, dtype=self.image.dtype)
496492

@@ -550,6 +546,163 @@ def __setstate__(self, d):
550546
_force_stepk=self._stepk, _force_maxk=self._maxk)
551547

552548

549+
class InterpolatedKImage(GSObject):
550+
"""A class describing non-parametric profiles specified by samples -- i.e., images -- of their
551+
complex Fourier transform.
552+
553+
The InterpolatedKImage class is useful if you have a non-parametric description of the Fourier
554+
transform of an object as a pair of Images -- one for the real part and one for the imaginary
555+
part -- of the complex-valued profile that you wish to manipulate / transform using GSObject
556+
methods such as shear(), magnify(), shift(), etc. Note that neither real-space convolution nor
557+
photon-shooting of InterpolatedKImages is currently implemented. Please submit an issue at
558+
http://github.com/GalSim-developers/GalSim/issues if you require either of these use cases.
559+
560+
The images required for creating an InterpolatedKImage are precisely those returned by the
561+
GSObject `.drawKImage()` method. The `a` and `b` objects in the following command will produce
562+
essentially equivalent images when drawn with the `.drawImage()` method:
563+
564+
>>> a = returns_a_GSObject()
565+
>>> b = galsim.InterpolatedKImage(*a.drawKImage())
566+
567+
The real- and imaginary-part Images must have the same data type, same bounds, and same scale.
568+
The only wcs permitted is a simple PixelScale. Furthermore, the complex-valued Fourier profile
569+
formed by `real_kimage` and `imag_kimage` must be Hermitian, since it represents a real-valued
570+
real-space profile. (To see an example of valid input to `InterpolatedKImage`, you can look at
571+
the output of `drawKImage`).
572+
573+
The user may optionally specify an interpolant, `k_interpolant`, for Fourier-space
574+
manipulations (e.g., shearing, resampling). If none is specified, then by default, a Quintic
575+
interpolant is used. The Quintic interpolant has been found to be a good compromise between
576+
speed and accuracy for real-and Fourier-space interpolation of objects specified by samples of
577+
their real-space profiles (e.g., in InterpolatedImage), though no extensive testing has been
578+
performed for objects specified by samples of their Fourier-space profiles (e.g., this
579+
class).
580+
581+
Initialization
582+
--------------
583+
584+
>>> interpolated_kimage = galsim.InterpolatedKImage(real_kimage, imag_kimage,
585+
k_interpolant=None, stepk=0., gsparams=None)
586+
587+
Initializes `interpolated_kimage` as an InterpolatedKImage instance.
588+
589+
@param real_kimage The Image corresponding to the real part of the Fourier-space samples.
590+
@param imag_kimage The Image corresponding to the imaginary part of the Fourier-space
591+
samples.
592+
@param k_interpolant Either an Interpolant instance or a string indicating which k-space
593+
interpolant should be used. Options are 'nearest', 'sinc', 'linear',
594+
'cubic', 'quintic', or 'lanczosN' where N should be the integer order
595+
to use. [default: galsim.Quintic()]
596+
@param stepk By default, the stepk value (the sampling frequency in Fourier-space)
597+
of the underlying SBProfile is set by the `scale` attribute of the
598+
supplied images. This keyword allows the user to specify a coarser
599+
sampling in Fourier-space, which may increase efficiency at the expense
600+
of decreasing the separation between neighboring copies of the
601+
DFT-rendered real-space profile. (See the GSParams docstring for the
602+
parameter `folding_threshold` for more information).
603+
[default: real_kimage.scale]
604+
@param gsparams An optional GSParams argument. See the docstring for GSParams for
605+
details. [default: None]
606+
607+
Methods
608+
-------
609+
610+
There are no additional methods for InterpolatedKImage beyond the usual GSObject methods.
611+
"""
612+
_req_params = { 'real_kimage' : str,
613+
'imag_kimage' : str }
614+
_opt_params = {
615+
'k_interpolant' : str,
616+
'stepk': float
617+
}
618+
_single_params = []
619+
_takes_rng = False
620+
_takes_logger = False
621+
622+
def __init__(self, real_kimage, imag_kimage, k_interpolant=None, stepk=None,
623+
gsparams=None):
624+
625+
# make sure real_kimage, imag_kimage are really `Image`s, are floats, and are congruent.
626+
if not isinstance(real_kimage, galsim.Image) or not isinstance(imag_kimage, galsim.Image):
627+
raise ValueError("Supplied kimage is not an Image instance")
628+
if ((real_kimage.dtype != np.float32 and real_kimage.dtype != np.float64)
629+
or (imag_kimage.dtype != np.float32 and imag_kimage.dtype != np.float64)):
630+
raise ValueError("Supplied image does not have dtype of float32 or float64!")
631+
if real_kimage.bounds != imag_kimage.bounds:
632+
raise ValueError("Real and Imag kimages must have same bounds.")
633+
if real_kimage.scale != imag_kimage.scale:
634+
raise ValueError("Real and Imag kimages must have same scale.")
635+
636+
# Make sure any `wcs`s are `PixelScale`s.
637+
if ((real_kimage.wcs is not None
638+
and not real_kimage.wcs.isPixelScale())
639+
or (imag_kimage.wcs is not None
640+
and not imag_kimage.wcs.isPixelScale())):
641+
raise ValueError("Real and Imag kimage wcs's must be PixelScale's or None.")
642+
643+
# Check for Hermitian symmetry properties of real_kimage and imag_kimage
644+
shape = real_kimage.array.shape
645+
# If image is even-sized, ignore first row/column since in this case not every pixel has
646+
# a symmetric partner to which to compare.
647+
bd = galsim.BoundsI(real_kimage.xmin + (1 if shape[1]%2==0 else 0),
648+
real_kimage.xmax,
649+
real_kimage.ymin + (1 if shape[0]%2==0 else 0),
650+
real_kimage.ymax)
651+
if not (np.allclose(real_kimage[bd].array,
652+
real_kimage[bd].array[::-1,::-1])
653+
or np.allclose(imag_kimage[bd].array,
654+
-imag_kimage[bd].array[::-1,::-1])):
655+
raise ValueError("Real and Imag kimages must form a Hermitian complex matrix.")
656+
657+
if stepk is None:
658+
stepk = real_kimage.scale
659+
else:
660+
if stepk < real_kimage.scale:
661+
import warnings
662+
warnings.warn(
663+
"Provided stepk is smaller than kimage.scale; overriding with kimage.scale.")
664+
stepk = real_kimage.scale
665+
666+
self._real_kimage = real_kimage
667+
self._imag_kimage = imag_kimage
668+
self._stepk = stepk
669+
self._gsparams = gsparams
670+
671+
# set up k_interpolant if none was provided by user, or check that the user-provided one
672+
# is of a valid type
673+
if k_interpolant is None:
674+
self.k_interpolant = galsim.Quintic(tol=1e-4)
675+
else:
676+
self.k_interpolant = galsim.utilities.convert_interpolant(k_interpolant)
677+
678+
GSObject.__init__(self, galsim._galsim.SBInterpolatedKImage(
679+
self._real_kimage.image, self._imag_kimage.image,
680+
self._real_kimage.scale, self._stepk, self.k_interpolant, gsparams))
681+
682+
def __repr__(self):
683+
return ('galsim.InterpolatedKImage(%r, %r, %r, stepk=%r, gsparams=%r)')%(
684+
self._real_kimage, self._imag_kimage, self.k_interpolant,
685+
self._stepk, self._gsparams)
686+
687+
def __str__(self): return 'galsim.InterpolatedKImage(real_kimage=%s)'%(
688+
self._real_kimage)
689+
690+
def __getstate__(self):
691+
# The SBInterpolatedKImage and the SBProfile both are picklable, but they are pretty
692+
# inefficient, due to the large images being written as strings. Better to pickle
693+
# the intermediate products and then call init again on the other side. There's still
694+
# an image to be pickled, but at least it will be through the normal pickling rules,
695+
# rather than the repr.
696+
d = self.__dict__.copy()
697+
del d['SBProfile']
698+
return d
699+
700+
def __setstate__(self, d):
701+
self.__dict__ = d
702+
self.SBProfile = galsim._galsim.SBInterpolatedKImage(
703+
self._real_kimage.image, self._imag_kimage.image,
704+
self._real_kimage.scale, self._stepk, self.k_interpolant, self._gsparams)
705+
553706
_galsim.SBInterpolatedImage.__getinitargs__ = lambda self: (
554707
self.getImage(), self.getXInterp(), self.getKInterp(), 1.0,
555708
self.stepK(), self.maxK(), self.getGSParams())
@@ -558,6 +711,20 @@ def __setstate__(self, d):
558711
_galsim.SBInterpolatedImage.__repr__ = lambda self: \
559712
'galsim._galsim.SBInterpolatedImage(%r, %r, %r, %r, %r, %r, %r)'%self.__getinitargs__()
560713

714+
def _SBIKI_getinitargs(self):
715+
if self._cenIsSet():
716+
return (self._getKData(), self.dK(), self.stepK(), self.maxK(), self.getKInterp(),
717+
self.centroid().x, self.centroid().y, True, self.getGSParams())
718+
else:
719+
return (self._getKData(), self.dK(), self.stepK(), self.maxK(), self.getKInterp(),
720+
0.0, 0.0, False, self.getGSParams())
721+
_galsim.SBInterpolatedKImage.__getinitargs__ = _SBIKI_getinitargs
722+
_galsim.SBInterpolatedKImage.__getstate__ = lambda self: None
723+
_galsim.SBInterpolatedKImage.__setstate__ = lambda self, state: 1
724+
_galsim.SBInterpolatedKImage.__repr__ = lambda self: (
725+
'galsim._galsim.SBInterpolatedKImage(%r, %r, %r, %r, %r, %r, %r, %r, %r, )'
726+
%self.__getinitargs__())
727+
561728
_galsim.Interpolant.__getinitargs__ = lambda self: (self.makeStr(), self.getTol())
562729
_galsim.Delta.__getinitargs__ = lambda self: (self.getTol(), )
563730
_galsim.Nearest.__getinitargs__ = lambda self: (self.getTol(), )

include/galsim/SBAddImpl.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ namespace galsim {
136136

137137
std::string repr() const;
138138

139-
protected: // This is protected since we want inheritance by AddCorrelationFunctionImpl
139+
private:
140140

141141
/// @brief The plist content is a pointer to a fresh copy of the summands.
142142
std::list<SBProfile> _plist;

0 commit comments

Comments
 (0)