Coverage for tests / test_psfs.py: 19%

91 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-21 08:47 +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. 

11 

12from __future__ import annotations 

13 

14import os 

15import unittest 

16import warnings 

17 

18import numpy as np 

19 

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 

29 

30DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None) 

31 

32 

33class PointSpreadFunctionTestCase(unittest.TestCase): 

34 """Tests for the PointSpreadFunction classes.""" 

35 

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) 

41 

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)) 

47 

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]) 

54 

55 with RoundtripFits(self, psf) as roundtrip: 

56 self.assertEqual(roundtrip.result, psf, f"{roundtrip.result} != {psf}") 

57 

58 with self.assertRaises(ValueError): 

59 # Even stamp size. 

60 GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=32) 

61 

62 with self.assertRaises(ValueError): 

63 # Negative stamp size. 

64 GaussianPointSpreadFunction(2.5, bounds=bounds, stamp_size=-33) 

65 

66 with self.assertRaises(ValueError): 

67 # Negative sigma. 

68 GaussianPointSpreadFunction(-2.5, bounds=bounds, stamp_size=33) 

69 

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() 

88 

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: 

92 

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. 

98 

99 This test is skipped if legacy modules cannot be imported. 

100 """ 

101 try: 

102 from piff import PSF 

103 

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) 

123 

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: 

127 

128 - read a legacy PSFEX PSF with afw; 

129 - wrap it inthe new `LegacyPointSpreadFunction` class; 

130 - get consistent behavior from the two. 

131 

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) 

156 

157 

158if __name__ == "__main__": 

159 unittest.main()