Coverage for python/lsst/ip/isr/intrinsicZernikes.py: 23%

76 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-27 08:22 +0000

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

22Intrinsic Zernikes storage class. 

23""" 

24 

25__all__ = ["IntrinsicZernikes"] 

26 

27import numpy as np 

28from astropy import units as u 

29from astropy.table import Table 

30from scipy.interpolate import LinearNDInterpolator 

31 

32from lsst.ip.isr import IsrCalib 

33 

34 

35class IntrinsicZernikes(IsrCalib): 

36 """Intrinsic Zernike coefficients. 

37 

38 Stores Zernike wavefront-error coefficients sampled at a set of 

39 focal-plane field angles. At query time the coefficients are 

40 interpolated to an arbitrary field position. 

41 

42 Field angles are expressed in the Camera Coordinate System (CCS), also 

43 known as the Engineering Diagram Coordinate System. See 

44 `LSE-349 <https://ls.st/LSE-349>`_ for the definition. 

45 

46 Parameters 

47 ---------- 

48 table : `astropy.table.Table`, optional 

49 Source table. Must contain columns: 

50 

51 ``"x"`` 

52 Field x positions (in CCS) with angular units (e.g. ``u.deg``). 

53 ``"y"`` 

54 Field y positions (in CCS) with angular units (e.g. ``u.deg``). 

55 ``"Z{j}"`` 

56 One column per Noll index *j*, with length units 

57 (e.g. ``u.um``). 

58 

59 Attributes 

60 ---------- 

61 field_x : `numpy.ndarray` 

62 CCS x field positions in degrees for all sample points, 

63 shape ``(n_points,)``. 

64 field_y : `numpy.ndarray` 

65 CCS y field positions in degrees for all sample points, 

66 shape ``(n_points,)``. 

67 noll_indices : `numpy.ndarray` 

68 Noll indices of the stored Zernike terms, shape ``(n_zernikes,)``. 

69 values : `numpy.ndarray` 

70 Zernike coefficients in microns, shape 

71 ``(n_points, n_zernikes)``. 

72 interpolator : `scipy.interpolate.LinearNDInterpolator` or `None` 

73 Interpolator built from ``field_x``, ``field_y``, and 

74 ``values``. ``None`` until the calibration is populated. 

75 """ 

76 

77 _OBSTYPE = "INTRINSIC_ZERNIKES" 

78 _SCHEMA = "Intrinsic Zernikes" 

79 _VERSION = 1.0 

80 

81 def __init__(self, table=None, **kwargs): 

82 self.field_x = np.array([]) 

83 self.field_y = np.array([]) 

84 self.values = np.array([]) 

85 self.noll_indices = np.array([]) 

86 self.interpolator = None 

87 

88 super().__init__(**kwargs) 

89 

90 if table is not None: 

91 self.field_x = table["x"].to("deg").value 

92 self.field_y = table["y"].to("deg").value 

93 zcols = [col for col in table.colnames if col.startswith("Z")] 

94 self.noll_indices = np.array(sorted([int(col[1:]) for col in zcols])) 

95 zks = np.column_stack( 

96 [ 

97 table[col].to("um").value for col in zcols 

98 ] 

99 ) 

100 self.values = zks 

101 self._createInterpolator() 

102 

103 self.requiredAttributes.update(["field_x", "field_y", "values", "noll_indices"]) 

104 

105 def _createInterpolator(self): 

106 self.interpolator = LinearNDInterpolator( 

107 np.column_stack((self.field_x, self.field_y)), 

108 self.values 

109 ) 

110 

111 @classmethod 

112 def fromDict(cls, dictionary): 

113 """Construct an IntrinsicZernikes from dictionary of properties. 

114 

115 Parameters 

116 ---------- 

117 dictionary : `dict` 

118 Dictionary of properties. 

119 

120 Returns 

121 ------- 

122 calib : `lsst.ip.isr.IntrinsicZernikes` 

123 Constructed calibration. 

124 

125 Raises 

126 ------ 

127 RuntimeError 

128 Raised if the supplied dictionary is for a different 

129 calibration type. 

