Coverage for tests/test_intrinsicZernikes.py: 96%

95 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-08 01:55 -0700

1# This file is part of ip_isr. 

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 unittest 

23import tempfile 

24 

25import numpy as np 

26from astropy.table import Table 

27import astropy.units as u 

28 

29import lsst.utils.tests 

30 

31from lsst.ip.isr import IntrinsicZernikes 

32 

33 

34class IntrinsicZernikesTestCase(lsst.utils.tests.TestCase): 

35 """Test the IntrinsicZernikes calibration class.""" 

36 

37 def setUp(self): 

38 """Create test data for intrinsic Zernikes.""" 

39 # Create test Zernike coefficients for Noll indices 4, 5, 6 

40 # (defocus, astigmatism) 

41 self.noll_indices = np.array([4, 5, 6]) 

42 

43 # Build a regular 3x3 grid of sample points 

44 x_unique = np.array([-1.0, 0.0, 1.0]) 

45 y_unique = np.array([-0.5, 0.0, 0.5]) 

46 x_grid, y_grid = np.meshgrid(x_unique, y_unique) 

47 self.field_x = x_grid.ravel() 

48 self.field_y = y_grid.ravel() 

49 

50 # Create values: shape (n_points, n_zernikes). The CCS and OCS 

51 # systems get independent values so tests can tell their (per-element) 

52 # contributions apart. 

53 rng = np.random.default_rng(seed=57721) 

54 self.values = rng.normal( 

55 scale=0.1, size=(len(self.field_x), len(self.noll_indices)) 

56 ) # microns 

57 self.values_ocs = rng.normal( 

58 scale=0.1, size=(len(self.field_x), len(self.noll_indices)) 

59 ) # microns 

60 

61 # Create astropy tables in the format expected by __init__ 

62 self.inputTable = Table() 

63 self.inputTable["x"] = self.field_x * u.deg 

64 self.inputTable["y"] = self.field_y * u.deg 

65 

66 self.inputTableOCS = Table() 

67 self.inputTableOCS["x"] = self.field_x * u.deg 

68 self.inputTableOCS["y"] = self.field_y * u.deg 

69 

70 # Add Zernike columns 

71 for i, noll in enumerate(self.noll_indices): 

72 self.inputTable[f"Z{noll}"] = self.values[:, i] * u.um 

73 self.inputTableOCS[f"Z{noll}"] = self.values_ocs[:, i] * u.um 

74 

75 self.inputTableOCS.meta["coord_sys"] = "OCS" 

76 # Create the calibration object 

77 self.calib = IntrinsicZernikes( 

78 table=self.inputTable, table_ocs=self.inputTableOCS 

79 ) 

80 

81 def test_initialization_with_table(self): 

82 """Test that IntrinsicZernikes initializes correctly from a table.""" 

83 np.testing.assert_array_equal(self.calib.field_x, self.field_x) 

84 np.testing.assert_array_equal(self.calib.field_y, self.field_y) 

85 np.testing.assert_array_equal(self.calib.noll_indices, self.noll_indices) 

86 np.testing.assert_array_equal(self.calib.values, self.values) 

87 self.assertIsNotNone(self.calib.interpolator) 

88 

89 def test_metadata(self): 

90 """Test that metadata is properly set.""" 

91 metadata = self.calib.getMetadata() 

92 self.assertEqual(metadata["OBSTYPE"], "INTRINSIC_ZERNIKES") 

93 self.assertEqual(metadata["INTRINSIC_ZERNIKES_SCHEMA"], "Intrinsic Zernikes") 

94 self.assertEqual(metadata["INTRINSIC_ZERNIKES_VERSION"], 1.1) 

95 

96 def test_dict_roundtrip(self): 

97 """Test round-tripping through dictionary.""" 

98 newCalib = IntrinsicZernikes.fromDict(self.calib.toDict()) 

99 self.assertEqual(newCalib, self.calib) 

100 

101 def test_table_roundtrip(self): 

102 """Test round-tripping through table.""" 

