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
« 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"""
25__all__ = ["IntrinsicZernikes"]
27import numpy as np
28from astropy import units as u
29from astropy.table import Table
30from scipy.interpolate import LinearNDInterpolator
32from lsst.ip.isr import IsrCalib
35class IntrinsicZernikes(IsrCalib):
36 """Intrinsic Zernike coefficients.
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.
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.
46 Parameters
47 ----------
48 table : `astropy.table.Table`, optional
49 Source table. Must contain columns:
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``).
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 """
77 _OBSTYPE = "INTRINSIC_ZERNIKES"
78 _SCHEMA = "Intrinsic Zernikes"
79 _VERSION = 1.0
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
88 super().__init__(**kwargs)
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()
103 self.requiredAttributes.update(["field_x", "field_y", "values", "noll_indices"])
105 def _createInterpolator(self):
106 self.interpolator = LinearNDInterpolator(
107 np.column_stack((self.field_x, self.field_y)),
108 self.values
109 )
111 @classmethod
112 def fromDict(cls, dictionary):
113 """Construct an IntrinsicZernikes from dictionary of properties.
115 Parameters
116 ----------
117 dictionary : `dict`
118 Dictionary of properties.
120 Returns
121 -------
122 calib : `lsst.ip.isr.IntrinsicZernikes`
123 Constructed calibration.
125 Raises
126 ------
127 RuntimeError
128 Raised if the supplied dictionary is for a different
129 calibration type.
130 """
131 calib = cls()
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 )
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()
146 calib.updateMetadata()
147 return calib
149 def toDict(self):
150 """Return a dictionary containing the calibration properties.
152 The dictionary should be able to be round-tripped through
153 `fromDict`.
155 Returns
156 -------
157 dictionary : `dict`
158 Dictionary of properties.
159 """
160 self.updateMetadata()
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()
169 return outDict
171 @classmethod
172 def fromTable(cls, tableList):
173 """Construct calibration from a list of tables.
175 Parameters
176 ----------
177 tableList : `list` [`astropy.table.Table`]
178 List of tables to use to construct the intrinsic zernikes
179 calibration.
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
192 def toTable(self):
193 """Construct a list of tables containing the information in this
194 calibration.
196 The list of tables should be able to be round-tripped through
197 `fromTable`.
199 Returns
200 -------
201 tableList : `list` [`astropy.table.Table`]
202 List of tables containing the intrinsic zernikes calibration
203 information.
204 """
205 self.updateMetadata()
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
214 table = Table(data)
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
221 return [table]
223 def getIntrinsicZernikes(self, field_x, field_y, noll_indices=None):
224 """
225 Get the intrinsic Zernike coefficients at a given field position.
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.
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
245 point = np.array([field_x, field_y]).T
246 interpolated_values = self.interpolator(point)
248 noll_indices = np.array(noll_indices)
249 noll_mask = np.isin(self.noll_indices, noll_indices)
250 return interpolated_values[..., noll_mask]