130 """ 

131 calib = cls() 

132 

133 if calib._OBSTYPE != dictionary["metadata"]["OBSTYPE"]: 

134 raise RuntimeError( 

135 f"Incorrect intrinsic zernikes supplied. " 

136 f"Expected {calib._OBSTYPE}, found {dictionary['metadata']['OBSTYPE']}" 

137 ) 

138 

139 calib.setMetadata(dictionary["metadata"]) 

140 calib.field_x = np.array(dictionary["field_x"]) 

141 calib.field_y = np.array(dictionary["field_y"]) 

142 calib.values = np.array(dictionary["values"]) 

143 calib.noll_indices = np.array(dictionary["noll_indices"]) 

144 calib._createInterpolator() 

145 

146 calib.updateMetadata() 

147 return calib 

148 

149 def toDict(self): 

150 """Return a dictionary containing the calibration properties. 

151 

152 The dictionary should be able to be round-tripped through 

153 `fromDict`. 

154 

155 Returns 

156 ------- 

157 dictionary : `dict` 

158 Dictionary of properties. 

159 """ 

160 self.updateMetadata() 

161 

162 outDict = {} 

163 outDict["metadata"] = self.getMetadata() 

164 outDict["field_x"] = self.field_x.tolist() 

165 outDict["field_y"] = self.field_y.tolist() 

166 outDict["values"] = self.values.tolist() 

167 outDict["noll_indices"] = self.noll_indices.tolist() 

168 

169 return outDict 

170 

171 @classmethod 

172 def fromTable(cls, tableList): 

173 """Construct calibration from a list of tables. 

174 

175 Parameters 

176 ---------- 

177 tableList : `list` [`astropy.table.Table`] 

178 List of tables to use to construct the intrinsic zernikes 

179 calibration. 

180 

181 Returns 

182 ------- 

183 calib : `lsst.ip.isr.IntrinsicZernikes` 

184 The calibration defined in the tables. 

185 """ 

186 table = tableList[0] 

187 calib = cls(table=table) 

188 calib.setMetadata(table.meta) 

189 calib.updateMetadata() 

190 return calib 

191 

192 def toTable(self): 

193 """Construct a list of tables containing the information in this 

194 calibration. 

195 

196 The list of tables should be able to be round-tripped through 

197 `fromTable`. 

198 

199 Returns 

200 ------- 

201 tableList : `list` [`astropy.table.Table`] 

202 List of tables containing the intrinsic zernikes calibration 

203 information. 

204 """ 

205 self.updateMetadata() 

206 

207 data = { 

208 "x": self.field_x * u.deg, 

209 "y": self.field_y * u.deg, 

210 } 

211 for i, j in enumerate(self.noll_indices): 

212 data[f"Z{j}"] = self.values[:, i] * u.um 

213 

214 table = Table(data) 

215 

216 inMeta = self.getMetadata().toDict() 

217 outMeta = {k: v for k, v in inMeta.items() if v is not None} 

218 outMeta.update({k: "" for k, v in inMeta.items() if v is None}) 

219 table.meta = outMeta 

220 

221 return [table] 

222 

223 def getIntrinsicZernikes(self, field_x, field_y, noll_indices=None): 

224 """ 

225 Get the intrinsic Zernike coefficients at a given field position. 

226 

227 Parameters 

228 ---------- 

229 field_x : `array-like` 

230 CCS x-field positions in degrees. 

231 field_y : `array-like` 

232 CCS y-field positions in degrees. 

233 noll_indices : `list` [`int`], optional 

234 List of Noll indices to return. If None, return all. 

235 

236 Returns 

237 ------- 

238 zernikes : `array-like` 

239 Array of Zernike coefficient values in microns corresponding to the 

240 requested Noll indices and field positions. 

241 """ 

242 if noll_indices is None: 

243 noll_indices = self.noll_indices 

244 

245 point = np.array([field_x, field_y]).T 

246 interpolated_values = self.interpolator(point) 

247 

248 noll_indices = np.array(noll_indices) 

249 noll_mask = np.isin(self.noll_indices, noll_indices) 

250 return interpolated_values[..., noll_mask]