Coverage for tests/test_psf.py: 15%
284 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-24 01:42 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-24 01:42 -0800
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 with self.assertLogs("lsst.psfDeterminer.Piff.piff", logging.WARNING) as cm:
363 if kwargs.get("zerothOrderInterpNotEnoughStars", False):
364 psf, cellSet = self.psfDeterminer.determinePsf(
365 self.exposure,
366 psfCandidateList,
367 metadata,
368 flagKey=self.usePsfFlag
369 )
370 else:
371 with self.assertNoLogs("lsst.psfDeterminer.Piff", logging.WARNING):
372 psf, cellSet = self.psfDeterminer.determinePsf(
373 self.exposure,
374 psfCandidateList,
375 metadata,
376 flagKey=self.usePsfFlag
377 )
379 # Check that the iterations are being logged.
380 logged = "\n".join(cm.output)
381 self.assertRegex(logged, "WARNING:.*:Iteration")
383 # And check that the levels are set correctly for suppression.
384 logger = logging.getLogger("lsst.psfDeterminer.Piff.piff")
385 if kwargs.get("withlog", False):
386 self.assertEqual(logger.level, logging.WARNING)
387 else:
388 self.assertEqual(logger.level, logging.CRITICAL)
390 self.exposure.setPsf(psf)
392 if kwargs.get("downsample", False):
393 # When downsampling the PSF model is not quite as
394 # good so the chi2 test limit needs to be modified.
395 numAvail = self.psfDeterminer.config.maxCandidates
396 chiLim = 7.0
397 elif kwargs.get("zerothOrderInterpNotEnoughStars", False):
398 numAvail = len(psfCandidateList)
399 chiLim = 20.31
400 else:
401 numAvail = len(psfCandidateList)
402 chiLim = 6.4
404 self.assertEqual(metadata['numAvailStars'], numAvail)
405 self.assertEqual(sum(self.catalog['use_psf']), metadata['numGoodStars'])
406 self.assertLessEqual(metadata['numGoodStars'], metadata['numAvailStars'])
408 self.assertEqual(
409 psf.getAveragePosition(),
410 geom.Point2D(
411 np.mean([s.x for s in psf._piffResult.stars
412 if not s.is_flagged and not s.is_reserve]),
413 np.mean([s.y for s in psf._piffResult.stars
414 if not s.is_flagged and not s.is_reserve])
415 )
416 )
417 if self.psfDeterminer.config.debugStarData:
418 self.assertIn('image', psf._piffResult.stars[0].data.__dict__)
419 else:
420 self.assertNotIn('image', psf._piffResult.stars[0].data.__dict__)
422 # Test how well we can subtract the PSF model
423 self.subtractStars(self.exposure, self.catalog, chi_lim=chiLim)
425 # Test bboxes
426 for point in [
427 psf.getAveragePosition(),
428 geom.Point2D(),
429 geom.Point2D(1, 1)
430 ]:
431 self.assertEqual(
432 psf.computeBBox(point),
433 psf.computeKernelImage(point).getBBox()
434 )
435 self.assertEqual(
436 psf.computeKernelBBox(point),
437 psf.computeKernelImage(point).getBBox()
438 )
439 self.assertEqual(
440 psf.computeImageBBox(point),
441 psf.computeImage(point).getBBox()
442 )
444 # Some roundtrips
445 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
446 self.exposure.writeFits(tmpFile)
447 fitsIm = afwImage.ExposureF(tmpFile)
448 copyIm = copy.deepcopy(self.exposure)
450 for newIm in [fitsIm, copyIm]:
451 # Piff doesn't enable __eq__ for its results, so we just check
452 # that some PSF images come out the same.
453 for point in [
454 geom.Point2D(0, 0),
455 geom.Point2D(10, 100),
456 geom.Point2D(-200, 30),
457 geom.Point2D(float("nan")) # "nullPoint"
458 ]:
459 self.assertImagesAlmostEqual(
460 psf.computeImage(point),
461 newIm.getPsf().computeImage(point)
462 )
463 # Also check average position
464 newPsf = newIm.getPsf()
465 self.assertImagesAlmostEqual(
466 psf.computeImage(psf.getAveragePosition()),
467 newPsf.computeImage(newPsf.getAveragePosition())
468 )
470 def testReadOldPiffVersions(self):
471 """Test we can read psfs serialized with older versions of Piff."""
472 point_names = [
473 (geom.Point2D(0, 0), "psfIm_0_0.fits"),
474 (geom.Point2D(10, 100), "psfIm_10_100.fits"),
475 (geom.Point2D(-200, 30), "psfIm_-200_30.fits"),
476 (geom.Point2D(float("nan")), "psfIm_nan.fits"),
477 ]
479 if False: # Documenting block that created the test data...
480 # Specifically interested in the case where the PSF is created with
481 # piff older than v1.4
482 assert Version(piff.version) < Version("1.4")
484 self.setupDeterminer()
485 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
486 psfCandidateList = self.makePsfCandidates.run(
487 stars.sourceCat,
488 exposure=self.exposure
489 ).psfCandidates
490 psf, _ = self.psfDeterminer.determinePsf(
491 self.exposure,
492 psfCandidateList,
493 )
494 self.exposure.setPsf(psf)
496 path = Path(__file__).parent / "data" / "exp.fits"
497 self.exposure.writeFits(str(path))
498 for point, name in point_names:
499 psfIm = psf.computeImage(point)
500 psfIm.writeFits(str(path.with_name(name)))
502 path = Path(__file__).parent / "data" / "exp.fits"
503 exposure = afwImage.ExposureF(str(path))
504 for point, name in point_names:
505 self.assertImagesAlmostEqual(
506 afwImage.ImageF(str(path.with_name(name))),
507 exposure.getPsf().computeImage(point)
508 )
510 def testPiffDeterminer_default(self):
511 """Test piff with the default config."""
512 self.checkPiffDeterminer()
514 def testPiffDeterminer_stampSize27(self):
515 """Test Piff with a psf stampSize of 27."""
516 self.checkPiffDeterminer(stampSize=27)
517 self.assertEqual(
518 self.exposure.psf.computeKernelImage(self.exposure.getBBox().getCenter()).getDimensions(),
519 geom.Extent2I(27, 27),
520 )
522 def testPiffDeterminer_debugStarData(self):
523 """Test Piff with debugStarData=True."""
524 self.checkPiffDeterminer(debugStarData=True)
526 def testPiffDeterminer_downsample(self):
527 """Test Piff determiner with downsampling."""
528 self.checkPiffDeterminer(downsample=True)
530 def testPiffDeterminer_withlog(self):
531 """Test Piff determiner with chatty logs."""
532 self.checkPiffDeterminer(withlog=True)
534 def testPiffDeterminer_stampSize26(self):
535 """Test Piff with a psf stampSize of 26."""
536 with self.assertRaises(ValueError):
537 self.checkPiffDeterminer(stampSize=26)
539 def testPiffDeterminer_modelSize26(self):
540 """Test Piff with a psf stampSize of 26."""
541 with self.assertRaises(ValueError):
542 self.checkPiffDeterminer(modelSize=26, stampSize=25)
544 def testPiffDeterminer_skyCoords(self):
545 """Test Piff sky coords."""
547 self.checkPiffDeterminer(useCoordinates='sky')
549 @lsst.utils.tests.methodParameters(angle_degrees=[0, 35, 45, 77, 135])
550 def testPiffDeterminer_skyCoords_with_rotation(self, angle_degrees):
551 """Test Piff sky coords with rotation."""
553 wcs = make_wcs(angle_degrees=angle_degrees)
554 self.exposure.setWcs(wcs)
555 self.checkPiffDeterminer(useCoordinates='sky', kernelSize=35)
557 def testPiffDeterminer_skyCoords_failure(self, angle_degrees=135):
558 """Test that using small PSF candidates with sky coordinates fails."""
559 wcs = make_wcs(angle_degrees=angle_degrees)
560 self.exposure.setWcs(wcs)
561 with self.assertRaises(ValueError):
562 self.checkPiffDeterminer(useCoordinates='sky', stampSize=15)
564 def testPiffZerothOrderInterpNotEnoughStars(self):
565 self.checkPiffDeterminer(spatialOrder=4, zerothOrderInterpNotEnoughStars=True)
566 if not self.useYaml:
567 self.assertEqual(self.psfDeterminer._piffConfig['interp']['order'], [0, 0])
568 self.assertEqual(self.psfDeterminer._piffConfig['max_iter'], 1)
569 else:
570 # Yaml will overwrite input value.
571 self.assertEqual(self.psfDeterminer._piffConfig['interp']['order'], 1)
572 self.assertNotIn('max_iter', self.psfDeterminer._piffConfig)
574 def testPiffRaiseErrorNotEnoughStars(self):
575 with self.assertRaises(AlgorithmError):
576 self.checkPiffDeterminer(spatialOrder=42,
577 zerothOrderInterpNotEnoughStars=False,
578 piffPsfConfigYaml=None)
580 def testPiffDummyColorfit(self):
581 self.checkPiffDeterminer(useColor=True,
582 colorOrder=0,
583 piffPsfConfigYaml=None)
586class piffPsfConfigYamlTestCase(SpatialModelPsfTestCase):
587 """A test case to trigger the codepath that uses piffPsfConfigYaml."""
589 def checkPiffDeterminer(self, **kwargs):
590 # Docstring inherited.
591 if "piffPsfConfigYaml" not in kwargs:
592 piffPsfConfigYaml = """
593 # A minimal Piff config corresponding to the defaults.
594 type: Simple
595 model:
596 type: PixelGrid
597 scale: 0.2
598 size: 25
599 interp: Lanczos(11)
600 interp:
601 type: BasisPolynomial
602 order: 1
603 outliers:
604 type: Chisq
605 nsigma: 4.0
606 max_remove: 0.05
607 """
608 kwargs["piffPsfConfigYaml"] = piffPsfConfigYaml
609 return super().checkPiffDeterminer(**kwargs)
612class PiffConfigTestCase(lsst.utils.tests.TestCase):
613 """A test case to check for valid Piff config"""
614 def testValidateGalsimInterpolant(self):
615 # Check that random strings are not valid interpolants.
616 self.assertFalse(_validateGalsimInterpolant("foo"))
617 # Check that the Lanczos order is an integer
618 self.assertFalse(_validateGalsimInterpolant("Lanczos(3.0"))
619 self.assertFalse(_validateGalsimInterpolant("Lanczos(-5.0"))
620 self.assertFalse(_validateGalsimInterpolant("Lanczos(N)"))
621 # Check for various valid Lanczos interpolants
622 for interp in ("Lanczos(4)", "galsim.Lanczos(7)"):
623 self.assertTrue(_validateGalsimInterpolant(interp))
624 self.assertFalse(_validateGalsimInterpolant(interp.lower()))
625 # Evaluating the string should succeed. This is how Piff does it.
626 self.assertTrue(eval(interp))
627 # Check that interpolation methods are case sensitive.
628 for interp in ("Linear", "Cubic", "Quintic", "Delta", "Nearest", "SincInterpolant"):
629 self.assertFalse(_validateGalsimInterpolant(f"galsim.{interp.lower()}"))
630 self.assertFalse(_validateGalsimInterpolant(interp))
631 self.assertTrue(_validateGalsimInterpolant(f"galsim.{interp}"))
632 self.assertTrue(eval(f"galsim.{interp}"))
635class TestMemory(lsst.utils.tests.MemoryTestCase):
636 pass
639def setup_module(module):
640 lsst.utils.tests.init()
643if __name__ == "__main__": 643 ↛ 644line 643 didn't jump to line 644 because the condition on line 643 was never true
644 lsst.utils.tests.init()
645 unittest.main()