Coverage for python/lsst/ip/isr/linearize.py : 12%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2016 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22import abc
23import copy
24import datetime
26import numpy as np
27import yaml
29import lsst.afw.table as afwTable
30from lsst.daf.base import PropertyList
31from lsst.pipe.base import Struct
32from lsst.geom import Box2I, Point2I, Extent2I
33from .applyLookupTable import applyLookupTable
35__all__ = ["Linearizer",
36 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared",
37 "LinearizeProportional", "LinearizePolynomial", "LinearizeNone"]
40class Linearizer(abc.ABC):
41 """Parameter set for linearization.
43 These parameters are included in cameraGeom.Amplifier, but
44 should be accessible externally to allow for testing.
46 Parameters
47 ----------
48 table : `numpy.array`, optional
49 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
52 To avoid copying the table the last index should vary fastest
53 (numpy default "C" order)
54 detector : `lsst.afw.cameraGeom.Detector`
55 Detector object
56 override : `bool`, optional
57 Override the parameters defined in the detector/amplifier.
58 log : `lsst.log.Log`, optional
59 Logger to handle messages.
61 Raises
62 ------
63 RuntimeError :
64 Raised if the supplied table is not 2D, or if the table has fewer
65 columns than rows (indicating that the indices are swapped).
66 """
68 _OBSTYPE = "linearizer"
69 """The dataset type name used for this class"""
71 def __init__(self, table=None, detector=None, override=False, log=None):
72 self._detectorName = None
73 self._detectorSerial = None
74 self._detectorId = None
75 self._metadata = PropertyList()
77 self.linearityCoeffs = dict()
78 self.linearityType = dict()
79 self.linearityThreshold = dict()
80 self.linearityMaximum = dict()
81 self.linearityUnits = dict()
82 self.linearityBBox = dict()
84 self.fitParams = dict()
85 self.fitParamsErr = dict()
86 self.linearityFitReducedChiSquared = dict()
88 self.override = override
89 self.populated = False
90 self.log = log
92 self.tableData = None
93 if table is not None:
94 if len(table.shape) != 2:
95 raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
96 if table.shape[1] < table.shape[0]:
97 raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
98 self.tableData = np.array(table, order="C")
100 if detector:
101 self.fromDetector(detector)
103 def __call__(self, exposure):
104 """Apply linearity, setting parameters if necessary.
106 Parameters
107 ----------
108 exposure : `lsst.afw.image.Exposure`
109 Exposure to correct.
111 Returns
112 -------
113 output : `lsst.pipe.base.Struct`
114 Linearization results:
115 ``"numAmps"``
116 Number of amplifiers considered.
117 ``"numLinearized"``
118 Number of amplifiers linearized.
119 """
121 def fromDetector(self, detector):
122 """Read linearity parameters from a detector.
124 Parameters
125 ----------
126 detector : `lsst.afw.cameraGeom.detector`
127 Input detector with parameters to use.
128 """
129 self._detectorName = detector.getName()
130 self._detectorSerial = detector.getSerial()
131 self._detectorId = detector.getId()
132 self.populated = True
134 # Do not translate Threshold, Maximum, Units.
135 for amp in detector.getAmplifiers():
136 ampName = amp.getName()
137 self.linearityCoeffs[ampName] = amp.getLinearityCoeffs()
138 self.linearityType[ampName] = amp.getLinearityType()
139 self.linearityBBox[ampName] = amp.getBBox()
141 def fromYaml(self, yamlObject):
142 """Read linearity parameters from a dict.
144 Parameters
145 ----------
146 yamlObject : `dict`
147 Dictionary containing detector and amplifier information.
148 """
149 self.setMetadata(metadata=yamlObject.get('metadata', None))
150 self._detectorName = yamlObject['detectorName']
151 self._detectorSerial = yamlObject['detectorSerial']
152 self._detectorId = yamlObject['detectorId']
153 self.populated = True
154 self.override = True
156 for ampName in yamlObject['amplifiers']:
157 amp = yamlObject['amplifiers'][ampName]
158 self.linearityCoeffs[ampName] = np.array(amp.get('linearityCoeffs', None), dtype=np.float64)
159 self.linearityType[ampName] = amp.get('linearityType', 'None')
160 self.linearityBBox[ampName] = amp.get('linearityBBox', None)
161 self.fitParams[ampName] = np.array(amp.get('linearityFitParams', None), dtype=np.float64)
162 self.fitParamsErr[ampName] = np.array(amp.get('linearityFitParamsErr', None), dtype=np.float64)
163 self.linearityFitReducedChiSquared[ampName] = amp.get('linearityFitReducedChiSquared', None)
165 if self.tableData is None:
166 self.tableData = yamlObject.get('tableData', None)
167 if self.tableData:
168 self.tableData = np.array(self.tableData)
170 return self
172 def toDict(self):
173 """Return linearity parameters as a dict.
175 Returns
176 -------
177 outDict : `dict`:
178 """
179 # metadata copied from defects code
180 now = datetime.datetime.utcnow()
181 self.updateMetadata(date=now)
183 outDict = {'metadata': self.getMetadata(),
184 'detectorName': self._detectorName,
185 'detectorSerial': self._detectorSerial,
186 'detectorId': self._detectorId,
187 'hasTable': self.tableData is not None,
188 'amplifiers': dict()}
189 for ampName in self.linearityType:
190 outDict['amplifiers'][ampName] = {'linearityType': self.linearityType[ampName],
191 'linearityCoeffs': self.linearityCoeffs[ampName],
192 'linearityBBox': self.linearityBBox[ampName],
193 'linearityFitParams': self.fitParams[ampName],
194 'linearityFitParamsErr': self.fitParamsErr[ampName],
195 'linearityFitReducedChiSquared': (
196 self.linearityFitReducedChiSquared[ampName])}
197 if self.tableData is not None:
198 outDict['tableData'] = self.tableData.tolist()
200 return outDict
202 @classmethod
203 def readText(cls, filename):
204 """Read linearity from text file.
206 Parameters
207 ----------
208 filename : `str`
209 Name of the file containing the linearity definition.
210 Returns
211 -------
212 linearity : `~lsst.ip.isr.linearize.Linearizer``
213 Linearity parameters.
214 """
215 data = ''
216 with open(filename, 'r') as f:
217 data = yaml.load(f, Loader=yaml.CLoader)
218 return cls().fromYaml(data)
220 def writeText(self, filename):
221 """Write the linearity model to a text file.
223 Parameters
224 ----------
225 filename : `str`
226 Name of the file to write.
228 Returns
229 -------
230 used : `str`
231 The name of the file used to write the data.
233 Raises
234 ------
235 RuntimeError :
236 Raised if filename does not end in ".yaml".
238 Notes
239 -----
240 The file is written to YAML format and will include any metadata
241 associated with the `Linearity`.
242 """
243 outDict = self.toDict()
244 if filename.lower().endswith((".yaml")):
245 with open(filename, 'w') as f:
246 yaml.dump(outDict, f)
247 else:
248 raise RuntimeError(f"Attempt to write to a file {filename} that does not end in '.yaml'")
250 return filename
252 @classmethod
253 def fromTable(cls, table, tableExtTwo=None):
254 """Read linearity from a FITS file.
256 Parameters
257 ----------
258 table : `lsst.afw.table`
259 afwTable read from input file name.
260 tableExtTwo: `lsst.afw.table`, optional
261 afwTable read from second extension of input file name
263 Returns
264 -------
265 linearity : `~lsst.ip.isr.linearize.Linearizer``
266 Linearity parameters.
268 Notes
269 -----
270 The method reads a FITS file with 1 or 2 extensions. The metadata is read from the header of
271 extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME', 'TYPE',
272 'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to
273 set each dictionary by looping over rows.
274 Eextension 2 is then attempted to read in the try block (which only exists for lookup tables).
275 It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row.
276 """
277 metadata = table.getMetadata()
278 schema = table.getSchema()
280 linDict = dict()
281 linDict['metadata'] = metadata
282 linDict['detectorId'] = metadata['DETECTOR']
283 linDict['detectorName'] = metadata['DETECTOR_NAME']
284 try:
285 linDict['detectorSerial'] = metadata['DETECTOR_SERIAL']
286 except Exception:
287 linDict['detectorSerial'] = 'NOT SET'
288 linDict['amplifiers'] = dict()
290 # Preselect the keys
291 ampNameKey = schema['AMPLIFIER_NAME'].asKey()
292 typeKey = schema['TYPE'].asKey()
293 coeffsKey = schema['COEFFS'].asKey()
294 x0Key = schema['BBOX_X0'].asKey()
295 y0Key = schema['BBOX_Y0'].asKey()
296 dxKey = schema['BBOX_DX'].asKey()
297 dyKey = schema['BBOX_DY'].asKey()
298 fitParamsKey = schema["FIT_PARAMS"].asKey()
299 fitParamsErrKey = schema["FIT_PARAMS_ERR"].asKey()
300 reducedChiSquaredKey = schema["RED_CHI_SQ"].asKey()
302 for record in table:
303 ampName = record[ampNameKey]
304 ampDict = dict()
305 ampDict['linearityType'] = record[typeKey]
306 ampDict['linearityCoeffs'] = record[coeffsKey]
307 ampDict['linearityBBox'] = Box2I(Point2I(record[x0Key], record[y0Key]),
308 Extent2I(record[dxKey], record[dyKey]))
309 ampDict['linearityFitParams'] = record[fitParamsKey]
310 ampDict['linearityFitParamsErr'] = record[fitParamsErrKey]
311 ampDict['linearityFitReducedChiSquared'] = record[reducedChiSquaredKey]
313 linDict['amplifiers'][ampName] = ampDict
315 if tableExtTwo is not None:
316 lookupValuesKey = 'LOOKUP_VALUES'
317 linDict["tableData"] = [record[lookupValuesKey] for record in tableExtTwo]
319 return cls().fromYaml(linDict)
321 @classmethod
322 def readFits(cls, filename):
323 """Read linearity from a FITS file.
325 Parameters
326 ----------
327 filename : `str`
328 Name of the file containing the linearity definition.
329 Returns
330 -------
331 linearity : `~lsst.ip.isr.linearize.Linearizer``
332 Linearity parameters.
334 Notes
335 -----
336 This method and `fromTable` read a FITS file with 1 or 2 extensions. The metadata is read from the
337 header of extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME',
338 'TYPE', 'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to
339 set each dictionary by looping over rows.
340 Extension 2 is then attempted to read in the try block (which only exists for lookup tables).
341 It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row.
342 """
343 table = afwTable.BaseCatalog.readFits(filename)
344 tableExtTwo = None
345 try:
346 tableExtTwo = afwTable.BaseCatalog.readFits(filename, 2)
347 except Exception:
348 pass
349 return cls().fromTable(table, tableExtTwo=tableExtTwo)
351 def toAmpTable(self, metadata):
352 """Produce linearity catalog
354 Parameters
355 ----------
356 metadata : `lsst.daf.base.PropertyList`
357 Linearizer metadata
359 Returns
360 -------
361 catalog : `lsst.afw.table.BaseCatalog`
362 Catalog to write
363 """
364 metadata["LINEARITY_SCHEMA"] = "Linearity table"
365 metadata["LINEARITY_VERSION"] = 1
367 # Now pack it into a fits table.
368 length = max([len(self.linearityCoeffs[x]) for x in self.linearityCoeffs.keys()])
370 schema = afwTable.Schema()
371 names = schema.addField("AMPLIFIER_NAME", type="String", size=16, doc="linearity amplifier name")
372 types = schema.addField("TYPE", type="String", size=16, doc="linearity type names")
373 coeffs = schema.addField("COEFFS", type="ArrayD", size=length, doc="linearity coefficients")
374 boxX = schema.addField("BBOX_X0", type="I", doc="linearity bbox minimum x")
375 boxY = schema.addField("BBOX_Y0", type="I", doc="linearity bbox minimum y")
376 boxDx = schema.addField("BBOX_DX", type="I", doc="linearity bbox x dimension")
377 boxDy = schema.addField("BBOX_DY", type="I", doc="linearity bbox y dimension")
379 if (self.fitParams):
380 lengthFitParams = max([len(self.fitParams[x]) for x in self.fitParams.keys()])
382 fitParams = schema.addField("FIT_PARAMS", type="ArrayD", size=lengthFitParams,
383 doc="parameters of linearity polynomial fit")
384 fitParamsErr = schema.addField("FIT_PARAMS_ERR", type="ArrayD", size=lengthFitParams,
385 doc="errors of parameters of linearity polynomial fit")
386 reducedChiSquared = schema.addField("RED_CHI_SQ", type="D",
387 doc="unweighted reduced chi sq. from linearity pol. fit")
389 catalog = afwTable.BaseCatalog(schema)
390 catalog.resize(len(self.linearityCoeffs.keys()))
392 for ii, ampName in enumerate(self.linearityType):
393 catalog[ii][names] = ampName
394 catalog[ii][types] = self.linearityType[ampName]
395 catalog[ii][coeffs] = np.array(self.linearityCoeffs[ampName], dtype=float)
396 if (self.fitParams):
397 catalog[ii][fitParams] = np.array(self.fitParams[ampName], dtype=float)
398 catalog[ii][fitParamsErr] = np.array(self.fitParamsErr[ampName], dtype=float)
399 catalog[ii][reducedChiSquared] = self.linearityFitReducedChiSquared[ampName]
401 bbox = self.linearityBBox[ampName]
402 catalog[ii][boxX], catalog[ii][boxY] = bbox.getMin()
403 catalog[ii][boxDx], catalog[ii][boxDy] = bbox.getDimensions()
404 catalog.setMetadata(metadata)
406 return catalog
408 def toTableDataTable(self, metadata):
409 """Produce linearity catalog from table data
411 Parameters
412 ----------
413 metadata : `lsst.daf.base.PropertyList`
414 Linearizer metadata
416 Returns
417 -------
418 catalog : `lsst.afw.table.BaseCatalog`
419 Catalog to write
420 """
422 schema = afwTable.Schema()
423 dimensions = self.tableData.shape
424 lut = schema.addField("LOOKUP_VALUES", type='ArrayF', size=dimensions[1],
425 doc="linearity lookup data")
426 catalog = afwTable.BaseCatalog(schema)
427 catalog.resize(dimensions[0])
429 for ii in range(dimensions[0]):
430 catalog[ii][lut] = np.array(self.tableData[ii], dtype=np.float32)
432 metadata["LINEARITY_LOOKUP"] = True
433 catalog.setMetadata(metadata)
435 return catalog
437 def writeFits(self, filename):
438 """Write the linearity model to a FITS file.
440 Parameters
441 ----------
442 filename : `str`
443 Name of the file to write.
445 Notes
446 -----
447 The file is written to YAML format and will include any metadata
448 associated with the `Linearity`.
449 """
450 now = datetime.datetime.utcnow()
451 self.updateMetadata(date=now)
452 metadata = copy.copy(self.getMetadata())
453 catalog = self.toAmpTable(metadata)
454 catalog.writeFits(filename)
456 if self.tableData is not None:
457 catalog = self.toTableDataTable(metadata)
458 catalog.writeFits(filename, "a")
460 return
462 def getMetadata(self):
463 """Retrieve metadata associated with this `Linearizer`.
465 Returns
466 -------
467 meta : `lsst.daf.base.PropertyList`
468 Metadata. The returned `~lsst.daf.base.PropertyList` can be
469 modified by the caller and the changes will be written to
470 external files.
471 """
472 return self._metadata
474 def setMetadata(self, metadata=None):
475 """Store a copy of the supplied metadata with the `Linearizer`.
477 Parameters
478 ----------
479 metadata : `lsst.daf.base.PropertyList`, optional
480 Metadata to associate with the linearizer. Will be copied and
481 overwrite existing metadata. If not supplied the existing
482 metadata will be reset.
483 """
484 if metadata is None:
485 self._metadata = PropertyList()
486 else:
487 self._metadata = copy.copy(metadata)
489 # Ensure that we have the obs type required by calibration ingest
490 self._metadata["OBSTYPE"] = self._OBSTYPE
492 def updateMetadata(self, date=None, detectorId=None, detectorName=None, instrumentName=None, calibId=None,
493 serial=None):
494 """Update metadata keywords with new values.
496 Parameters
497 ----------
498 date : `datetime.datetime`, optional
499 detectorId : `int`, optional
500 detectorName: `str`, optional
501 instrumentName : `str`, optional
502 calibId: `str`, optional
503 serial: detector serial, `str`, optional
505 """
506 mdOriginal = self.getMetadata()
507 mdSupplemental = dict()
509 if date:
510 mdSupplemental['CALIBDATE'] = date.isoformat()
511 mdSupplemental['CALIB_CREATION_DATE'] = date.date().isoformat(),
512 mdSupplemental['CALIB_CREATION_TIME'] = date.time().isoformat(),
513 if detectorId:
514 mdSupplemental['DETECTOR'] = f"{detectorId}"
515 if detectorName:
516 mdSupplemental['DETECTOR_NAME'] = detectorName
517 if instrumentName:
518 mdSupplemental['INSTRUME'] = instrumentName
519 if calibId:
520 mdSupplemental['CALIB_ID'] = calibId
521 if serial:
522 mdSupplemental['DETECTOR_SERIAL'] = serial
524 mdOriginal.update(mdSupplemental)
526 def getLinearityTypeByName(self, linearityTypeName):
527 """Determine the linearity class to use from the type name.
529 Parameters
530 ----------
531 linearityTypeName : str
532 String name of the linearity type that is needed.
534 Returns
535 -------
536 linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
537 The appropriate linearity class to use. If no matching class
538 is found, `None` is returned.
539 """
540 for t in [LinearizeLookupTable,
541 LinearizeSquared,
542 LinearizePolynomial,
543 LinearizeProportional,
544 LinearizeNone]:
545 if t.LinearityType == linearityTypeName:
546 return t
547 return None
549 def validate(self, detector=None, amplifier=None):
550 """Validate linearity for a detector/amplifier.
552 Parameters
553 ----------
554 detector : `lsst.afw.cameraGeom.Detector`, optional
555 Detector to validate, along with its amplifiers.
556 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
557 Single amplifier to validate.
559 Raises
560 ------
561 RuntimeError :
562 Raised if there is a mismatch in linearity parameters, and
563 the cameraGeom parameters are not being overridden.
564 """
565 amplifiersToCheck = []
566 if detector:
567 if self._detectorName != detector.getName():
568 raise RuntimeError("Detector names don't match: %s != %s" %
569 (self._detectorName, detector.getName()))
570 if int(self._detectorId) != int(detector.getId()):
571 raise RuntimeError("Detector IDs don't match: %s != %s" %
572 (int(self._detectorId), int(detector.getId())))
573 if self._detectorSerial != detector.getSerial():
574 raise RuntimeError("Detector serial numbers don't match: %s != %s" %
575 (self._detectorSerial, detector.getSerial()))
576 if len(detector.getAmplifiers()) != len(self.linearityCoeffs.keys()):
577 raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
578 (len(detector.getAmplifiers()),
579 len(self.linearityCoeffs.keys())))
580 amplifiersToCheck.extend(detector.getAmplifiers())
582 if amplifier:
583 amplifiersToCheck.extend(amplifier)
585 for amp in amplifiersToCheck:
586 ampName = amp.getName()
587 if ampName not in self.linearityCoeffs.keys():
588 raise RuntimeError("Amplifier %s is not in linearity data" %
589 (ampName, ))
590 if amp.getLinearityType() != self.linearityType[ampName]:
591 if self.override:
592 self.log.warn("Overriding amplifier defined linearityType (%s) for %s",
593 self.linearityType[ampName], ampName)
594 else:
595 raise RuntimeError("Amplifier %s type %s does not match saved value %s" %
596 (ampName, amp.getLinearityType(), self.linearityType[ampName]))
597 if (amp.getLinearityCoeffs().shape != self.linearityCoeffs[ampName].shape or not
598 np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffs[ampName], equal_nan=True)):
599 if self.override:
600 self.log.warn("Overriding amplifier defined linearityCoeffs (%s) for %s",
601 self.linearityCoeffs[ampName], ampName)
602 else:
603 raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" %
604 (ampName, amp.getLinearityCoeffs(), self.linearityCoeffs[ampName]))
606 def applyLinearity(self, image, detector=None, log=None):
607 """Apply the linearity to an image.
609 If the linearity parameters are populated, use those,
610 otherwise use the values from the detector.
612 Parameters
613 ----------
614 image : `~lsst.afw.image.image`
615 Image to correct.
616 detector : `~lsst.afw.cameraGeom.detector`
617 Detector to use for linearity parameters if not already
618 populated.
619 log : `~lsst.log.Log`, optional
620 Log object to use for logging.
621 """
622 if log is None:
623 log = self.log
624 if detector and not self.populated:
625 self.fromDetector(detector)
627 self.validate(detector)
629 numAmps = 0
630 numLinearized = 0
631 numOutOfRange = 0
632 for ampName in self.linearityType.keys():
633 linearizer = self.getLinearityTypeByName(self.linearityType[ampName])
634 numAmps += 1
635 if linearizer is not None:
636 ampView = image.Factory(image, self.linearityBBox[ampName])
637 success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffs[ampName],
638 'table': self.tableData,
639 'log': self.log})
640 numOutOfRange += outOfRange
641 if success:
642 numLinearized += 1
643 elif log is not None:
644 log.warn("Amplifier %s did not linearize.",
645 ampName)
646 return Struct(
647 numAmps=numAmps,
648 numLinearized=numLinearized,
649 numOutOfRange=numOutOfRange
650 )
653class LinearizeBase(metaclass=abc.ABCMeta):
654 """Abstract base class functor for correcting non-linearity.
656 Subclasses must define __call__ and set class variable
657 LinearityType to a string that will be used for linearity type in
658 the cameraGeom.Amplifier.linearityType field.
660 All linearity corrections should be defined in terms of an
661 additive correction, such that:
663 corrected_value = uncorrected_value + f(uncorrected_value)
664 """
665 LinearityType = None # linearity type, a string used for AmpInfoCatalogs
667 @abc.abstractmethod
668 def __call__(self, image, **kwargs):
669 """Correct non-linearity.
671 Parameters
672 ----------
673 image : `lsst.afw.image.Image`
674 Image to be corrected
675 kwargs : `dict`
676 Dictionary of parameter keywords:
677 ``"coeffs"``
678 Coefficient vector (`list` or `numpy.array`).
679 ``"table"``
680 Lookup table data (`numpy.array`).
681 ``"log"``
682 Logger to handle messages (`lsst.log.Log`).
684 Returns
685 -------
686 output : `bool`
687 If true, a correction was applied successfully.
689 Raises
690 ------
691 RuntimeError:
692 Raised if the linearity type listed in the
693 detector does not match the class type.
694 """
695 pass
698class LinearizeLookupTable(LinearizeBase):
699 """Correct non-linearity with a persisted lookup table.
701 The lookup table consists of entries such that given
702 "coefficients" c0, c1:
704 for each i,j of image:
705 rowInd = int(c0)
706 colInd = int(c1 + uncorrImage[i,j])
707 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
709 - c0: row index; used to identify which row of the table to use
710 (typically one per amplifier, though one can have multiple
711 amplifiers use the same table)
712 - c1: column index offset; added to the uncorrected image value
713 before truncation; this supports tables that can handle
714 negative image values; also, if the c1 ends with .5 then
715 the nearest index is used instead of truncating to the
716 next smaller index
717 """
718 LinearityType = "LookupTable"
720 def __call__(self, image, **kwargs):
721 """Correct for non-linearity.
723 Parameters
724 ----------
725 image : `lsst.afw.image.Image`
726 Image to be corrected
727 kwargs : `dict`
728 Dictionary of parameter keywords:
729 ``"coeffs"``
730 Columnation vector (`list` or `numpy.array`).
731 ``"table"``
732 Lookup table data (`numpy.array`).
733 ``"log"``
734 Logger to handle messages (`lsst.log.Log`).
736 Returns
737 -------
738 output : `bool`
739 If true, a correction was applied successfully.
741 Raises
742 ------
743 RuntimeError:
744 Raised if the requested row index is out of the table
745 bounds.
746 """
747 numOutOfRange = 0
749 rowInd, colIndOffset = kwargs['coeffs'][0:2]
750 table = kwargs['table']
751 log = kwargs['log']
753 numTableRows = table.shape[0]
754 rowInd = int(rowInd)
755 if rowInd < 0 or rowInd > numTableRows:
756 raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" %
757 (rowInd, numTableRows))
758 tableRow = table[rowInd, :]
759 numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
761 if numOutOfRange > 0 and log is not None:
762 log.warn("%s pixels were out of range of the linearization table",
763 numOutOfRange)
764 if numOutOfRange < image.getArray().size:
765 return True, numOutOfRange
766 else:
767 return False, numOutOfRange
770class LinearizePolynomial(LinearizeBase):
771 """Correct non-linearity with a polynomial mode.
773 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
775 where c_i are the linearity coefficients for each amplifier.
776 Lower order coefficients are not included as they duplicate other
777 calibration parameters:
778 ``"k0"``
779 A coefficient multiplied by uncorrImage**0 is equivalent to
780 bias level. Irrelevant for correcting non-linearity.
781 ``"k1"``
782 A coefficient multiplied by uncorrImage**1 is proportional
783 to the gain. Not necessary for correcting non-linearity.
784 """
785 LinearityType = "Polynomial"
787 def __call__(self, image, **kwargs):
788 """Correct non-linearity.
790 Parameters
791 ----------
792 image : `lsst.afw.image.Image`
793 Image to be corrected
794 kwargs : `dict`
795 Dictionary of parameter keywords:
796 ``"coeffs"``
797 Coefficient vector (`list` or `numpy.array`).
798 If the order of the polynomial is n, this list
799 should have a length of n-1 ("k0" and "k1" are
800 not needed for the correction).
801 ``"log"``
802 Logger to handle messages (`lsst.log.Log`).
804 Returns
805 -------
806 output : `bool`
807 If true, a correction was applied successfully.
808 """
809 if not np.any(np.isfinite(kwargs['coeffs'])):
810 return False, 0
811 if not np.any(kwargs['coeffs']):
812 return False, 0
814 ampArray = image.getArray()
815 correction = np.zeros_like(ampArray)
816 for order, coeff in enumerate(kwargs['coeffs'], start=2):
817 correction += coeff * np.power(ampArray, order)
818 ampArray += correction
820 return True, 0
823class LinearizeSquared(LinearizeBase):
824 """Correct non-linearity with a squared model.
826 corrImage = uncorrImage + c0*uncorrImage^2
828 where c0 is linearity coefficient 0 for each amplifier.
829 """
830 LinearityType = "Squared"
832 def __call__(self, image, **kwargs):
833 """Correct for non-linearity.
835 Parameters
836 ----------
837 image : `lsst.afw.image.Image`
838 Image to be corrected
839 kwargs : `dict`
840 Dictionary of parameter keywords:
841 ``"coeffs"``
842 Coefficient vector (`list` or `numpy.array`).
843 ``"log"``
844 Logger to handle messages (`lsst.log.Log`).
846 Returns
847 -------
848 output : `bool`
849 If true, a correction was applied successfully.
850 """
852 sqCoeff = kwargs['coeffs'][0]
853 if sqCoeff != 0:
854 ampArr = image.getArray()
855 ampArr *= (1 + sqCoeff*ampArr)
856 return True, 0
857 else:
858 return False, 0
861class LinearizeProportional(LinearizeBase):
862 """Do not correct non-linearity.
863 """
864 LinearityType = "Proportional"
866 def __call__(self, image, **kwargs):
867 """Do not correct for non-linearity.
869 Parameters
870 ----------
871 image : `lsst.afw.image.Image`
872 Image to be corrected
873 kwargs : `dict`
874 Dictionary of parameter keywords:
875 ``"coeffs"``
876 Coefficient vector (`list` or `numpy.array`).
877 ``"log"``
878 Logger to handle messages (`lsst.log.Log`).
880 Returns
881 -------
882 output : `bool`
883 If true, a correction was applied successfully.
884 """
885 return True, 0
888class LinearizeNone(LinearizeBase):
889 """Do not correct non-linearity.
890 """
891 LinearityType = "None"
893 def __call__(self, image, **kwargs):
894 """Do not correct for non-linearity.
896 Parameters
897 ----------
898 image : `lsst.afw.image.Image`
899 Image to be corrected
900 kwargs : `dict`
901 Dictionary of parameter keywords:
902 ``"coeffs"``
903 Coefficient vector (`list` or `numpy.array`).
904 ``"log"``
905 Logger to handle messages (`lsst.log.Log`).
907 Returns
908 -------
909 output : `bool`
910 If true, a correction was applied successfully.
911 """
912 return True, 0