Coverage for tests/test_extended_psf.py: 16%

306 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-29 08:48 +0000

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 lsst.images.tests 

28import lsst.utils.tests 

29import numpy as np 

30from astropy.table import Table 

31from lsst.afw.cameraGeom import FOCAL_PLANE, PIXELS 

32from lsst.afw.cameraGeom.testUtils import CameraWrapper 

33from lsst.afw.geom import makeCdMatrix, makeSkyWcs 

34from lsst.afw.image import ( 

35 ExposureF, 

36 ImageD, 

37 ImageF, 

38 MaskedImageF, 

39 VisitInfo, 

40 makePhotoCalibFromCalibZeroPoint, 

41) 

42from lsst.afw.math import FixedKernel 

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

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

45from lsst.meas.algorithms import KernelPsf 

46from lsst.pipe.tasks.extended_psf import ( 

47 ExtendedPsfCandidate, 

48 ExtendedPsfCandidateInfo, 

49 ExtendedPsfCandidates, 

50 ExtendedPsfCutoutConfig, 

51 ExtendedPsfCutoutTask, 

52 ExtendedPsfFit, 

53 ExtendedPsfImage, 

54 ExtendedPsfImageInfo, 

55 ExtendedPsfMoffatFit, 

56 ExtendedPsfStackConfig, 

57 ExtendedPsfStackTask, 

58) 

59 

60 

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

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

63 

64 @classmethod 

65 def setUpClass(cls): 

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

67 cutout_size = (25, 25) 

68 

69 # Generate simulated stars 

70 extended_psf_candidates = [] 

71 cls.star_infos = [] 

72 mask_schema = MaskSchema([]) 

73 

74 for i in range(3): 

75 candidate_masked_image = MaskedImage( 

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

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

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

79 ) 

80 

81 star_info = ExtendedPsfCandidateInfo( 

82 visit=100 + i, 

83 detector=200 + i, 

84 ref_id=1000 + i, 

85 ref_mag=10.0 + i, 

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

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

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

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

90 ) 

91 cls.star_infos.append(star_info) 

92 

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

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

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

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

97 sigma = 1.5 

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

99 kernel_array /= np.sum(kernel_array) 

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

101 

102 extended_psf_candidates.append( 

103 ExtendedPsfCandidate( 

104 image=candidate_masked_image.image, 

105 mask=candidate_masked_image.mask, 

106 variance=candidate_masked_image.variance, 

107 psf_kernel_image=psf_kernel_image, 

108 star_info=star_info, 

109 ) 

110 ) 

111 

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

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

114 

115 @classmethod 

116 def tearDownClass(cls): 

117 del cls.extended_psf_candidates 

118 del cls.star_infos 

119 del cls.global_metadata 

120 

121 def test_fits_roundtrip(self): 

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

123 

124 with lsst.images.tests.RoundtripFits( 

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

126 ) as roundtrip: 

127 pass 

128 extended_psf_candidates = roundtrip.result 

129 

130 global_metadata = extended_psf_candidates.metadata 

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

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

133 for input_info, input_candidate, output_candidate in zip( 

134 self.star_infos, self.extended_psf_candidates, extended_psf_candidates 

135 ): 

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

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

138 

139 output_info = output_candidate.star_info 

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

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

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

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

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

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

146 

147 self.assertIsNotNone(input_info.focal_plane_radius) 

148 self.assertIsNotNone(output_info.focal_plane_radius) 

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

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

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

152 

153 self.assertIsNotNone(input_info.focal_plane_angle) 

154 self.assertIsNotNone(output_info.focal_plane_angle) 

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

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

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

158 

159 np.testing.assert_allclose( 

160 input_candidate.image.array, 

161 output_candidate.image.array, 

162 rtol=0.0, 

163 atol=1e-10, 

164 ) 

165 np.testing.assert_array_equal( 

166 input_candidate.mask.array, 

167 output_candidate.mask.array, 

168 ) 

169 np.testing.assert_allclose( 

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

171 ) 

172 np.testing.assert_allclose( 

173 input_candidate.psf_kernel_image.array, 

174 output_candidate.psf_kernel_image.array, 

175 rtol=0.0, 

176 atol=1e-10, 

177 ) 

178 

179 def test_ref_id_map(self): 

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

181 ref_id_map = self.extended_psf_candidates.ref_id_map 

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

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

184 self.assertIn(star_info.ref_id, ref_id_map) 

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

186 

187 def test_slice_preserves_subcollection_and_metadata(self): 

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

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

190 self.assertIsInstance(sub, ExtendedPsfCandidates) 

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

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

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

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

195 

196 def test_spatial_box_slicing(self): 

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

198 candidate = self.extended_psf_candidates[0] 

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

200 sub = candidate[subbox] 

