Coverage for python / lsst / ip / isr / flatGradient.py: 19%
110 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:28 +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"""
22Flat gradient fit storage class.
23"""
25__all__ = ["FlatGradient"]
27from astropy.table import Table
28import numpy as np
29from scipy.interpolate import Akima1DInterpolator
31from lsst.ip.isr import IsrCalib
34class FlatGradient(IsrCalib):
35 """Flat gradient measurements.
37 Parameters
38 ----------
39 log : `logging.Logger`, optional
40 Log to write messages to. If `None` a default logger will be used.
41 **kwargs :
42 Additional parameters.
43 """
45 _OBSTYPE = "flatGradient"
46 _SCHEMA = "FlatGradient"
47 _VERSION = 1.0
49 def __init__(self, **kwargs):
51 self.radialSplineNodes = np.zeros(1)
52 self.radialSplineValues = np.zeros(1)
53 self.itlRatio = 1.0
54 self.centroidX = 0.0
55 self.centroidY = 0.0
56 self.centroidDeltaX = 0.0
57 self.centroidDeltaY = 0.0
58 self.gradientX = 0.0
59 self.gradientY = 0.0
60 self.normalizationFactor = 1.0
62 super().__init__(**kwargs)
64 self.requiredAttributes.update(
65 [
66 "radialSplineNodes",
67 "radialSplineValues",
68 "itlRatio",
69 "centroidX",
70 "centroidY",
71 "centroidDeltaX",
72 "centroidDeltaY",
73 "gradientX",
74 "gradientY",
75 "normalizationFactor",
76 ],
77 )
79 self.updateMetadata(setCalibInfo=True, setCalibId=True, **kwargs)
81 def setParameters(
82 self,
83 *,
84 radialSplineNodes,
85 radialSplineValues,
86 itlRatio=1.0,
87 centroidX=0.0,
88 centroidY=0.0,
89 centroidDeltaX=0.0,
90 centroidDeltaY=0.0,
91 gradientX=0.0,
92 gradientY=0.0,
93 normalizationFactor=1.0,
94 ):
95 """Set the parameters for the gradient model.
97 Parameters
98 ----------
99 radialSplineNodes : `np.ndarray`
100 Array of spline nodes.
101 radialSplineValues : `np.ndarray`
102 Array of spline values (same length as ``radialSplineNodes``).
103 itlRatio : `float`, optional
104 Ratio of flat for ITL detectors to E2V detectors.
105 centroidX : `float`, optional
106 X centroid of the focal plane (mm). This will be used as the
107 pivot for the gradient plane.
108 centroidY : `float`, optional
109 Y centroid of the focal plane (mm). This will be used as the
110 pivot for the gradient plane.
111 centroidDeltaX : `float`, optional
112 Centroid offset (mm). This is used in the radial function to
113 allow for mis-centering in the illumination gradient.
114 centroidDeltaY : `float`, optional
115 Centroid offset (mm). This is used in the radial function to
116 allow for mis-centering in the illumination gradient.
117 gradientX : `float`, optional
118 Slope of gradient in x direction (throughput/mm).
119 gradientY : `float`, optional
120 Slope of gradient in y direction (throughput/mm).
121 normalizationFactor : `float`, optional
122 Overall normalization factor (used to, e.g. make the
123 center of the focal plane equal to 1.0 vs. a focal-plane
124 average.
125 """
126 if len(radialSplineNodes) != len(radialSplineValues):
127 raise ValueError("The number of spline nodes and values must be equal.")
129 self.radialSplineNodes = radialSplineNodes
130 self.radialSplineValues = radialSplineValues
131 self.itlRatio = itlRatio
132 self.centroidX = centroidX
133 self.centroidY = centroidY
134 self.centroidDeltaX = centroidDeltaX
135 self.centroidDeltaY = centroidDeltaY
136 self.gradientX = gradientX
137 self.gradientY = gradientY
138 self.normalizationFactor = normalizationFactor
140 def computeRadialSplineModelXY(self, x, y):
141 """Compute the radial spline model values from x/y.
143 The spline model is a 1D Akima spline. When computed, the values
144 from the model describe the radial function of the full focal
145 plane flat-field. Dividing by this model will yield a radially
146 flattened flat-field.
148 Parameters
149 ----------
150 x : `np.ndarray`
151 Array of focal plane x values (mm).
152 y : `np.ndarray`
153 Array of focal plane y values (mm).
155 Returns
156 -------
157 splineModel : `np.ndarray`
158 Spline model values at the x/y positions.
159 """
160 centroidX = self.centroidX + self.centroidDeltaX
161 centroidY = self.centroidY + self.centroidDeltaY
163 radius = np.sqrt((x - centroidX)**2. + (y - centroidY)**2.)
165 return self.computeRadialSplineModel(radius)
167 def computeRadialSplineModel(self, radius):
168 """Compute the radial spline model values from radii.
170 The spline model is a 1D Akima spline. When computed, the values
171 from the model describe the radial function of the full focal
172 plane flat-field. Dividing by this model will yield a radially
173 flattened flat-field.
175 Parameters
176 ----------
177 radius : `np.ndarray`
178 Array of focal plane radii (mm).
180 Returns
181 -------
182 splineModel : `np.ndarray`
183 Spline model values at the radius positions.
184 """
185 spl = Akima1DInterpolator(self.radialSplineNodes, self.radialSplineValues)
187 return spl(np.clip(radius, self.radialSplineNodes[0], self.radialSplineNodes[-1]))
189 def computeGradientModel(self, x, y):
190 """Compute the gradient model values.
192 The gradient model is a plane constrained to be 1.0 at the
193 ``centroidX``, ``centroidY`` values. Dividing by this model will
194 remove the planar gradient in a flat field. Note that the planar
195 gradient pivot is always at the same position, and does not
196 move with the radial gradient centroid so as to keep the
197 model fit more stable.
199 Parameters
200 ----------
201 x : `np.ndarray`
202 Array of focal plane x values (mm).
203 y : `np.ndarray`
204 Array of focal plane y values (mm).
206 Returns
207 -------
208 gradientModel : `np.ndarray`
209 Gradient model values at the x/y positions.
210 """
211 gradient = 1 + self.gradientX*(x - self.centroidX) + self.gradientY*(y - self.centroidY)
213 return gradient
215 def computeFullModel(self, x, y, is_itl):
216 """Compute the full gradient model given x/y and itl booleans.
218 This returns the full model that can be applied directly
219 to data that was used in a fit.
221 Parameters
222 ----------
223 x : `np.ndarray`
224 Array of focal plane x values (mm).
225 y : `np.ndarray`
226 Array of focal plane y values (mm).
227 is_itl : `np.ndarray`
228 Boolean array of whether each point is from an ITL detector.
230 Returns
231 -------
232 model : `np.ndarray`
233 Model values at each position.
234 """
235 model = self.computeRadialSplineModelXY(x, y) / self.computeGradientModel(x, y)
236 model[is_itl] *= self.itlRatio
238 return model
240 @classmethod
241 def fromDict(cls, dictionary):
242 """Construct a FlatGradient from a dictionary of properties.
244 Parameters
245 ----------
246 dictionary : `dict`
247 Dictionary of properties.
249 Returns
250 -------
251 calib : `lsst.ip.isr.FlatGradient`
252 Constructed calibration.
253 """
254 calib = cls()
256 calib.setMetadata(dictionary["metadata"])
258 calib.radialSplineNodes = np.asarray(dictionary["radialSplineNodes"])
259 calib.radialSplineValues = np.asarray(dictionary["radialSplineValues"])
260 calib.itlRatio = dictionary["itlRatio"]
261 calib.centroidX = dictionary["centroidX"]
262 calib.centroidY = dictionary["centroidY"]
263 calib.centroidDeltaX = dictionary["centroidDeltaX"]
264 calib.centroidDeltaY = dictionary["centroidDeltaY"]
265 calib.gradientX = dictionary["gradientX"]
266 calib.gradientY = dictionary["gradientY"]
267 calib.normalizationFactor = dictionary["normalizationFactor"]
269 calib.updateMetadata()
270 return calib
272 def toDict(self):
273 """Return a dictionary containing the calibration properties.
275 Returns
276 -------
277 dictionary : `dict`
278 Dictionary of properties.
279 """
280 self.updateMetadata()
282 outDict = dict()
283 metadata = self.getMetadata()
284 outDict["metadata"] = metadata
286 outDict["radialSplineNodes"] = self.radialSplineNodes.tolist()
287 outDict["radialSplineValues"] = self.radialSplineValues.tolist()
288 outDict["itlRatio"] = float(self.itlRatio)
289 outDict["centroidX"] = float(self.centroidX)
290 outDict["centroidY"] = float(self.centroidY)
291 outDict["centroidDeltaX"] = float(self.centroidDeltaX)
292 outDict["centroidDeltaY"] = float(self.centroidDeltaY)
293 outDict["gradientX"] = float(self.gradientX)
294 outDict["gradientY"] = float(self.gradientY)
295 outDict["normalizationFactor"] = float(self.normalizationFactor)
297 return outDict
299 @classmethod
300 def fromTable(cls, tableList):
301 """Construct a calibration from a list of tables.
303 Parameters
304 ----------
305 tableList : `list` [`astropy.table.Table`]
306 List of table(s) to use to construct the FlatGradient.
308 Returns
309 -------
310 calib : `lsst.ip.isr.FlatGradient`
311 The calibration defined in the table(s).
312 """
313 gradientTable = tableList[0]
315 metadata = gradientTable.meta
316 inDict = dict()
317 inDict["metadata"] = metadata
318 inDict["radialSplineNodes"] = np.array(gradientTable[0]["RADIAL_SPLINE_NODES"], dtype=np.float64)
319 inDict["radialSplineValues"] = np.array(gradientTable[0]["RADIAL_SPLINE_VALUES"], dtype=np.float64)
320 inDict["itlRatio"] = float(gradientTable[0]["ITL_RATIO"][0])
321 inDict["centroidX"] = float(gradientTable[0]["CENTROID_X"][0])
322 inDict["centroidY"] = float(gradientTable[0]["CENTROID_Y"][0])
323 inDict["centroidDeltaX"] = float(gradientTable[0]["CENTROID_DELTA_X"][0])
324 inDict["centroidDeltaY"] = float(gradientTable[0]["CENTROID_DELTA_Y"][0])
325 inDict["gradientX"] = float(gradientTable[0]["GRADIENT_X"][0])
326 inDict["gradientY"] = float(gradientTable[0]["GRADIENT_Y"][0])
327 inDict["normalizationFactor"] = float(gradientTable[0]["NORMALIZATION_FACTOR"][0])
329 return cls().fromDict(inDict)
331 def toTable(self):
332 """Construct a list of table(s) containing the FlatGradient data.
334 Returns
335 -------
336 tableList : `list` [`astropy.table.Table`]
337 List of tables containing the FlatGradient information.
338 """
339 tableList = []
340 self.updateMetadata()
342 catalog = Table(
343 data=({
344 "RADIAL_SPLINE_NODES": self.radialSplineNodes,
345 "RADIAL_SPLINE_VALUES": self.radialSplineValues,
346 "ITL_RATIO": np.array([self.itlRatio]),
347 "CENTROID_X": np.array([self.centroidX]),
348 "CENTROID_Y": np.array([self.centroidY]),
349 "CENTROID_DELTA_X": np.array([self.centroidDeltaX]),
350 "CENTROID_DELTA_Y": np.array([self.centroidDeltaY]),
351 "GRADIENT_X": np.array([self.gradientX]),
352 "GRADIENT_Y": np.array([self.gradientY]),
353 "NORMALIZATION_FACTOR": np.array([self.normalizationFactor]),
354 },)
355 )
357 inMeta = self.getMetadata().toDict()
358 outMeta = {k: v for k, v in inMeta.items() if v is not None}
359 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
360 catalog.meta = outMeta
361 tableList.append(catalog)
363 return tableList