23__all__ = [
"Linearizer",
24 "LinearizeBase",
"LinearizeLookupTable",
"LinearizeSquared",
25 "LinearizeProportional",
"LinearizePolynomial",
"LinearizeSpline",
"LinearizeNone"]
30from astropy.table
import Table
33from lsst.pipe.base
import Struct
34from lsst.geom import Box2I, Point2I, Extent2I
35from .applyLookupTable
import applyLookupTable
36from .calibType
import IsrCalib
40 """Parameter set for linearization.
42 These parameters are included in `lsst.afw.cameraGeom.Amplifier`, but
43 should be accessible externally to allow for testing.
47 table : `numpy.array`, optional
48 Lookup table; a 2-dimensional array of floats:
50 - one row for each row index (value of coef[0] in the amplifier)
51 - one column for each image value
53 To avoid copying the table the last index should vary fastest
54 (numpy default "C" order)
55 detector : `lsst.afw.cameraGeom.Detector`, optional
56 Detector object. Passed to self.fromDetector() on init.
57 log : `logging.Logger`, optional
58 Logger to handle messages.
59 kwargs : `dict`, optional
60 Other keyword arguments to pass to the parent init.
65 Raised if the supplied table is not 2D, or if the table has fewer
66 columns than rows (indicating that the indices are swapped).
70 The linearizer attributes stored are:
73 Whether a linearity correction is defined for this detector.
75 Whether the detector parameters should be overridden.
76 ampNames : `list` [`str`]
77 List of amplifier names to correct.
78 linearityCoeffs : `dict` [`str`, `numpy.array`]
79 Coefficients to use in correction. Indexed by amplifier
80 names. The format of the array depends on the type of
82 linearityType : `dict` [`str`, `str`]
83 Type of correction to use, indexed by amplifier names.
84 linearityBBox : `dict` [`str`, `lsst.geom.Box2I`]
85 Bounding box the correction is valid over, indexed by
87 fitParams : `dict` [`str`, `numpy.array`], optional
88 Linearity fit parameters used to construct the correction
89 coefficients, indexed as above.
90 fitParamsErr : `dict` [`str`, `numpy.array`], optional
91 Uncertainty values of the linearity fit parameters used to
92 construct the correction coefficients, indexed as above.
93 fitChiSq : `dict` [`str`, `float`], optional
94 Chi-squared value of the linearity fit, indexed as above.
95 fitResiduals : `dict` [`str`, `numpy.array`], optional
96 Residuals of the fit, indexed as above. Used for
97 calculating photdiode corrections
98 fitResidualsSigmaMad : `dict` [`str`, `float`], optional
99 Robust median-absolute-deviation of fit residuals, scaled
101 linearFit : The linear fit to the low flux region of the curve.
103 tableData : `numpy.array`, optional
104 Lookup table data for the linearity correction.
106 _OBSTYPE =
"LINEARIZER"
107 _SCHEMA =
'Gen3 Linearizer'
125 if table
is not None:
126 if len(table.shape) != 2:
127 raise RuntimeError(
"table shape = %s; must have two dimensions" % (table.shape,))
128 if table.shape[1] < table.shape[0]:
129 raise RuntimeError(
"table shape = %s; indices are switched" % (table.shape,))
130 self.
tableData = np.array(table, order=
"C")
135 'linearityCoeffs',
'linearityType',
'linearityBBox',
136 'fitParams',
'fitParamsErr',
'fitChiSq',
137 'fitResiduals',
'fitResidualsSigmaMad',
'linearFit',
'tableData'])
140 """Update metadata keywords with new values.
142 This calls the base class's method after ensuring the required
143 calibration keywords will be saved.
147 setDate : `bool`, optional
148 Update the CALIBDATE fields in the metadata to the current
149 time. Defaults to False.
151 Other keyword parameters to set in the metadata.
155 kwargs[
'HAS_TABLE'] = self.
tableData is not None
160 """Read linearity parameters from a detector.
164 detector : `lsst.afw.cameraGeom.detector`
165 Input detector with parameters to use.
169 calib : `lsst.ip.isr.Linearizer`
170 The calibration constructed from the detector.
178 for amp
in detector.getAmplifiers():
179 ampName = amp.getName()
189 """Construct a calibration from a dictionary of properties
194 Dictionary of properties
198 calib : `lsst.ip.isr.Linearity`
199 Constructed calibration.
204 Raised if the supplied dictionary is for a different
210 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
211 raise RuntimeError(f
"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
212 f
"found {dictionary['metadata']['OBSTYPE']}")
214 calib.setMetadata(dictionary[
'metadata'])
216 calib.hasLinearity = dictionary.get(
'hasLinearity',
217 dictionary[
'metadata'].get(
'HAS_LINEARITY',
False))
218 calib.override = dictionary.get(
'override',
True)
220 if calib.hasLinearity:
221 for ampName
in dictionary[
'amplifiers']:
222 amp = dictionary[
'amplifiers'][ampName]
223 calib.ampNames.append(ampName)
224 calib.linearityCoeffs[ampName] = np.array(amp.get(
'linearityCoeffs', [0.0]))
225 calib.linearityType[ampName] = amp.get(
'linearityType',
'None')
226 calib.linearityBBox[ampName] = amp.get(
'linearityBBox',
None)
228 calib.fitParams[ampName] = np.array(amp.get(
'fitParams', [0.0]))
229 calib.fitParamsErr[ampName] = np.array(amp.get(
'fitParamsErr', [0.0]))
230 calib.fitChiSq[ampName] = amp.get(
'fitChiSq', np.nan)
231 calib.fitResiduals[ampName] = np.array(amp.get(
'fitResiduals', [0.0]))
232 calib.fitResidualsSigmaMad[ampName] = np.array(amp.get(
'fitResidualsSigmaMad', np.nan))
233 calib.linearFit[ampName] = np.array(amp.get(
'linearFit', [0.0]))
235 calib.tableData = dictionary.get(
'tableData',
None)
237 calib.tableData = np.array(calib.tableData)
242 """Return linearity parameters as a dict.
255 'amplifiers': dict(),
258 outDict[
'amplifiers'][ampName] = {
'linearityType': self.
linearityType[ampName],
261 'fitParams': self.
fitParams[ampName].tolist(),
266 'linearFit': self.
linearFit[ampName].tolist()}
268 outDict[
'tableData'] = self.
tableData.tolist()
274 """Read linearity from a FITS file.
276 This method uses the `fromDict` method to create the
277 calibration, after constructing an appropriate dictionary from
282 tableList : `list` [`astropy.table.Table`]
283 afwTable read from input file name.
287 linearity : `~lsst.ip.isr.linearize.Linearizer``
288 Linearity parameters.
292 The method reads a FITS file with 1 or 2 extensions. The metadata is
293 read from the header of extension 1, which must exist. Then the table
294 is loaded, and the ['AMPLIFIER_NAME', 'TYPE', 'COEFFS', 'BBOX_X0',
295 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to set each
296 dictionary by looping over rows.
297 Extension 2 is then attempted to read in the try block (which only
298 exists for lookup tables). It has a column named 'LOOKUP_VALUES' that
299 contains a vector of the lookup entries in each row.
301 coeffTable = tableList[0]
303 metadata = coeffTable.meta
305 inDict[
'metadata'] = metadata
306 inDict[
'hasLinearity'] = metadata.get(
'HAS_LINEARITY',
False)
307 inDict[
'amplifiers'] = dict()
309 for record
in coeffTable:
310 ampName = record[
'AMPLIFIER_NAME']
312 fitParams = record[
'FIT_PARAMS']
if 'FIT_PARAMS' in record.columns
else np.array([0.0])
313 fitParamsErr = record[
'FIT_PARAMS_ERR']
if 'FIT_PARAMS_ERR' in record.columns
else np.array([0.0])
314 fitChiSq = record[
'RED_CHI_SQ']
if 'RED_CHI_SQ' in record.columns
else np.nan
315 fitResiduals = record[
'FIT_RES']
if 'FIT_RES' in record.columns
else np.array([0.0])
316 fitResidualsSigmaMad = record[
'FIT_RES_SIGMAD']
if 'FIT_RES_SIGMAD' in record.columns
else np.nan
317 linearFit = record[
'LIN_FIT']
if 'LIN_FIT' in record.columns
else np.array([0.0])
319 inDict[
'amplifiers'][ampName] = {
320 'linearityType': record[
'TYPE'],
321 'linearityCoeffs': record[
'COEFFS'],
322 'linearityBBox':
Box2I(Point2I(record[
'BBOX_X0'], record[
'BBOX_Y0']),
323 Extent2I(record[
'BBOX_DX'], record[
'BBOX_DY'])),
324 'fitParams': fitParams,
325 'fitParamsErr': fitParamsErr,
326 'fitChiSq': fitChiSq,
327 'fitResiduals': fitResiduals,
328 'fitResidualsSigmaMad': fitResidualsSigmaMad,
329 'linearFit': linearFit,
332 if len(tableList) > 1:
333 tableData = tableList[1]
334 inDict[
'tableData'] = [record[
'LOOKUP_VALUES']
for record
in tableData]
339 """Construct a list of tables containing the information in this
342 The list of tables should create an identical calibration
343 after being passed to this class's fromTable method.
347 tableList : `list` [`astropy.table.Table`]
348 List of tables containing the linearity calibration
354 catalog = Table([{
'AMPLIFIER_NAME': ampName,
363 'RED_CHI_SQ': self.
fitChiSq[ampName],
369 tableList.append(catalog)
372 catalog = Table([{
'LOOKUP_VALUES': value}
for value
in self.
tableData])
373 tableList.append(catalog)
377 """Determine the linearity class to use from the type name.
381 linearityTypeName : str
382 String name of the linearity type that is needed.
386 linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
387 The appropriate linearity class to use. If no matching class
388 is found, `None` is returned.
390 for t
in [LinearizeLookupTable,
393 LinearizeProportional,
396 if t.LinearityType == linearityTypeName:
401 """Validate linearity for a detector/amplifier.
405 detector : `lsst.afw.cameraGeom.Detector`, optional
406 Detector to validate, along with its amplifiers.
407 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
408 Single amplifier to validate.
413 Raised if there is a mismatch in linearity parameters, and
414 the cameraGeom parameters are not being overridden.
416 amplifiersToCheck = []
419 raise RuntimeError(
"Detector names don't match: %s != %s" %
422 raise RuntimeError(
"Detector IDs don't match: %s != %s" %
431 raise RuntimeError(
"Detector number of amps = %s does not match saved value %s" %
432 (len(detector.getAmplifiers()),
434 amplifiersToCheck.extend(detector.getAmplifiers())
437 amplifiersToCheck.extend(amplifier)
439 for amp
in amplifiersToCheck:
440 ampName = amp.getName()
442 raise RuntimeError(
"Amplifier %s is not in linearity data" %
446 self.
log.warning(
"Overriding amplifier defined linearityType (%s) for %s",
449 raise RuntimeError(
"Amplifier %s type %s does not match saved value %s" %
450 (ampName, amp.getLinearityType(), self.
linearityType[ampName]))
451 if (amp.getLinearityCoeffs().shape != self.
linearityCoeffs[ampName].shape
or not
452 np.allclose(amp.getLinearityCoeffs(), self.
linearityCoeffs[ampName], equal_nan=
True)):
454 self.
log.warning(
"Overriding amplifier defined linearityCoeffs (%s) for %s",
457 raise RuntimeError(
"Amplifier %s coeffs %s does not match saved value %s" %
461 """Apply the linearity to an image.
463 If the linearity parameters are populated, use those,
464 otherwise use the values from the detector.
468 image : `~lsst.afw.image.image`
470 detector : `~lsst.afw.cameraGeom.detector`
471 Detector to use for linearity parameters if not already
473 log : `~logging.Logger`, optional
474 Log object to use for logging.
489 if linearizer
is not None:
491 success, outOfRange = linearizer()(ampView, **{
'coeffs': self.
linearityCoeffs[ampName],
494 numOutOfRange += outOfRange
497 elif log
is not None:
498 log.warning(
"Amplifier %s did not linearize.",
502 numLinearized=numLinearized,
503 numOutOfRange=numOutOfRange
508 """Abstract base class functor for correcting non-linearity.
510 Subclasses must define ``__call__`` and set class variable
511 LinearityType to a string that will be used for linearity type in
512 the cameraGeom.Amplifier.linearityType field.
514 All linearity corrections should be defined in terms of an
515 additive correction, such that:
517 corrected_value = uncorrected_value + f(uncorrected_value)
523 """Correct non-linearity.
527 image : `lsst.afw.image.Image`
528 Image to be corrected
530 Dictionary of parameter keywords:
533 Coefficient vector (`list` or `numpy.array`).
535 Lookup table data (`numpy.array`).
537 Logger to handle messages (`logging.Logger`).
542 If `True`, a correction was applied successfully.
547 Raised if the linearity type listed in the
548 detector does not match the class type.
553class LinearizeLookupTable(LinearizeBase):
554 """Correct non-linearity with a persisted lookup table.
556 The lookup table consists of entries such that given
557 "coefficients" c0, c1:
559 for each i,j of image:
561 colInd = int(c1 + uncorrImage[i,j])
562 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
564 - c0: row index; used to identify which row of the table to use
565 (typically one per amplifier, though one can have multiple
566 amplifiers use the same table)
567 - c1: column index offset; added to the uncorrected image value
568 before truncation; this supports tables that can handle
569 negative image values; also, if the c1 ends with .5 then
570 the nearest index is used instead of truncating to the
573 LinearityType =
"LookupTable"
576 """Correct for non-linearity.
580 image : `lsst.afw.image.Image`
581 Image to be corrected
583 Dictionary of parameter keywords:
586 Columnation vector (`list` or `numpy.array`).
588 Lookup table data (`numpy.array`).
590 Logger to handle messages (`logging.Logger`).
594 output : `tuple` [`bool`, `int`]
595 If true, a correction was applied successfully. The
596 integer indicates the number of pixels that were
597 uncorrectable by being out of range.
602 Raised if the requested row index is out of the table
607 rowInd, colIndOffset = kwargs[
'coeffs'][0:2]
608 table = kwargs[
'table']
611 numTableRows = table.shape[0]
613 if rowInd < 0
or rowInd > numTableRows:
614 raise RuntimeError(
"LinearizeLookupTable rowInd=%s not in range[0, %s)" %
615 (rowInd, numTableRows))
616 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
618 numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
620 if numOutOfRange > 0
and log
is not None:
621 log.warning(
"%s pixels were out of range of the linearization table",
623 if numOutOfRange < image.getArray().size:
624 return True, numOutOfRange
626 return False, numOutOfRange
630 """Correct non-linearity with a polynomial mode.
634 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
636 where ``c_i`` are the linearity coefficients for each amplifier.
637 Lower order coefficients are not included as they duplicate other
638 calibration parameters:
641 A coefficient multiplied by ``uncorrImage**0`` is equivalent to
642 bias level. Irrelevant for correcting non-linearity.
644 A coefficient multiplied by ``uncorrImage**1`` is proportional
645 to the gain. Not necessary for correcting non-linearity.
647 LinearityType =
"Polynomial"
650 """Correct non-linearity.
654 image : `lsst.afw.image.Image`
655 Image to be corrected
657 Dictionary of parameter keywords:
660 Coefficient vector (`list` or `numpy.array`).
661 If the order of the polynomial is n, this list
662 should have a length of n-1 ("k0" and "k1" are
663 not needed for the correction).
665 Logger to handle messages (`logging.Logger`).
669 output : `tuple` [`bool`, `int`]
670 If true, a correction was applied successfully. The
671 integer indicates the number of pixels that were
672 uncorrectable by being out of range.
674 if not np.any(np.isfinite(kwargs[
'coeffs'])):
676 if not np.any(kwargs[
'coeffs']):
679 ampArray = image.getArray()
680 correction = np.zeros_like(ampArray)
681 for order, coeff
in enumerate(kwargs[
'coeffs'], start=2):
682 correction += coeff * np.power(ampArray, order)
683 ampArray += correction
689 """Correct non-linearity with a squared model.
691 corrImage = uncorrImage + c0*uncorrImage^2
693 where c0 is linearity coefficient 0 for each amplifier.
695 LinearityType =
"Squared"
698 """Correct for non-linearity.
702 image : `lsst.afw.image.Image`
703 Image to be corrected
705 Dictionary of parameter keywords:
708 Coefficient vector (`list` or `numpy.array`).
710 Logger to handle messages (`logging.Logger`).
714 output : `tuple` [`bool`, `int`]
715 If true, a correction was applied successfully. The
716 integer indicates the number of pixels that were
717 uncorrectable by being out of range.
720 sqCoeff = kwargs[
'coeffs'][0]
722 ampArr = image.getArray()
723 ampArr *= (1 + sqCoeff*ampArr)
730 """Correct non-linearity with a spline model.
732 corrImage = uncorrImage - Spline(coeffs, uncorrImage)
737 The spline fit calculates a correction as a function of the
738 expected linear flux term. Because of this, the correction needs
739 to be subtracted from the observed flux.
742 LinearityType =
"Spline"
745 """Correct for non-linearity.
749 image : `lsst.afw.image.Image`
750 Image to be corrected
752 Dictionary of parameter keywords:
755 Coefficient vector (`list` or `numpy.array`).
757 Logger to handle messages (`logging.Logger`).
761 output : `tuple` [`bool`, `int`]
762 If true, a correction was applied successfully. The
763 integer indicates the number of pixels that were
764 uncorrectable by being out of range.
766 splineCoeff = kwargs[
'coeffs']
767 centers, values = np.split(splineCoeff, 2)
774 if centers[0] != 0.0:
775 centers = np.concatenate(([0.0], centers))
776 values = np.concatenate(([0.0], values))
778 interp = afwMath.makeInterpolate(centers.tolist(), values.tolist(),
779 afwMath.stringToInterpStyle(
"AKIMA_SPLINE"))
781 ampArr = image.getArray()
782 delta = interp.interpolate(ampArr.flatten())
783 ampArr -= np.array(delta).reshape(ampArr.shape)
789 """Do not correct non-linearity.
791 LinearityType =
"Proportional"
794 """Do not correct for non-linearity.
798 image : `lsst.afw.image.Image`
799 Image to be corrected
801 Dictionary of parameter keywords:
804 Coefficient vector (`list` or `numpy.array`).
806 Logger to handle messages (`logging.Logger`).
810 output : `tuple` [`bool`, `int`]
811 If true, a correction was applied successfully. The
812 integer indicates the number of pixels that were
813 uncorrectable by being out of range.
819 """Do not correct non-linearity.
821 LinearityType =
"None"
824 """Do not correct for non-linearity.
828 image : `lsst.afw.image.Image`
829 Image to be corrected
831 Dictionary of parameter keywords:
834 Coefficient vector (`list` or `numpy.array`).
836 Logger to handle messages (`logging.Logger`).
840 output : `tuple` [`bool`, `int`]
841 If true, a correction was applied successfully. The
842 integer indicates the number of pixels that were
843 uncorrectable by being out of range.
validate(self, other=None)
requiredAttributes(self, value)
fromDetector(self, detector)
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
__call__(self, image, **kwargs)
__call__(self, image, **kwargs)
__call__(self, image, **kwargs)
__call__(self, image, **kwargs)
__call__(self, image, **kwargs)
__call__(self, image, **kwargs)
__call__(self, image, **kwargs)
applyLinearity(self, image, detector=None, log=None)
getLinearityTypeByName(self, linearityTypeName)
fromDetector(self, detector)
fromTable(cls, tableList)
__init__(self, table=None, **kwargs)
updateMetadata(self, setDate=False, **kwargs)
fromDict(cls, dictionary)
validate(self, detector=None, amplifier=None)