Coverage for tests/test_psf.py: 15%

284 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-11 01:53 -0700

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/>. 

21 

22from pathlib import Path 

23 

24import galsim # noqa: F401 

25import piff 

26import unittest 

27import numpy as np 

28import copy 

29from galsim import Lanczos # noqa: F401 

30import logging 

31 

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 

46 

47 

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 

58 

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) 

61 

62 

63def make_wcs(angle_degrees=None): 

64 """Make a simple SkyWcs that is rotated around the origin. 

65 

66 Parameters 

67 ---------- 

68 angle_degrees : `float`, optional 

69 The angle to rotate the WCS by, in degrees. 

70 

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 

80 

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) 

90 

91 return afwGeom.makeSkyWcs( 

92 crpix=geom.PointD(0, 0), 

93 crval=geom.SpherePoint(0.0, 0.0, geom.degrees), 

94 cdMatrix=cdMatrix, 

95 ) 

96 

97 

98class SpatialModelPsfTestCase(lsst.utils.tests.TestCase): 

99 """A test case for SpatialModelPsf""" 

100 

101 def measure(self, footprintSet, exposure): 

102 """Measure a set of Footprints, returning a SourceCatalog""" 

103 catalog = afwTable.SourceCatalog(self.schema) 

104 

105 footprintSet.makeSources(catalog) 

106 

107 self.measureSources.run(catalog, exposure) 

108 return catalog 

109 

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() 

122 

123 self.measureSources = SingleFrameMeasurementTask( 

124 self.schema, config=config 

125 ) 

126 self.usePsfFlag = self.schema.addField("use_psf", type="Flag") 

127 

128 width, height = 110, 301 

129 

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") 

135 

136 self.ksize = 31 # size of desired kernel 

137 

138 sigma1 = 1.75 

139 sigma2 = 2*sigma1 

140 

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) 

146 

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()) 

159 

160 if sigma == sigma1: 

161 basisImage0 = basisImage 

162 else: 

163 basisImage -= basisImage0 

164 

165 basisKernelList.append(afwMath.FixedKernel(basisImage)) 

166 

167 order = 1 # 1 => up to linear 

168 spFunc = afwMath.PolynomialFunction2D(order) 

169 

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 ) 

175 

176 rand = afwMath.Random() # make these tests repeatable by setting seed 

177 

178 im = self.mi.getImage() 

179 afwMath.randomGaussianImage(im, rand) # N(0, 1) 

180 im *= sd # N(0, sd^2) 

181 

182 xarr, yarr = [], [] 

183 

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) 

193 

194 for x, y in zip(xarr, yarr): 

195 dx = rand.uniform() - 0.5 # random (centered) offsets 

196 dy = rand.uniform() - 0.5 

197 

198 k = exactKernel.getSpatialFunction(1)(x, y) 

199 b = (k*sigma1**2/((1 - k)*sigma2**2)) 

200 

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 

206 

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 

210 

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 

215 

216 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height)) 

217 self.cellSet = afwMath.SpatialCellSet(bbox, 100) 

218 

219 self.footprintSet = afwDetection.FootprintSet( 

220 self.mi, afwDetection.Threshold(100), "DETECTED" 

221 ) 

222 

223 self.catalog = self.measure(self.footprintSet, self.exposure) 

224 

225 for source in self.catalog: 

226 cand = measAlg.makePsfCandidate(source, self.exposure) 

227 self.cellSet.insertCandidate(cand) 

228 

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 

245 

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 

281 

282 self.starSelector = starSelectorClass(config=starSelectorConfig) 

283 

284 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

285 if kernelSize: 

286 makePsfCandidatesConfig.kernelSize = kernelSize 

287 if stampSize is not None: 

288 makePsfCandidatesConfig.kernelSize = stampSize 

289 

290 self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig) 

291 

292 psfDeterminerConfig = PiffPsfDeterminerConfig() 

293 psfDeterminerConfig.spatialOrder = spatialOrder 

294 psfDeterminerConfig.zerothOrderInterpNotEnoughStars = zerothOrderInterpNotEnoughStars 

295 psfDeterminerConfig.stampSize = stampSize 

296 psfDeterminerConfig.modelSize = modelSize 

297 

298 psfDeterminerConfig.debugStarData = debugStarData 

299 psfDeterminerConfig.useCoordinates = useCoordinates 

300 psfDeterminerConfig.piffPsfConfigYaml = piffPsfConfigYaml 

301 

302 psfDeterminerConfig.colorOrder = colorOrder 

303 psfDeterminerConfig.useColor = useColor 

304 

305 if piffPsfConfigYaml is None: 

306 self.useYaml = False 

307 else: 

308 self.useYaml = True 

309 

310 if downsample: 

311 psfDeterminerConfig.maxCandidates = 10 

312 if withlog: 

313 psfDeterminerConfig.piffLoggingLevel = 1 

314 

315 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

316 

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() 

320 

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 

331 

332 chi_min = np.min(chi.getImage().getArray()) 

333 chi_max = np.max(chi.getImage().getArray()) 

334 

335 if chi_lim > 0: 

336 self.assertGreater(chi_min, -chi_lim) 

337 self.assertLess(chi_max, chi_lim) 

338 

339 def checkPiffDeterminer(self, **kwargs): 

340 """Configure PiffPsfDeterminerTask and run basic tests on it. 

