lsst.ip.isr g24cdac46ce+2390a8f09a
Loading...
Searching...
No Matches
intrinsicZernikes.py
Go to the documentation of this file.
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
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
102
103 self.requiredAttributes.update(["field_x", "field_y", "values", "noll_indices"])
104
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]
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:210
getIntrinsicZernikes(self, field_x, field_y, noll_indices=None)