Coverage for tests/test_extended_psf.py: 99%

306 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-26 00:48 -0700

1# This file is part of pipe_tasks. 

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 

22import logging 

23import unittest 

24from typing import cast 

25 

26import astropy.units as u 

27import numpy as np 

28from astropy.table import Table 

29 

30import lsst.images.tests 

31import lsst.utils.tests 

32from lsst.afw.cameraGeom import FOCAL_PLANE, PIXELS 

33from lsst.afw.cameraGeom.testUtils import CameraWrapper 

34from lsst.afw.geom import makeCdMatrix, makeSkyWcs 

35from lsst.afw.image import ( 

36 ExposureF, 

37 ImageD, 

38 ImageF, 

39 MaskedImageF, 

40 VisitInfo, 

41 makePhotoCalibFromCalibZeroPoint, 

42) 

43from lsst.afw.math import FixedKernel 

44from lsst.geom import Box2I, Extent2I, Point2D, Point2I, SpherePoint, arcseconds, degrees 

45from lsst.images import Box, Image, Mask, MaskedImage, MaskSchema 

46from lsst.meas.algorithms import KernelPsf 

47from lsst.pipe.tasks.extended_psf import ( 

48 ExtendedPsfCandidate, 

49 ExtendedPsfCandidateInfo, 

50 ExtendedPsfCandidates, 

51 ExtendedPsfCutoutConfig, 

52 ExtendedPsfCutoutTask, 

53 ExtendedPsfFit, 

54 ExtendedPsfImage, 

55 ExtendedPsfImageInfo, 

56 ExtendedPsfMoffatFit, 

57 ExtendedPsfStackConfig, 

58 ExtendedPsfStackTask, 

59) 

60 

61 

62class ExtendedPsfCandidatesTestCase(lsst.utils.tests.TestCase): 

63 """Test ExtendedPsfCandidate and ExtendedPsfCandidates data structures.""" 

64 

65 @classmethod 

66 def setUpClass(cls): 

67 rng = np.random.Generator(np.random.MT19937(seed=5)) 

68 cutout_size = (25, 25) 

69 

70 # Generate simulated stars 

71 extended_psf_candidates = [] 

72 cls.star_infos = [] 

73 mask_schema = MaskSchema([]) 

74 

75 for i in range(3): 

76 candidate_masked_image = MaskedImage( 

77 image=Image(rng.random(cutout_size).astype(np.float32)), 

78 mask=Mask(0, schema=mask_schema, shape=cutout_size), 

79 variance=Image(rng.random(cutout_size).astype(np.float32)), 

80 ) 

81 

82 star_info = ExtendedPsfCandidateInfo( 

83 visit=100 + i, 

84 detector=200 + i, 

85 ref_id=1000 + i, 

86 ref_mag=10.0 + i, 

87 position_x=float(rng.random()), 

88 position_y=float(rng.random()), 

89 focal_plane_radius=float(rng.random()) * u.mm, 

90 focal_plane_angle=float(rng.random()) * u.rad, 

91 ) 

92 cls.star_infos.append(star_info) 

93 

94 # Build a normalized 2-D Gaussian kernel image directly. 

95 yy, xx = np.mgrid[: cutout_size[0], : cutout_size[1]] 

96 cy = (cutout_size[0] - 1) / 2.0 

97 cx = (cutout_size[1] - 1) / 2.0 

98 sigma = 1.5 

99 kernel_array = np.exp(-((yy - cy) ** 2 + (xx - cx) ** 2) / (2.0 * sigma**2)) 

100 kernel_array /= np.sum(kernel_array) 

101 psf_kernel_image = Image(kernel_array.astype(np.float64)) 

102 

103 extended_psf_candidates.append( 

104 ExtendedPsfCandidate( 

105 image=candidate_masked_image.image, 

106 mask=candidate_masked_image.mask, 

107 variance=candidate_masked_image.variance, 

108 psf_kernel_image=psf_kernel_image, 

109 star_info=star_info, 

110 ) 

111 ) 

112 

113 cls.global_metadata = {"CANDIDATES_TEST_KEY": "CANDIDATES_TEST_VALUE"} 

