Coverage for tests / test_psf.py: 15%

289 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 08:38 +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/>. 

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

368 

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 ) 

385 

386 # Check that the iterations are being logged. 

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

388 self.assertRegex(logged, log_regex) 

389 

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) 

396 

397 self.exposure.setPsf(psf) 

398 

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 

410 

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

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

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

414 

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

428 

429 # Test how well we can subtract the PSF model 

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

431 

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 ) 

450 

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) 

456 

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 ) 

476 

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 ] 

485 

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

490 

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) 

502 

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

508 

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 ) 

516 

517 def testPiffDeterminer_default(self): 

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

519 self.checkPiffDeterminer() 

520 

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 ) 

528 

529 def testPiffDeterminer_debugStarData(self): 

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

531 self.checkPiffDeterminer(debugStarData=True) 

532 

533 def testPiffDeterminer_downsample(self): 

534 """Test Piff determiner with downsampling.""" 

535 self.checkPiffDeterminer(downsample=True) 

536 

537 def testPiffDeterminer_withlog(self): 

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

539 self.checkPiffDeterminer(withlog=True) 

540 

541 def testPiffDeterminer_stampSize26(self): 

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

543 with self.assertRaises(ValueError): 

544 self.checkPiffDeterminer(stampSize=26) 

545 

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) 

550 

551 def testPiffDeterminer_skyCoords(self): 

552 """Test Piff sky coords.""" 

553 

554 self.checkPiffDeterminer(useCoordinates='sky') 

555 

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

559 

560 wcs = make_wcs(angle_degrees=angle_degrees) 

561 self.exposure.setWcs(wcs) 

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

563 

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) 

570 

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) 

580 

581 def testPiffRaiseErrorNotEnoughStars(self): 

582 with self.assertRaises(AlgorithmError): 

583 self.checkPiffDeterminer(spatialOrder=42, 

584 zerothOrderInterpNotEnoughStars=False, 

585 piffPsfConfigYaml=None) 

586 

587 def testPiffDummyColorfit(self): 

588 self.checkPiffDeterminer(useColor=True, 

589 colorOrder=0, 

590 piffPsfConfigYaml=None) 

591 

592 

593class piffPsfConfigYamlTestCase(SpatialModelPsfTestCase): 

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

595 

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) 

617 

618 

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

640 

641 

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

643 pass 

644 

645 

646def setup_module(module): 

647 lsst.utils.tests.init() 

648 

649 

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