201 self.assertIsInstance(sub, ExtendedPsfCandidate) 

202 self.assertEqual(sub.bbox, subbox) 

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

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

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

206 

207 def test_copy(self): 

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

209 candidate = self.extended_psf_candidates[0] 

210 copied = candidate.copy() 

211 self.assertIsInstance(copied, ExtendedPsfCandidate) 

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

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

214 # Modifying the copy must not affect the original. 

215 copied.image.array[:] = 0.0 

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

217 

218 

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

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

221 

222 def setUp(self): 

223 image = Image( 

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

225 start=(10, -3), 

226 unit=u.nJy, 

227 ) 

228 variance = Image( 

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

230 bbox=image.bbox, 

231 unit=u.nJy**2, 

232 ) 

233 info = ExtendedPsfImageInfo( 

234 n_stars=17, 

235 ) 

236 fit = ExtendedPsfMoffatFit( 

237 chi2=12.3, 

238 reduced_chi2=1.23, 

239 dof=10, 

240 amplitude=0.8, 

241 x_0=-0.2, 

242 y_0=0.4, 

243 gamma=2.3, 

244 alpha=4.5, 

245 ) 

246 self.extended_psf_image = ExtendedPsfImage( 

247 image=image, 

248 variance=variance, 

249 info=info, 

250 fit=fit, 

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

252 ) 

253 

254 def tearDown(self): 

255 del self.extended_psf_image 

256 

257 def test_fits_roundtrip(self): 

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

259 with lsst.images.tests.RoundtripFits( 

260 self, 

261 self.extended_psf_image, 

262 storage_class="ExtendedPsfImage", 

263 ) as roundtrip: 

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

265 subimage = roundtrip.get(bbox=subbox) 

266 expected_subimage = self.extended_psf_image[subbox] 

267 

268 np.testing.assert_allclose( 

269 subimage.image.array, 

270 expected_subimage.image.array, 

271 rtol=0.0, 

272 atol=1e-8, 

273 ) 

274 np.testing.assert_allclose( 

275 subimage.variance.array, 

276 expected_subimage.variance.array, 

277 rtol=0.0, 

278 atol=1e-8, 

279 ) 

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

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

282 

283 roundtripped = roundtrip.result 

284 np.testing.assert_allclose( 

285 roundtripped.image.array, 

286 self.extended_psf_image.image.array, 

287 rtol=0.0, 

288 atol=1e-8, 

289 ) 

290 np.testing.assert_allclose( 

291 roundtripped.variance.array, 

292 self.extended_psf_image.variance.array, 

293 rtol=0.0, 

294 atol=1e-8, 

295 ) 

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

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

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

299 

300 def test_unit_projection_bbox_properties(self): 

301 """Test ExtendedPsfImage properties: unit, projection, and bbox.""" 

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

303 self.assertIsNone(self.extended_psf_image.projection) 

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

305 

306 def test_copy_independence(self): 

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

308 copied = self.extended_psf_image.copy() 

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

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

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

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

313 # Modifying the copy must not affect the original. 

314 copied.image.array[:] = 0.0 

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

316 

317 def test_setitem_subregion_assignment(self): 

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

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

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

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

322 np.testing.assert_array_equal( 

323 target[subbox].image.array, 

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

325 ) 

326 np.testing.assert_array_equal( 

327 target[subbox].variance.array, 

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

329 ) 

330 

331 def test_constructor_invalid_inputs(self): 

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

333 # Mismatched bboxes 

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

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

336 with self.assertRaises(ValueError): 

337 ExtendedPsfImage(image=image, variance=variance) 

338 

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

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

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

342 with self.assertRaises(ValueError): 

343 ExtendedPsfImage(image=image, variance=variance_wrong_units) 

344 

345 # Image has no units but variance does. 

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

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

348 with self.assertRaises(ValueError): 

349 ExtendedPsfImage(image=image_no_units, variance=variance_with_units) 

350 

351 

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

353 """Test ExtendedPsfFit.""" 

354 

355 def test_construction(self): 

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

357 # With all fields 

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

359 self.assertEqual(fit.chi2, 10.5) 

360 self.assertEqual(fit.dof, 10) 

361 self.assertEqual(fit.reduced_chi2, 1.05) 

362 

363 # With only the required field 

364 fit_minimal = ExtendedPsfFit(chi2=10.5) 

365 self.assertEqual(fit_minimal.chi2, 10.5) 

366 self.assertIsNone(fit_minimal.dof) 

367 self.assertIsNone(fit_minimal.reduced_chi2) 

368 

369 

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

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

372 

373 @classmethod 

374 def setUpClass(cls): 

375 rng = np.random.default_rng(42) 

376 

377 # Background coefficients 