114 cls.extended_psf_candidates = ExtendedPsfCandidates(extended_psf_candidates, cls.global_metadata) 

115 

116 @classmethod 

117 def tearDownClass(cls): 

118 del cls.extended_psf_candidates 

119 del cls.star_infos 

120 del cls.global_metadata 

121 

122 def test_fits_roundtrip(self): 

123 """Test that ExtendedPsfCandidates can be serialized/deserialized.""" 

124 

125 with lsst.images.tests.RoundtripFits( 

126 self, self.extended_psf_candidates, storage_class="ExtendedPsfCandidates" 

127 ) as roundtrip: 

128 pass 

129 extended_psf_candidates = roundtrip.result 

130 

131 global_metadata = extended_psf_candidates.metadata 

132 self.assertEqual(self.global_metadata["CANDIDATES_TEST_KEY"], global_metadata["CANDIDATES_TEST_KEY"]) 

133 self.assertEqual(len(self.extended_psf_candidates), len(extended_psf_candidates)) 

134 for input_info, input_candidate, output_candidate in zip( 

135 self.star_infos, self.extended_psf_candidates, extended_psf_candidates 

136 ): 

137 self.assertEqual(input_candidate.metadata, {}) 

138 self.assertEqual(output_candidate.metadata, {}) 

139 

140 output_info = output_candidate.star_info 

141 self.assertEqual(input_info.visit, output_info.visit) 

142 self.assertEqual(input_info.detector, output_info.detector) 

143 self.assertEqual(input_info.ref_id, output_info.ref_id) 

144 self.assertAlmostEqual(input_info.ref_mag, output_info.ref_mag, places=10) 

145 self.assertAlmostEqual(input_info.position_x, output_info.position_x, places=10) 

146 self.assertAlmostEqual(input_info.position_y, output_info.position_y, places=10) 

147 

148 self.assertIsNotNone(input_info.focal_plane_radius) 

149 self.assertIsNotNone(output_info.focal_plane_radius) 

150 input_radius = cast(u.Quantity, input_info.focal_plane_radius) 

151 output_radius = cast(u.Quantity, output_info.focal_plane_radius) 

152 self.assertAlmostEqual(input_radius.to_value(u.mm), output_radius.to_value(u.mm), places=10) 

153 

154 self.assertIsNotNone(input_info.focal_plane_angle) 

155 self.assertIsNotNone(output_info.focal_plane_angle) 

156 input_angle = cast(u.Quantity, input_info.focal_plane_angle) 

157 output_angle = cast(u.Quantity, output_info.focal_plane_angle) 

158 self.assertAlmostEqual(input_angle.to_value(u.rad), output_angle.to_value(u.rad), places=10) 

159 

160 np.testing.assert_allclose( 

161 input_candidate.image.array, 

162 output_candidate.image.array, 

163 rtol=0.0, 

164 atol=1e-10, 

165 ) 

166 np.testing.assert_array_equal( 

167 input_candidate.mask.array, 

168 output_candidate.mask.array, 

169 ) 

170 np.testing.assert_allclose( 

171 input_candidate.variance.array, output_candidate.variance.array, rtol=0.0, atol=1e-10 

172 ) 

173 np.testing.assert_allclose( 

174 input_candidate.psf_kernel_image.array, 

175 output_candidate.psf_kernel_image.array, 

176 rtol=0.0, 

177 atol=1e-10, 

178 ) 

179 

180 def test_ref_id_map(self): 

181 """Test that ref_id_map maps reference catalog IDs to candidates.""" 

182 ref_id_map = self.extended_psf_candidates.ref_id_map 

183 self.assertEqual(len(ref_id_map), len(self.star_infos)) 

184 for i, star_info in enumerate(self.star_infos): 

185 self.assertIn(star_info.ref_id, ref_id_map) 

186 self.assertIs(ref_id_map[star_info.ref_id], self.extended_psf_candidates[i]) 

187 

188 def test_slice_preserves_subcollection_and_metadata(self): 

189 """Test that slicing candidates returns a sub-collection.""" 

190 sub = self.extended_psf_candidates[1:3] # length=2 

191 self.assertIsInstance(sub, ExtendedPsfCandidates) 

