LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
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#
22import abc
23import numpy as np
24
25from astropy.table import Table
26
27import lsst.afw.math as afwMath
28from lsst.pipe.base import Struct
29from lsst.geom import Box2I, Point2I, Extent2I
30from .applyLookupTable import applyLookupTable
31from .calibType import IsrCalib
32
33__all__ = ["Linearizer",
34 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared",
35 "LinearizeProportional", "LinearizePolynomial", "LinearizeSpline", "LinearizeNone"]
36
37
39 """Parameter set for linearization.
40
41 These parameters are included in cameraGeom.Amplifier, but
42 should be accessible externally to allow for testing.
43
44 Parameters
45 ----------
46 table : `numpy.array`, optional
47 Lookup table; a 2-dimensional array of floats:
48 - one row for each row index (value of coef[0] in the amplifier)
49 - one column for each image value
50 To avoid copying the table the last index should vary fastest
51 (numpy default "C" order)
52 detector : `lsst.afw.cameraGeom.Detector`, optional
53 Detector object. Passed to self.fromDetectorfromDetectorfromDetector() on init.
54 log : `logging.Logger`, optional
55 Logger to handle messages.
56 kwargs : `dict`, optional
57 Other keyword arguments to pass to the parent init.
58
59 Raises
60 ------
61 RuntimeError :
62 Raised if the supplied table is not 2D, or if the table has fewer
63 columns than rows (indicating that the indices are swapped).
64
65 Notes
66 -----
67 The linearizer attributes stored are:
68
69 hasLinearity : `bool`
70 Whether a linearity correction is defined for this detector.
71 override : `bool`
72 Whether the detector parameters should be overridden.
73 ampNames : `list` [`str`]
74 List of amplifier names to correct.
75 linearityCoeffs : `dict` [`str`, `numpy.array`]
76 Coefficients to use in correction. Indexed by amplifier
77 names. The format of the array depends on the type of
78 correction to apply.
79 linearityType : `dict` [`str`, `str`]
80 Type of correction to use, indexed by amplifier names.
81 linearityBBox : `dict` [`str`, `lsst.geom.Box2I`]
82 Bounding box the correction is valid over, indexed by
83 amplifier names.
84 fitParams : `dict` [`str`, `numpy.array`], optional
85 Linearity fit parameters used to construct the correction
86 coefficients, indexed as above.
87 fitParamsErr : `dict` [`str`, `numpy.array`], optional
88 Uncertainty values of the linearity fit parameters used to
89 construct the correction coefficients, indexed as above.
90 fitChiSq : `dict` [`str`, `float`], optional
91 Chi-squared value of the linearity fit, indexed as above.
92 fitResiduals : `dict` [`str`, `numpy.array`], optional
93 Residuals of the fit, indexed as above. Used for
94 calculating photdiode corrections
95 linearFit : The linear fit to the low flux region of the curve.
96 [intercept, slope].
97 tableData : `numpy.array`, optional
98 Lookup table data for the linearity correction.
99 """
100 _OBSTYPE = "LINEARIZER"
101 _SCHEMA = 'Gen3 Linearizer'
102 _VERSION = 1.1
103
104 def __init__(self, table=None, **kwargs):
105 self.hasLinearityhasLinearity = False
106 self.overrideoverride = False
107
108 self.ampNamesampNames = list()
109 self.linearityCoeffslinearityCoeffs = dict()
110 self.linearityTypelinearityType = dict()
111 self.linearityBBoxlinearityBBox = dict()
112 self.fitParamsfitParams = dict()
113 self.fitParamsErrfitParamsErr = dict()
114 self.fitChiSqfitChiSq = dict()
115 self.fitResidualsfitResiduals = dict()
116 self.linearFitlinearFit = dict()
117 self.tableDatatableData = None
118 if table is not None:
119 if len(table.shape) != 2:
120 raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
121 if table.shape[1] < table.shape[0]:
122 raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
123 self.tableDatatableData = np.array(table, order="C")
124
125 super().__init__(**kwargs)
126 self.requiredAttributesrequiredAttributesrequiredAttributesrequiredAttributes.update(['hasLinearity', 'override',
127 'ampNames',
128 'linearityCoeffs', 'linearityType', 'linearityBBox',
129 'fitParams', 'fitParamsErr', 'fitChiSq',
130 'fitResiduals', 'linearFit', 'tableData'])
131
132 def updateMetadata(self, setDate=False, **kwargs):
133 """Update metadata keywords with new values.
134
135 This calls the base class's method after ensuring the required
136 calibration keywords will be saved.
137
138 Parameters
139 ----------
140 setDate : `bool`, optional
141 Update the CALIBDATE fields in the metadata to the current
142 time. Defaults to False.
143 kwargs :
144 Other keyword parameters to set in the metadata.
145 """
146 kwargs['HAS_LINEARITY'] = self.hasLinearityhasLinearity
147 kwargs['OVERRIDE'] = self.overrideoverride
148 kwargs['HAS_TABLE'] = self.tableDatatableData is not None
149
150 super().updateMetadata(setDate=setDate, **kwargs)
151
152 def fromDetector(self, detector):
153 """Read linearity parameters from a detector.
154
155 Parameters
156 ----------
157 detector : `lsst.afw.cameraGeom.detector`
158 Input detector with parameters to use.
159
160 Returns
161 -------
162 calib : `lsst.ip.isr.Linearizer`
163 The calibration constructed from the detector.
164 """
165 self._detectorName_detectorName_detectorName = detector.getName()
166 self._detectorSerial_detectorSerial_detectorSerial = detector.getSerial()
167 self._detectorId_detectorId_detectorId = detector.getId()
168 self.hasLinearityhasLinearity = True
169
170 # Do not translate Threshold, Maximum, Units.
171 for amp in detector.getAmplifiers():
172 ampName = amp.getName()
173 self.ampNamesampNames.append(ampName)
174 self.linearityTypelinearityType[ampName] = amp.getLinearityType()
175 self.linearityCoeffslinearityCoeffs[ampName] = amp.getLinearityCoeffs()
176 self.linearityBBoxlinearityBBox[ampName] = amp.getBBox()
177
178 return self
179
180 @classmethod
181 def fromDict(cls, dictionary):
182 """Construct a calibration from a dictionary of properties
183
184 Parameters
185 ----------
186 dictionary : `dict`
187 Dictionary of properties
188
189 Returns
190 -------
191 calib : `lsst.ip.isr.Linearity`
192 Constructed calibration.
193
194 Raises
195 ------
196 RuntimeError
197 Raised if the supplied dictionary is for a different
198 calibration.
199 """
200
201 calib = cls()
202
203 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
204 raise RuntimeError(f"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
205 f"found {dictionary['metadata']['OBSTYPE']}")
206
207 calib.setMetadata(dictionary['metadata'])
208
209 calib.hasLinearity = dictionary.get('hasLinearity',
210 dictionary['metadata'].get('HAS_LINEARITY', False))
211 calib.override = dictionary.get('override', True)
212
213 if calib.hasLinearity:
214 for ampName in dictionary['amplifiers']:
215 amp = dictionary['amplifiers'][ampName]
216 calib.ampNames.append(ampName)
217 calib.linearityCoeffs[ampName] = np.array(amp.get('linearityCoeffs', [0.0]))
218 calib.linearityType[ampName] = amp.get('linearityType', 'None')
219 calib.linearityBBox[ampName] = amp.get('linearityBBox', None)
220
221 calib.fitParams[ampName] = np.array(amp.get('fitParams', [0.0]))
222 calib.fitParamsErr[ampName] = np.array(amp.get('fitParamsErr', [0.0]))
223 calib.fitChiSq[ampName] = amp.get('fitChiSq', np.nan)
224 calib.fitResiduals[ampName] = np.array(amp.get('fitResiduals', [0.0]))
225 calib.linearFit[ampName] = np.array(amp.get('linearFit', [0.0]))
226
227 calib.tableData = dictionary.get('tableData', None)
228 if calib.tableData:
229 calib.tableData = np.array(calib.tableData)
230
231 return calib
232
233 def toDict(self):
234 """Return linearity parameters as a dict.
235
236 Returns
237 -------
238 outDict : `dict`:
239 """
240 self.updateMetadataupdateMetadataupdateMetadata()
241
242 outDict = {'metadata': self.getMetadatagetMetadata(),
243 'detectorName': self._detectorName_detectorName_detectorName,
244 'detectorSerial': self._detectorSerial_detectorSerial_detectorSerial,
245 'detectorId': self._detectorId_detectorId_detectorId,
246 'hasTable': self.tableDatatableData is not None,
247 'amplifiers': dict(),
248 }
249 for ampName in self.linearityTypelinearityType:
250 outDict['amplifiers'][ampName] = {'linearityType': self.linearityTypelinearityType[ampName],
251 'linearityCoeffs': self.linearityCoeffslinearityCoeffs[ampName].tolist(),
252 'linearityBBox': self.linearityBBoxlinearityBBox[ampName],
253 'fitParams': self.fitParamsfitParams[ampName].tolist(),
254 'fitParamsErr': self.fitParamsErrfitParamsErr[ampName].tolist(),
255 'fitChiSq': self.fitChiSqfitChiSq[ampName],
256 'fitResiduals': self.fitResidualsfitResiduals[ampName].tolist(),
257 'linearFit': self.linearFitlinearFit[ampName].tolist()}
258 if self.tableDatatableData is not None:
259 outDict['tableData'] = self.tableDatatableData.tolist()
260
261 return outDict
262
263 @classmethod
264 def fromTable(cls, tableList):
265 """Read linearity from a FITS file.
266
267 This method uses the `fromDict` method to create the
268 calibration, after constructing an appropriate dictionary from
269 the input tables.
270
271 Parameters
272 ----------
273 tableList : `list` [`astropy.table.Table`]
274 afwTable read from input file name.
275
276 Returns
277 -------
279 Linearity parameters.
280
281 Notes
282 -----
283 The method reads a FITS file with 1 or 2 extensions. The metadata is
284 read from the header of extension 1, which must exist. Then the table
285 is loaded, and the ['AMPLIFIER_NAME', 'TYPE', 'COEFFS', 'BBOX_X0',
286 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to set each
287 dictionary by looping over rows.
288 Extension 2 is then attempted to read in the try block (which only
289 exists for lookup tables). It has a column named 'LOOKUP_VALUES' that
290 contains a vector of the lookup entries in each row.
291 """
292 coeffTable = tableList[0]
293
294 metadata = coeffTable.meta
295 inDict = dict()
296 inDict['metadata'] = metadata
297 inDict['hasLinearity'] = metadata.get('HAS_LINEARITY', False)
298 inDict['amplifiers'] = dict()
299
300 for record in coeffTable:
301 ampName = record['AMPLIFIER_NAME']
302
303 fitParams = record['FIT_PARAMS'] if 'FIT_PARAMS' in record.columns else np.array([0.0])
304 fitParamsErr = record['FIT_PARAMS_ERR'] if 'FIT_PARAMS_ERR' in record.columns else np.array([0.0])
305 fitChiSq = record['RED_CHI_SQ'] if 'RED_CHI_SQ' in record.columns else np.nan
306 fitResiduals = record['FIT_RES'] if 'FIT_RES' in record.columns else np.array([0.0])
307 linearFit = record['LIN_FIT'] if 'LIN_FIT' in record.columns else np.array([0.0])
308
309 inDict['amplifiers'][ampName] = {
310 'linearityType': record['TYPE'],
311 'linearityCoeffs': record['COEFFS'],
312 'linearityBBox': Box2I(Point2I(record['BBOX_X0'], record['BBOX_Y0']),
313 Extent2I(record['BBOX_DX'], record['BBOX_DY'])),
314 'fitParams': fitParams,
315 'fitParamsErr': fitParamsErr,
316 'fitChiSq': fitChiSq,
317 'fitResiduals': fitResiduals,
318 'linearFit': linearFit,
319 }
320
321 if len(tableList) > 1:
322 tableData = tableList[1]
323 inDict['tableData'] = [record['LOOKUP_VALUES'] for record in tableData]
324
325 return cls().fromDict(inDict)
326
327 def toTable(self):
328 """Construct a list of tables containing the information in this
329 calibration.
330
331 The list of tables should create an identical calibration
332 after being passed to this class's fromTable method.
333
334 Returns
335 -------
336 tableList : `list` [`astropy.table.Table`]
337 List of tables containing the linearity calibration
338 information.
339 """
340
341 tableList = []
342 self.updateMetadataupdateMetadataupdateMetadata()
343 catalog = Table([{'AMPLIFIER_NAME': ampName,
344 'TYPE': self.linearityTypelinearityType[ampName],
345 'COEFFS': self.linearityCoeffslinearityCoeffs[ampName],
346 'BBOX_X0': self.linearityBBoxlinearityBBox[ampName].getMinX(),
347 'BBOX_Y0': self.linearityBBoxlinearityBBox[ampName].getMinY(),
348 'BBOX_DX': self.linearityBBoxlinearityBBox[ampName].getWidth(),
349 'BBOX_DY': self.linearityBBoxlinearityBBox[ampName].getHeight(),
350 'FIT_PARAMS': self.fitParamsfitParams[ampName],
351 'FIT_PARAMS_ERR': self.fitParamsErrfitParamsErr[ampName],
352 'RED_CHI_SQ': self.fitChiSqfitChiSq[ampName],
353 'FIT_RES': self.fitResidualsfitResiduals[ampName],
354 'LIN_FIT': self.linearFitlinearFit[ampName],
355 } for ampName in self.ampNamesampNames])
356 catalog.meta = self.getMetadatagetMetadata().toDict()
357 tableList.append(catalog)
358
359 if self.tableDatatableData is not None:
360 catalog = Table([{'LOOKUP_VALUES': value} for value in self.tableDatatableData])
361 tableList.append(catalog)
362 return(tableList)
363
364 def getLinearityTypeByName(self, linearityTypeName):
365 """Determine the linearity class to use from the type name.
366
367 Parameters
368 ----------
369 linearityTypeName : str
370 String name of the linearity type that is needed.
371
372 Returns
373 -------
374 linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
375 The appropriate linearity class to use. If no matching class
376 is found, `None` is returned.
377 """
378 for t in [LinearizeLookupTable,
379 LinearizeSquared,
380 LinearizePolynomial,
381 LinearizeProportional,
382 LinearizeSpline,
383 LinearizeNone]:
384 if t.LinearityType == linearityTypeName:
385 return t
386 return None
387
388 def validate(self, detector=None, amplifier=None):
389 """Validate linearity for a detector/amplifier.
390
391 Parameters
392 ----------
393 detector : `lsst.afw.cameraGeom.Detector`, optional
394 Detector to validate, along with its amplifiers.
395 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
396 Single amplifier to validate.
397
398 Raises
399 ------
400 RuntimeError :
401 Raised if there is a mismatch in linearity parameters, and
402 the cameraGeom parameters are not being overridden.
403 """
404 amplifiersToCheck = []
405 if detector:
406 if self._detectorName_detectorName_detectorName != detector.getName():
407 raise RuntimeError("Detector names don't match: %s != %s" %
408 (self._detectorName_detectorName_detectorName, detector.getName()))
409 if int(self._detectorId_detectorId_detectorId) != int(detector.getId()):
410 raise RuntimeError("Detector IDs don't match: %s != %s" %
411 (int(self._detectorId_detectorId_detectorId), int(detector.getId())))
412 if self._detectorSerial_detectorSerial_detectorSerial != detector.getSerial():
413 raise RuntimeError("Detector serial numbers don't match: %s != %s" %
414 (self._detectorSerial_detectorSerial_detectorSerial, detector.getSerial()))
415 if len(detector.getAmplifiers()) != len(self.linearityCoeffslinearityCoeffs.keys()):
416 raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
417 (len(detector.getAmplifiers()),
418 len(self.linearityCoeffslinearityCoeffs.keys())))
419 amplifiersToCheck.extend(detector.getAmplifiers())
420
421 if amplifier:
422 amplifiersToCheck.extend(amplifier)
423
424 for amp in amplifiersToCheck:
425 ampName = amp.getName()
426 if ampName not in self.linearityCoeffslinearityCoeffs.keys():
427 raise RuntimeError("Amplifier %s is not in linearity data" %
428 (ampName, ))
429 if amp.getLinearityType() != self.linearityTypelinearityType[ampName]:
430 if self.overrideoverride:
431 self.loglog.warning("Overriding amplifier defined linearityType (%s) for %s",
432 self.linearityTypelinearityType[ampName], ampName)
433 else:
434 raise RuntimeError("Amplifier %s type %s does not match saved value %s" %
435 (ampName, amp.getLinearityType(), self.linearityTypelinearityType[ampName]))
436 if (amp.getLinearityCoeffs().shape != self.linearityCoeffslinearityCoeffs[ampName].shape or not
437 np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffslinearityCoeffs[ampName], equal_nan=True)):
438 if self.overrideoverride:
439 self.loglog.warning("Overriding amplifier defined linearityCoeffs (%s) for %s",
440 self.linearityCoeffslinearityCoeffs[ampName], ampName)
441 else:
442 raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" %
443 (ampName, amp.getLinearityCoeffs(), self.linearityCoeffslinearityCoeffs[ampName]))
444
445 def applyLinearity(self, image, detector=None, log=None):
446 """Apply the linearity to an image.
447
448 If the linearity parameters are populated, use those,
449 otherwise use the values from the detector.
450
451 Parameters
452 ----------
453 image : `~lsst.afw.image.image`
454 Image to correct.
455 detector : `~lsst.afw.cameraGeom.detector`
456 Detector to use for linearity parameters if not already
457 populated.
458 log : `~logging.Logger`, optional
459 Log object to use for logging.
460 """
461 if log is None:
462 log = self.loglog
463 if detector and not self.hasLinearityhasLinearity:
464 self.fromDetectorfromDetectorfromDetector(detector)
465
466 self.validatevalidatevalidate(detector)
467
468 numAmps = 0
469 numLinearized = 0
470 numOutOfRange = 0
471 for ampName in self.linearityTypelinearityType.keys():
472 linearizer = self.getLinearityTypeByNamegetLinearityTypeByName(self.linearityTypelinearityType[ampName])
473 numAmps += 1
474 if linearizer is not None:
475 ampView = image.Factory(image, self.linearityBBoxlinearityBBox[ampName])
476 success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffslinearityCoeffs[ampName],
477 'table': self.tableDatatableData,
478 'log': self.loglog})
479 numOutOfRange += outOfRange
480 if success:
481 numLinearized += 1
482 elif log is not None:
483 log.warning("Amplifier %s did not linearize.",
484 ampName)
485 return Struct(
486 numAmps=numAmps,
487 numLinearized=numLinearized,
488 numOutOfRange=numOutOfRange
489 )
490
491
492class LinearizeBase(metaclass=abc.ABCMeta):
493 """Abstract base class functor for correcting non-linearity.
494
495 Subclasses must define __call__ and set class variable
496 LinearityType to a string that will be used for linearity type in
497 the cameraGeom.Amplifier.linearityType field.
498
499 All linearity corrections should be defined in terms of an
500 additive correction, such that:
501
502 corrected_value = uncorrected_value + f(uncorrected_value)
503 """
504 LinearityType = None # linearity type, a string used for AmpInfoCatalogs
505
506 @abc.abstractmethod
507 def __call__(self, image, **kwargs):
508 """Correct non-linearity.
509
510 Parameters
511 ----------
512 image : `lsst.afw.image.Image`
513 Image to be corrected
514 kwargs : `dict`
515 Dictionary of parameter keywords:
516 ``"coeffs"``
517 Coefficient vector (`list` or `numpy.array`).
518 ``"table"``
519 Lookup table data (`numpy.array`).
520 ``"log"``
521 Logger to handle messages (`logging.Logger`).
522
523 Returns
524 -------
525 output : `bool`
526 If true, a correction was applied successfully.
527
528 Raises
529 ------
530 RuntimeError:
531 Raised if the linearity type listed in the
532 detector does not match the class type.
533 """
534 pass
535
536
538 """Correct non-linearity with a persisted lookup table.
539
540 The lookup table consists of entries such that given
541 "coefficients" c0, c1:
542
543 for each i,j of image:
544 rowInd = int(c0)
545 colInd = int(c1 + uncorrImage[i,j])
546 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
547
548 - c0: row index; used to identify which row of the table to use
549 (typically one per amplifier, though one can have multiple
550 amplifiers use the same table)
551 - c1: column index offset; added to the uncorrected image value
552 before truncation; this supports tables that can handle
553 negative image values; also, if the c1 ends with .5 then
554 the nearest index is used instead of truncating to the
555 next smaller index
556 """
557 LinearityType = "LookupTable"
558
559 def __call__(self, image, **kwargs):
560 """Correct for non-linearity.
561
562 Parameters
563 ----------
564 image : `lsst.afw.image.Image`
565 Image to be corrected
566 kwargs : `dict`
567 Dictionary of parameter keywords:
568 ``"coeffs"``
569 Columnation vector (`list` or `numpy.array`).
570 ``"table"``
571 Lookup table data (`numpy.array`).
572 ``"log"``
573 Logger to handle messages (`logging.Logger`).
574
575 Returns
576 -------
577 output : `tuple` [`bool`, `int`]
578 If true, a correction was applied successfully. The
579 integer indicates the number of pixels that were
580 uncorrectable by being out of range.
581
582 Raises
583 ------
584 RuntimeError:
585 Raised if the requested row index is out of the table
586 bounds.
587 """
588 numOutOfRange = 0
589
590 rowInd, colIndOffset = kwargs['coeffs'][0:2]
591 table = kwargs['table']
592 log = kwargs['log']
593
594 numTableRows = table.shape[0]
595 rowInd = int(rowInd)
596 if rowInd < 0 or rowInd > numTableRows:
597 raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" %
598 (rowInd, numTableRows))
599 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
600
601 numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
602
603 if numOutOfRange > 0 and log is not None:
604 log.warning("%s pixels were out of range of the linearization table",
605 numOutOfRange)
606 if numOutOfRange < image.getArray().size:
607 return True, numOutOfRange
608 else:
609 return False, numOutOfRange
610
611
613 """Correct non-linearity with a polynomial mode.
614
615 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
616
617 where c_i are the linearity coefficients for each amplifier.
618 Lower order coefficients are not included as they duplicate other
619 calibration parameters:
620 ``"k0"``
621 A coefficient multiplied by uncorrImage**0 is equivalent to
622 bias level. Irrelevant for correcting non-linearity.
623 ``"k1"``
624 A coefficient multiplied by uncorrImage**1 is proportional
625 to the gain. Not necessary for correcting non-linearity.
626 """
627 LinearityType = "Polynomial"
628
629 def __call__(self, image, **kwargs):
630 """Correct non-linearity.
631
632 Parameters
633 ----------
634 image : `lsst.afw.image.Image`
635 Image to be corrected
636 kwargs : `dict`
637 Dictionary of parameter keywords:
638 ``"coeffs"``
639 Coefficient vector (`list` or `numpy.array`).
640 If the order of the polynomial is n, this list
641 should have a length of n-1 ("k0" and "k1" are
642 not needed for the correction).
643 ``"log"``
644 Logger to handle messages (`logging.Logger`).
645
646 Returns
647 -------
648 output : `tuple` [`bool`, `int`]
649 If true, a correction was applied successfully. The
650 integer indicates the number of pixels that were
651 uncorrectable by being out of range.
652 """
653 if not np.any(np.isfinite(kwargs['coeffs'])):
654 return False, 0
655 if not np.any(kwargs['coeffs']):
656 return False, 0
657
658 ampArray = image.getArray()
659 correction = np.zeros_like(ampArray)
660 for order, coeff in enumerate(kwargs['coeffs'], start=2):
661 correction += coeff * np.power(ampArray, order)
662 ampArray += correction
663
664 return True, 0
665
666
668 """Correct non-linearity with a squared model.
669
670 corrImage = uncorrImage + c0*uncorrImage^2
671
672 where c0 is linearity coefficient 0 for each amplifier.
673 """
674 LinearityType = "Squared"
675
676 def __call__(self, image, **kwargs):
677 """Correct for non-linearity.
678
679 Parameters
680 ----------
681 image : `lsst.afw.image.Image`
682 Image to be corrected
683 kwargs : `dict`
684 Dictionary of parameter keywords:
685 ``"coeffs"``
686 Coefficient vector (`list` or `numpy.array`).
687 ``"log"``
688 Logger to handle messages (`logging.Logger`).
689
690 Returns
691 -------
692 output : `tuple` [`bool`, `int`]
693 If true, a correction was applied successfully. The
694 integer indicates the number of pixels that were
695 uncorrectable by being out of range.
696 """
697
698 sqCoeff = kwargs['coeffs'][0]
699 if sqCoeff != 0:
700 ampArr = image.getArray()
701 ampArr *= (1 + sqCoeff*ampArr)
702 return True, 0
703 else:
704 return False, 0
705
706
708 """Correct non-linearity with a spline model.
709
710 corrImage = uncorrImage - Spline(coeffs, uncorrImage)
711
712 Notes
713 -----
714
715 The spline fit calculates a correction as a function of the
716 expected linear flux term. Because of this, the correction needs
717 to be subtracted from the observed flux.
718
719 """
720 LinearityType = "Spline"
721
722 def __call__(self, image, **kwargs):
723 """Correct for non-linearity.
724
725 Parameters
726 ----------
727 image : `lsst.afw.image.Image`
728 Image to be corrected
729 kwargs : `dict`
730 Dictionary of parameter keywords:
731 ``"coeffs"``
732 Coefficient vector (`list` or `numpy.array`).
733 ``"log"``
734 Logger to handle messages (`logging.Logger`).
735
736 Returns
737 -------
738 output : `tuple` [`bool`, `int`]
739 If true, a correction was applied successfully. The
740 integer indicates the number of pixels that were
741 uncorrectable by being out of range.
742 """
743 splineCoeff = kwargs['coeffs']
744 centers, values = np.split(splineCoeff, 2)
745 interp = afwMath.makeInterpolate(centers.tolist(), values.tolist(),
746 afwMath.stringToInterpStyle("AKIMA_SPLINE"))
747
748 ampArr = image.getArray()
749 delta = interp.interpolate(ampArr.flatten())
750 ampArr -= np.array(delta).reshape(ampArr.shape)
751
752 return True, 0
753
754
756 """Do not correct non-linearity.
757 """
758 LinearityType = "Proportional"
759
760 def __call__(self, image, **kwargs):
761 """Do not 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 ``"coeffs"``
770 Coefficient vector (`list` or `numpy.array`).
771 ``"log"``
772 Logger to handle messages (`logging.Logger`).
773
774 Returns
775 -------
776 output : `tuple` [`bool`, `int`]
777 If true, a correction was applied successfully. The
778 integer indicates the number of pixels that were
779 uncorrectable by being out of range.
780 """
781 return True, 0
782
783
785 """Do not correct non-linearity.
786 """
787 LinearityType = "None"
788
789 def __call__(self, image, **kwargs):
790 """Do not correct for non-linearity.
791
792 Parameters
793 ----------
794 image : `lsst.afw.image.Image`
795 Image to be corrected
796 kwargs : `dict`
797 Dictionary of parameter keywords:
798 ``"coeffs"``
799 Coefficient vector (`list` or `numpy.array`).
800 ``"log"``
801 Logger to handle messages (`logging.Logger`).
802
803 Returns
804 -------
805 output : `tuple` [`bool`, `int`]
806 If true, a correction was applied successfully. The
807 integer indicates the number of pixels that were
808 uncorrectable by being out of range.
809 """
810 return True, 0
table::Key< int > type
Definition: Detector.cc:163
table::Key< int > to
Geometry and electronic information about raw amplifier images.
Definition: Amplifier.h:86
A representation of a detector in a mosaic camera.
Definition: Detector.h:185
A class to represent a 2-dimensional array of pixels.
Definition: Image.h:51
An integer coordinate rectangle.
Definition: Box.h:55
def validate(self, other=None)
Definition: calibType.py:598
def requiredAttributes(self, value)
Definition: calibType.py:142
def updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition: calibType.py:181
def fromDetector(self, detector)
Definition: calibType.py:496
def __call__(self, image, **kwargs)
Definition: linearize.py:507
def __call__(self, image, **kwargs)
Definition: linearize.py:559
def __call__(self, image, **kwargs)
Definition: linearize.py:789
def __call__(self, image, **kwargs)
Definition: linearize.py:629
def __call__(self, image, **kwargs)
Definition: linearize.py:760
def __call__(self, image, **kwargs)
Definition: linearize.py:722
def __call__(self, image, **kwargs)
Definition: linearize.py:676
def getLinearityTypeByName(self, linearityTypeName)
Definition: linearize.py:364
def validate(self, detector=None, amplifier=None)
Definition: linearize.py:388
def applyLinearity(self, image, detector=None, log=None)
Definition: linearize.py:445
def fromTable(cls, tableList)
Definition: linearize.py:264
def fromDetector(self, detector)
Definition: linearize.py:152
def updateMetadata(self, setDate=False, **kwargs)
Definition: linearize.py:132
def __init__(self, table=None, **kwargs)
Definition: linearize.py:104
def fromDict(cls, dictionary)
Definition: linearize.py:181
daf::base::PropertyList * list
Definition: fits.cc:913
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
Definition: Interpolate.cc:256
std::shared_ptr< Interpolate > makeInterpolate(std::vector< double > const &x, std::vector< double > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
A factory function to make Interpolate objects.
Definition: Interpolate.cc:342
Extent< int, 2 > Extent2I
Definition: Extent.h:397
Point< int, 2 > Point2I
Definition: Point.h:321