25 from astropy.table
import Table
28 from lsst.geom import Box2I, Point2I, Extent2I
29 from .applyLookupTable
import applyLookupTable
30 from .calibType
import IsrCalib
32 __all__ = [
"Linearizer",
33 "LinearizeBase",
"LinearizeLookupTable",
"LinearizeSquared",
34 "LinearizeProportional",
"LinearizePolynomial",
"LinearizeNone"]
38 """Parameter set for linearization.
40 These parameters are included in cameraGeom.Amplifier, but
41 should be accessible externally to allow for testing.
45 table : `numpy.array`, optional
46 Lookup table; a 2-dimensional array of floats:
47 - one row for each row index (value of coef[0] in the amplifier)
48 - one column for each image value
49 To avoid copying the table the last index should vary fastest
50 (numpy default "C" order)
51 detector : `lsst.afw.cameraGeom.Detector`, optional
52 Detector object. Passed to self.fromDetector() on init.
53 log : `lsst.log.Log`, optional
54 Logger to handle messages.
55 kwargs : `dict`, optional
56 Other keyword arguments to pass to the parent init.
61 Raised if the supplied table is not 2D, or if the table has fewer
62 columns than rows (indicating that the indices are swapped).
66 The linearizer attributes stored are:
69 Whether a linearity correction is defined for this detector.
71 Whether the detector parameters should be overridden.
72 ampNames : `list` [`str`]
73 List of amplifier names to correct.
74 linearityCoeffs : `dict` [`str`, `numpy.array`]
75 Coefficients to use in correction. Indexed by amplifier
76 names. The format of the array depends on the type of
78 linearityType : `dict` [`str`, `str`]
79 Type of correction to use, indexed by amplifier names.
80 linearityBBox : `dict` [`str`, `lsst.geom.Box2I`]
81 Bounding box the correction is valid over, indexed by
83 fitParams : `dict` [`str`, `numpy.array`], optional
84 Linearity fit parameters used to construct the correction
85 coefficients, indexed as above.
86 fitParamsErr : `dict` [`str`, `numpy.array`], optional
87 Uncertainty values of the linearity fit parameters used to
88 construct the correction coefficients, indexed as above.
89 fitChiSq : `dict` [`str`, `float`], optional
90 Chi-squared value of the linearity fit, indexed as above.
91 tableData : `numpy.array`, optional
92 Lookup table data for the linearity correction.
94 _OBSTYPE =
"LINEARIZER"
95 _SCHEMA =
'Gen3 Linearizer'
112 if table
is not None:
113 if len(table.shape) != 2:
114 raise RuntimeError(
"table shape = %s; must have two dimensions" % (table.shape,))
115 if table.shape[1] < table.shape[0]:
116 raise RuntimeError(
"table shape = %s; indices are switched" % (table.shape,))
117 self.
tableData = np.array(table, order=
"C")
122 'linearityCoeffs',
'linearityType',
'linearityBBox',
123 'fitParams',
'fitParamsErr',
'fitChiSq',
127 """Update metadata keywords with new values.
129 This calls the base class's method after ensuring the required
130 calibration keywords will be saved.
134 setDate : `bool`, optional
135 Update the CALIBDATE fields in the metadata to the current
136 time. Defaults to False.
138 Other keyword parameters to set in the metadata.
142 kwargs[
'HAS_TABLE'] = self.
tableData is not None
147 """Read linearity parameters from a detector.
151 detector : `lsst.afw.cameraGeom.detector`
152 Input detector with parameters to use.
156 calib : `lsst.ip.isr.Linearizer`
157 The calibration constructed from the detector.
165 for amp
in detector.getAmplifiers():
166 ampName = amp.getName()
176 """Construct a calibration from a dictionary of properties
181 Dictionary of properties
185 calib : `lsst.ip.isr.Linearity`
186 Constructed calibration.
191 Raised if the supplied dictionary is for a different
197 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
198 raise RuntimeError(f
"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
199 f
"found {dictionary['metadata']['OBSTYPE']}")
201 calib.setMetadata(dictionary[
'metadata'])
203 calib.hasLinearity = dictionary.get(
'hasLinearity',
204 dictionary[
'metadata'].get(
'HAS_LINEARITY',
False))
205 calib.override = dictionary.get(
'override',
True)
207 if calib.hasLinearity:
208 for ampName
in dictionary[
'amplifiers']:
209 amp = dictionary[
'amplifiers'][ampName]
210 calib.ampNames.append(ampName)
211 calib.linearityCoeffs[ampName] = np.array(amp.get(
'linearityCoeffs',
None), dtype=np.float64)
212 calib.linearityType[ampName] = amp.get(
'linearityType',
'None')
213 calib.linearityBBox[ampName] = amp.get(
'linearityBBox',
None)
215 calib.fitParams[ampName] = np.array(amp.get(
'fitParams',
None), dtype=np.float64)
216 calib.fitParamsErr[ampName] = np.array(amp.get(
'fitParamsErr',
None), dtype=np.float64)
217 calib.fitChiSq[ampName] = amp.get(
'fitChiSq',
None)
219 calib.tableData = dictionary.get(
'tableData',
None)
221 calib.tableData = np.array(calib.tableData)
226 """Return linearity parameters as a dict.
239 'amplifiers': dict(),
242 outDict[
'amplifiers'][ampName] = {
'linearityType': self.
linearityType[ampName],
245 'fitParams': self.
fitParams[ampName].toList(),
249 outDict[
'tableData'] = self.
tableData.tolist()
255 """Read linearity from a FITS file.
257 This method uses the `fromDict` method to create the
258 calibration, after constructing an appropriate dictionary from
263 tableList : `list` [`astropy.table.Table`]
264 afwTable read from input file name.
268 linearity : `~lsst.ip.isr.linearize.Linearizer``
269 Linearity parameters.
273 The method reads a FITS file with 1 or 2 extensions. The metadata is read from the header of
274 extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME', 'TYPE',
275 'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to
276 set each dictionary by looping over rows.
277 Eextension 2 is then attempted to read in the try block (which only exists for lookup tables).
278 It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row.
281 coeffTable = tableList[0]
283 metadata = coeffTable.meta
285 inDict[
'metadata'] = metadata
286 inDict[
'hasLinearity'] = metadata[
'HAS_LINEARITY']
287 inDict[
'amplifiers'] = dict()
289 for record
in coeffTable:
290 ampName = record[
'AMPLIFIER_NAME']
292 fitParams = record[
'FIT_PARAMS']
if 'FIT_PARAMS' in record
else None
293 fitParamsErr = record[
'FIT_PARAMS_ERR']
if 'FIT_PARAMS_ERR' in record
else None
294 fitChiSq = record[
'RED_CHI_SQ']
if 'RED_CHI_SQ' in record
else None
296 inDict[
'amplifiers'][ampName] = {
297 'linearityType': record[
'TYPE'],
298 'linearityCoeffs': record[
'COEFFS'],
299 'linearityBBox':
Box2I(
Point2I(record[
'BBOX_X0'], record[
'BBOX_Y0']),
300 Extent2I(record[
'BBOX_DX'], record[
'BBOX_DY'])),
301 'fitParams': fitParams,
302 'fitParamsErr': fitParamsErr,
303 'fitChiSq': fitChiSq,
306 if len(tableList) > 1:
307 tableData = tableList[1]
308 inDict[
'tableData'] = [record[
'LOOKUP_VALUES']
for record
in tableData]
313 """Construct a list of tables containing the information in this calibration
315 The list of tables should create an identical calibration
316 after being passed to this class's fromTable method.
320 tableList : `list` [`astropy.table.Table`]
321 List of tables containing the linearity calibration
327 catalog = Table([{
'AMPLIFIER_NAME': ampName,
336 'RED_CHI_SQ': self.
fitChiSq[ampName],
339 tableList.append(catalog)
342 catalog = Table([{
'LOOKUP_VALUES': value}
for value
in self.
tableData])
343 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,
366 if t.LinearityType == linearityTypeName:
371 """Validate linearity for a detector/amplifier.
375 detector : `lsst.afw.cameraGeom.Detector`, optional
376 Detector to validate, along with its amplifiers.
377 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
378 Single amplifier to validate.
383 Raised if there is a mismatch in linearity parameters, and
384 the cameraGeom parameters are not being overridden.
386 amplifiersToCheck = []
389 raise RuntimeError(
"Detector names don't match: %s != %s" %
392 raise RuntimeError(
"Detector IDs don't match: %s != %s" %
395 raise RuntimeError(
"Detector serial numbers don't match: %s != %s" %
398 raise RuntimeError(
"Detector number of amps = %s does not match saved value %s" %
399 (len(detector.getAmplifiers()),
401 amplifiersToCheck.extend(detector.getAmplifiers())
404 amplifiersToCheck.extend(amplifier)
406 for amp
in amplifiersToCheck:
407 ampName = amp.getName()
409 raise RuntimeError(
"Amplifier %s is not in linearity data" %
413 self.
log.warn(
"Overriding amplifier defined linearityType (%s) for %s",
416 raise RuntimeError(
"Amplifier %s type %s does not match saved value %s" %
417 (ampName, amp.getLinearityType(), self.
linearityType[ampName]))
418 if (amp.getLinearityCoeffs().shape != self.
linearityCoeffs[ampName].shape
or not
419 np.allclose(amp.getLinearityCoeffs(), self.
linearityCoeffs[ampName], equal_nan=
True)):
421 self.
log.warn(
"Overriding amplifier defined linearityCoeffs (%s) for %s",
424 raise RuntimeError(
"Amplifier %s coeffs %s does not match saved value %s" %
428 """Apply the linearity to an image.
430 If the linearity parameters are populated, use those,
431 otherwise use the values from the detector.
435 image : `~lsst.afw.image.image`
437 detector : `~lsst.afw.cameraGeom.detector`
438 Detector to use for linearity parameters if not already
440 log : `~lsst.log.Log`, optional
441 Log object to use for logging.
456 if linearizer
is not None:
458 success, outOfRange = linearizer()(ampView, **{
'coeffs': self.
linearityCoeffs[ampName],
461 numOutOfRange += outOfRange
464 elif log
is not None:
465 log.warn(
"Amplifier %s did not linearize.",
469 numLinearized=numLinearized,
470 numOutOfRange=numOutOfRange
475 """Abstract base class functor for correcting non-linearity.
477 Subclasses must define __call__ and set class variable
478 LinearityType to a string that will be used for linearity type in
479 the cameraGeom.Amplifier.linearityType field.
481 All linearity corrections should be defined in terms of an
482 additive correction, such that:
484 corrected_value = uncorrected_value + f(uncorrected_value)
490 """Correct non-linearity.
494 image : `lsst.afw.image.Image`
495 Image to be corrected
497 Dictionary of parameter keywords:
499 Coefficient vector (`list` or `numpy.array`).
501 Lookup table data (`numpy.array`).
503 Logger to handle messages (`lsst.log.Log`).
508 If true, a correction was applied successfully.
513 Raised if the linearity type listed in the
514 detector does not match the class type.
519 class LinearizeLookupTable(LinearizeBase):
520 """Correct non-linearity with a persisted lookup table.
522 The lookup table consists of entries such that given
523 "coefficients" c0, c1:
525 for each i,j of image:
527 colInd = int(c1 + uncorrImage[i,j])
528 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
530 - c0: row index; used to identify which row of the table to use
531 (typically one per amplifier, though one can have multiple
532 amplifiers use the same table)
533 - c1: column index offset; added to the uncorrected image value
534 before truncation; this supports tables that can handle
535 negative image values; also, if the c1 ends with .5 then
536 the nearest index is used instead of truncating to the
539 LinearityType =
"LookupTable"
542 """Correct for non-linearity.
546 image : `lsst.afw.image.Image`
547 Image to be corrected
549 Dictionary of parameter keywords:
551 Columnation vector (`list` or `numpy.array`).
553 Lookup table data (`numpy.array`).
555 Logger to handle messages (`lsst.log.Log`).
560 If true, a correction was applied successfully.
565 Raised if the requested row index is out of the table
570 rowInd, colIndOffset = kwargs[
'coeffs'][0:2]
571 table = kwargs[
'table']
574 numTableRows = table.shape[0]
576 if rowInd < 0
or rowInd > numTableRows:
577 raise RuntimeError(
"LinearizeLookupTable rowInd=%s not in range[0, %s)" %
578 (rowInd, numTableRows))
579 tableRow = table[rowInd, :]
582 if numOutOfRange > 0
and log
is not None:
583 log.warn(
"%s pixels were out of range of the linearization table",
585 if numOutOfRange < image.getArray().size:
586 return True, numOutOfRange
588 return False, numOutOfRange
592 """Correct non-linearity with a polynomial mode.
594 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
596 where c_i are the linearity coefficients for each amplifier.
597 Lower order coefficients are not included as they duplicate other
598 calibration parameters:
600 A coefficient multiplied by uncorrImage**0 is equivalent to
601 bias level. Irrelevant for correcting non-linearity.
603 A coefficient multiplied by uncorrImage**1 is proportional
604 to the gain. Not necessary for correcting non-linearity.
606 LinearityType =
"Polynomial"
609 """Correct non-linearity.
613 image : `lsst.afw.image.Image`
614 Image to be corrected
616 Dictionary of parameter keywords:
618 Coefficient vector (`list` or `numpy.array`).
619 If the order of the polynomial is n, this list
620 should have a length of n-1 ("k0" and "k1" are
621 not needed for the correction).
623 Logger to handle messages (`lsst.log.Log`).
628 If true, a correction was applied successfully.
630 if not np.any(np.isfinite(kwargs[
'coeffs'])):
632 if not np.any(kwargs[
'coeffs']):
635 ampArray = image.getArray()
636 correction = np.zeros_like(ampArray)
637 for order, coeff
in enumerate(kwargs[
'coeffs'], start=2):
638 correction += coeff * np.power(ampArray, order)
639 ampArray += correction
645 """Correct non-linearity with a squared model.
647 corrImage = uncorrImage + c0*uncorrImage^2
649 where c0 is linearity coefficient 0 for each amplifier.
651 LinearityType =
"Squared"
654 """Correct for non-linearity.
658 image : `lsst.afw.image.Image`
659 Image to be corrected
661 Dictionary of parameter keywords:
663 Coefficient vector (`list` or `numpy.array`).
665 Logger to handle messages (`lsst.log.Log`).
670 If true, a correction was applied successfully.
673 sqCoeff = kwargs[
'coeffs'][0]
675 ampArr = image.getArray()
676 ampArr *= (1 + sqCoeff*ampArr)
683 """Do not correct non-linearity.
685 LinearityType =
"Proportional"
688 """Do not correct for non-linearity.
692 image : `lsst.afw.image.Image`
693 Image to be corrected
695 Dictionary of parameter keywords:
697 Coefficient vector (`list` or `numpy.array`).
699 Logger to handle messages (`lsst.log.Log`).
704 If true, a correction was applied successfully.
710 """Do not correct non-linearity.
712 LinearityType =
"None"
715 """Do not correct for non-linearity.
719 image : `lsst.afw.image.Image`
720 Image to be corrected
722 Dictionary of parameter keywords:
724 Coefficient vector (`list` or `numpy.array`).
726 Logger to handle messages (`lsst.log.Log`).
731 If true, a correction was applied successfully.