192 self.assertEqual(len(sub), 2) 

193 self.assertEqual(sub.metadata, self.global_metadata) 

194 self.assertIs(sub[0], self.extended_psf_candidates[1]) 

195 self.assertIs(sub[1], self.extended_psf_candidates[2]) 

196 

197 def test_spatial_box_slicing(self): 

198 """Test that candidates support spatial slicing with a Box.""" 

199 candidate = self.extended_psf_candidates[0] 

200 subbox = Box.from_shape((10, 10), start=(5, 5)) 

201 sub = candidate[subbox] 

202 self.assertIsInstance(sub, ExtendedPsfCandidate) 

203 self.assertEqual(sub.bbox, subbox) 

204 self.assertIs(sub.star_info, candidate.star_info) 

205 np.testing.assert_array_equal(sub.image.array, candidate.image[subbox].array) 

206 np.testing.assert_array_equal(sub.mask.array, candidate.mask[subbox].array) 

207 

208 def test_copy(self): 

209 """Test that copy() produces an independent deep copy.""" 

210 candidate = self.extended_psf_candidates[0] 

211 copied = candidate.copy() 

212 self.assertIsInstance(copied, ExtendedPsfCandidate) 

213 np.testing.assert_array_equal(copied.image.array, candidate.image.array) 

214 np.testing.assert_array_equal(copied.variance.array, candidate.variance.array) 

215 # Modifying the copy must not affect the original. 

216 copied.image.array[:] = 0.0 

217 self.assertFalse(np.all(candidate.image.array == 0.0)) 

218 

219 

220class ExtendedPsfImageTestCase(lsst.utils.tests.TestCase): 

221 """Test ExtendedPsfImage data model, properties, and operations.""" 

222 

223 def setUp(self): 

224 image = Image( 

225 np.arange(30, dtype=np.float32).reshape(5, 6), 

226 yx0=(10, -3), 

227 unit=u.nJy, 

228 ) 

229 variance = Image( 

230 np.full((5, 6), 2.5, dtype=np.float32), 

231 bbox=image.bbox, 

232 unit=u.nJy**2, 

233 ) 

234 info = ExtendedPsfImageInfo( 

235 n_stars=17, 

236 ) 

237 fit = ExtendedPsfMoffatFit( 

238 chi2=12.3, 

239 reduced_chi2=1.23, 

240 dof=10, 

241 amplitude=0.8, 

242 x_0=-0.2, 

243 y_0=0.4, 

244 gamma=2.3, 

245 alpha=4.5, 

246 ) 

247 self.extended_psf_image = ExtendedPsfImage( 

248 image=image, 

249 variance=variance, 

250 info=info, 

251 fit=fit, 

252 metadata={"EPSF_TEST_KEY": "EPSF_TEST VALUE"}, 

253 ) 

254 

255 def tearDown(self): 

256 del self.extended_psf_image 

257 

258 def test_fits_roundtrip(self): 

259 """Test that ExtendedPsfImage can be serialized/deserialized.""" 

260 with lsst.images.tests.RoundtripFits( 

261 self, 

262 self.extended_psf_image, 

263 storage_class="ExtendedPsfImage", 

264 ) as roundtrip: 

265 subbox = Box.from_shape((3, 3), start=(11, -1)) 

266 subimage = roundtrip.get(bbox=subbox) 

267 expected_subimage = self.extended_psf_image[subbox] 

268 

269 np.testing.assert_allclose( 

270 subimage.image.array, 

271 expected_subimage.image.array, 

272 rtol=0.0, 

273 atol=1e-8, 

274 ) 

275 np.testing.assert_allclose( 

276 subimage.variance.array, 

277 expected_subimage.variance.array, 

278 rtol=0.0, 

279 atol=1e-8, 

280 ) 

281 self.assertEqual(subimage.info, self.extended_psf_image.info) 

282 self.assertEqual(subimage.fit, self.extended_psf_image.fit) 

283 

284 roundtripped = roundtrip.result 

285 np.testing.assert_allclose( 

286 roundtripped.image.array, 

287 self.extended_psf_image.image.array, 

288 rtol=0.0, 

289 atol=1e-8, 

290 ) 

