Coverage for tests / test_psfs.py: 19%
91 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 01:57 -0700
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 01:57 -0700
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
16import warnings
18import numpy as np
20from lsst.images import Box
21from lsst.images.psfs import (
22 GaussianPointSpreadFunction,
23 PiffWrapper,
24 PointSpreadFunction,
25 PSFExWrapper,
26)
27from lsst.images.psfs._piff import _ArchivePiffWriter
28from lsst.images.tests import RoundtripFits, RoundtripJson, compare_psf_to_legacy
30DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
33class PointSpreadFunctionTestCase(unittest.TestCase):
34 """Tests for the PointSpreadFunction classes."""
36 def test_gaussian(self) -> None:
37 """Test the built-in Gaussian PSF implementation."""
38 bounds = Box.factory[-1024:1024, -2048:2048]
39 psf = GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=33)
40 self.assertEqual(psf.bounds, bounds)
42 kernel = psf.compute_kernel_image(x=5.0, y=3.0)
43 self.assertEqual(kernel.bbox, psf.kernel_bbox)
44 self.assertAlmostEqual(float(kernel.array.sum()), 1.0)
45 center = kernel.array.shape[0] // 2
46 self.assertEqual(np.unravel_index(np.argmax(kernel.array), kernel.array.shape), (center, center))
48 stellar = psf.compute_stellar_image(x=5.25, y=3.75)
49 self.assertEqual(stellar.bbox, psf.compute_stellar_bbox(x=5.25, y=3.75))
50 self.assertAlmostEqual(float(stellar.array.sum()), 1.0)
51 self.assertGreater(stellar.array[center - 1, center], stellar.array[center + 1, center])
52 self.assertGreater(stellar.array[center, center], stellar.array[center, center - 1])
53 self.assertGreater(stellar.array[center, center], stellar.array[center - 1, center])
55 with RoundtripFits(self, psf) as roundtrip:
56 self.assertEqual(roundtrip.result, psf, f"{roundtrip.result} != {psf}")
58 with self.assertRaises(ValueError):
59 # Even stamp size.
60 GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=32)
62 with self.assertRaises(ValueError):
63 # Negative stamp size.
64 GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=-33)
66 with self.assertRaises(ValueError):
67 # Negative sigma.
68 GaussianPointSpreadFunction(-2.5, bounds=bounds, stamp_size=33)
70 def test_piff_writer_normalizes_tuple_metadata(self) -> None:
71 """Piff metadata should be normalized to JSON-like values."""
72 writer = _ArchivePiffWriter()
73 writer.write_struct(
74 "interp",
75 {
76 "keys": ("u", "v"),
77 "scale": np.float64(1.5),
78 "flags": [np.bool_(True), np.int64(3)],
79 },
80 )
81 model = writer.serialize(None) # type: ignore[arg-type]
82 self.assertEqual(model.structs["interp"]["keys"], ["u", "v"])
83 self.assertEqual(model.structs["interp"]["scale"], 1.5)
84 self.assertEqual(model.structs["interp"]["flags"], [True, 3])
85 with warnings.catch_warnings():
86 warnings.simplefilter("error", UserWarning)
87 model.model_dump_json()
89 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
90 def test_piff(self) -> None:
91 """Test that we can:
93 - read a legacy Piff PSF with afw;
94 - convert it to the new `PiffWrapper` class;
95 - get consistent behavior from the two;
96 - round-trip the new PSF through a FITS archive;
97 - still get consistent behavior with the round-tripped PSF.
99 This test is skipped if legacy modules cannot be imported.
100 """
101 try:
102 from piff import PSF
104 from lsst.afw.image import ExposureFitsReader
105 except ImportError:
106 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None
107 assert DATA_DIR is not None, "Guaranteed by decorator."
108 filename = os.path.join(DATA_DIR, "dp2", "legacy", "visit_image.fits")
109 reader = ExposureFitsReader(filename)
110 legacy_psf = reader.readPsf()
111 bounds = Box.from_legacy(reader.readBBox())
112 psf = PointSpreadFunction.from_legacy(legacy_psf, bounds)
113 self.assertIsInstance(psf, PiffWrapper)
114 self.assertEqual(psf.bounds, bounds)
115 self.assertIsInstance(psf.piff_psf, PSF)
116 compare_psf_to_legacy(self, psf, legacy_psf)
117 with RoundtripFits(self, psf) as roundtrip1:
118 pass
119 compare_psf_to_legacy(self, roundtrip1.result, legacy_psf)
120 with RoundtripJson(self, psf) as roundtrip2:
121 pass
122 compare_psf_to_legacy(self, roundtrip2.result, legacy_psf)
124 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
125 def test_psfex(self) -> None:
126 """Test that we can:
128 - read a legacy PSFEX PSF with afw;
129 - wrap it inthe new `LegacyPointSpreadFunction` class;
130 - get consistent behavior from the two.
132 This test is skipped if legacy modules cannot be imported.
133 """
134 try:
135 from lsst.afw.image import ExposureFitsReader
136 from lsst.meas.extensions.psfex import PsfexPsf
137 except ImportError:
138 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None
139 assert DATA_DIR is not None, "Guaranteed by decorator."
140 filename = os.path.join(DATA_DIR, "dp2", "legacy", "preliminary_visit_image.fits")
141 reader = ExposureFitsReader(filename)
142 legacy_psf = reader.readPsf()
143 bounds = Box.from_legacy(reader.readBBox())
144 psf = PointSpreadFunction.from_legacy(legacy_psf, bounds)
145 self.assertIsInstance(psf, PSFExWrapper)
146 self.assertEqual(psf.bounds, bounds)
147 self.assertIsInstance(psf.legacy_psf, PsfexPsf)
148 compare_psf_to_legacy(self, psf, legacy_psf)
149 compare_psf_to_legacy(self, psf, legacy_psf)
150 with RoundtripFits(self, psf) as roundtrip1:
151 pass
152 compare_psf_to_legacy(self, roundtrip1.result, legacy_psf)
153 with RoundtripJson(self, psf) as roundtrip2:
154 pass
155 compare_psf_to_legacy(self, roundtrip2.result, legacy_psf)
158if __name__ == "__main__":
159 unittest.main()