Coverage for tests/test_extended_psf.py: 16%
306 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 01:54 -0700
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 01:54 -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/>.
22import logging
23import unittest
24from typing import cast
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)
61class ExtendedPsfCandidatesTestCase(lsst.utils.tests.TestCase):
62 """Test ExtendedPsfCandidate and ExtendedPsfCandidates data structures."""
64 @classmethod
65 def setUpClass(cls):
66 rng = np.random.Generator(np.random.MT19937(seed=5))
67 cutout_size = (25, 25)
69 # Generate simulated stars
70 extended_psf_candidates = []
71 cls.star_infos = []
72 mask_schema = MaskSchema([])
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 )
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)
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))
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 )
112 cls.global_metadata = {"CANDIDATES_TEST_KEY": "CANDIDATES_TEST_VALUE"}
113 cls.extended_psf_candidates = ExtendedPsfCandidates(extended_psf_candidates, cls.global_metadata)
115 @classmethod
116 def tearDownClass(cls):
117 del cls.extended_psf_candidates
118 del cls.star_infos
119 del cls.global_metadata
121 def test_fits_roundtrip(self):
122 """Test that ExtendedPsfCandidates can be serialized/deserialized."""
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
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, {})
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)
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)
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)
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 )
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])
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])
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)
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))
219class ExtendedPsfImageTestCase(lsst.utils.tests.TestCase):
220 """Test ExtendedPsfImage data model, properties, and operations."""
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 )
254 def tearDown(self):
255 del self.extended_psf_image
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]
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)
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")
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)
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))
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 )
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)
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)
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)
352class ExtendedPsfFitTestCase(lsst.utils.tests.TestCase):
353 """Test ExtendedPsfFit."""
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)
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)
370class ExtendedPsfSubtractionTestCase(lsst.utils.tests.TestCase):
371 """Integration tests for all extended PSF subtraction pipeline tasks."""
373 @classmethod
374 def setUpClass(cls):
375 rng = np.random.default_rng(42)
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
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)
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 )
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())
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
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 )
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
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
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)
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"])
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)
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)
564class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
565 pass
568def setup_module(module):
569 lsst.utils.tests.init()
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()