25from astropy.table
import Table
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 fitResiduals : `dict` [`str`, `numpy.array`], optional
93 Residuals of the fit, indexed
as above. Used
for
94 calculating photdiode corrections
95 linearFit : The linear fit to the low flux region of the curve.
97 tableData : `numpy.array`, optional
98 Lookup table data
for the linearity correction.
100 _OBSTYPE = "LINEARIZER"
101 _SCHEMA =
'Gen3 Linearizer'
118 if table
is not None:
119 if len(table.shape) != 2:
120 raise RuntimeError(
"table shape = %s; must have two dimensions" % (table.shape,))
121 if table.shape[1] < table.shape[0]:
122 raise RuntimeError(
"table shape = %s; indices are switched" % (table.shape,))
123 self.
tableDatatableData = np.array(table, order=
"C")
128 'linearityCoeffs',
'linearityType',
'linearityBBox',
129 'fitParams',
'fitParamsErr',
'fitChiSq',
130 'fitResiduals',
'linearFit',
'tableData'])
133 """Update metadata keywords with new values.
135 This calls the base class's method after ensuring the required
136 calibration keywords will be saved.
140 setDate : `bool`, optional
141 Update the CALIBDATE fields in the metadata to the current
142 time. Defaults to
False.
144 Other keyword parameters to set
in the metadata.
147 kwargs[
'OVERRIDE'] = self.
overrideoverride
148 kwargs[
'HAS_TABLE'] = self.
tableDatatableData
is not None
153 """Read linearity parameters from a detector.
157 detector : `lsst.afw.cameraGeom.detector`
158 Input detector with parameters to use.
163 The calibration constructed
from the detector.
171 for amp
in detector.getAmplifiers():
172 ampName = amp.getName()
174 self.
linearityTypelinearityType[ampName] = amp.getLinearityType()
175 self.
linearityCoeffslinearityCoeffs[ampName] = amp.getLinearityCoeffs()
182 """Construct a calibration from a dictionary of properties
187 Dictionary of properties
191 calib : `lsst.ip.isr.Linearity`
192 Constructed calibration.
197 Raised if the supplied dictionary
is for a different
203 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
204 raise RuntimeError(f
"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
205 f
"found {dictionary['metadata']['OBSTYPE']}")
207 calib.setMetadata(dictionary[
'metadata'])
209 calib.hasLinearity = dictionary.get(
'hasLinearity',
210 dictionary[
'metadata'].get(
'HAS_LINEARITY',
False))
211 calib.override = dictionary.get(
'override',
True)
213 if calib.hasLinearity:
214 for ampName
in dictionary[
'amplifiers']:
215 amp = dictionary[
'amplifiers'][ampName]
216 calib.ampNames.append(ampName)
217 calib.linearityCoeffs[ampName] = np.array(amp.get(
'linearityCoeffs', [0.0]))
218 calib.linearityType[ampName] = amp.get(
'linearityType',
'None')
219 calib.linearityBBox[ampName] = amp.get(
'linearityBBox',
None)
221 calib.fitParams[ampName] = np.array(amp.get(
'fitParams', [0.0]))
222 calib.fitParamsErr[ampName] = np.array(amp.get(
'fitParamsErr', [0.0]))
223 calib.fitChiSq[ampName] = amp.get(
'fitChiSq', np.nan)
224 calib.fitResiduals[ampName] = np.array(amp.get(
'fitResiduals', [0.0]))
225 calib.linearFit[ampName] = np.array(amp.get(
'linearFit', [0.0]))
227 calib.tableData = dictionary.get(
'tableData',
None)
229 calib.tableData = np.array(calib.tableData)
234 """Return linearity parameters as a dict.
242 outDict = {'metadata': self.
getMetadatagetMetadata(),
246 'hasTable': self.
tableDatatableData
is not None,
247 'amplifiers': dict(),
250 outDict[
'amplifiers'][ampName] = {
'linearityType': self.
linearityTypelinearityType[ampName],
251 'linearityCoeffs': self.
linearityCoeffslinearityCoeffs[ampName].tolist(),
253 'fitParams': self.
fitParamsfitParams[ampName].tolist(),
254 'fitParamsErr': self.
fitParamsErrfitParamsErr[ampName].tolist(),
255 'fitChiSq': self.
fitChiSqfitChiSq[ampName],
256 'fitResiduals': self.
fitResidualsfitResiduals[ampName].tolist(),
257 'linearFit': self.
linearFitlinearFit[ampName].tolist()}
259 outDict[
'tableData'] = self.
tableDatatableData.tolist()
265 """Read linearity from a FITS file.
267 This method uses the `fromDict` method to create the
268 calibration, after constructing an appropriate dictionary from
273 tableList : `list` [`astropy.table.Table`]
274 afwTable read
from input file name.
279 Linearity parameters.
283 The method reads a FITS file
with 1
or 2 extensions. The metadata
is
284 read
from the header of extension 1, which must exist. Then the table
285 is loaded,
and the [
'AMPLIFIER_NAME',
'TYPE',
'COEFFS',
'BBOX_X0',
286 'BBOX_Y0',
'BBOX_DX',
'BBOX_DY'] columns are read
and used to set each
287 dictionary by looping over rows.
288 Extension 2
is then attempted to read
in the
try block (which only
289 exists
for lookup tables). It has a column named
'LOOKUP_VALUES' that
290 contains a vector of the lookup entries
in each row.
292 coeffTable = tableList[0]
294 metadata = coeffTable.meta
296 inDict['metadata'] = metadata
297 inDict[
'hasLinearity'] = metadata.get(
'HAS_LINEARITY',
False)
298 inDict[
'amplifiers'] = dict()
300 for record
in coeffTable:
301 ampName = record[
'AMPLIFIER_NAME']
303 fitParams = record[
'FIT_PARAMS']
if 'FIT_PARAMS' in record.columns
else np.array([0.0])
304 fitParamsErr = record[
'FIT_PARAMS_ERR']
if 'FIT_PARAMS_ERR' in record.columns
else np.array([0.0])
305 fitChiSq = record[
'RED_CHI_SQ']
if 'RED_CHI_SQ' in record.columns
else np.nan
306 fitResiduals = record[
'FIT_RES']
if 'FIT_RES' in record.columns
else np.array([0.0])
307 linearFit = record[
'LIN_FIT']
if 'LIN_FIT' in record.columns
else np.array([0.0])
309 inDict[
'amplifiers'][ampName] = {
310 'linearityType': record[
'TYPE'],
311 'linearityCoeffs': record[
'COEFFS'],
312 'linearityBBox':
Box2I(
Point2I(record[
'BBOX_X0'], record[
'BBOX_Y0']),
313 Extent2I(record[
'BBOX_DX'], record[
'BBOX_DY'])),
314 'fitParams': fitParams,
315 'fitParamsErr': fitParamsErr,
316 'fitChiSq': fitChiSq,
317 'fitResiduals': fitResiduals,
318 'linearFit': linearFit,
321 if len(tableList) > 1:
322 tableData = tableList[1]
323 inDict[
'tableData'] = [record[
'LOOKUP_VALUES']
for record
in tableData]
328 """Construct a list of tables containing the information in this
331 The list of tables should create an identical calibration
332 after being passed to this class's fromTable method.
336 tableList : `list` [`astropy.table.Table`]
337 List of tables containing the linearity calibration
343 catalog = Table([{'AMPLIFIER_NAME': ampName,
346 'BBOX_X0': self.
linearityBBoxlinearityBBox[ampName].getMinX(),
347 'BBOX_Y0': self.
linearityBBoxlinearityBBox[ampName].getMinY(),
348 'BBOX_DX': self.
linearityBBoxlinearityBBox[ampName].getWidth(),
349 'BBOX_DY': self.
linearityBBoxlinearityBBox[ampName].getHeight(),
350 'FIT_PARAMS': self.
fitParamsfitParams[ampName],
351 'FIT_PARAMS_ERR': self.
fitParamsErrfitParamsErr[ampName],
352 'RED_CHI_SQ': self.
fitChiSqfitChiSq[ampName],
354 'LIN_FIT': self.
linearFitlinearFit[ampName],
355 }
for ampName
in self.
ampNamesampNames])
357 tableList.append(catalog)
360 catalog = Table([{
'LOOKUP_VALUES': value}
for value
in self.
tableDatatableData])
361 tableList.append(catalog)
365 """Determine the linearity class to use from the type name.
369 linearityTypeName : str
370 String name of the linearity type that is needed.
375 The appropriate linearity
class to use. If no matching
class
376 is found, `
None`
is returned.
378 for t
in [LinearizeLookupTable,
381 LinearizeProportional,
384 if t.LinearityType == linearityTypeName:
389 """Validate linearity for a detector/amplifier.
394 Detector to validate, along with its amplifiers.
396 Single amplifier to validate.
401 Raised
if there
is a mismatch
in linearity parameters,
and
402 the cameraGeom parameters are
not being overridden.
404 amplifiersToCheck = []
407 raise RuntimeError(
"Detector names don't match: %s != %s" %
410 raise RuntimeError(
"Detector IDs don't match: %s != %s" %
413 raise RuntimeError(
"Detector serial numbers don't match: %s != %s" %
416 raise RuntimeError(
"Detector number of amps = %s does not match saved value %s" %
417 (len(detector.getAmplifiers()),
419 amplifiersToCheck.extend(detector.getAmplifiers())
422 amplifiersToCheck.extend(amplifier)
424 for amp
in amplifiersToCheck:
425 ampName = amp.getName()
427 raise RuntimeError(
"Amplifier %s is not in linearity data" %
429 if amp.getLinearityType() != self.
linearityTypelinearityType[ampName]:
431 self.
loglog.
warning(
"Overriding amplifier defined linearityType (%s) for %s",
434 raise RuntimeError(
"Amplifier %s type %s does not match saved value %s" %
435 (ampName, amp.getLinearityType(), self.
linearityTypelinearityType[ampName]))
436 if (amp.getLinearityCoeffs().shape != self.
linearityCoeffslinearityCoeffs[ampName].shape
or not
437 np.allclose(amp.getLinearityCoeffs(), self.
linearityCoeffslinearityCoeffs[ampName], equal_nan=
True)):
439 self.
loglog.
warning(
"Overriding amplifier defined linearityCoeffs (%s) for %s",
442 raise RuntimeError(
"Amplifier %s coeffs %s does not match saved value %s" %
443 (ampName, amp.getLinearityCoeffs(), self.
linearityCoeffslinearityCoeffs[ampName]))
446 """Apply the linearity to an image.
448 If the linearity parameters are populated, use those,
449 otherwise use the values from the detector.
455 detector : `~lsst.afw.cameraGeom.detector`
456 Detector to use
for linearity parameters
if not already
458 log : `~logging.Logger`, optional
459 Log object to use
for logging.
474 if linearizer
is not None:
475 ampView = image.Factory(image, self.
linearityBBoxlinearityBBox[ampName])
476 success, outOfRange = linearizer()(ampView, **{
'coeffs': self.
linearityCoeffslinearityCoeffs[ampName],
479 numOutOfRange += outOfRange
482 elif log
is not None:
483 log.warning(
"Amplifier %s did not linearize.",
487 numLinearized=numLinearized,
488 numOutOfRange=numOutOfRange
493 """Abstract base class functor for correcting non-linearity.
495 Subclasses must define __call__ and set
class variable
496 LinearityType to a string that will be used
for linearity type
in
497 the cameraGeom.Amplifier.linearityType field.
499 All linearity corrections should be defined
in terms of an
500 additive correction, such that:
502 corrected_value = uncorrected_value + f(uncorrected_value)
508 """Correct non-linearity.
513 Image to be corrected
515 Dictionary of parameter keywords:
517 Coefficient vector (`list`
or `numpy.array`).
519 Lookup table data (`numpy.array`).
521 Logger to handle messages (`logging.Logger`).
526 If true, a correction was applied successfully.
531 Raised
if the linearity type listed
in the
532 detector does
not match the
class type.
538 """Correct non-linearity with a persisted lookup table.
540 The lookup table consists of entries such that given
541 "coefficients" c0, c1:
543 for each i,j of image:
545 colInd =
int(c1 + uncorrImage[i,j])
546 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
548 - c0: row index; used to identify which row of the table to use
549 (typically one per amplifier, though one can have multiple
550 amplifiers use the same table)
551 - c1: column index offset; added to the uncorrected image value
552 before truncation; this supports tables that can handle
553 negative image values; also,
if the c1 ends
with .5 then
554 the nearest index
is used instead of truncating to the
557 LinearityType = "LookupTable"
560 """Correct for non-linearity.
565 Image to be corrected
567 Dictionary of parameter keywords:
569 Columnation vector (`list`
or `numpy.array`).
571 Lookup table data (`numpy.array`).
573 Logger to handle messages (`logging.Logger`).
577 output : `tuple` [`bool`, `int`]
578 If true, a correction was applied successfully. The
579 integer indicates the number of pixels that were
580 uncorrectable by being out of range.
585 Raised
if the requested row index
is out of the table
590 rowInd, colIndOffset = kwargs['coeffs'][0:2]
591 table = kwargs[
'table']
594 numTableRows = table.shape[0]
596 if rowInd < 0
or rowInd > numTableRows:
597 raise RuntimeError(
"LinearizeLookupTable rowInd=%s not in range[0, %s)" %
598 (rowInd, numTableRows))
599 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
603 if numOutOfRange > 0
and log
is not None:
604 log.warning(
"%s pixels were out of range of the linearization table",
606 if numOutOfRange < image.getArray().size:
607 return True, numOutOfRange
609 return False, numOutOfRange
613 """Correct non-linearity with a polynomial mode.
615 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
617 where c_i are the linearity coefficients for each amplifier.
618 Lower order coefficients are
not included
as they duplicate other
619 calibration parameters:
621 A coefficient multiplied by uncorrImage**0
is equivalent to
622 bias level. Irrelevant
for correcting non-linearity.
624 A coefficient multiplied by uncorrImage**1
is proportional
625 to the gain. Not necessary
for correcting non-linearity.
627 LinearityType = "Polynomial"
630 """Correct non-linearity.
635 Image to be corrected
637 Dictionary of parameter keywords:
639 Coefficient vector (`list`
or `numpy.array`).
640 If the order of the polynomial
is n, this list
641 should have a length of n-1 (
"k0" and "k1" are
642 not needed
for the correction).
644 Logger to handle messages (`logging.Logger`).
648 output : `tuple` [`bool`, `int`]
649 If true, a correction was applied successfully. The
650 integer indicates the number of pixels that were
651 uncorrectable by being out of range.
653 if not np.any(np.isfinite(kwargs[
'coeffs'])):
655 if not np.any(kwargs[
'coeffs']):
658 ampArray = image.getArray()
659 correction = np.zeros_like(ampArray)
660 for order, coeff
in enumerate(kwargs[
'coeffs'], start=2):
661 correction += coeff * np.power(ampArray, order)
662 ampArray += correction
668 """Correct non-linearity with a squared model.
670 corrImage = uncorrImage + c0*uncorrImage^2
672 where c0 is linearity coefficient 0
for each amplifier.
674 LinearityType = "Squared"
677 """Correct for non-linearity.
682 Image to be corrected
684 Dictionary of parameter keywords:
686 Coefficient vector (`list`
or `numpy.array`).
688 Logger to handle messages (`logging.Logger`).
692 output : `tuple` [`bool`, `int`]
693 If true, a correction was applied successfully. The
694 integer indicates the number of pixels that were
695 uncorrectable by being out of range.
698 sqCoeff = kwargs['coeffs'][0]
700 ampArr = image.getArray()
701 ampArr *= (1 + sqCoeff*ampArr)
708 """Correct non-linearity with a spline model.
710 corrImage = uncorrImage - Spline(coeffs, uncorrImage)
715 The spline fit calculates a correction as a function of the
716 expected linear flux term. Because of this, the correction needs
717 to be subtracted
from the observed flux.
720 LinearityType = "Spline"
723 """Correct for non-linearity.
728 Image to be corrected
730 Dictionary of parameter keywords:
732 Coefficient vector (`list`
or `numpy.array`).
734 Logger to handle messages (`logging.Logger`).
738 output : `tuple` [`bool`, `int`]
739 If true, a correction was applied successfully. The
740 integer indicates the number of pixels that were
741 uncorrectable by being out of range.
743 splineCoeff = kwargs['coeffs']
744 centers, values = np.split(splineCoeff, 2)
748 ampArr = image.getArray()
749 delta = interp.interpolate(ampArr.flatten())
750 ampArr -= np.array(delta).reshape(ampArr.shape)
756 """Do not correct non-linearity.
758 LinearityType = "Proportional"
761 """Do not correct for non-linearity.
766 Image to be corrected
768 Dictionary of parameter keywords:
770 Coefficient vector (`list`
or `numpy.array`).
772 Logger to handle messages (`logging.Logger`).
776 output : `tuple` [`bool`, `int`]
777 If true, a correction was applied successfully. The
778 integer indicates the number of pixels that were
779 uncorrectable by being out of range.
785 """Do not correct non-linearity.
787 LinearityType = "None"
790 """Do not correct for non-linearity.
795 Image to be corrected
797 Dictionary of parameter keywords:
799 Coefficient vector (`list`
or `numpy.array`).
801 Logger to handle messages (`logging.Logger`).
805 output : `tuple` [`bool`, `int`]
806 If true, a correction was applied successfully. The
807 integer indicates the number of pixels that were
808 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.
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