25 from astropy.table
import Table
29 from lsst.geom import Box2I, Point2I, Extent2I
30 from .applyLookupTable
import applyLookupTable
31 from .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)
52 detector : `lsst.afw.cameraGeom.Detector`, optional
53 Detector object. Passed to self.fromDetector() on init.
54 log : `lsst.log.Log`, 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.
81 linearityBBox : `dict` [`str`, `lsst.geom.Box2I`]
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.
157 calib : `lsst.ip.isr.Linearizer`
158 The calibration constructed from the detector.
166 for amp
in detector.getAmplifiers():
167 ampName = amp.getName()
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.
269 linearity : `~lsst.ip.isr.linearize.Linearizer``
270 Linearity parameters.
274 The method reads a FITS file with 1 or 2 extensions. The metadata is read from the header of
275 extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME', 'TYPE',
276 'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to
277 set each dictionary by looping over rows.
278 Eextension 2 is then attempted to read in the try block (which only exists for lookup tables).
279 It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row.
282 coeffTable = tableList[0]
284 metadata = coeffTable.meta
286 inDict[
'metadata'] = metadata
287 inDict[
'hasLinearity'] = metadata.get(
'HAS_LINEARITY',
False)
288 inDict[
'amplifiers'] = dict()
290 for record
in coeffTable:
291 ampName = record[
'AMPLIFIER_NAME']
293 fitParams = record[
'FIT_PARAMS']
if 'FIT_PARAMS' in record.columns
else np.array([0.0])
294 fitParamsErr = record[
'FIT_PARAMS_ERR']
if 'FIT_PARAMS_ERR' in record.columns
else np.array([0.0])
295 fitChiSq = record[
'RED_CHI_SQ']
if 'RED_CHI_SQ' in record.columns
else np.nan
297 inDict[
'amplifiers'][ampName] = {
298 'linearityType': record[
'TYPE'],
299 'linearityCoeffs': record[
'COEFFS'],
300 'linearityBBox':
Box2I(
Point2I(record[
'BBOX_X0'], record[
'BBOX_Y0']),
301 Extent2I(record[
'BBOX_DX'], record[
'BBOX_DY'])),
302 'fitParams': fitParams,
303 'fitParamsErr': fitParamsErr,
304 'fitChiSq': fitChiSq,
307 if len(tableList) > 1:
308 tableData = tableList[1]
309 inDict[
'tableData'] = [record[
'LOOKUP_VALUES']
for record
in tableData]
314 """Construct a list of tables containing the information in this calibration
316 The list of tables should create an identical calibration
317 after being passed to this class's fromTable method.
321 tableList : `list` [`astropy.table.Table`]
322 List of tables containing the linearity calibration
328 catalog = Table([{
'AMPLIFIER_NAME': ampName,
331 'BBOX_X0': self.
linearityBBoxlinearityBBox[ampName].getMinX(),
332 'BBOX_Y0': self.
linearityBBoxlinearityBBox[ampName].getMinY(),
333 'BBOX_DX': self.
linearityBBoxlinearityBBox[ampName].getWidth(),
334 'BBOX_DY': self.
linearityBBoxlinearityBBox[ampName].getHeight(),
335 'FIT_PARAMS': self.
fitParamsfitParams[ampName],
336 'FIT_PARAMS_ERR': self.
fitParamsErrfitParamsErr[ampName],
337 'RED_CHI_SQ': self.
fitChiSqfitChiSq[ampName],
338 }
for ampName
in self.
ampNamesampNames])
340 tableList.append(catalog)
343 catalog = Table([{
'LOOKUP_VALUES': value}
for value
in self.
tableDatatableData])
344 tableList.append(catalog)
348 """Determine the linearity class to use from the type name.
352 linearityTypeName : str
353 String name of the linearity type that is needed.
357 linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
358 The appropriate linearity class to use. If no matching class
359 is found, `None` is returned.
361 for t
in [LinearizeLookupTable,
364 LinearizeProportional,
367 if t.LinearityType == linearityTypeName:
372 """Validate linearity for a detector/amplifier.
376 detector : `lsst.afw.cameraGeom.Detector`, optional
377 Detector to validate, along with its amplifiers.
378 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
379 Single amplifier to validate.
384 Raised if there is a mismatch in linearity parameters, and
385 the cameraGeom parameters are not being overridden.
387 amplifiersToCheck = []
390 raise RuntimeError(
"Detector names don't match: %s != %s" %
393 raise RuntimeError(
"Detector IDs don't match: %s != %s" %
396 raise RuntimeError(
"Detector serial numbers don't match: %s != %s" %
399 raise RuntimeError(
"Detector number of amps = %s does not match saved value %s" %
400 (len(detector.getAmplifiers()),
402 amplifiersToCheck.extend(detector.getAmplifiers())
405 amplifiersToCheck.extend(amplifier)
407 for amp
in amplifiersToCheck:
408 ampName = amp.getName()
410 raise RuntimeError(
"Amplifier %s is not in linearity data" %
412 if amp.getLinearityType() != self.
linearityTypelinearityType[ampName]:
414 self.
loglog.
warn(
"Overriding amplifier defined linearityType (%s) for %s",
417 raise RuntimeError(
"Amplifier %s type %s does not match saved value %s" %
418 (ampName, amp.getLinearityType(), self.
linearityTypelinearityType[ampName]))
419 if (amp.getLinearityCoeffs().shape != self.
linearityCoeffslinearityCoeffs[ampName].shape
or not
420 np.allclose(amp.getLinearityCoeffs(), self.
linearityCoeffslinearityCoeffs[ampName], equal_nan=
True)):
422 self.
loglog.
warn(
"Overriding amplifier defined linearityCoeffs (%s) for %s",
425 raise RuntimeError(
"Amplifier %s coeffs %s does not match saved value %s" %
426 (ampName, amp.getLinearityCoeffs(), self.
linearityCoeffslinearityCoeffs[ampName]))
429 """Apply the linearity to an image.
431 If the linearity parameters are populated, use those,
432 otherwise use the values from the detector.
436 image : `~lsst.afw.image.image`
438 detector : `~lsst.afw.cameraGeom.detector`
439 Detector to use for linearity parameters if not already
441 log : `~lsst.log.Log`, optional
442 Log object to use for logging.
457 if linearizer
is not None:
458 ampView = image.Factory(image, self.
linearityBBoxlinearityBBox[ampName])
459 success, outOfRange = linearizer()(ampView, **{
'coeffs': self.
linearityCoeffslinearityCoeffs[ampName],
462 numOutOfRange += outOfRange
465 elif log
is not None:
466 log.warn(
"Amplifier %s did not linearize.",
470 numLinearized=numLinearized,
471 numOutOfRange=numOutOfRange
476 """Abstract base class functor for correcting non-linearity.
478 Subclasses must define __call__ and set class variable
479 LinearityType to a string that will be used for linearity type in
480 the cameraGeom.Amplifier.linearityType field.
482 All linearity corrections should be defined in terms of an
483 additive correction, such that:
485 corrected_value = uncorrected_value + f(uncorrected_value)
491 """Correct non-linearity.
495 image : `lsst.afw.image.Image`
496 Image to be corrected
498 Dictionary of parameter keywords:
500 Coefficient vector (`list` or `numpy.array`).
502 Lookup table data (`numpy.array`).
504 Logger to handle messages (`lsst.log.Log`).
509 If true, a correction was applied successfully.
514 Raised if the linearity type listed in the
515 detector does not match the class type.
520 class LinearizeLookupTable(LinearizeBase):
521 """Correct non-linearity with a persisted lookup table.
523 The lookup table consists of entries such that given
524 "coefficients" c0, c1:
526 for each i,j of image:
528 colInd = int(c1 + uncorrImage[i,j])
529 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
531 - c0: row index; used to identify which row of the table to use
532 (typically one per amplifier, though one can have multiple
533 amplifiers use the same table)
534 - c1: column index offset; added to the uncorrected image value
535 before truncation; this supports tables that can handle
536 negative image values; also, if the c1 ends with .5 then
537 the nearest index is used instead of truncating to the
540 LinearityType =
"LookupTable"
543 """Correct for non-linearity.
547 image : `lsst.afw.image.Image`
548 Image to be corrected
550 Dictionary of parameter keywords:
552 Columnation vector (`list` or `numpy.array`).
554 Lookup table data (`numpy.array`).
556 Logger to handle messages (`lsst.log.Log`).
560 output : `tuple` [`bool`, `int`]
561 If true, a correction was applied successfully. The
562 integer indicates the number of pixels that were
563 uncorrectable by being out of range.
568 Raised if the requested row index is out of the table
573 rowInd, colIndOffset = kwargs[
'coeffs'][0:2]
574 table = kwargs[
'table']
577 numTableRows = table.shape[0]
579 if rowInd < 0
or rowInd > numTableRows:
580 raise RuntimeError(
"LinearizeLookupTable rowInd=%s not in range[0, %s)" %
581 (rowInd, numTableRows))
582 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
586 if numOutOfRange > 0
and log
is not None:
587 log.warn(
"%s pixels were out of range of the linearization table",
589 if numOutOfRange < image.getArray().size:
590 return True, numOutOfRange
592 return False, numOutOfRange
596 """Correct non-linearity with a polynomial mode.
598 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
600 where c_i are the linearity coefficients for each amplifier.
601 Lower order coefficients are not included as they duplicate other
602 calibration parameters:
604 A coefficient multiplied by uncorrImage**0 is equivalent to
605 bias level. Irrelevant for correcting non-linearity.
607 A coefficient multiplied by uncorrImage**1 is proportional
608 to the gain. Not necessary for correcting non-linearity.
610 LinearityType =
"Polynomial"
613 """Correct non-linearity.
617 image : `lsst.afw.image.Image`
618 Image to be corrected
620 Dictionary of parameter keywords:
622 Coefficient vector (`list` or `numpy.array`).
623 If the order of the polynomial is n, this list
624 should have a length of n-1 ("k0" and "k1" are
625 not needed for the correction).
627 Logger to handle messages (`lsst.log.Log`).
631 output : `tuple` [`bool`, `int`]
632 If true, a correction was applied successfully. The
633 integer indicates the number of pixels that were
634 uncorrectable by being out of range.
636 if not np.any(np.isfinite(kwargs[
'coeffs'])):
638 if not np.any(kwargs[
'coeffs']):
641 ampArray = image.getArray()
642 correction = np.zeros_like(ampArray)
643 for order, coeff
in enumerate(kwargs[
'coeffs'], start=2):
644 correction += coeff * np.power(ampArray, order)
645 ampArray += correction
651 """Correct non-linearity with a squared model.
653 corrImage = uncorrImage + c0*uncorrImage^2
655 where c0 is linearity coefficient 0 for each amplifier.
657 LinearityType =
"Squared"
660 """Correct for non-linearity.
664 image : `lsst.afw.image.Image`
665 Image to be corrected
667 Dictionary of parameter keywords:
669 Coefficient vector (`list` or `numpy.array`).
671 Logger to handle messages (`lsst.log.Log`).
675 output : `tuple` [`bool`, `int`]
676 If true, a correction was applied successfully. The
677 integer indicates the number of pixels that were
678 uncorrectable by being out of range.
681 sqCoeff = kwargs[
'coeffs'][0]
683 ampArr = image.getArray()
684 ampArr *= (1 + sqCoeff*ampArr)
691 """Correct non-linearity with a spline model.
693 corrImage = uncorrImage - Spline(coeffs, uncorrImage)
698 The spline fit calculates a correction as a function of the
699 expected linear flux term. Because of this, the correction needs
700 to be subtracted from the observed flux.
703 LinearityType =
"Spline"
706 """Correct for non-linearity.
710 image : `lsst.afw.image.Image`
711 Image to be corrected
713 Dictionary of parameter keywords:
715 Coefficient vector (`list` or `numpy.array`).
717 Logger to handle messages (`lsst.log.Log`).
721 output : `tuple` [`bool`, `int`]
722 If true, a correction was applied successfully. The
723 integer indicates the number of pixels that were
724 uncorrectable by being out of range.
726 splineCoeff = kwargs[
'coeffs']
727 centers, values = np.split(splineCoeff, 2)
731 ampArr = image.getArray()
732 delta = interp.interpolate(ampArr.flatten())
733 ampArr -= np.array(delta).reshape(ampArr.shape)
739 """Do not correct non-linearity.
741 LinearityType =
"Proportional"
744 """Do not correct for non-linearity.
748 image : `lsst.afw.image.Image`
749 Image to be corrected
751 Dictionary of parameter keywords:
753 Coefficient vector (`list` or `numpy.array`).
755 Logger to handle messages (`lsst.log.Log`).
759 output : `tuple` [`bool`, `int`]
760 If true, a correction was applied successfully. The
761 integer indicates the number of pixels that were
762 uncorrectable by being out of range.
768 """Do not correct non-linearity.
770 LinearityType =
"None"
773 """Do not correct for non-linearity.
777 image : `lsst.afw.image.Image`
778 Image to be corrected
780 Dictionary of parameter keywords:
782 Coefficient vector (`list` or `numpy.array`).
784 Logger to handle messages (`lsst.log.Log`).
788 output : `tuple` [`bool`, `int`]
789 If true, a correction was applied successfully. The
790 integer indicates the number of pixels that were
791 uncorrectable by being out of range.
An integer coordinate rectangle.
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)
daf::base::PropertyList * list
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
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.
Extent< int, 2 > Extent2I