Coverage for tests / test_psfs.py: 19%
73 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:00 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:00 +0000
1# This file is part of lsst-images.
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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14import os
15import unittest
17import numpy as np
19from lsst.images import Box
20from lsst.images.psfs import (
21 GaussianPointSpreadFunction,
22 PiffWrapper,
23 PointSpreadFunction,
24 PSFExWrapper,
25)
26from lsst.images.tests import RoundtripFits, compare_psf_to_legacy
28DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
31class PointSpreadFunctionTestCase(unittest.TestCase):
32 """Tests for the PointSpreadFunction classes."""
34 def test_gaussian(self) -> None:
35 """Test the built-in Gaussian PSF implementation."""
36 bounds = Box.factory[-1024:1024, -2048:2048]
37 psf = GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=33)
38 self.assertEqual(psf.bounds, bounds)
40 kernel = psf.compute_kernel_image(x=5.0, y=3.0)
41 self.assertEqual(kernel.bbox, psf.kernel_bbox)
42 self.assertAlmostEqual(float(kernel.array.sum()), 1.0)
43 center = kernel.array.shape[0] // 2
44 self.assertEqual(np.unravel_index(np.argmax(kernel.array), kernel.array.shape), (center, center))
46 stellar = psf.compute_stellar_image(x=5.25, y=3.75)
47 self.assertEqual(stellar.bbox, psf.compute_stellar_bbox(x=5.25, y=3.75))
48 self.assertAlmostEqual(float(stellar.array.sum()), 1.0)
49 self.assertGreater(stellar.array[center - 1, center], stellar.array[center + 1, center])
50 self.assertGreater(stellar.array[center, center], stellar.array[center, center - 1])
51 self.assertGreater(stellar.array[center, center], stellar.array[center - 1, center])
53 with RoundtripFits(self, psf) as roundtrip:
54 self.assertEqual(roundtrip.result, psf, f"{roundtrip.result} != {psf}")
56 with self.assertRaises(ValueError):
57 # Even stamp size.
58 GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=32)
60 with self.assertRaises(ValueError):
61 # Negative stamp size.
62 GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=-33)
64 with self.assertRaises(ValueError):
65 # Negative sigma.
66 GaussianPointSpreadFunction(-2.5, bounds=bounds, stamp_size=33)
68 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
69 def test_piff(self) -> None:
70 """Test that we can:
72 - read a legacy Piff PSF with afw;
73 - convert it to the new `PiffWrapper` class;
74 - get consistent behavior from the two;
75 - round-trip the new PSF through a FITS archive;
76 - still get consistent behavior with the round-tripped PSF.
78 This test is skipped if legacy modules cannot be imported.
79 """
80 try:
81 from piff import PSF
83 from lsst.afw.image import ExposureFitsReader
84 except ImportError:
85 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None
86 assert DATA_DIR is not None, "Guaranteed by decorator."
87 filename = os.path.join(DATA_DIR, "dp2", "legacy", "visit_image.fits")
88 reader = ExposureFitsReader(filename)
89 legacy_psf = reader.readPsf()
90 bounds = Box.from_legacy(reader.readBBox())
91 psf = PointSpreadFunction.from_legacy(legacy_psf, bounds)
92 self.assertIsInstance(psf, PiffWrapper)
93 self.assertEqual(psf.bounds, bounds)
94 self.assertIsInstance(psf.piff_psf, PSF)
95 compare_psf_to_legacy(self, psf, legacy_psf)
96 with RoundtripFits(self, psf) as roundtrip:
97 pass
98 compare_psf_to_legacy(self, roundtrip.result, legacy_psf)
100 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
101 def test_psfex(self) -> None:
102 """Test that we can:
104 - read a legacy PSFEX PSF with afw;
105 - wrap it inthe new `LegacyPointSpreadFunction` class;
106 - get consistent behavior from the two.
108 This test is skipped if legacy modules cannot be imported.
109 """
110 try:
111 from lsst.afw.image import ExposureFitsReader
112 from lsst.meas.extensions.psfex import PsfexPsf
113 except ImportError:
114 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None
115 assert DATA_DIR is not None, "Guaranteed by decorator."
116 filename = os.path.join(DATA_DIR, "dp2", "legacy", "preliminary_visit_image.fits")
117 reader = ExposureFitsReader(filename)
118 legacy_psf = reader.readPsf()
119 bounds = Box.from_legacy(reader.readBBox())
120 psf = PointSpreadFunction.from_legacy(legacy_psf, bounds)
121 self.assertIsInstance(psf, PSFExWrapper)
122 self.assertEqual(psf.bounds, bounds)
123 self.assertIsInstance(psf.legacy_psf, PsfexPsf)
124 compare_psf_to_legacy(self, psf, legacy_psf)
125 compare_psf_to_legacy(self, psf, legacy_psf)
126 with RoundtripFits(self, psf) as roundtrip:
127 pass
128 compare_psf_to_legacy(self, roundtrip.result, legacy_psf)
131if __name__ == "__main__":
132 unittest.main()