341 

342 Parameters 

343 ---------- 

344 kwargs : `dict`, optional 

345 Additional keyword arguments to pass to setupDeterminer. 

346 """ 

347 self.setupDeterminer(**kwargs) 

348 metadata = dafBase.PropertyList() 

349 

350 stars = self.starSelector.run(self.catalog, exposure=self.exposure) 

351 psfCandidateList = self.makePsfCandidates.run( 

352 stars.sourceCat, 

353 exposure=self.exposure 

354 ).psfCandidates 

355 

356 for psf in psfCandidateList: 

357 psf.setPsfColorValue(0.42) 

358 psf.setPsfColorType("g-r") 

359 

360 logger = logging.getLogger("lsst.psfDeterminer.Piff") 

361 

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 ) 

378 

379 # Check that the iterations are being logged. 

380 logged = "\n".join(cm.output) 

381 self.assertRegex(logged, "WARNING:.*:Iteration") 

382 

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) 

389 

390 self.exposure.setPsf(psf) 

391 

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 

403 

404 self.assertEqual(metadata['numAvailStars'], numAvail) 

405 self.assertEqual(sum(self.catalog['use_psf']), metadata['numGoodStars']) 

406 self.assertLessEqual(metadata['numGoodStars'], metadata['numAvailStars']) 

407 

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__) 

421 

422 # Test how well we can subtract the PSF model 

423 self.subtractStars(self.exposure, self.catalog, chi_lim=chiLim) 

424 

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 ) 

443 

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) 

449 

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 ) 

469 

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 ] 

478 

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") 

483 

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) 

495 

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))) 

501 

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 ) 

509 

510 def testPiffDeterminer_default(self): 

511 """Test piff with the default config.""" 

512 self.checkPiffDeterminer() 

513 

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 ) 

521 

522 def testPiffDeterminer_debugStarData(self): 

523 """Test Piff with debugStarData=True.""" 

524 self.checkPiffDeterminer(debugStarData=True) 

525 

526 def testPiffDeterminer_downsample(self): 

527 """Test Piff determiner with downsampling.""" 

528 self.checkPiffDeterminer(downsample=True) 

529 

530 def testPiffDeterminer_withlog(self): 

531 """Test Piff determiner with chatty logs.""" 

532 self.checkPiffDeterminer(withlog=True) 

533 

534 def testPiffDeterminer_stampSize26(self): 

535 """Test Piff with a psf stampSize of 26.""" 

536 with self.assertRaises(ValueError): 

537 self.checkPiffDeterminer(stampSize=26) 

538 

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) 

543 

544 def testPiffDeterminer_skyCoords(self): 

545 """Test Piff sky coords.""" 

546 

547 self.checkPiffDeterminer(useCoordinates='sky') 

548 

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.""" 

552 

553 wcs = make_wcs(angle_degrees=angle_degrees) 

554 self.exposure.setWcs(wcs) 

555 self.checkPiffDeterminer(useCoordinates='sky', kernelSize=35) 

556 

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) 

563 

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) 

573 

574 def testPiffRaiseErrorNotEnoughStars(self): 

575 with self.assertRaises(AlgorithmError): 

576 self.checkPiffDeterminer(spatialOrder=42, 

577 zerothOrderInterpNotEnoughStars=False, 

578 piffPsfConfigYaml=None) 

579 

580 def testPiffDummyColorfit(self): 

581 self.checkPiffDeterminer(useColor=True, 

582 colorOrder=0, 

583 piffPsfConfigYaml=None) 

584 

585 

586class piffPsfConfigYamlTestCase(SpatialModelPsfTestCase): 

587 """A test case to trigger the codepath that uses piffPsfConfigYaml.""" 

588 

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) 

610 

611 

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}")) 

633 

634 

635class TestMemory(lsst.utils.tests.MemoryTestCase): 

636 pass 

637 

638 

639def setup_module(module): 

640 lsst.utils.tests.init() 

641 

642 

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()