291 np.testing.assert_allclose( 

292 roundtripped.variance.array, 

293 self.extended_psf_image.variance.array, 

294 rtol=0.0, 

295 atol=1e-8, 

296 ) 

297 self.assertEqual(roundtripped.info, self.extended_psf_image.info) 

298 self.assertEqual(roundtripped.fit, self.extended_psf_image.fit) 

299 self.assertEqual(roundtripped.metadata["EPSF_TEST_KEY"], "EPSF_TEST VALUE") 

300 

301 def test_unit_sky_projection_bbox_properties(self): 

302 """Test ExtendedPsfImage properties: unit, sky_projection, and bbox.""" 

303 self.assertEqual(self.extended_psf_image.unit, u.nJy) 

304 self.assertIsNone(self.extended_psf_image.sky_projection) 

305 self.assertEqual(self.extended_psf_image.bbox, self.extended_psf_image.image.bbox) 

306 

307 def test_copy_independence(self): 

308 """Test that copy() produces an independent deep copy.""" 

309 copied = self.extended_psf_image.copy() 

310 np.testing.assert_array_equal(copied.image.array, self.extended_psf_image.image.array) 

311 np.testing.assert_array_equal(copied.variance.array, self.extended_psf_image.variance.array) 

312 self.assertEqual(copied.info, self.extended_psf_image.info) 

313 self.assertEqual(copied.fit, self.extended_psf_image.fit) 

314 # Modifying the copy must not affect the original. 

315 copied.image.array[:] = 0.0 

316 self.assertFalse(np.all(self.extended_psf_image.image.array == 0.0)) 

317 

318 def test_setitem_subregion_assignment(self): 

319 """Test __setitem__ writes image and variance into a subregion.""" 

320 target = ExtendedPsfImage(Image(np.zeros((5, 6), dtype=np.float32), yx0=(10, -3), unit=u.nJy)) 

321 subbox = Box.from_shape((3, 3), start=(11, -1)) 

322 target[subbox] = self.extended_psf_image[subbox] 

323 np.testing.assert_array_equal( 

324 target[subbox].image.array, 

325 self.extended_psf_image[subbox].image.array, 

326 ) 

327 np.testing.assert_array_equal( 

328 target[subbox].variance.array, 

329 self.extended_psf_image[subbox].variance.array, 

330 ) 

331 

332 def test_constructor_invalid_inputs(self): 

333 """Test that ValueError is raised for invalid constructor inputs.""" 

334 # Mismatched bboxes 

335 image = Image(np.ones((5, 6), dtype=np.float32)) 

336 variance = Image(np.ones((4, 6), dtype=np.float32)) 

337 with self.assertRaises(ValueError): 

338 ExtendedPsfImage(image=image, variance=variance) 

339 

340 # Variance has wrong units (nJy instead of nJy**2). 

341 image = Image(np.ones((5, 6), dtype=np.float32), unit=u.nJy) 

342 variance_wrong_units = Image(np.ones((5, 6), dtype=np.float32), unit=u.nJy) 

343 with self.assertRaises(ValueError): 

344 ExtendedPsfImage(image=image, variance=variance_wrong_units) 

345 

346 # Image has no units but variance does. 

347 image_no_units = Image(np.ones((5, 6), dtype=np.float32)) 

348 variance_with_units = Image(np.ones((5, 6), dtype=np.float32), unit=u.nJy**2) 

349 with self.assertRaises(ValueError): 

350 ExtendedPsfImage(image=image_no_units, variance=variance_with_units) 

351 

352 

353class ExtendedPsfFitTestCase(lsst.utils.tests.TestCase): 

354 """Test ExtendedPsfFit.""" 

355 

356 def test_construction(self): 

357 """Test construction with and without optional fields.""" 

358 # With all fields 

359 fit = ExtendedPsfFit(chi2=10.5, dof=10, reduced_chi2=1.05) 

360 self.assertEqual(fit.chi2, 10.5) 

361 self.assertEqual(fit.dof, 10) 

362 self.assertEqual(fit.reduced_chi2, 1.05) 

363 

364 # With only the required field 

