Coverage for tests / test_psf.py: 15%
289 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:10 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:10 +0000
1# This file is part of meas_extensions_piff.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22from pathlib import Path
24import galsim # noqa: F401
25import piff
26import unittest
27import numpy as np
28import copy
29from galsim import Lanczos # noqa: F401
30import logging
32import lsst.utils.tests
33import lsst.afw.detection as afwDetection
34import lsst.afw.geom as afwGeom
35import lsst.afw.image as afwImage
36import lsst.afw.math as afwMath
37import lsst.afw.table as afwTable
38import lsst.daf.base as dafBase
39import lsst.geom as geom
40import lsst.meas.algorithms as measAlg
41from lsst.pipe.base import AlgorithmError
42from lsst.meas.base import SingleFrameMeasurementTask
43from lsst.meas.extensions.piff.piffPsfDeterminer import PiffPsfDeterminerConfig, PiffPsfDeterminerTask
44from lsst.meas.extensions.piff.piffPsfDeterminer import _validateGalsimInterpolant
45from packaging.version import Version
48def psfVal(ix, iy, x, y, sigma1, sigma2, b):
49 """Return the value at (ix, iy) of a double Gaussian
50 (N(0, sigma1^2) + b*N(0, sigma2^2))/(1 + b)
51 centered at (x, y)
52 """
53 dx, dy = x - ix, y - iy
54 theta = np.radians(30)
55 ab = 1.0/0.75 # axis ratio
56 c, s = np.cos(theta), np.sin(theta)
57 u, v = c*dx - s*dy, s*dx + c*dy
59 return (np.exp(-0.5*(u**2 + (v*ab)**2)/sigma1**2)
60 + b*np.exp(-0.5*(u**2 + (v*ab)**2)/sigma2**2))/(1 + b)
63def make_wcs(angle_degrees=None):
64 """Make a simple SkyWcs that is rotated around the origin.
66 Parameters
67 ----------
68 angle_degrees : `float`, optional
69 The angle to rotate the WCS by, in degrees.
71 Returns
72 -------
73 wcs : `~lsst.afw.geom.SkyWcs`
74 The WCS object.
75 """
76 cdMatrix = np.array([
77 [1.0, 0.0],
78 [0.0, 1.0]
79 ]) * 0.2 / 3600
81 if angle_degrees is not None:
82 angle_radians = np.radians(angle_degrees)
83 cosang = np.cos(angle_radians)
84 sinang = np.sin(angle_radians)
85 rot = np.array([
86 [cosang, -sinang],
87 [sinang, cosang]
88 ])
89 cdMatrix = np.dot(cdMatrix, rot)
91 return afwGeom.makeSkyWcs(
92 crpix=geom.PointD(0, 0),
93 crval=geom.SpherePoint(0.0, 0.0, geom.degrees),
94 cdMatrix=cdMatrix,
95 )
98class SpatialModelPsfTestCase(lsst.utils.tests.TestCase):
99 """A test case for SpatialModelPsf"""
101 def measure(self, footprintSet, exposure):
102 """Measure a set of Footprints, returning a SourceCatalog"""
103 catalog = afwTable.SourceCatalog(self.schema)
105 footprintSet.makeSources(catalog)
107 self.measureSources.run(catalog, exposure)
108 return catalog
110 def setUp(self):
111 config = SingleFrameMeasurementTask.ConfigClass()
112 config.plugins.names = [
113 "base_PsfFlux",
114 "base_GaussianFlux",
115 "base_SdssCentroid",
116 "base_SdssShape",
117 "base_PixelFlags",
118 "base_CircularApertureFlux",
119 ]
120 config.slots.apFlux = 'base_CircularApertureFlux_12_0'
121 self.schema = afwTable.SourceTable.makeMinimalSchema()
123 self.measureSources = SingleFrameMeasurementTask(
124 self.schema, config=config
125 )
126 self.usePsfFlag = self.schema.addField("use_psf", type="Flag")
128 width, height = 110, 301
130 self.mi = afwImage.MaskedImageF(geom.ExtentI(width, height))
131 self.mi.set(0)
132 sd = 3 # standard deviation of image
133 self.mi.getVariance().set(sd*sd)
134 self.mi.getMask().addMaskPlane("DETECTED")
136 self.ksize = 31 # size of desired kernel
138 sigma1 = 1.75
139 sigma2 = 2*sigma1
141 self.exposure = afwImage.makeExposure(self.mi)
142 self.exposure.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize,
143 1.5*sigma1, 1, 0.1))
144 wcs = make_wcs()
145 self.exposure.setWcs(wcs)
147 #
148 # Make a kernel with the exactly correct basis functions.
149 # Useful for debugging
150 #
151 basisKernelList = []
152 for sigma in (sigma1, sigma2):
153 basisKernel = afwMath.AnalyticKernel(
154 self.ksize, self.ksize, afwMath.GaussianFunction2D(sigma, sigma)
155 )
156 basisImage = afwImage.ImageD(basisKernel.getDimensions())
157 basisKernel.computeImage(basisImage, True)
158 basisImage /= np.sum(basisImage.getArray())
160 if sigma == sigma1:
161 basisImage0 = basisImage
162 else:
163 basisImage -= basisImage0
165 basisKernelList.append(afwMath.FixedKernel(basisImage))
167 order = 1 # 1 => up to linear
168 spFunc = afwMath.PolynomialFunction2D(order)
170 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
171 exactKernel.setSpatialParameters(
172 [[1.0, 0, 0],
173 [0.0, 0.5*1e-2, 0.2e-2]]
174 )
176 rand = afwMath.Random() # make these tests repeatable by setting seed
178 im = self.mi.getImage()
179 afwMath.randomGaussianImage(im, rand) # N(0, 1)
180 im *= sd # N(0, sd^2)
182 xarr, yarr = [], []
184 for x, y in [(20, 20), (60, 20),
185 (30, 35),
186 (50, 50),
187 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30),
188 (50, 120), (70, 80),
189 (60, 210), (20, 210),
190 ]:
191 xarr.append(x)
192 yarr.append(y)
194 for x, y in zip(xarr, yarr):
195 dx = rand.uniform() - 0.5 # random (centered) offsets
196 dy = rand.uniform() - 0.5
198 k = exactKernel.getSpatialFunction(1)(x, y)
199 b = (k*sigma1**2/((1 - k)*sigma2**2))
201 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5))
202 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2))
203 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1):
204 if iy < 0 or iy >= self.mi.getHeight():
205 continue
207 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1):
208 if ix < 0 or ix >= self.mi.getWidth():
209 continue
211 II = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b)
212 Isample = rand.poisson(II)
213 self.mi.image[ix, iy, afwImage.LOCAL] += Isample
214 self.mi.variance[ix, iy, afwImage.LOCAL] += II
216 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height))
217 self.cellSet = afwMath.SpatialCellSet(bbox, 100)
219 self.footprintSet = afwDetection.FootprintSet(
220 self.mi, afwDetection.Threshold(100), "DETECTED"
221 )
223 self.catalog = self.measure(self.footprintSet, self.exposure)
225 for source in self.catalog:
226 cand = measAlg.makePsfCandidate(source, self.exposure)
227 self.cellSet.insertCandidate(cand)
229 def setupDeterminer(
230 self,
231 stampSize=None,
232 kernelSize=None,
233 modelSize=25,
234 debugStarData=False,
235 useCoordinates='pixel',
236 spatialOrder=1,
237 zerothOrderInterpNotEnoughStars=False,
238 piffPsfConfigYaml=None,
239 downsample=False,
240 useColor=False,
241 colorOrder=0,
242 withlog=False,
243 ):
244 """Setup the starSelector and psfDeterminer
246 Parameters
247 ----------
248 stampSize : `int`, optional
249 Set ``config.stampSize`` to this, if not None.
250 kernelSize : `int`, optional
251 Cutout size for the PSF candidates. This is unused if ``stampSize``
252 if provided and its value is used for cutout size instead.
253 modelSize : `int`, optional
254 Internal model size for PIFF.
255 debugStarData : `bool`, optional
256 Include star images used for fitting in PSF model object?
257 useCoordinates : `str`, optional
258 Spatial coordinates to regress against for PSF modelling.
259 spatialOrder : `int`, optional
260 Spatial order for PSF parameter interpolation.
261 zerothOrderInterpNotEnoughStars : `bool`, optional
262 If True, use zeroth order interpolation if not enough star.
263 piffPsfConfigYaml : `str`, optional
264 Configuration file for PIFF in YAML format.
265 downsample : `bool`, optional
266 Whether to downsample the PSF candidates before modelling?
267 withlog : `bool`, optional
268 Should Piff produce chatty log messages?
269 """
270 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"]
271 starSelectorConfig = starSelectorClass.ConfigClass()
272 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux"
273 starSelectorConfig.badFlags = [
274 "base_PixelFlags_flag_edge",
275 "base_PixelFlags_flag_interpolatedCenter",
276 "base_PixelFlags_flag_saturatedCenter",
277 "base_PixelFlags_flag_crCenter",
278 ]
279 # Set to match when the tolerance of the test was set
280 starSelectorConfig.widthStdAllowed = 0.5
282 self.starSelector = starSelectorClass(config=starSelectorConfig)
284 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass()
285 if kernelSize:
286 makePsfCandidatesConfig.kernelSize = kernelSize
287 if stampSize is not None:
288 makePsfCandidatesConfig.kernelSize = stampSize
290 self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig)
292 psfDeterminerConfig = PiffPsfDeterminerConfig()
293 psfDeterminerConfig.spatialOrder = spatialOrder
294 psfDeterminerConfig.zerothOrderInterpNotEnoughStars = zerothOrderInterpNotEnoughStars
295 psfDeterminerConfig.stampSize = stampSize
296 psfDeterminerConfig.modelSize = modelSize
298 psfDeterminerConfig.debugStarData = debugStarData
299 psfDeterminerConfig.useCoordinates = useCoordinates
300 psfDeterminerConfig.piffPsfConfigYaml = piffPsfConfigYaml
302 psfDeterminerConfig.colorOrder = colorOrder
303 psfDeterminerConfig.useColor = useColor
305 if piffPsfConfigYaml is None:
306 self.useYaml = False
307 else:
308 self.useYaml = True
310 if downsample:
311 psfDeterminerConfig.maxCandidates = 10
312 if withlog:
313 psfDeterminerConfig.piffLoggingLevel = 1
315 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig)
317 def subtractStars(self, exposure, catalog, chi_lim=-1.):
318 """Subtract the exposure's PSF from all the sources in catalog"""
319 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
321 subtracted = mi.Factory(mi, True)
322 for s in catalog:
323 xc, yc = s.getX(), s.getY()
324 bbox = subtracted.getBBox(afwImage.PARENT)
325 if bbox.contains(geom.PointI(int(xc), int(yc))):
326 measAlg.subtractPsf(psf, subtracted, xc, yc)
327 chi = subtracted.Factory(subtracted, True)
328 var = subtracted.getVariance()
329 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
330 chi /= var
332 chi_min = np.min(chi.getImage().getArray())
333 chi_max = np.max(chi.getImage().getArray())
335 if chi_lim > 0:
336 self.assertGreater(chi_min, -chi_lim)
337 self.assertLess(chi_max, chi_lim)
339 def checkPiffDeterminer(self, **kwargs):
340 """Configure PiffPsfDeterminerTask and run basic tests on it.
342 Parameters
343 ----------
344 kwargs : `dict`, optional
345 Additional keyword arguments to pass to setupDeterminer.
346 """
347 self.setupDeterminer(**kwargs)
348 metadata = dafBase.PropertyList()
350 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
351 psfCandidateList = self.makePsfCandidates.run(
352 stars.sourceCat,
353 exposure=self.exposure
354 ).psfCandidates
356 for psf in psfCandidateList:
357 psf.setPsfColorValue(0.42)
358 psf.setPsfColorType("g-r")
360 logger = logging.getLogger("lsst.psfDeterminer.Piff")
362 if Version(piff.version) >= Version("1.6"):
363 log_level = logging.INFO
364 log_regex = "INFO:.*:Iteration"
365 else:
366 log_level = logging.WARNING
367 log_regex = "WARNING:.*:Iteration"
369 with self.assertLogs("lsst.psfDeterminer.Piff.piff", log_level) as cm:
370 if kwargs.get("zerothOrderInterpNotEnoughStars", False):
371 psf, cellSet = self.psfDeterminer.determinePsf(
372 self.exposure,
373 psfCandidateList,
374 metadata,
375 flagKey=self.usePsfFlag
376 )
377 else:
378 with self.assertNoLogs("lsst.psfDeterminer.Piff", logging.WARNING):
379 psf, cellSet = self.psfDeterminer.determinePsf(
380 self.exposure,
381 psfCandidateList,
382 metadata,
383 flagKey=self.usePsfFlag
384 )
386 # Check that the iterations are being logged.
387 logged = "\n".join(cm.output)
388 self.assertRegex(logged, log_regex)
390 # And check that the levels are set correctly for suppression.
391 logger = logging.getLogger("lsst.psfDeterminer.Piff.piff")
392 if kwargs.get("withlog", False):
393 self.assertEqual(logger.level, logging.WARNING)
394 else:
395 self.assertEqual(logger.level, logging.CRITICAL)
397 self.exposure.setPsf(psf)
399 if kwargs.get("downsample", False):
400 # When downsampling the PSF model is not quite as
401 # good so the chi2 test limit needs to be modified.
402 numAvail = self.psfDeterminer.config.maxCandidates
403 chiLim = 7.0
404 elif kwargs.get("zerothOrderInterpNotEnoughStars", False):
405 numAvail = len(psfCandidateList)
406 chiLim = 20.31
407 else:
408 numAvail = len(psfCandidateList)
409 chiLim = 6.4
411 self.assertEqual(metadata['numAvailStars'], numAvail)
412 self.assertEqual(sum(self.catalog['use_psf']), metadata['numGoodStars'])
413 self.assertLessEqual(metadata['numGoodStars'], metadata['numAvailStars'])
415 self.assertEqual(
416 psf.getAveragePosition(),
417 geom.Point2D(
418 np.mean([s.x for s in psf._piffResult.stars
419 if not s.is_flagged and not s.is_reserve]),
420 np.mean([s.y for s in psf._piffResult.stars
421 if not s.is_flagged and not s.is_reserve])
422 )
423 )
424 if self.psfDeterminer.config.debugStarData:
425 self.assertIn('image', psf._piffResult.stars[0].data.__dict__)
426 else:
427 self.assertNotIn('image', psf._piffResult.stars[0].data.__dict__)
429 # Test how well we can subtract the PSF model
430 self.subtractStars(self.exposure, self.catalog, chi_lim=chiLim)
432 # Test bboxes
433 for point in [
434 psf.getAveragePosition(),
435 geom.Point2D(),
436 geom.Point2D(1, 1)
437 ]:
438 self.assertEqual(
439 psf.computeBBox(point),
440 psf.computeKernelImage(point).getBBox()
441 )
442 self.assertEqual(
443 psf.computeKernelBBox(point),
444 psf.computeKernelImage(point).getBBox()
445 )
446 self.assertEqual(
447 psf.computeImageBBox(point),
448 psf.computeImage(point).getBBox()
449 )
451 # Some roundtrips
452 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
453 self.exposure.writeFits(tmpFile)
454 fitsIm = afwImage.ExposureF(tmpFile)
455 copyIm = copy.deepcopy(self.exposure)
457 for newIm in [fitsIm, copyIm]:
458 # Piff doesn't enable __eq__ for its results, so we just check
459 # that some PSF images come out the same.
460 for point in [
461 geom.Point2D(0, 0),
462 geom.Point2D(10, 100),
463 geom.Point2D(-200, 30),
464 geom.Point2D(float("nan")) # "nullPoint"
465 ]:
466 self.assertImagesAlmostEqual(
467 psf.computeImage(point),
468 newIm.getPsf().computeImage(point)
469 )
470 # Also check average position
471 newPsf = newIm.getPsf()
472 self.assertImagesAlmostEqual(
473 psf.computeImage(psf.getAveragePosition()),
474 newPsf.computeImage(newPsf.getAveragePosition())
475 )
477 def testReadOldPiffVersions(self):
478 """Test we can read psfs serialized with older versions of Piff."""
479 point_names = [
480 (geom.Point2D(0, 0), "psfIm_0_0.fits"),
481 (geom.Point2D(10, 100), "psfIm_10_100.fits"),
482 (geom.Point2D(-200, 30), "psfIm_-200_30.fits"),
483 (geom.Point2D(float("nan")), "psfIm_nan.fits"),
484 ]
486 if False: # Documenting block that created the test data...
487 # Specifically interested in the case where the PSF is created with
488 # piff older than v1.4
489 assert Version(piff.version) < Version("1.4")
491 self.setupDeterminer()
492 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
493 psfCandidateList = self.makePsfCandidates.run(
494 stars.sourceCat,
495 exposure=self.exposure
496 ).psfCandidates
497 psf, _ = self.psfDeterminer.determinePsf(
498 self.exposure,
499 psfCandidateList,
500 )
501 self.exposure.setPsf(psf)
503 path = Path(__file__).parent / "data" / "exp.fits"
504 self.exposure.writeFits(str(path))
505 for point, name in point_names:
506 psfIm = psf.computeImage(point)
507 psfIm.writeFits(str(path.with_name(name)))
509 path = Path(__file__).parent / "data" / "exp.fits"
510 exposure = afwImage.ExposureF(str(path))
511 for point, name in point_names:
512 self.assertImagesAlmostEqual(
513 afwImage.ImageF(str(path.with_name(name))),
514 exposure.getPsf().computeImage(point)
515 )
517 def testPiffDeterminer_default(self):
518 """Test piff with the default config."""
519 self.checkPiffDeterminer()
521 def testPiffDeterminer_stampSize27(self):
522 """Test Piff with a psf stampSize of 27."""
523 self.checkPiffDeterminer(stampSize=27)
524 self.assertEqual(
525 self.exposure.psf.computeKernelImage(self.exposure.getBBox().getCenter()).getDimensions(),
526 geom.Extent2I(27, 27),
527 )
529 def testPiffDeterminer_debugStarData(self):
530 """Test Piff with debugStarData=True."""
531 self.checkPiffDeterminer(debugStarData=True)
533 def testPiffDeterminer_downsample(self):
534 """Test Piff determiner with downsampling."""
535 self.checkPiffDeterminer(downsample=True)
537 def testPiffDeterminer_withlog(self):
538 """Test Piff determiner with chatty logs."""
539 self.checkPiffDeterminer(withlog=True)
541 def testPiffDeterminer_stampSize26(self):
542 """Test Piff with a psf stampSize of 26."""
543 with self.assertRaises(ValueError):
544 self.checkPiffDeterminer(stampSize=26)
546 def testPiffDeterminer_modelSize26(self):
547 """Test Piff with a psf stampSize of 26."""
548 with self.assertRaises(ValueError):
549 self.checkPiffDeterminer(modelSize=26, stampSize=25)
551 def testPiffDeterminer_skyCoords(self):
552 """Test Piff sky coords."""
554 self.checkPiffDeterminer(useCoordinates='sky')
556 @lsst.utils.tests.methodParameters(angle_degrees=[0, 35, 45, 77, 135])
557 def testPiffDeterminer_skyCoords_with_rotation(self, angle_degrees):
558 """Test Piff sky coords with rotation."""
560 wcs = make_wcs(angle_degrees=angle_degrees)
561 self.exposure.setWcs(wcs)
562 self.checkPiffDeterminer(useCoordinates='sky', kernelSize=35)
564 def testPiffDeterminer_skyCoords_failure(self, angle_degrees=135):
565 """Test that using small PSF candidates with sky coordinates fails."""
566 wcs = make_wcs(angle_degrees=angle_degrees)
567 self.exposure.setWcs(wcs)
568 with self.assertRaises(ValueError):
569 self.checkPiffDeterminer(useCoordinates='sky', stampSize=15)
571 def testPiffZerothOrderInterpNotEnoughStars(self):
572 self.checkPiffDeterminer(spatialOrder=4, zerothOrderInterpNotEnoughStars=True)
573 if not self.useYaml:
574 self.assertEqual(self.psfDeterminer._piffConfig['interp']['order'], [0, 0])
575 self.assertEqual(self.psfDeterminer._piffConfig['max_iter'], 1)
576 else:
577 # Yaml will overwrite input value.
578 self.assertEqual(self.psfDeterminer._piffConfig['interp']['order'], 1)
579 self.assertNotIn('max_iter', self.psfDeterminer._piffConfig)
581 def testPiffRaiseErrorNotEnoughStars(self):
582 with self.assertRaises(AlgorithmError):
583 self.checkPiffDeterminer(spatialOrder=42,
584 zerothOrderInterpNotEnoughStars=False,
585 piffPsfConfigYaml=None)
587 def testPiffDummyColorfit(self):
588 self.checkPiffDeterminer(useColor=True,
589 colorOrder=0,
590 piffPsfConfigYaml=None)
593class piffPsfConfigYamlTestCase(SpatialModelPsfTestCase):
594 """A test case to trigger the codepath that uses piffPsfConfigYaml."""
596 def checkPiffDeterminer(self, **kwargs):
597 # Docstring inherited.
598 if "piffPsfConfigYaml" not in kwargs:
599 piffPsfConfigYaml = """
600 # A minimal Piff config corresponding to the defaults.
601 type: Simple
602 model:
603 type: PixelGrid
604 scale: 0.2
605 size: 25
606 interp: Lanczos(11)
607 interp:
608 type: BasisPolynomial
609 order: 1
610 outliers:
611 type: Chisq
612 nsigma: 4.0
613 max_remove: 0.05
614 """
615 kwargs["piffPsfConfigYaml"] = piffPsfConfigYaml
616 return super().checkPiffDeterminer(**kwargs)
619class PiffConfigTestCase(lsst.utils.tests.TestCase):
620 """A test case to check for valid Piff config"""
621 def testValidateGalsimInterpolant(self):
622 # Check that random strings are not valid interpolants.
623 self.assertFalse(_validateGalsimInterpolant("foo"))
624 # Check that the Lanczos order is an integer
625 self.assertFalse(_validateGalsimInterpolant("Lanczos(3.0"))
626 self.assertFalse(_validateGalsimInterpolant("Lanczos(-5.0"))
627 self.assertFalse(_validateGalsimInterpolant("Lanczos(N)"))
628 # Check for various valid Lanczos interpolants
629 for interp in ("Lanczos(4)", "galsim.Lanczos(7)"):
630 self.assertTrue(_validateGalsimInterpolant(interp))
631 self.assertFalse(_validateGalsimInterpolant(interp.lower()))
632 # Evaluating the string should succeed. This is how Piff does it.
633 self.assertTrue(eval(interp))
634 # Check that interpolation methods are case sensitive.
635 for interp in ("Linear", "Cubic", "Quintic", "Delta", "Nearest", "SincInterpolant"):
636 self.assertFalse(_validateGalsimInterpolant(f"galsim.{interp.lower()}"))
637 self.assertFalse(_validateGalsimInterpolant(interp))
638 self.assertTrue(_validateGalsimInterpolant(f"galsim.{interp}"))
639 self.assertTrue(eval(f"galsim.{interp}"))
642class TestMemory(lsst.utils.tests.MemoryTestCase):
643 pass
646def setup_module(module):
647 lsst.utils.tests.init()
650if __name__ == "__main__": 650 ↛ 651line 650 didn't jump to line 651 because the condition on line 650 was never true
651 lsst.utils.tests.init()
652 unittest.main()