25from astropy.table
import Table
28from lsst.pipe.base
import Struct
29from lsst.geom import Box2I, Point2I, Extent2I
30from .applyLookupTable
import applyLookupTable
31from .calibType
import IsrCalib
33__all__ = [
"Linearizer",
34 "LinearizeBase",
"LinearizeLookupTable",
"LinearizeSquared",
35 "LinearizeProportional",
"LinearizePolynomial",
"LinearizeSpline",
"LinearizeNone"]
39 """Parameter set for linearization.
41 These parameters are included in cameraGeom.Amplifier, but
42 should be accessible externally to allow
for testing.
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)
54 log : `logging.Logger`, optional
55 Logger to handle messages.
56 kwargs : `dict`, optional
57 Other keyword arguments to
pass to the parent init.
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).
67 The linearizer attributes stored are:
70 Whether a linearity correction
is defined
for this detector.
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
79 linearityType : `dict` [`str`, `str`]
80 Type of correction to use, indexed by amplifier names.
82 Bounding box the correction
is valid over, indexed by
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 tableData : `numpy.array`, optional
93 Lookup table data
for the linearity correction.
95 _OBSTYPE = "LINEARIZER"
96 _SCHEMA =
'Gen3 Linearizer'
113 if table
is not None:
114 if len(table.shape) != 2:
115 raise RuntimeError(
"table shape = %s; must have two dimensions" % (table.shape,))
116 if table.shape[1] < table.shape[0]:
117 raise RuntimeError(
"table shape = %s; indices are switched" % (table.shape,))
118 self.
tableDatatableData = np.array(table, order=
"C")
123 'linearityCoeffs',
'linearityType',
'linearityBBox',
124 'fitParams',
'fitParamsErr',
'fitChiSq',
128 """Update metadata keywords with new values.
130 This calls the base class's method after ensuring the required
131 calibration keywords will be saved.
135 setDate : `bool`, optional
136 Update the CALIBDATE fields in the metadata to the current
137 time. Defaults to
False.
139 Other keyword parameters to set
in the metadata.
142 kwargs[
'OVERRIDE'] = self.
overrideoverride
143 kwargs[
'HAS_TABLE'] = self.
tableDatatableData
is not None
148 """Read linearity parameters from a detector.
152 detector : `lsst.afw.cameraGeom.detector`
153 Input detector with parameters to use.
158 The calibration constructed
from the detector.
166 for amp
in detector.getAmplifiers():
167 ampName = amp.getName()
168 self.
ampNamesampNames.append(ampName)
169 self.
linearityTypelinearityType[ampName] = amp.getLinearityType()
170 self.
linearityCoeffslinearityCoeffs[ampName] = amp.getLinearityCoeffs()
177 """Construct a calibration from a dictionary of properties
182 Dictionary of properties
186 calib : `lsst.ip.isr.Linearity`
187 Constructed calibration.
192 Raised if the supplied dictionary
is for a different
198 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
199 raise RuntimeError(f
"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
200 f
"found {dictionary['metadata']['OBSTYPE']}")
202 calib.setMetadata(dictionary[
'metadata'])
204 calib.hasLinearity = dictionary.get(
'hasLinearity',
205 dictionary[
'metadata'].get(
'HAS_LINEARITY',
False))
206 calib.override = dictionary.get(
'override',
True)
208 if calib.hasLinearity:
209 for ampName
in dictionary[
'amplifiers']:
210 amp = dictionary[
'amplifiers'][ampName]
211 calib.ampNames.append(ampName)
212 calib.linearityCoeffs[ampName] = np.array(amp.get(
'linearityCoeffs', [0.0]))
213 calib.linearityType[ampName] = amp.get(
'linearityType',
'None')
214 calib.linearityBBox[ampName] = amp.get(
'linearityBBox',
None)
216 calib.fitParams[ampName] = np.array(amp.get(
'fitParams', [0.0]))
217 calib.fitParamsErr[ampName] = np.array(amp.get(
'fitParamsErr', [0.0]))
218 calib.fitChiSq[ampName] = amp.get(
'fitChiSq', np.nan)
220 calib.tableData = dictionary.get(
'tableData',
None)
222 calib.tableData = np.array(calib.tableData)
227 """Return linearity parameters as a dict.
235 outDict = {'metadata': self.
getMetadatagetMetadata(),
239 'hasTable': self.
tableDatatableData
is not None,
240 'amplifiers': dict(),
243 outDict[
'amplifiers'][ampName] = {
'linearityType': self.
linearityTypelinearityType[ampName],
244 'linearityCoeffs': self.
linearityCoeffslinearityCoeffs[ampName].tolist(),
246 'fitParams': self.
fitParamsfitParams[ampName].tolist(),
247 'fitParamsErr': self.
fitParamsErrfitParamsErr[ampName].tolist(),
248 'fitChiSq': self.
fitChiSqfitChiSq[ampName]}
250 outDict[
'tableData'] = self.
tableDatatableData.tolist()
256 """Read linearity from a FITS file.
258 This method uses the `fromDict` method to create the
259 calibration, after constructing an appropriate dictionary from
264 tableList : `list` [`astropy.table.Table`]
265 afwTable read
from input file name.
270 Linearity parameters.
274 The method reads a FITS file
with 1
or 2 extensions. The metadata
is
275 read
from the header of extension 1, which must exist. Then the table
276 is loaded,
and the [
'AMPLIFIER_NAME',
'TYPE',
'COEFFS',
'BBOX_X0',
277 'BBOX_Y0',
'BBOX_DX',
'BBOX_DY'] columns are read
and used to set each
278 dictionary by looping over rows.
279 Extension 2
is then attempted to read
in the
try block (which only
280 exists
for lookup tables). It has a column named
'LOOKUP_VALUES' that
281 contains a vector of the lookup entries
in each row.
283 coeffTable = tableList[0]
285 metadata = coeffTable.meta
287 inDict['metadata'] = metadata
288 inDict[
'hasLinearity'] = metadata.get(
'HAS_LINEARITY',
False)
289 inDict[
'amplifiers'] = dict()
291 for record
in coeffTable:
292 ampName = record[
'AMPLIFIER_NAME']
294 fitParams = record[
'FIT_PARAMS']
if 'FIT_PARAMS' in record.columns
else np.array([0.0])
295 fitParamsErr = record[
'FIT_PARAMS_ERR']
if 'FIT_PARAMS_ERR' in record.columns
else np.array([0.0])
296 fitChiSq = record[
'RED_CHI_SQ']
if 'RED_CHI_SQ' in record.columns
else np.nan
298 inDict[
'amplifiers'][ampName] = {
299 'linearityType': record[
'TYPE'],
300 'linearityCoeffs': record[
'COEFFS'],
301 'linearityBBox':
Box2I(Point2I(record[
'BBOX_X0'], record[
'BBOX_Y0']),
302 Extent2I(record[
'BBOX_DX'], record[
'BBOX_DY'])),
303 'fitParams': fitParams,
304 'fitParamsErr': fitParamsErr,
305 'fitChiSq': fitChiSq,
308 if len(tableList) > 1:
309 tableData = tableList[1]
310 inDict[
'tableData'] = [record[
'LOOKUP_VALUES']
for record
in tableData]
315 """Construct a list of tables containing the information in this
318 The list of tables should create an identical calibration
319 after being passed to this class's fromTable method.
323 tableList : `list` [`astropy.table.Table`]
324 List of tables containing the linearity calibration
330 catalog = Table([{'AMPLIFIER_NAME': ampName,
333 'BBOX_X0': self.
linearityBBoxlinearityBBox[ampName].getMinX(),
334 'BBOX_Y0': self.
linearityBBoxlinearityBBox[ampName].getMinY(),
335 'BBOX_DX': self.
linearityBBoxlinearityBBox[ampName].getWidth(),
336 'BBOX_DY': self.
linearityBBoxlinearityBBox[ampName].getHeight(),
337 'FIT_PARAMS': self.
fitParamsfitParams[ampName],
338 'FIT_PARAMS_ERR': self.
fitParamsErrfitParamsErr[ampName],
339 'RED_CHI_SQ': self.
fitChiSqfitChiSq[ampName],
340 }
for ampName
in self.
ampNamesampNames])
342 tableList.append(catalog)
345 catalog = Table([{
'LOOKUP_VALUES': value}
for value
in self.
tableDatatableData])
346 tableList.append(catalog)
350 """Determine the linearity class to use from the type name.
354 linearityTypeName : str
355 String name of the linearity type that is needed.
360 The appropriate linearity
class to use. If no matching
class
361 is found, `
None`
is returned.
363 for t
in [LinearizeLookupTable,
366 LinearizeProportional,
369 if t.LinearityType == linearityTypeName:
374 """Validate linearity for a detector/amplifier.
379 Detector to validate, along with its amplifiers.
381 Single amplifier to validate.
386 Raised
if there
is a mismatch
in linearity parameters,
and
387 the cameraGeom parameters are
not being overridden.
389 amplifiersToCheck = []
392 raise RuntimeError(
"Detector names don't match: %s != %s" %
395 raise RuntimeError(
"Detector IDs don't match: %s != %s" %
398 raise RuntimeError(
"Detector serial numbers don't match: %s != %s" %
400 if len(detector.getAmplifiers()) != len(self.
linearityCoeffslinearityCoeffs.keys()):
401 raise RuntimeError(
"Detector number of amps = %s does not match saved value %s" %
402 (len(detector.getAmplifiers()),
404 amplifiersToCheck.extend(detector.getAmplifiers())
407 amplifiersToCheck.extend(amplifier)
409 for amp
in amplifiersToCheck:
410 ampName = amp.getName()
412 raise RuntimeError(
"Amplifier %s is not in linearity data" %
414 if amp.getLinearityType() != self.
linearityTypelinearityType[ampName]:
416 self.
loglog.warning(
"Overriding amplifier defined linearityType (%s) for %s",
419 raise RuntimeError(
"Amplifier %s type %s does not match saved value %s" %
420 (ampName, amp.getLinearityType(), self.
linearityTypelinearityType[ampName]))
421 if (amp.getLinearityCoeffs().shape != self.
linearityCoeffslinearityCoeffs[ampName].shape
or not
422 np.allclose(amp.getLinearityCoeffs(), self.
linearityCoeffslinearityCoeffs[ampName], equal_nan=
True)):
424 self.
loglog.warning(
"Overriding amplifier defined linearityCoeffs (%s) for %s",
427 raise RuntimeError(
"Amplifier %s coeffs %s does not match saved value %s" %
428 (ampName, amp.getLinearityCoeffs(), self.
linearityCoeffslinearityCoeffs[ampName]))
431 """Apply the linearity to an image.
433 If the linearity parameters are populated, use those,
434 otherwise use the values from the detector.
440 detector : `~lsst.afw.cameraGeom.detector`
441 Detector to use
for linearity parameters
if not already
443 log : `~logging.Logger`, optional
444 Log object to use
for logging.
459 if linearizer
is not None:
460 ampView = image.Factory(image, self.
linearityBBoxlinearityBBox[ampName])
461 success, outOfRange = linearizer()(ampView, **{
'coeffs': self.
linearityCoeffslinearityCoeffs[ampName],
464 numOutOfRange += outOfRange
467 elif log
is not None:
468 log.warning(
"Amplifier %s did not linearize.",
472 numLinearized=numLinearized,
473 numOutOfRange=numOutOfRange
478 """Abstract base class functor for correcting non-linearity.
480 Subclasses must define __call__ and set
class variable
481 LinearityType to a string that will be used
for linearity type
in
482 the cameraGeom.Amplifier.linearityType field.
484 All linearity corrections should be defined
in terms of an
485 additive correction, such that:
487 corrected_value = uncorrected_value + f(uncorrected_value)
493 """Correct non-linearity.
498 Image to be corrected
500 Dictionary of parameter keywords:
502 Coefficient vector (`list`
or `numpy.array`).
504 Lookup table data (`numpy.array`).
506 Logger to handle messages (`logging.Logger`).
511 If true, a correction was applied successfully.
516 Raised
if the linearity type listed
in the
517 detector does
not match the
class type.
523 """Correct non-linearity with a persisted lookup table.
525 The lookup table consists of entries such that given
526 "coefficients" c0, c1:
528 for each i,j of image:
530 colInd = int(c1 + uncorrImage[i,j])
531 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
533 - c0: row index; used to identify which row of the table to use
534 (typically one per amplifier, though one can have multiple
535 amplifiers use the same table)
536 - c1: column index offset; added to the uncorrected image value
537 before truncation; this supports tables that can handle
538 negative image values; also,
if the c1 ends
with .5 then
539 the nearest index
is used instead of truncating to the
542 LinearityType = "LookupTable"
545 """Correct for non-linearity.
550 Image to be corrected
552 Dictionary of parameter keywords:
554 Columnation vector (`list`
or `numpy.array`).
556 Lookup table data (`numpy.array`).
558 Logger to handle messages (`logging.Logger`).
562 output : `tuple` [`bool`, `int`]
563 If true, a correction was applied successfully. The
564 integer indicates the number of pixels that were
565 uncorrectable by being out of range.
570 Raised
if the requested row index
is out of the table
575 rowInd, colIndOffset = kwargs['coeffs'][0:2]
576 table = kwargs[
'table']
579 numTableRows = table.shape[0]
581 if rowInd < 0
or rowInd > numTableRows:
582 raise RuntimeError(
"LinearizeLookupTable rowInd=%s not in range[0, %s)" %
583 (rowInd, numTableRows))
584 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
588 if numOutOfRange > 0
and log
is not None:
589 log.warning(
"%s pixels were out of range of the linearization table",
591 if numOutOfRange < image.getArray().size:
592 return True, numOutOfRange
594 return False, numOutOfRange
598 """Correct non-linearity with a polynomial mode.
600 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
602 where c_i are the linearity coefficients for each amplifier.
603 Lower order coefficients are
not included
as they duplicate other
604 calibration parameters:
606 A coefficient multiplied by uncorrImage**0
is equivalent to
607 bias level. Irrelevant
for correcting non-linearity.
609 A coefficient multiplied by uncorrImage**1
is proportional
610 to the gain. Not necessary
for correcting non-linearity.
612 LinearityType = "Polynomial"
615 """Correct non-linearity.
620 Image to be corrected
622 Dictionary of parameter keywords:
624 Coefficient vector (`list`
or `numpy.array`).
625 If the order of the polynomial
is n, this list
626 should have a length of n-1 (
"k0" and "k1" are
627 not needed
for the correction).
629 Logger to handle messages (`logging.Logger`).
633 output : `tuple` [`bool`, `int`]
634 If true, a correction was applied successfully. The
635 integer indicates the number of pixels that were
636 uncorrectable by being out of range.
638 if not np.any(np.isfinite(kwargs[
'coeffs'])):
640 if not np.any(kwargs[
'coeffs']):
643 ampArray = image.getArray()
644 correction = np.zeros_like(ampArray)
645 for order, coeff
in enumerate(kwargs[
'coeffs'], start=2):
646 correction += coeff * np.power(ampArray, order)
647 ampArray += correction
653 """Correct non-linearity with a squared model.
655 corrImage = uncorrImage + c0*uncorrImage^2
657 where c0 is linearity coefficient 0
for each amplifier.
659 LinearityType = "Squared"
662 """Correct for non-linearity.
667 Image to be corrected
669 Dictionary of parameter keywords:
671 Coefficient vector (`list`
or `numpy.array`).
673 Logger to handle messages (`logging.Logger`).
677 output : `tuple` [`bool`, `int`]
678 If true, a correction was applied successfully. The
679 integer indicates the number of pixels that were
680 uncorrectable by being out of range.
683 sqCoeff = kwargs['coeffs'][0]
685 ampArr = image.getArray()
686 ampArr *= (1 + sqCoeff*ampArr)
693 """Correct non-linearity with a spline model.
695 corrImage = uncorrImage - Spline(coeffs, uncorrImage)
700 The spline fit calculates a correction as a function of the
701 expected linear flux term. Because of this, the correction needs
702 to be subtracted
from the observed flux.
705 LinearityType = "Spline"
708 """Correct for non-linearity.
713 Image to be corrected
715 Dictionary of parameter keywords:
717 Coefficient vector (`list`
or `numpy.array`).
719 Logger to handle messages (`logging.Logger`).
723 output : `tuple` [`bool`, `int`]
724 If true, a correction was applied successfully. The
725 integer indicates the number of pixels that were
726 uncorrectable by being out of range.
728 splineCoeff = kwargs['coeffs']
729 centers, values = np.split(splineCoeff, 2)
730 interp = afwMath.makeInterpolate(centers.tolist(), values.tolist(),
731 afwMath.stringToInterpStyle(
"AKIMA_SPLINE"))
733 ampArr = image.getArray()
734 delta = interp.interpolate(ampArr.flatten())
735 ampArr -= np.array(delta).reshape(ampArr.shape)
741 """Do not correct non-linearity.
743 LinearityType = "Proportional"
746 """Do not correct for non-linearity.
751 Image to be corrected
753 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.
770 """Do not correct non-linearity.
772 LinearityType = "None"
775 """Do not correct for non-linearity.
780 Image to be corrected
782 Dictionary of parameter keywords:
784 Coefficient vector (`list`
or `numpy.array`).
786 Logger to handle messages (`logging.Logger`).
790 output : `tuple` [`bool`, `int`]
791 If true, a correction was applied successfully. The
792 integer indicates the number of pixels that were
793 uncorrectable by being out of range.
def validate(self, other=None)
def requiredAttributes(self, value)
def updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
def fromDetector(self, detector)
def requiredAttributes(self)
def __call__(self, image, **kwargs)
def __call__(self, image, **kwargs)
def __call__(self, image, **kwargs)
def __call__(self, image, **kwargs)
def __call__(self, image, **kwargs)
def __call__(self, image, **kwargs)
def __call__(self, image, **kwargs)
def getLinearityTypeByName(self, linearityTypeName)
def validate(self, detector=None, amplifier=None)
def applyLinearity(self, image, detector=None, log=None)
def fromTable(cls, tableList)
def fromDetector(self, detector)
def updateMetadata(self, setDate=False, **kwargs)
def __init__(self, table=None, **kwargs)
def fromDict(cls, dictionary)