103 newCalib = IntrinsicZernikes.fromTable(self.calib.toTable()) 

104 self.assertEqual(newCalib, self.calib) 

105 

106 def test_text_io_not_implemented(self): 

107 """Test that text I/O is intentionally not implemented.""" 

108 for ext in ("yaml", "ecsv"): 

109 with self.subTest(ext=ext): 

110 filename = f"intrinsic_zernikes.{ext}" 

111 

112 with self.assertRaises(NotImplementedError): 

113 self.calib.writeText(filename) 

114 

115 with self.assertRaises(NotImplementedError): 

116 self.calib.readText(filename) 

117 

118 def test_fits_roundtrip(self): 

119 """Test round-tripping through FITS file.""" 

120 with tempfile.TemporaryDirectory() as tempdir: 

121 import os 

122 

123 filename = os.path.join(tempdir, "intrinsic_zernikes.fits") 

124 

125 self.calib.writeFits(filename) 

126 newCalib = IntrinsicZernikes.readFits(filename) 

127 self.assertEqual(newCalib, self.calib) 

128 

129 def test_fromDict_wrong_obstype(self): 

130 """Test that fromDict raises error for wrong OBSTYPE.""" 

131 outDict = self.calib.toDict() 

132 outDict["metadata"]["OBSTYPE"] = "WRONG_TYPE" 

133 

134 with self.assertRaises(RuntimeError) as context: 

135 IntrinsicZernikes.fromDict(outDict) 

136 

137 self.assertIn("Incorrect intrinsic zernikes supplied", str(context.exception)) 

138 self.assertIn("INTRINSIC_ZERNIKES", str(context.exception)) 

139 self.assertIn("WRONG_TYPE", str(context.exception)) 

140 

141 def test_getIntrinsicZernikes(self): 

142 """Test interpolation of Zernike coefficients.""" 

143 # Test at a grid point 

144 field_x_test = 0.0 

145 field_y_test = 0.0 

146 

147 zernikes = self.calib.getIntrinsicZernikes(field_x_test, field_y_test) 

148 

149 # On a grid point each interpolator returns its own stored value, so 

150 # the result is the element-wise sum of the CCS and OCS coefficients 

151 # (rotTelPos defaults to 0, so the OCS query point is unrotated). 

152 center_idx = np.flatnonzero((self.field_x == 0.0) & (self.field_y == 0.0))[0] 

153 expected = self.values[center_idx, :] + self.values_ocs[center_idx, :] 

154 self.assertFloatsEqual(zernikes, expected) 

155 

156 # Test with specific Noll indices 

157 zernikes_subset = self.calib.getIntrinsicZernikes( 

158 field_x_test, field_y_test, noll_indices=[4] 

159 ) 

160 self.assertFloatsEqual(zernikes_subset, zernikes[:, 0]) 

161 

162 def test_getIntrinsicZernikes_array(self): 

163 """Test interpolation with array inputs.""" 

164 field_x_test = np.array([0.0, 0.5]) 

165 field_y_test = np.array([0.0, 0.25]) 

166 

167 zernikes = self.calib.getIntrinsicZernikes(field_x_test, field_y_test) 

168 z0 = self.calib.getIntrinsicZernikes(field_x_test[0], field_y_test[0]) 

169 z1 = self.calib.getIntrinsicZernikes(field_x_test[1], field_y_test[1]) 

170 np.testing.assert_array_almost_equal(zernikes[[0]], z0) 

171 np.testing.assert_array_almost_equal(zernikes[[1]], z1) 

172 

173 # Should return shape (n_points, n_zernikes) 

174 self.assertEqual(zernikes.shape, (2, len(self.noll_indices))) 

175 

176 

177class MemoryTester(lsst.utils.tests.MemoryTestCase): 

178 pass 

179 

180 

181def setup_module(module): 

182 lsst.utils.tests.init() 

183 

184 

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

186 import sys 

187 

188 setup_module(sys.modules[__name__]) 

189 unittest.main()