365 fit_minimal = ExtendedPsfFit(chi2=10.5) 

366 self.assertEqual(fit_minimal.chi2, 10.5) 

367 self.assertIsNone(fit_minimal.dof) 

368 self.assertIsNone(fit_minimal.reduced_chi2) 

369 

370 

371class ExtendedPsfSubtractionTestCase(lsst.utils.tests.TestCase): 

372 """Integration tests for all extended PSF subtraction pipeline tasks.""" 

373 

374 @classmethod 

375 def setUpClass(cls): 

376 rng = np.random.default_rng(42) 

377 

378 # Background coefficients 

379 sigma = 60.0 # noise 

380 pedestal = 3210.1 

381 coef_x = 1e-2 

382 coef_y = 2e-2 

383 coef_x2 = 1e-5 

384 coef_xy = 2e-5 

385 coef_y2 = 3e-5 

386 

387 # Make an input exposure 

388 wcs = makeSkyWcs( 

389 crpix=Point2D(0, 0), 

390 crval=SpherePoint(0, 0, degrees), 

391 cdMatrix=makeCdMatrix(scale=0.2 * arcseconds, flipX=True), 

392 ) 

393 cls.exposure = ExposureF(1001, 1001, wcs) 

394 cls.exposure.setPhotoCalib(makePhotoCalibFromCalibZeroPoint(10 ** (0.4 * 30), 1.0)) 

395 ny, nx = cls.exposure.image.array.shape 

