23__all__ = [
"Linearizer",
24 "LinearizeBase",
"LinearizeLookupTable",
"LinearizeSquared",
25 "LinearizeProportional",
"LinearizePolynomial",
"LinearizeSpline",
"LinearizeNone"]
30from astropy.table
import Table
34from lsst.geom import Box2I, Point2I, Extent2I
35from .applyLookupTable
import applyLookupTable
36from .calibType
import IsrCalib
40 """Parameter set for linearization.
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)
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.
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 linearFit : The linear fit to the low flux region of the curve.
100 tableData : `numpy.array`, optional
101 Lookup table data
for the linearity correction.
103 _OBSTYPE = "LINEARIZER"
104 _SCHEMA =
'Gen3 Linearizer'
121 if table
is not None:
122 if len(table.shape) != 2:
123 raise RuntimeError(
"table shape = %s; must have two dimensions" % (table.shape,))
124 if table.shape[1] < table.shape[0]:
125 raise RuntimeError(
"table shape = %s; indices are switched" % (table.shape,))
126 self.
tableData = np.array(table, order=
"C")
131 'linearityCoeffs',
'linearityType',
'linearityBBox',
132 'fitParams',
'fitParamsErr',
'fitChiSq',
133 'fitResiduals',
'linearFit',
'tableData'])
136 """Update metadata keywords with new values.
138 This calls the base class's method after ensuring the required
139 calibration keywords will be saved.
143 setDate : `bool`, optional
144 Update the CALIBDATE fields in the metadata to the current
145 time. Defaults to
False.
147 Other keyword parameters to set
in the metadata.
151 kwargs[
'HAS_TABLE'] = self.
tableData is not None
156 """Read linearity parameters from a detector.
160 detector : `lsst.afw.cameraGeom.detector`
161 Input detector with parameters to use.
166 The calibration constructed
from the detector.
174 for amp
in detector.getAmplifiers():
175 ampName = amp.getName()
185 """Construct a calibration from a dictionary of properties
190 Dictionary of properties
194 calib : `lsst.ip.isr.Linearity`
195 Constructed calibration.
200 Raised if the supplied dictionary
is for a different
206 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
207 raise RuntimeError(f
"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
208 f
"found {dictionary['metadata']['OBSTYPE']}")
210 calib.setMetadata(dictionary[
'metadata'])
212 calib.hasLinearity = dictionary.get(
'hasLinearity',
213 dictionary[
'metadata'].get(
'HAS_LINEARITY',
False))
214 calib.override = dictionary.get(
'override',
True)
216 if calib.hasLinearity:
217 for ampName
in dictionary[
'amplifiers']:
218 amp = dictionary[
'amplifiers'][ampName]
219 calib.ampNames.append(ampName)
220 calib.linearityCoeffs[ampName] = np.array(amp.get(
'linearityCoeffs', [0.0]))
221 calib.linearityType[ampName] = amp.get(
'linearityType',
'None')
222 calib.linearityBBox[ampName] = amp.get(
'linearityBBox',
None)
224 calib.fitParams[ampName] = np.array(amp.get(
'fitParams', [0.0]))
225 calib.fitParamsErr[ampName] = np.array(amp.get(
'fitParamsErr', [0.0]))
226 calib.fitChiSq[ampName] = amp.get(
'fitChiSq', np.nan)
227 calib.fitResiduals[ampName] = np.array(amp.get(
'fitResiduals', [0.0]))
228 calib.linearFit[ampName] = np.array(amp.get(
'linearFit', [0.0]))
230 calib.tableData = dictionary.get(
'tableData',
None)
232 calib.tableData = np.array(calib.tableData)
237 """Return linearity parameters as a dict.
250 'amplifiers': dict(),
253 outDict[
'amplifiers'][ampName] = {
'linearityType': self.
linearityType[ampName],
256 'fitParams': self.
fitParams[ampName].tolist(),
260 'linearFit': self.
linearFit[ampName].tolist()}
262 outDict[
'tableData'] = self.
tableData.tolist()
268 """Read linearity from a FITS file.
270 This method uses the `fromDict` method to create the
271 calibration, after constructing an appropriate dictionary from
276 tableList : `list` [`astropy.table.Table`]
277 afwTable read
from input file name.
282 Linearity parameters.
286 The method reads a FITS file
with 1
or 2 extensions. The metadata
is
287 read
from the header of extension 1, which must exist. Then the table
288 is loaded,
and the [
'AMPLIFIER_NAME',
'TYPE',
'COEFFS',
'BBOX_X0',
289 'BBOX_Y0',
'BBOX_DX',
'BBOX_DY'] columns are read
and used to set each
290 dictionary by looping over rows.
291 Extension 2
is then attempted to read
in the
try block (which only
292 exists
for lookup tables). It has a column named
'LOOKUP_VALUES' that
293 contains a vector of the lookup entries
in each row.
295 coeffTable = tableList[0]
297 metadata = coeffTable.meta
299 inDict['metadata'] = metadata
300 inDict[
'hasLinearity'] = metadata.get(
'HAS_LINEARITY',
False)
301 inDict[
'amplifiers'] = dict()
303 for record
in coeffTable:
304 ampName = record[
'AMPLIFIER_NAME']
306 fitParams = record[
'FIT_PARAMS']
if 'FIT_PARAMS' in record.columns
else np.array([0.0])
307 fitParamsErr = record[
'FIT_PARAMS_ERR']
if 'FIT_PARAMS_ERR' in record.columns
else np.array([0.0])
308 fitChiSq = record[
'RED_CHI_SQ']
if 'RED_CHI_SQ' in record.columns
else np.nan
309 fitResiduals = record[
'FIT_RES']
if 'FIT_RES' in record.columns
else np.array([0.0])
310 linearFit = record[
'LIN_FIT']
if 'LIN_FIT' in record.columns
else np.array([0.0])
312 inDict[
'amplifiers'][ampName] = {
313 'linearityType': record[
'TYPE'],
314 'linearityCoeffs': record[
'COEFFS'],
315 'linearityBBox':
Box2I(Point2I(record[
'BBOX_X0'], record[
'BBOX_Y0']),
316 Extent2I(record[
'BBOX_DX'], record[
'BBOX_DY'])),
317 'fitParams': fitParams,
318 'fitParamsErr': fitParamsErr,
319 'fitChiSq': fitChiSq,
320 'fitResiduals': fitResiduals,
321 'linearFit': linearFit,
324 if len(tableList) > 1:
325 tableData = tableList[1]
326 inDict[
'tableData'] = [record[
'LOOKUP_VALUES']
for record
in tableData]
331 """Construct a list of tables containing the information in this
334 The list of tables should create an identical calibration
335 after being passed to this class's fromTable method.
339 tableList : `list` [`astropy.table.Table`]
340 List of tables containing the linearity calibration
346 catalog = Table([{'AMPLIFIER_NAME': ampName,
355 'RED_CHI_SQ': self.
fitChiSq[ampName],
360 tableList.append(catalog)
363 catalog = Table([{
'LOOKUP_VALUES': value}
for value
in self.
tableData])
364 tableList.append(catalog)
368 """Determine the linearity class to use from the type name.
372 linearityTypeName : str
373 String name of the linearity type that is needed.
378 The appropriate linearity
class to use. If no matching
class
379 is found, `
None`
is returned.
381 for t
in [LinearizeLookupTable,
384 LinearizeProportional,
387 if t.LinearityType == linearityTypeName:
392 """Validate linearity for a detector/amplifier.
397 Detector to validate, along with its amplifiers.
399 Single amplifier to validate.
404 Raised
if there
is a mismatch
in linearity parameters,
and
405 the cameraGeom parameters are
not being overridden.
407 amplifiersToCheck = []
410 raise RuntimeError(
"Detector names don't match: %s != %s" %
413 raise RuntimeError(
"Detector IDs don't match: %s != %s" %
422 raise RuntimeError(
"Detector number of amps = %s does not match saved value %s" %
423 (len(detector.getAmplifiers()),
425 amplifiersToCheck.extend(detector.getAmplifiers())
428 amplifiersToCheck.extend(amplifier)
430 for amp
in amplifiersToCheck:
431 ampName = amp.getName()
433 raise RuntimeError(
"Amplifier %s is not in linearity data" %
437 self.
log.warning(
"Overriding amplifier defined linearityType (%s) for %s",
440 raise RuntimeError(
"Amplifier %s type %s does not match saved value %s" %
441 (ampName, amp.getLinearityType(), self.
linearityType[ampName]))
442 if (amp.getLinearityCoeffs().shape != self.
linearityCoeffs[ampName].shape
or not
443 np.allclose(amp.getLinearityCoeffs(), self.
linearityCoeffs[ampName], equal_nan=
True)):
445 self.
log.warning(
"Overriding amplifier defined linearityCoeffs (%s) for %s",
448 raise RuntimeError(
"Amplifier %s coeffs %s does not match saved value %s" %
452 """Apply the linearity to an image.
454 If the linearity parameters are populated, use those,
455 otherwise use the values from the detector.
459 image : `~lsst.afw.image.image`
461 detector : `~lsst.afw.cameraGeom.detector`
462 Detector to use
for linearity parameters
if not already
464 log : `~logging.Logger`, optional
465 Log object to use
for logging.
480 if linearizer
is not None:
482 success, outOfRange = linearizer()(ampView, **{
'coeffs': self.
linearityCoeffs[ampName],
485 numOutOfRange += outOfRange
488 elif log
is not None:
489 log.warning(
"Amplifier %s did not linearize.",
493 numLinearized=numLinearized,
494 numOutOfRange=numOutOfRange
499 """Abstract base class functor for correcting non-linearity.
501 Subclasses must define ``__call__`` and set
class variable
502 LinearityType to a string that will be used
for linearity type
in
503 the cameraGeom.Amplifier.linearityType field.
505 All linearity corrections should be defined
in terms of an
506 additive correction, such that:
508 corrected_value = uncorrected_value + f(uncorrected_value)
514 """Correct non-linearity.
519 Image to be corrected
521 Dictionary of parameter keywords:
524 Coefficient vector (`list` or `numpy.array`).
526 Lookup table data (`numpy.array`).
528 Logger to handle messages (`logging.Logger`).
533 If `
True`, a correction was applied successfully.
538 Raised
if the linearity type listed
in the
539 detector does
not match the
class type.
545 """Correct non-linearity with a persisted lookup table.
547 The lookup table consists of entries such that given
548 "coefficients" c0, c1:
550 for each i,j of image:
552 colInd = int(c1 + uncorrImage[i,j])
553 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
555 - c0: row index; used to identify which row of the table to use
556 (typically one per amplifier, though one can have multiple
557 amplifiers use the same table)
558 - c1: column index offset; added to the uncorrected image value
559 before truncation; this supports tables that can handle
560 negative image values; also,
if the c1 ends
with .5 then
561 the nearest index
is used instead of truncating to the
564 LinearityType = "LookupTable"
567 """Correct for non-linearity.
572 Image to be corrected
574 Dictionary of parameter keywords:
577 Columnation vector (`list` or `numpy.array`).
579 Lookup table data (`numpy.array`).
581 Logger to handle messages (`logging.Logger`).
585 output : `tuple` [`bool`, `int`]
586 If true, a correction was applied successfully. The
587 integer indicates the number of pixels that were
588 uncorrectable by being out of range.
593 Raised
if the requested row index
is out of the table
598 rowInd, colIndOffset = kwargs['coeffs'][0:2]
599 table = kwargs[
'table']
602 numTableRows = table.shape[0]
604 if rowInd < 0
or rowInd > numTableRows:
605 raise RuntimeError(
"LinearizeLookupTable rowInd=%s not in range[0, %s)" %
606 (rowInd, numTableRows))
607 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
611 if numOutOfRange > 0
and log
is not None:
612 log.warning(
"%s pixels were out of range of the linearization table",
614 if numOutOfRange < image.getArray().size:
615 return True, numOutOfRange
617 return False, numOutOfRange
621 """Correct non-linearity with a polynomial mode.
625 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
627 where ``c_i`` are the linearity coefficients for each amplifier.
628 Lower order coefficients are
not included
as they duplicate other
629 calibration parameters:
632 A coefficient multiplied by ``uncorrImage**0``
is equivalent to
633 bias level. Irrelevant
for correcting non-linearity.
635 A coefficient multiplied by ``uncorrImage**1``
is proportional
636 to the gain. Not necessary
for correcting non-linearity.
638 LinearityType = "Polynomial"
641 """Correct non-linearity.
646 Image to be corrected
648 Dictionary of parameter keywords:
651 Coefficient vector (`list` or `numpy.array`).
652 If the order of the polynomial
is n, this list
653 should have a length of n-1 (
"k0" and "k1" are
654 not needed
for the correction).
656 Logger to handle messages (`logging.Logger`).
660 output : `tuple` [`bool`, `int`]
661 If true, a correction was applied successfully. The
662 integer indicates the number of pixels that were
663 uncorrectable by being out of range.
665 if not np.any(np.isfinite(kwargs[
'coeffs'])):
667 if not np.any(kwargs[
'coeffs']):
670 ampArray = image.getArray()
671 correction = np.zeros_like(ampArray)
672 for order, coeff
in enumerate(kwargs[
'coeffs'], start=2):
673 correction += coeff * np.power(ampArray, order)
674 ampArray += correction
680 """Correct non-linearity with a squared model.
682 corrImage = uncorrImage + c0*uncorrImage^2
684 where c0 is linearity coefficient 0
for each amplifier.
686 LinearityType = "Squared"
689 """Correct for non-linearity.
694 Image to be corrected
696 Dictionary of parameter keywords:
699 Coefficient vector (`list` or `numpy.array`).
701 Logger to handle messages (`logging.Logger`).
705 output : `tuple` [`bool`, `int`]
706 If true, a correction was applied successfully. The
707 integer indicates the number of pixels that were
708 uncorrectable by being out of range.
711 sqCoeff = kwargs['coeffs'][0]
713 ampArr = image.getArray()
714 ampArr *= (1 + sqCoeff*ampArr)
721 """Correct non-linearity with a spline model.
723 corrImage = uncorrImage - Spline(coeffs, uncorrImage)
728 The spline fit calculates a correction as a function of the
729 expected linear flux term. Because of this, the correction needs
730 to be subtracted
from the observed flux.
733 LinearityType = "Spline"
736 """Correct for non-linearity.
741 Image to be corrected
743 Dictionary of parameter keywords:
746 Coefficient vector (`list` or `numpy.array`).
748 Logger to handle messages (`logging.Logger`).
752 output : `tuple` [`bool`, `int`]
753 If true, a correction was applied successfully. The
754 integer indicates the number of pixels that were
755 uncorrectable by being out of range.
757 splineCoeff = kwargs['coeffs']
758 centers, values = np.split(splineCoeff, 2)
765 if centers[0] != 0.0:
766 centers = np.concatenate(([0.0], centers))
767 values = np.concatenate(([0.0], values))
772 ampArr = image.getArray()
773 delta = interp.interpolate(ampArr.flatten())
774 ampArr -= np.array(delta).reshape(ampArr.shape)
780 """Do not correct non-linearity.
782 LinearityType = "Proportional"
785 """Do not correct for non-linearity.
790 Image to be corrected
792 Dictionary of parameter keywords:
795 Coefficient vector (`list` or `numpy.array`).
797 Logger to handle messages (`logging.Logger`).
801 output : `tuple` [`bool`, `int`]
802 If true, a correction was applied successfully. The
803 integer indicates the number of pixels that were
804 uncorrectable by being out of range.
810 """Do not correct non-linearity.
812 LinearityType = "None"
815 """Do not correct for non-linearity.
820 Image to be corrected
822 Dictionary of parameter keywords:
825 Coefficient vector (`list` or `numpy.array`).
827 Logger to handle messages (`logging.Logger`).
831 output : `tuple` [`bool`, `int`]
832 If true, a correction was applied successfully. The
833 integer indicates the number of pixels that were
834 uncorrectable by being out of range.
Geometry and electronic information about raw amplifier images.
A representation of a detector in a mosaic camera.
A class to represent a 2-dimensional array of pixels.
An integer coordinate rectangle.
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)
daf::base::PropertyList * list
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.