378 sigma = 60.0 # noise 

379 pedestal = 3210.1 

380 coef_x = 1e-2 

381 coef_y = 2e-2 

382 coef_x2 = 1e-5 

383 coef_xy = 2e-5 

384 coef_y2 = 3e-5 

385 

386 # Make an input exposure 

387 wcs = makeSkyWcs( 

388 crpix=Point2D(0, 0), 

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

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

391 ) 

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

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

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

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

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

397 cls.exposure.image.array += pedestal 

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

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

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

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

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

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

404 camera = CameraWrapper().camera 

405 detector = camera[10] 

406 cls.exposure.setDetector(detector) 

407 for mask_plane in [ 

408 "BAD", 

409 "CR", 

410 "CROSSTALK", 

411 "EDGE", 

412 "NO_DATA", 

413 "SAT", 

414 "SUSPECT", 

415 "UNMASKEDNAN", 

416 "NEIGHBOR", 

417 ]: 

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

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

420 

421 # Make a table of extended PSF candidate stars 

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

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

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

425 num_stars = 10 

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

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

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

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

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

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

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

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

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

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

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

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

438 theta_radians = np.arctan2(mm_coords_y, mm_coords_x) 

439 cls.extended_psf_candidate_table = Table( 

440 { 

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

442 "coord_ra": ras, 

443 "coord_dec": decs, 

444 "phot_g_mean_flux": fluxes, 

445 "mag": mags, 

446 "pixel_x": pixel_x, 

447 "pixel_y": pixel_y, 

448 "radius_mm": radius_mm, 

449 "angle_radians": theta_radians, 

450 } 

451 ) 

452 

453 # Make a synthetic star 

454 cutout_radius = 25 

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

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

457 sigma = 1.5 

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

459 psf_array /= np.sum(psf_array) 

460 fixed_kernel = FixedKernel(ImageD(psf_array)) 

461 kernel_psf = KernelPsf(fixed_kernel) 

462 cls.exposure.setPsf(kernel_psf) 

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

464 

465 # Add synthetic stars to the exposure 

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

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

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

469 cutout_radius 

470 ) 

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

472 candidate_im = MaskedImageF(bbox_star) 

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

474 candidate_im = candidate_im[bbox_star_clipped] 

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

476 detected = candidate_im.image.array > detection_threshold 

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

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

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

480 candidate_im.variance.array.fill(1.0) 

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

482 cls.footprints = footprints.array 

483 

484 # Run the cutout task 

485 extendedPsfCutoutConfig = ExtendedPsfCutoutConfig() 

486 extendedPsfCutoutTask = ExtendedPsfCutoutTask(config=extendedPsfCutoutConfig) 

487 cls.extended_psf_candidates = extendedPsfCutoutTask._get_extended_psf_candidates( 

488 input_exposure=cls.exposure, 

489 input_background=None, 

490 footprints=cls.footprints, 

491 extended_psf_candidate_table=cls.extended_psf_candidate_table, 

492 ) 

493 

494 # Run the stack task 

495 extendedPsfStackConfig = ExtendedPsfStackConfig() 

496 extendedPsfStackTask = ExtendedPsfStackTask(config=extendedPsfStackConfig) 

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

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

499 

500 @classmethod 

501 def tearDownClass(cls): 

502 del cls.exposure 

503 del cls.extended_psf_candidate_table 

504 del cls.footprints 

505 del cls.extended_psf_candidates 

506 del cls.extended_psf 

507 

508 def test_cutout_task_candidate_extraction(self): 

509 """Test ExtendedPsfCutoutTask candidate extraction.""" 

510 assert self.extended_psf_candidates is not None 

511 self.assertAlmostEqual( 

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

513 ) 

514 self.assertAlmostEqual( 

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

516 ) 

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

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

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

520 

521 for candidate, candidate_entry in zip( 

522 self.extended_psf_candidates, self.extended_psf_candidate_table 

523 ): 

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

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

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

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

528 self.assertIsInstance(candidate.psf_kernel_image, Image) 

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

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

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

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

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

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

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

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

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

538 

539 def test_stack_task_moffat_fitting(self): 

540 """Test Moffat fitting.""" 

541 assert self.extended_psf is not None 

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

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

544 fit = self.extended_psf.fit 

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

546 self.assertEqual(fit.dof, 62996) 

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

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

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

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

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

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

553 

554 def test_stack_no_candidates(self): 

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

556 config = ExtendedPsfStackConfig() 

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

558 task = ExtendedPsfStackTask(config=config) 

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

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

561 self.assertIsNone(result) 

562 

563 

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

565 pass 

566 

567 

568def setup_module(module): 

569 lsst.utils.tests.init() 

570 

571 

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

573 lsst.utils.tests.init() 

574 unittest.main()