396 grid_y, grid_x = np.mgrid[(-ny + 1) // 2 : ny // 2 + 1, (-nx + 1) // 2 : nx // 2 + 1] 

397 cls.exposure.image.array[:] += rng.normal(scale=sigma, size=cls.exposure.image.array.shape) 

398 cls.exposure.image.array += pedestal 

399 cls.exposure.image.array += coef_x * grid_x 

400 cls.exposure.image.array += coef_y * grid_y 

401 cls.exposure.image.array += coef_x2 * grid_x * grid_x 

402 cls.exposure.image.array += coef_xy * grid_x * grid_y 

403 cls.exposure.image.array += coef_y2 * grid_y * grid_y 

404 cls.exposure.info.setVisitInfo(VisitInfo(id=12345)) 

405 camera = CameraWrapper().camera 

406 detector = camera[10] 

407 cls.exposure.setDetector(detector) 

408 for mask_plane in [ 

409 "BAD", 

410 "CR", 

411 "CROSSTALK", 

412 "EDGE", 

413 "NO_DATA", 

414 "SAT", 

415 "SUSPECT", 

416 "UNMASKEDNAN", 

417 "NEIGHBOR", 

418 ]: 

419 _ = cls.exposure.mask.addMaskPlane(mask_plane) 

420 cls.exposure.variance.array.fill(1.0) 

421 

422 # Make a table of extended PSF candidate stars 

423 corners = cls.exposure.wcs.pixelToSky([Point2D(x) for x in cls.exposure.getBBox().getCorners()]) 

424 corner_ras = [corner.getRa().asDegrees() for corner in corners] 

425 corner_decs = [corner.getDec().asDegrees() for corner in corners] 

426 num_stars = 10 

427 ras = rng.uniform(np.min(corner_ras), np.max(corner_ras), num_stars) 

428 decs = rng.uniform(np.min(corner_decs), np.max(corner_decs), num_stars) 

429 sky_coords = [SpherePoint(ra, dec, degrees) for ra, dec in zip(ras, decs)] 

430 pixel_coords = cls.exposure.wcs.skyToPixel(sky_coords) 

431 pixel_x = [coord.getX() for coord in pixel_coords] 

432 pixel_y = [coord.getY() for coord in pixel_coords] 

433 mags = rng.uniform(10, 20, num_stars) 

434 fluxes = [cls.exposure.photoCalib.magnitudeToInstFlux(mag) for mag in mags] 

435 mm_coords = detector.transform(pixel_coords, PIXELS, FOCAL_PLANE) 

436 mm_coords_x = np.array([mm_coord.x for mm_coord in mm_coords]) 

437 mm_coords_y = np.array([mm_coord.y for mm_coord in mm_coords]) 

438 radius_mm = np.sqrt(mm_coords_x**2 + mm_coords_y**2) 

439 theta_radians = np.arctan2(mm_coords_y, mm_coords_x) 

440 cls.extended_psf_candidate_table = Table( 

441 { 

442 "id": np.arange(num_stars), 

443 "coord_ra": ras, 

444 "coord_dec": decs, 

445 "phot_g_mean_flux": fluxes, 

446 "mag": mags, 

447 "pixel_x": pixel_x, 

448 "pixel_y": pixel_y, 

449 "radius_mm": radius_mm, 

450 "angle_radians": theta_radians, 

451 } 

452 ) 

453 

454 # Make a synthetic star 

455 cutout_radius = 25 

456 grid_y, grid_x = np.mgrid[-cutout_radius : cutout_radius + 1, -cutout_radius : cutout_radius + 1] 

457 dist_from_center = np.sqrt(grid_x**2 + grid_y**2) 

458 sigma = 1.5 

459 psf_array = np.exp(-(dist_from_center**2) / (2 * sigma**2)) 

460 psf_array /= np.sum(psf_array) 

461 fixed_kernel = FixedKernel(ImageD(psf_array)) 

462 kernel_psf = KernelPsf(fixed_kernel) 

463 cls.exposure.setPsf(kernel_psf) 

464 psf = kernel_psf.computeKernelImage(kernel_psf.getAveragePosition()) 

465 

466 # Add synthetic stars to the exposure 

467 footprints = ImageF(cls.exposure.getBBox()) 

468 for candidate_id, candidate in enumerate(cls.extended_psf_candidate_table): 

469 bbox_star = Box2I(Point2I(candidate["pixel_x"], candidate["pixel_y"]), Extent2I(1, 1)).dilatedBy( 

470 cutout_radius 

471 ) 

472 bbox_star_clipped = bbox_star.clippedTo(cls.exposure.getBBox()) 

473 candidate_im = MaskedImageF(bbox_star) 

474 candidate_im.image.array = candidate["phot_g_mean_flux"] * psf.getArray() 

475 candidate_im = candidate_im[bbox_star_clipped] 

476 detection_threshold = cls.exposure.getPhotoCalib().magnitudeToInstFlux(25) 

477 detected = candidate_im.image.array > detection_threshold 

478 footprints[bbox_star_clipped].array = detected * (candidate_id + 1) 

479 _ = candidate_im.mask.addMaskPlane("DETECTED") 

480 candidate_im.mask.array[detected] |= candidate_im.mask.getPlaneBitMask("DETECTED") 

481 candidate_im.variance.array.fill(1.0) 

482 cls.exposure.maskedImage[bbox_star_clipped] += candidate_im 

483 cls.footprints = footprints.array 

484 

485 # Run the cutout task 

486 extendedPsfCutoutConfig = ExtendedPsfCutoutConfig() 

487 extendedPsfCutoutTask = ExtendedPsfCutoutTask(config=extendedPsfCutoutConfig) 

488 cls.extended_psf_candidates = extendedPsfCutoutTask._get_extended_psf_candidates( 

489 input_exposure=cls.exposure, 

490 input_background=None, 

491 footprints=cls.footprints, 

492 extended_psf_candidate_table=cls.extended_psf_candidate_table, 

493 ) 

494 

495 # Run the stack task 

496 extendedPsfStackConfig = ExtendedPsfStackConfig() 

497 extendedPsfStackTask = ExtendedPsfStackTask(config=extendedPsfStackConfig) 

498 stack_result = extendedPsfStackTask.run(extended_psf_candidates=cls.extended_psf_candidates) 

499 cls.extended_psf = stack_result.extended_psf if stack_result is not None else None 

500 

501 @classmethod 

502 def tearDownClass(cls): 

503 del cls.exposure 

504 del cls.extended_psf_candidate_table 

505 del cls.footprints 

506 del cls.extended_psf_candidates 

507 del cls.extended_psf 

508 

509 def test_cutout_task_candidate_extraction(self): 

510 """Test ExtendedPsfCutoutTask candidate extraction.""" 

511 assert self.extended_psf_candidates is not None 

512 self.assertAlmostEqual( 

513 float(self.extended_psf_candidates.metadata["FOCAL_PLANE_RADIUS_MM_MIN"]), 5.22408977, 7 

514 ) 

515 self.assertAlmostEqual( 

516 float(self.extended_psf_candidates.metadata["FOCAL_PLANE_RADIUS_MM_MAX"]), 14.6045832, 7 

517 ) 

518 self.assertEqual(len(self.extended_psf_candidates), len(self.extended_psf_candidate_table)) 

519 self.assertEqual(self.extended_psf_candidates[0].star_info.visit, 12345) 

520 self.assertEqual(self.extended_psf_candidates[0].star_info.detector, 10) 

521 

522 for candidate, candidate_entry in zip( 

523 self.extended_psf_candidates, self.extended_psf_candidate_table 

524 ): 

525 self.assertEqual(candidate.star_info.ref_id, candidate_entry["id"]) 

526 self.assertEqual(candidate.star_info.ref_mag, candidate_entry["mag"]) 

527 self.assertEqual(candidate.star_info.position_x, candidate_entry["pixel_x"]) 

528 self.assertEqual(candidate.star_info.position_y, candidate_entry["pixel_y"]) 

529 self.assertIsInstance(candidate.psf_kernel_image, Image) 

530 self.assertEqual(candidate.psf_kernel_image.array.ndim, 2) 

531 self.assertGreater(candidate.psf_kernel_image.array.shape[0], 0) 

532 self.assertGreater(candidate.psf_kernel_image.array.shape[1], 0) 

533 self.assertTrue(np.isfinite(candidate.psf_kernel_image.array).all()) 

534 self.assertGreater(candidate.psf_kernel_image.array.sum(), 0.0) 

535 focal_plane_radius = cast(u.Quantity, candidate.star_info.focal_plane_radius) 

536 focal_plane_angle = cast(u.Quantity, candidate.star_info.focal_plane_angle) 

537 self.assertEqual(focal_plane_radius.to_value(u.mm), candidate_entry["radius_mm"]) 

538 self.assertEqual(focal_plane_angle.to_value(u.rad), candidate_entry["angle_radians"]) 

539 

540 def test_stack_task_moffat_fitting(self): 

541 """Test Moffat fitting.""" 

542 assert self.extended_psf is not None 

543 self.assertAlmostEqual(np.sum(self.extended_psf.image.array), 0.8233417, places=2) 

544 self.assertAlmostEqual(np.sum(self.extended_psf.variance.array), 0.0075618913, places=4) 

545 fit = self.extended_psf.fit 

546 self.assertAlmostEqual(fit.chi2, 107652.97393353, delta=5) 

547 self.assertEqual(fit.dof, 62996) 

548 self.assertAlmostEqual(fit.reduced_chi2, 1.7088858647141, places=2) 

549 self.assertAlmostEqual(fit.amplitude, 0.078900464260488, places=2) 

550 self.assertAlmostEqual(fit.x_0, -0.68834523633912, places=2) 

551 self.assertAlmostEqual(fit.y_0, -0.069005412739451, places=2) 

552 self.assertAlmostEqual(fit.gamma, 8.0966823485900, places=2) 

553 self.assertAlmostEqual(fit.alpha, 16.048683662812, places=2) 

554 

555 def test_stack_no_candidates(self): 

556 """Test that None returned when no candidates pass the radius check.""" 

557 config = ExtendedPsfStackConfig() 

558 config.min_focal_plane_radius = 1e6 # Excludes all candidates. 

559 task = ExtendedPsfStackTask(config=config) 

560 with self.assertLogs(level=logging.INFO): 

561 result = task.run(extended_psf_candidates=self.extended_psf_candidates) 

562 self.assertIsNone(result) 

563 

564 

565class MemoryTestCase(lsst.utils.tests.MemoryTestCase): 

566 pass 

567 

568 

569def setup_module(module): 

570 lsst.utils.tests.init() 

571 

572 

573if __name__ == "__main__": 573 ↛ 574line 573 didn't jump to line 574 because the condition on line 573 was never true

574 lsst.utils.tests.init() 

575 unittest.main()