LSST Applications g063fba187b+cac8b7c890,g0f08755f38+6aee506743,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g1a2382251a+b4475c5878,g1dcb35cd9c+8f9bc1652e,g20f6ffc8e0+6aee506743,g217e2c1bcf+73dee94bd0,g28da252d5a+1f19c529b9,g2bbee38e9b+3f2625acfc,g2bc492864f+3f2625acfc,g3156d2b45e+6e55a43351,g32e5bea42b+1bb94961c2,g347aa1857d+3f2625acfc,g35bb328faa+a8ce1bb630,g3a166c0a6a+3f2625acfc,g3e281a1b8c+c5dd892a6c,g3e8969e208+a8ce1bb630,g414038480c+5927e1bc1e,g41af890bb2+8a9e676b2a,g7af13505b9+809c143d88,g80478fca09+6ef8b1810f,g82479be7b0+f568feb641,g858d7b2824+6aee506743,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,ga5288a1d22+2903d499ea,gb58c049af0+d64f4d3760,gc28159a63d+3f2625acfc,gcab2d0539d+b12535109e,gcf0d15dbbd+46a3f46ba9,gda6a2b7d83+46a3f46ba9,gdaeeff99f8+1711a396fd,ge79ae78c31+3f2625acfc,gef2f8181fd+0a71e47438,gf0baf85859+c1f95f4921,gfa517265be+6aee506743,gfa999e8aa5+17cd334064,w.2024.51
LSST Data Management Base Package
Loading...
Searching...
No Matches
linearize.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2016 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22
23__all__ = ["Linearizer",
24 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared",
25 "LinearizeProportional", "LinearizePolynomial", "LinearizeSpline", "LinearizeNone"]
26
27import abc
28import numpy as np
29from scipy.interpolate import Akima1DInterpolator
30
31from astropy.table import Table
32
33from lsst.pipe.base import Struct
34from lsst.geom import Box2I, Point2I, Extent2I
35from .applyLookupTable import applyLookupTable
36from .calibType import IsrCalib
37from .isrFunctions import isTrimmedImage
38
39
41 """Parameter set for linearization.
42
43 These parameters are included in `lsst.afw.cameraGeom.Amplifier`, but
44 should be accessible externally to allow for testing.
45
46 Parameters
47 ----------
48 table : `numpy.array`, optional
49 Lookup table; a 2-dimensional array of floats:
50
51 - one row for each row index (value of coef[0] in the amplifier)
52 - one column for each image value
53
54 To avoid copying the table the last index should vary fastest
55 (numpy default "C" order)
56 detector : `lsst.afw.cameraGeom.Detector`, optional
57 Detector object. Passed to self.fromDetector() on init.
58 log : `logging.Logger`, optional
59 Logger to handle messages.
60 kwargs : `dict`, optional
61 Other keyword arguments to pass to the parent init.
62
63 Raises
64 ------
65 RuntimeError
66 Raised if the supplied table is not 2D, or if the table has fewer
67 columns than rows (indicating that the indices are swapped).
68
69 Notes
70 -----
71 The linearizer attributes stored are:
72
73 hasLinearity : `bool`
74 Whether a linearity correction is defined for this detector.
75 override : `bool`
76 Whether the detector parameters should be overridden.
77 ampNames : `list` [`str`]
78 List of amplifier names to correct.
79 linearityCoeffs : `dict` [`str`, `numpy.array`]
80 Coefficients to use in correction. Indexed by amplifier
81 names. The format of the array depends on the type of
82 correction to apply.
83 linearityType : `dict` [`str`, `str`]
84 Type of correction to use, indexed by amplifier names.
85 linearityBBox : `dict` [`str`, `lsst.geom.Box2I`]
86 Bounding box the correction is valid over, indexed by
87 amplifier names.
88 fitParams : `dict` [`str`, `numpy.array`], optional
89 Linearity fit parameters used to construct the correction
90 coefficients, indexed as above.
91 fitParamsErr : `dict` [`str`, `numpy.array`], optional
92 Uncertainty values of the linearity fit parameters used to
93 construct the correction coefficients, indexed as above.
94 fitChiSq : `dict` [`str`, `float`], optional
95 Chi-squared value of the linearity fit, indexed as above.
96 fitResiduals : `dict` [`str`, `numpy.array`], optional
97 Residuals of the fit, indexed as above. Used for
98 calculating photdiode corrections
99 fitResidualsSigmaMad : `dict` [`str`, `float`], optional
100 Robust median-absolute-deviation of fit residuals, scaled
101 by the signal level.
102 linearFit : The linear fit to the low flux region of the curve.
103 [intercept, slope].
104 tableData : `numpy.array`, optional
105 Lookup table data for the linearity correction.
106
107 Version 1.4 adds ``linearityTurnoff`` and ``linearityMaxSignal``.
108 """
109 _OBSTYPE = "LINEARIZER"
110 _SCHEMA = 'Gen3 Linearizer'
111 _VERSION = 1.4
112
113 def __init__(self, table=None, **kwargs):
114 self.hasLinearity = False
115 self.override = False
116
117 self.ampNames = list()
118 self.linearityCoeffs = dict()
119 self.linearityType = dict()
120 self.linearityBBox = dict()
121 self.fitParams = dict()
122 self.fitParamsErr = dict()
123 self.fitChiSq = dict()
124 self.fitResiduals = dict()
126 self.linearFit = dict()
127 self.linearityTurnoff = dict()
128 self.linearityMaxSignal = dict()
129 self.tableData = None
130 if table is not None:
131 if len(table.shape) != 2:
132 raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
133 if table.shape[1] < table.shape[0]:
134 raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
135 self.tableData = np.array(table, order="C")
136
137 # The linearizer is always natively in adu because it
138 # is computed prior to computing gains.
139 self.linearityUnits = 'adu'
140
141 super().__init__(**kwargs)
142 self.requiredAttributesrequiredAttributesrequiredAttributes.update(['hasLinearity', 'override',
143 'ampNames',
144 'linearityCoeffs', 'linearityType', 'linearityBBox',
145 'fitParams', 'fitParamsErr', 'fitChiSq',
146 'fitResiduals', 'fitResidualsSigmaMad', 'linearFit', 'tableData',
147 'units', 'linearityTurnoff', 'linearityMaxSignal'])
148
149 def updateMetadata(self, setDate=False, **kwargs):
150 """Update metadata keywords with new values.
151
152 This calls the base class's method after ensuring the required
153 calibration keywords will be saved.
154
155 Parameters
156 ----------
157 setDate : `bool`, optional
158 Update the CALIBDATE fields in the metadata to the current
159 time. Defaults to False.
160 kwargs :
161 Other keyword parameters to set in the metadata.
162 """
163 kwargs['HAS_LINEARITY'] = self.hasLinearity
164 kwargs['OVERRIDE'] = self.override
165 kwargs['HAS_TABLE'] = self.tableData is not None
166 kwargs['LINEARITY_UNITS'] = self.linearityUnits
167
168 super().updateMetadata(setDate=setDate, **kwargs)
169
170 def fromDetector(self, detector):
171 """Read linearity parameters from a detector.
172
173 Parameters
174 ----------
175 detector : `lsst.afw.cameraGeom.detector`
176 Input detector with parameters to use.
177
178 Returns
179 -------
180 calib : `lsst.ip.isr.Linearizer`
181 The calibration constructed from the detector.
182 """
183 self._detectorName_detectorName = detector.getName()
184 self._detectorSerial_detectorSerial = detector.getSerial()
185 self._detectorId_detectorId = detector.getId()
186 self.hasLinearity = True
187
188 # Do not translate Threshold, Maximum, Units.
189 for amp in detector.getAmplifiers():
190 ampName = amp.getName()
191 self.ampNames.append(ampName)
192 self.linearityType[ampName] = amp.getLinearityType()
193 self.linearityCoeffs[ampName] = amp.getLinearityCoeffs()
194 self.linearityBBox[ampName] = amp.getBBox()
195
196 # Detector linearizers (legacy) are assumed to be adu units.
197 self.linearityUnits = 'adu'
198
199 return self
200
201 @classmethod
202 def fromDict(cls, dictionary):
203 """Construct a calibration from a dictionary of properties
204
205 Parameters
206 ----------
207 dictionary : `dict`
208 Dictionary of properties
209
210 Returns
211 -------
212 calib : `lsst.ip.isr.Linearity`
213 Constructed calibration.
214
215 Raises
216 ------
217 RuntimeError
218 Raised if the supplied dictionary is for a different
219 calibration.
220 """
221
222 calib = cls()
223
224 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
225 raise RuntimeError(f"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
226 f"found {dictionary['metadata']['OBSTYPE']}")
227
228 calib.setMetadata(dictionary['metadata'])
229
230 calib.hasLinearity = dictionary.get('hasLinearity',
231 dictionary['metadata'].get('HAS_LINEARITY', False))
232 calib.override = dictionary.get('override', True)
233
234 # Old linearizers which do not have linearityUnits are
235 # assumed to be adu because that's all that has been
236 # supported.
237 calib.linearityUnits = dictionary.get('linearityUnits', 'adu')
238
239 if calib.hasLinearity:
240 for ampName in dictionary['amplifiers']:
241 amp = dictionary['amplifiers'][ampName]
242 calib.ampNames.append(ampName)
243 calib.linearityCoeffs[ampName] = np.array(amp.get('linearityCoeffs', [0.0]))
244 calib.linearityType[ampName] = amp.get('linearityType', 'None')
245 calib.linearityBBox[ampName] = amp.get('linearityBBox', None)
246
247 calib.fitParams[ampName] = np.array(amp.get('fitParams', [0.0]))
248 calib.fitParamsErr[ampName] = np.array(amp.get('fitParamsErr', [0.0]))
249 calib.fitChiSq[ampName] = amp.get('fitChiSq', np.nan)
250 calib.fitResiduals[ampName] = np.array(amp.get('fitResiduals', [0.0]))
251 calib.fitResidualsSigmaMad[ampName] = np.array(amp.get('fitResidualsSigmaMad', np.nan))
252 calib.linearFit[ampName] = np.array(amp.get('linearFit', [0.0]))
253
254 calib.linearityTurnoff[ampName] = np.array(amp.get('linearityTurnoff', np.nan))
255 calib.linearityMaxSignal[ampName] = np.array(amp.get('linearityMaxSignal', np.nan))
256
257 calib.tableData = dictionary.get('tableData', None)
258 if calib.tableData:
259 calib.tableData = np.array(calib.tableData)
260
261 return calib
262
263 def toDict(self):
264 """Return linearity parameters as a dict.
265
266 Returns
267 -------
268 outDict : `dict`:
269 """
271
272 outDict = {'metadata': self.getMetadata(),
273 'detectorName': self._detectorName_detectorName,
274 'detectorSerial': self._detectorSerial_detectorSerial,
275 'detectorId': self._detectorId_detectorId,
276 'hasTable': self.tableData is not None,
277 'amplifiers': dict(),
278 'linearityUnits': self.linearityUnits,
279 }
280 for ampName in self.linearityType:
281 outDict['amplifiers'][ampName] = {
282 'linearityType': self.linearityType[ampName],
283 'linearityCoeffs': self.linearityCoeffs[ampName].tolist(),
284 'linearityBBox': self.linearityBBox[ampName],
285 'fitParams': self.fitParams[ampName].tolist(),
286 'fitParamsErr': self.fitParamsErr[ampName].tolist(),
287 'fitChiSq': self.fitChiSq[ampName],
288 'fitResiduals': self.fitResiduals[ampName].tolist(),
289 'fitResidualsSigmaMad': self.fitResiduals[ampName],
290 'linearFit': self.linearFit[ampName].tolist(),
291 'linearityTurnoff': self.linearityTurnoff[ampName],
292 'linearityMaxSignal': self.linearityMaxSignal[ampName],
293 }
294 if self.tableData is not None:
295 outDict['tableData'] = self.tableData.tolist()
296
297 return outDict
298
299 @classmethod
300 def fromTable(cls, tableList):
301 """Read linearity from a FITS file.
302
303 This method uses the `fromDict` method to create the
304 calibration, after constructing an appropriate dictionary from
305 the input tables.
306
307 Parameters
308 ----------
309 tableList : `list` [`astropy.table.Table`]
310 afwTable read from input file name.
311
312 Returns
313 -------
314 linearity : `~lsst.ip.isr.linearize.Linearizer``
315 Linearity parameters.
316
317 Notes
318 -----
319 The method reads a FITS file with 1 or 2 extensions. The metadata is
320 read from the header of extension 1, which must exist. Then the table
321 is loaded, and the ['AMPLIFIER_NAME', 'TYPE', 'COEFFS', 'BBOX_X0',
322 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to set each
323 dictionary by looping over rows.
324 Extension 2 is then attempted to read in the try block (which only
325 exists for lookup tables). It has a column named 'LOOKUP_VALUES' that
326 contains a vector of the lookup entries in each row.
327 """
328 coeffTable = tableList[0]
329
330 metadata = coeffTable.meta
331 inDict = dict()
332 inDict['metadata'] = metadata
333 inDict['hasLinearity'] = metadata.get('HAS_LINEARITY', False)
334 inDict['amplifiers'] = dict()
335 inDict['linearityUnits'] = metadata.get('LINEARITY_UNITS', 'adu')
336
337 for record in coeffTable:
338 ampName = record['AMPLIFIER_NAME']
339
340 fitParams = record['FIT_PARAMS'] if 'FIT_PARAMS' in record.columns else np.array([0.0])
341 fitParamsErr = record['FIT_PARAMS_ERR'] if 'FIT_PARAMS_ERR' in record.columns else np.array([0.0])
342 fitChiSq = record['RED_CHI_SQ'] if 'RED_CHI_SQ' in record.columns else np.nan
343 fitResiduals = record['FIT_RES'] if 'FIT_RES' in record.columns else np.array([0.0])
344 fitResidualsSigmaMad = record['FIT_RES_SIGMAD'] if 'FIT_RES_SIGMAD' in record.columns else np.nan
345 linearFit = record['LIN_FIT'] if 'LIN_FIT' in record.columns else np.array([0.0])
346
347 linearityTurnoff = record['LINEARITY_TURNOFF'] if 'LINEARITY_TURNOFF' in record.columns \
348 else np.nan
349 linearityMaxSignal = record['LINEARITY_MAX_SIGNAL'] if 'LINEARITY_MAX_SIGNAL' in record.columns \
350 else np.nan
351
352 inDict['amplifiers'][ampName] = {
353 'linearityType': record['TYPE'],
354 'linearityCoeffs': record['COEFFS'],
355 'linearityBBox': Box2I(Point2I(record['BBOX_X0'], record['BBOX_Y0']),
356 Extent2I(record['BBOX_DX'], record['BBOX_DY'])),
357 'fitParams': fitParams,
358 'fitParamsErr': fitParamsErr,
359 'fitChiSq': fitChiSq,
360 'fitResiduals': fitResiduals,
361 'fitResidualsSigmaMad': fitResidualsSigmaMad,
362 'linearFit': linearFit,
363 'linearityTurnoff': linearityTurnoff,
364 'linearityMaxSignal': linearityMaxSignal,
365 }
366
367 if len(tableList) > 1:
368 tableData = tableList[1]
369 inDict['tableData'] = [record['LOOKUP_VALUES'] for record in tableData]
370
371 return cls().fromDict(inDict)
372
373 def toTable(self):
374 """Construct a list of tables containing the information in this
375 calibration.
376
377 The list of tables should create an identical calibration
378 after being passed to this class's fromTable method.
379
380 Returns
381 -------
382 tableList : `list` [`astropy.table.Table`]
383 List of tables containing the linearity calibration
384 information.
385 """
386
387 tableList = []
389 catalog = Table([{'AMPLIFIER_NAME': ampName,
390 'TYPE': self.linearityType[ampName],
391 'COEFFS': self.linearityCoeffs[ampName],
392 'BBOX_X0': self.linearityBBox[ampName].getMinX(),
393 'BBOX_Y0': self.linearityBBox[ampName].getMinY(),
394 'BBOX_DX': self.linearityBBox[ampName].getWidth(),
395 'BBOX_DY': self.linearityBBox[ampName].getHeight(),
396 'FIT_PARAMS': self.fitParams[ampName],
397 'FIT_PARAMS_ERR': self.fitParamsErr[ampName],
398 'RED_CHI_SQ': self.fitChiSq[ampName],
399 'FIT_RES': self.fitResiduals[ampName],
400 'FIT_RES_SIGMAD': self.fitResidualsSigmaMad[ampName],
401 'LIN_FIT': self.linearFit[ampName],
402 'LINEARITY_TURNOFF': self.linearityTurnoff[ampName],
403 'LINEARITY_MAX_SIGNAL': self.linearityMaxSignal[ampName],
404 } for ampName in self.ampNames])
405 catalog.meta = self.getMetadata().toDict()
406 tableList.append(catalog)
407
408 if self.tableData is not None:
409 catalog = Table([{'LOOKUP_VALUES': value} for value in self.tableData])
410 tableList.append(catalog)
411 return tableList
412
413 def getLinearityTypeByName(self, linearityTypeName):
414 """Determine the linearity class to use from the type name.
415
416 Parameters
417 ----------
418 linearityTypeName : str
419 String name of the linearity type that is needed.
420
421 Returns
422 -------
423 linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
424 The appropriate linearity class to use. If no matching class
425 is found, `None` is returned.
426 """
427 for t in [LinearizeLookupTable,
428 LinearizeSquared,
429 LinearizePolynomial,
430 LinearizeProportional,
431 LinearizeSpline,
432 LinearizeNone]:
433 if t.LinearityType == linearityTypeName:
434 return t
435 return None
436
437 def validate(self, detector=None, amplifier=None):
438 """Validate linearity for a detector/amplifier.
439
440 Parameters
441 ----------
442 detector : `lsst.afw.cameraGeom.Detector`, optional
443 Detector to validate, along with its amplifiers.
444 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
445 Single amplifier to validate.
446
447 Raises
448 ------
449 RuntimeError
450 Raised if there is a mismatch in linearity parameters, and
451 the cameraGeom parameters are not being overridden.
452 """
453 amplifiersToCheck = []
454 if detector:
455 if self._detectorName_detectorName != detector.getName():
456 raise RuntimeError("Detector names don't match: %s != %s" %
457 (self._detectorName_detectorName, detector.getName()))
458 if int(self._detectorId_detectorId) != int(detector.getId()):
459 raise RuntimeError("Detector IDs don't match: %s != %s" %
460 (int(self._detectorId_detectorId), int(detector.getId())))
461 # TODO: DM-38778: This check fails on LATISS due to an
462 # error in the camera configuration.
463 # if self._detectorSerial != detector.getSerial():
464 # raise RuntimeError(
465 # "Detector serial numbers don't match: %s != %s" %
466 # (self._detectorSerial, detector.getSerial()))
467 if len(detector.getAmplifiers()) != len(self.linearityCoeffs.keys()):
468 raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
469 (len(detector.getAmplifiers()),
470 len(self.linearityCoeffs.keys())))
471 amplifiersToCheck.extend(detector.getAmplifiers())
472
473 if amplifier:
474 amplifiersToCheck.extend(amplifier)
475
476 for amp in amplifiersToCheck:
477 ampName = amp.getName()
478 if ampName not in self.linearityCoeffs.keys():
479 raise RuntimeError("Amplifier %s is not in linearity data" %
480 (ampName, ))
481 if amp.getLinearityType() != self.linearityType[ampName]:
482 if self.override:
483 self.loglog.debug("Overriding amplifier defined linearityType (%s) for %s",
484 self.linearityType[ampName], ampName)
485 else:
486 raise RuntimeError("Amplifier %s type %s does not match saved value %s" %
487 (ampName, amp.getLinearityType(), self.linearityType[ampName]))
488 if (amp.getLinearityCoeffs().shape != self.linearityCoeffs[ampName].shape or not
489 np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffs[ampName], equal_nan=True)):
490 if self.override:
491 self.loglog.debug("Overriding amplifier defined linearityCoeffs (%s) for %s",
492 self.linearityCoeffs[ampName], ampName)
493 else:
494 raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" %
495 (ampName, amp.getLinearityCoeffs(), self.linearityCoeffs[ampName]))
496
497 def applyLinearity(self, image, detector=None, log=None, gains=None):
498 """Apply the linearity to an image.
499
500 If the linearity parameters are populated, use those,
501 otherwise use the values from the detector.
502
503 Parameters
504 ----------
505 image : `~lsst.afw.image.image`
506 Image to correct.
507 detector : `~lsst.afw.cameraGeom.detector`, optional
508 Detector to use to determine exposure trimmed state. If
509 supplied, but no other linearity information is provided
510 by the calibration, then the static solution stored in the
511 detector will be used.
512 log : `~logging.Logger`, optional
513 Log object to use for logging.
514 gains : `dict` [`str`, `float`], optional
515 Dictionary of amp name to gain. If this is provided then
516 linearity terms will be converted from adu to electrons.
517 Only used for Spline linearity corrections.
518 """
519 if log is None:
520 log = self.loglog
521 if detector and not self.hasLinearity:
522 self.fromDetectorfromDetector(detector)
523
524 self.validatevalidate(detector)
525
526 # Check if the image is trimmed.
527 isTrimmed = None
528 if detector:
529 isTrimmed = isTrimmedImage(image, detector)
530
531 numAmps = 0
532 numLinearized = 0
533 numOutOfRange = 0
534 for ampName in self.linearityType.keys():
535 linearizer = self.getLinearityTypeByName(self.linearityType[ampName])
536 numAmps += 1
537
538 if gains and self.linearityUnits == 'adu':
539 gainValue = gains[ampName]
540 else:
541 gainValue = 1.0
542
543 if linearizer is not None:
544 match isTrimmed:
545 case True:
546 bbox = detector[ampName].getBBox()
547 case False:
548 bbox = detector[ampName].getRawBBox()
549 case None:
550 bbox = self.linearityBBox[ampName]
551
552 ampView = image.Factory(image, bbox)
553 success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffs[ampName],
554 'table': self.tableData,
555 'log': self.loglog,
556 'gain': gainValue})
557 numOutOfRange += outOfRange
558 if success:
559 numLinearized += 1
560 elif log is not None:
561 log.warning("Amplifier %s did not linearize.",
562 ampName)
563 return Struct(
564 numAmps=numAmps,
565 numLinearized=numLinearized,
566 numOutOfRange=numOutOfRange
567 )
568
569
570class LinearizeBase(metaclass=abc.ABCMeta):
571 """Abstract base class functor for correcting non-linearity.
572
573 Subclasses must define ``__call__`` and set class variable
574 LinearityType to a string that will be used for linearity type in
575 the cameraGeom.Amplifier.linearityType field.
576
577 All linearity corrections should be defined in terms of an
578 additive correction, such that:
579
580 corrected_value = uncorrected_value + f(uncorrected_value)
581 """
582 LinearityType = None # linearity type, a string used for AmpInfoCatalogs
583
584 @abc.abstractmethod
585 def __call__(self, image, **kwargs):
586 """Correct non-linearity.
587
588 Parameters
589 ----------
590 image : `lsst.afw.image.Image`
591 Image to be corrected
592 kwargs : `dict`
593 Dictionary of parameter keywords:
594
595 ``coeffs``
596 Coefficient vector (`list` or `numpy.array`).
597 ``table``
598 Lookup table data (`numpy.array`).
599 ``log``
600 Logger to handle messages (`logging.Logger`).
601
602 Returns
603 -------
604 output : `bool`
605 If `True`, a correction was applied successfully.
606
607 Raises
608 ------
609 RuntimeError:
610 Raised if the linearity type listed in the
611 detector does not match the class type.
612 """
613 pass
614
615
616class LinearizeLookupTable(LinearizeBase):
617 """Correct non-linearity with a persisted lookup table.
618
619 The lookup table consists of entries such that given
620 "coefficients" c0, c1:
621
622 for each i,j of image:
623 rowInd = int(c0)
624 colInd = int(c1 + uncorrImage[i,j])
625 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
626
627 - c0: row index; used to identify which row of the table to use
628 (typically one per amplifier, though one can have multiple
629 amplifiers use the same table)
630 - c1: column index offset; added to the uncorrected image value
631 before truncation; this supports tables that can handle
632 negative image values; also, if the c1 ends with .5 then
633 the nearest index is used instead of truncating to the
634 next smaller index
635 """
636 LinearityType = "LookupTable"
637
638 def __call__(self, image, **kwargs):
639 """Correct for non-linearity.
640
641 Parameters
642 ----------
643 image : `lsst.afw.image.Image`
644 Image to be corrected
645 kwargs : `dict`
646 Dictionary of parameter keywords:
647
648 ``coeffs``
649 Columnation vector (`list` or `numpy.array`).
650 ``table``
651 Lookup table data (`numpy.array`).
652 ``log``
653 Logger to handle messages (`logging.Logger`).
654
655 Returns
656 -------
657 output : `tuple` [`bool`, `int`]
658 If true, a correction was applied successfully. The
659 integer indicates the number of pixels that were
660 uncorrectable by being out of range.
661
662 Raises
663 ------
664 RuntimeError:
665 Raised if the requested row index is out of the table
666 bounds.
667 """
668 numOutOfRange = 0
669
670 rowInd, colIndOffset = kwargs['coeffs'][0:2]
671 table = kwargs['table']
672 log = kwargs['log']
673
674 numTableRows = table.shape[0]
675 rowInd = int(rowInd)
676 if rowInd < 0 or rowInd > numTableRows:
677 raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" %
678 (rowInd, numTableRows))
679 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
680
681 numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
682
683 if numOutOfRange > 0 and log is not None:
684 log.warning("%s pixels were out of range of the linearization table",
685 numOutOfRange)
686 if numOutOfRange < image.getArray().size:
687 return True, numOutOfRange
688 else:
689 return False, numOutOfRange
690
691
693 """Correct non-linearity with a polynomial mode.
694
695 .. code-block::
696
697 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
698
699 where ``c_i`` are the linearity coefficients for each amplifier.
700 Lower order coefficients are not included as they duplicate other
701 calibration parameters:
702
703 ``k0``
704 A coefficient multiplied by ``uncorrImage**0`` is equivalent to
705 bias level. Irrelevant for correcting non-linearity.
706 ``k1``
707 A coefficient multiplied by ``uncorrImage**1`` is proportional
708 to the gain. Not necessary for correcting non-linearity.
709 """
710 LinearityType = "Polynomial"
711
712 def __call__(self, image, **kwargs):
713 """Correct non-linearity.
714
715 Parameters
716 ----------
717 image : `lsst.afw.image.Image`
718 Image to be corrected
719 kwargs : `dict`
720 Dictionary of parameter keywords:
721
722 ``coeffs``
723 Coefficient vector (`list` or `numpy.array`).
724 If the order of the polynomial is n, this list
725 should have a length of n-1 ("k0" and "k1" are
726 not needed for the correction).
727 ``log``
728 Logger to handle messages (`logging.Logger`).
729
730 Returns
731 -------
732 output : `tuple` [`bool`, `int`]
733 If true, a correction was applied successfully. The
734 integer indicates the number of pixels that were
735 uncorrectable by being out of range.
736 """
737 if not np.any(np.isfinite(kwargs['coeffs'])):
738 return False, 0
739 if not np.any(kwargs['coeffs']):
740 return False, 0
741
742 ampArray = image.getArray()
743 correction = np.zeros_like(ampArray)
744 for order, coeff in enumerate(kwargs['coeffs'], start=2):
745 correction += coeff * np.power(ampArray, order)
746 ampArray += correction
747
748 return True, 0
749
750
752 """Correct non-linearity with a squared model.
753
754 corrImage = uncorrImage + c0*uncorrImage^2
755
756 where c0 is linearity coefficient 0 for each amplifier.
757 """
758 LinearityType = "Squared"
759
760 def __call__(self, image, **kwargs):
761 """Correct for non-linearity.
762
763 Parameters
764 ----------
765 image : `lsst.afw.image.Image`
766 Image to be corrected
767 kwargs : `dict`
768 Dictionary of parameter keywords:
769
770 ``coeffs``
771 Coefficient vector (`list` or `numpy.array`).
772 ``log``
773 Logger to handle messages (`logging.Logger`).
774
775 Returns
776 -------
777 output : `tuple` [`bool`, `int`]
778 If true, a correction was applied successfully. The
779 integer indicates the number of pixels that were
780 uncorrectable by being out of range.
781 """
782
783 sqCoeff = kwargs['coeffs'][0]
784 if sqCoeff != 0:
785 ampArr = image.getArray()
786 ampArr *= (1 + sqCoeff*ampArr)
787 return True, 0
788 else:
789 return False, 0
790
791
793 """Correct non-linearity with a spline model.
794
795 corrImage = uncorrImage - Spline(coeffs, uncorrImage)
796
797 Notes
798 -----
799
800 The spline fit calculates a correction as a function of the
801 expected linear flux term. Because of this, the correction needs
802 to be subtracted from the observed flux.
803
804 """
805 LinearityType = "Spline"
806
807 def __call__(self, image, **kwargs):
808 """Correct for non-linearity.
809
810 Parameters
811 ----------
812 image : `lsst.afw.image.Image`
813 Image to be corrected
814 kwargs : `dict`
815 Dictionary of parameter keywords:
816
817 ``coeffs``
818 Coefficient vector (`list` or `numpy.array`).
819 ``log``
820 Logger to handle messages (`logging.Logger`).
821 ``gain``
822 Gain value to apply.
823
824 Returns
825 -------
826 output : `tuple` [`bool`, `int`]
827 If true, a correction was applied successfully. The
828 integer indicates the number of pixels that were
829 uncorrectable by being out of range.
830 """
831 splineCoeff = kwargs['coeffs']
832 gain = kwargs.get('gain', 1.0)
833 centers, values = np.split(splineCoeff, 2)
834 values = values*gain
835 # If the spline is not anchored at zero, remove the offset
836 # found at the lowest flux available, and add an anchor at
837 # flux=0.0 if there's no entry at that point.
838 if values[0] != 0:
839 offset = values[0]
840 values -= offset
841 if centers[0] != 0.0:
842 centers = np.concatenate(([0.0], centers))
843 values = np.concatenate(([0.0], values))
844
845 ampArr = image.array
846
847 if np.any(~np.isfinite(values)):
848 # This cannot be used; turns everything into nans.
849 ampArr[:] = np.nan
850 else:
851 interp = Akima1DInterpolator(centers, values, method="akima")
852 # Clip to avoid extrapolation and hitting the top end.
853 ampArr -= interp(np.clip(ampArr, centers[0], centers[-1] - 0.1))
854
855 return True, 0
856
857
859 """Do not correct non-linearity.
860 """
861 LinearityType = "Proportional"
862
863 def __call__(self, image, **kwargs):
864 """Do not correct for non-linearity.
865
866 Parameters
867 ----------
868 image : `lsst.afw.image.Image`
869 Image to be corrected
870 kwargs : `dict`
871 Dictionary of parameter keywords:
872
873 ``coeffs``
874 Coefficient vector (`list` or `numpy.array`).
875 ``log``
876 Logger to handle messages (`logging.Logger`).
877
878 Returns
879 -------
880 output : `tuple` [`bool`, `int`]
881 If true, a correction was applied successfully. The
882 integer indicates the number of pixels that were
883 uncorrectable by being out of range.
884 """
885 return True, 0
886
887
889 """Do not correct non-linearity.
890 """
891 LinearityType = "None"
892
893 def __call__(self, image, **kwargs):
894 """Do not correct for non-linearity.
895
896 Parameters
897 ----------
898 image : `lsst.afw.image.Image`
899 Image to be corrected
900 kwargs : `dict`
901 Dictionary of parameter keywords:
902
903 ``coeffs``
904 Coefficient vector (`list` or `numpy.array`).
905 ``log``
906 Logger to handle messages (`logging.Logger`).
907
908 Returns
909 -------
910 output : `tuple` [`bool`, `int`]
911 If true, a correction was applied successfully. The
912 integer indicates the number of pixels that were
913 uncorrectable by being out of range.
914 """
915 return True, 0
An integer coordinate rectangle.
Definition Box.h:55
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:207
__call__(self, image, **kwargs)
Definition linearize.py:585
__call__(self, image, **kwargs)
Definition linearize.py:893
getLinearityTypeByName(self, linearityTypeName)
Definition linearize.py:413
__init__(self, table=None, **kwargs)
Definition linearize.py:113
updateMetadata(self, setDate=False, **kwargs)
Definition linearize.py:149
applyLinearity(self, image, detector=None, log=None, gains=None)
Definition linearize.py:497
validate(self, detector=None, amplifier=None)
Definition linearize.py:437