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.override = override
85 self.populated = False
86 self.log = log
88 self.tableData = None
89 if table is not None:
90 if len(table.shape) != 2:
91 raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
92 if table.shape[1] < table.shape[0]:
93 raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
94 self.tableData = np.array(table, order="C")
96 if detector:
97 self.fromDetector(detector)
99 def __call__(self, exposure):
100 """Apply linearity, setting parameters if necessary.
102 Parameters
103 ----------
104 exposure : `lsst.afw.image.Exposure`
105 Exposure to correct.
107 Returns
108 -------
109 output : `lsst.pipe.base.Struct`
110 Linearization results:
111 ``"numAmps"``
112 Number of amplifiers considered.
113 ``"numLinearized"``
114 Number of amplifiers linearized.
115 """
117 def fromDetector(self, detector):
118 """Read linearity parameters from a detector.
120 Parameters
121 ----------
122 detector : `lsst.afw.cameraGeom.detector`
123 Input detector with parameters to use.
124 """
125 self._detectorName = detector.getName()
126 self._detectorSerial = detector.getSerial()
127 self._detectorId = detector.getId()
128 self.populated = True
130 # Do not translate Threshold, Maximum, Units.
131 for amp in detector.getAmplifiers():
132 ampName = amp.getName()
133 self.linearityCoeffs[ampName] = amp.getLinearityCoeffs()
134 self.linearityType[ampName] = amp.getLinearityType()
135 self.linearityBBox[ampName] = amp.getBBox()
137 def fromYaml(self, yamlObject):
138 """Read linearity parameters from a dict.
140 Parameters
141 ----------
142 yamlObject : `dict`
143 Dictionary containing detector and amplifier information.
144 """
145 self.setMetadata(metadata=yamlObject.get('metadata', None))
146 self._detectorName = yamlObject['detectorName']
147 self._detectorSerial = yamlObject['detectorSerial']
148 self._detectorId = yamlObject['detectorId']
149 self.populated = True
150 self.override = True
152 for ampName in yamlObject['amplifiers']:
153 amp = yamlObject['amplifiers'][ampName]
154 self.linearityCoeffs[ampName] = np.array(amp.get('linearityCoeffs', None), dtype=np.float64)
155 self.linearityType[ampName] = amp.get('linearityType', 'None')
156 self.linearityBBox[ampName] = amp.get('linearityBBox', None)
158 if self.tableData is None:
159 self.tableData = yamlObject.get('tableData', None)
160 if self.tableData:
161 self.tableData = np.array(self.tableData)
163 return self
165 def toDict(self):
166 """Return linearity parameters as a dict.
168 Returns
169 -------
170 outDict : `dict`:
171 """
172 # metadata copied from defects code
173 now = datetime.datetime.utcnow()
174 self.updateMetadata(date=now)
176 outDict = {'metadata': self.getMetadata(),
177 'detectorName': self._detectorName,
178 'detectorSerial': self._detectorSerial,
179 'detectorId': self._detectorId,
180 'hasTable': self.tableData is not None,
181 'amplifiers': dict()}
182 for ampName in self.linearityType:
183 outDict['amplifiers'][ampName] = {'linearityType': self.linearityType[ampName],
184 'linearityCoeffs': self.linearityCoeffs[ampName],
185 'linearityBBox': self.linearityBBox[ampName]}
186 if self.tableData is not None:
187 outDict['tableData'] = self.tableData.tolist()
189 return outDict
191 @classmethod
192 def readText(cls, filename):
193 """Read linearity from text file.
195 Parameters
196 ----------
197 filename : `str`
198 Name of the file containing the linearity definition.
199 Returns
200 -------
201 linearity : `~lsst.ip.isr.linearize.Linearizer``
202 Linearity parameters.
203 """
204 data = ''
205 with open(filename, 'r') as f:
206 data = yaml.load(f, Loader=yaml.CLoader)
207 return cls().fromYaml(data)
209 def writeText(self, filename):
210 """Write the linearity model to a text file.
212 Parameters
213 ----------
214 filename : `str`
215 Name of the file to write.
217 Returns
218 -------
219 used : `str`
220 The name of the file used to write the data.
222 Raises
223 ------
224 RuntimeError :
225 Raised if filename does not end in ".yaml".
227 Notes
228 -----
229 The file is written to YAML format and will include any metadata
230 associated with the `Linearity`.
231 """
232 outDict = self.toDict()
233 if filename.lower().endswith((".yaml")):
234 with open(filename, 'w') as f:
235 yaml.dump(outDict, f)
236 else:
237 raise RuntimeError(f"Attempt to write to a file {filename} that does not end in '.yaml'")
239 return filename
241 @classmethod
242 def fromTable(cls, table, tableExtTwo=None):
243 """Read linearity from a FITS file.
245 Parameters
246 ----------
247 table : `lsst.afw.table`
248 afwTable read from input file name.
249 tableExtTwo: `lsst.afw.table`, optional
250 afwTable read from second extension of input file name
252 Returns
253 -------
254 linearity : `~lsst.ip.isr.linearize.Linearizer``
255 Linearity parameters.
257 Notes
258 -----
259 The method reads a FITS file with 1 or 2 extensions. The metadata is read from the header of
260 extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME', 'TYPE',
261 'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to
262 set each dictionary by looping over rows.
263 Eextension 2 is then attempted to read in the try block (which only exists for lookup tables).
264 It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row.
265 """
266 metadata = table.getMetadata()
267 schema = table.getSchema()
269 linDict = dict()
270 linDict['metadata'] = metadata
271 linDict['detectorId'] = metadata['DETECTOR']
272 linDict['detectorName'] = metadata['DETECTOR_NAME']
273 try:
274 linDict['detectorSerial'] = metadata['DETECTOR_SERIAL']
275 except Exception:
276 linDict['detectorSerial'] = 'NOT SET'
277 linDict['amplifiers'] = dict()
279 # Preselect the keys
280 ampNameKey = schema['AMPLIFIER_NAME'].asKey()
281 typeKey = schema['TYPE'].asKey()
282 coeffsKey = schema['COEFFS'].asKey()
283 x0Key = schema['BBOX_X0'].asKey()
284 y0Key = schema['BBOX_Y0'].asKey()
285 dxKey = schema['BBOX_DX'].asKey()
286 dyKey = schema['BBOX_DY'].asKey()
288 for record in table:
289 ampName = record[ampNameKey]
290 ampDict = dict()
291 ampDict['linearityType'] = record[typeKey]
292 ampDict['linearityCoeffs'] = record[coeffsKey]
293 ampDict['linearityBBox'] = Box2I(Point2I(record[x0Key], record[y0Key]),
294 Extent2I(record[dxKey], record[dyKey]))
296 linDict['amplifiers'][ampName] = ampDict
298 if tableExtTwo is not None:
299 lookupValuesKey = 'LOOKUP_VALUES'
300 linDict["tableData"] = [record[lookupValuesKey] for record in tableExtTwo]
302 return cls().fromYaml(linDict)
304 @classmethod
305 def readFits(cls, filename):
306 """Read linearity from a FITS file.
308 Parameters
309 ----------
310 filename : `str`
311 Name of the file containing the linearity definition.
312 Returns
313 -------
314 linearity : `~lsst.ip.isr.linearize.Linearizer``
315 Linearity parameters.
317 Notes
318 -----
319 This method and `fromTable` read a FITS file with 1 or 2 extensions. The metadata is read from the
320 header of extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME',
321 'TYPE', 'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to
322 set each dictionary by looping over rows.
323 Extension 2 is then attempted to read in the try block (which only exists for lookup tables).
324 It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row.
325 """
326 table = afwTable.BaseCatalog.readFits(filename)
327 tableExtTwo = None
328 try:
329 tableExtTwo = afwTable.BaseCatalog.readFits(filename, 2)
330 except Exception:
331 pass
332 return cls().fromTable(table, tableExtTwo=tableExtTwo)
334 def toAmpTable(self, metadata):
335 """Produce linearity catalog
337 Parameters
338 ----------
339 metadata : `lsst.daf.base.PropertyList`
340 Linearizer metadata
342 Returns
343 -------
344 catalog : `lsst.afw.table.BaseCatalog`
345 Catalog to write
346 """
347 metadata["LINEARITY_SCHEMA"] = "Linearity table"
348 metadata["LINEARITY_VERSION"] = 1
350 # Now pack it into a fits table.
351 length = max([len(self.linearityCoeffs[x]) for x in self.linearityCoeffs.keys()])
353 schema = afwTable.Schema()
354 names = schema.addField("AMPLIFIER_NAME", type="String", size=16, doc="linearity amplifier name")
355 types = schema.addField("TYPE", type="String", size=16, doc="linearity type names")
356 coeffs = schema.addField("COEFFS", type="ArrayD", size=length, doc="linearity coefficients")
357 boxX = schema.addField("BBOX_X0", type="I", doc="linearity bbox minimum x")
358 boxY = schema.addField("BBOX_Y0", type="I", doc="linearity bbox minimum y")
359 boxDx = schema.addField("BBOX_DX", type="I", doc="linearity bbox x dimension")
360 boxDy = schema.addField("BBOX_DY", type="I", doc="linearity bbox y dimension")
362 catalog = afwTable.BaseCatalog(schema)
363 catalog.resize(len(self.linearityCoeffs.keys()))
365 for ii, ampName in enumerate(self.linearityType):
366 catalog[ii][names] = ampName
367 catalog[ii][types] = self.linearityType[ampName]
368 catalog[ii][coeffs] = np.array(self.linearityCoeffs[ampName], dtype=float)
370 bbox = self.linearityBBox[ampName]
371 catalog[ii][boxX], catalog[ii][boxY] = bbox.getMin()
372 catalog[ii][boxDx], catalog[ii][boxDy] = bbox.getDimensions()
373 catalog.setMetadata(metadata)
375 return catalog
377 def toTableDataTable(self, metadata):
378 """Produce linearity catalog from table data
380 Parameters
381 ----------
382 metadata : `lsst.daf.base.PropertyList`
383 Linearizer metadata
385 Returns
386 -------
387 catalog : `lsst.afw.table.BaseCatalog`
388 Catalog to write
389 """
391 schema = afwTable.Schema()
392 dimensions = self.tableData.shape
393 lut = schema.addField("LOOKUP_VALUES", type='ArrayF', size=dimensions[1],
394 doc="linearity lookup data")
395 catalog = afwTable.BaseCatalog(schema)
396 catalog.resize(dimensions[0])
398 for ii in range(dimensions[0]):
399 catalog[ii][lut] = np.array(self.tableData[ii], dtype=np.float32)
401 metadata["LINEARITY_LOOKUP"] = True
402 catalog.setMetadata(metadata)
404 return catalog
406 def writeFits(self, filename):
407 """Write the linearity model to a FITS file.
409 Parameters
410 ----------
411 filename : `str`
412 Name of the file to write.
414 Notes
415 -----
416 The file is written to YAML format and will include any metadata
417 associated with the `Linearity`.
418 """
419 now = datetime.datetime.utcnow()
420 self.updateMetadata(date=now)
421 metadata = copy.copy(self.getMetadata())
422 catalog = self.toAmpTable(metadata)
423 catalog.writeFits(filename)
425 if self.tableData is not None:
426 catalog = self.toTableDataTable(metadata)
427 catalog.writeFits(filename, "a")
429 return
431 def getMetadata(self):
432 """Retrieve metadata associated with this `Linearizer`.
434 Returns
435 -------
436 meta : `lsst.daf.base.PropertyList`
437 Metadata. The returned `~lsst.daf.base.PropertyList` can be
438 modified by the caller and the changes will be written to
439 external files.
440 """
441 return self._metadata
443 def setMetadata(self, metadata=None):
444 """Store a copy of the supplied metadata with the `Linearizer`.
446 Parameters
447 ----------
448 metadata : `lsst.daf.base.PropertyList`, optional
449 Metadata to associate with the linearizer. Will be copied and
450 overwrite existing metadata. If not supplied the existing
451 metadata will be reset.
452 """
453 if metadata is None:
454 self._metadata = PropertyList()
455 else:
456 self._metadata = copy.copy(metadata)
458 # Ensure that we have the obs type required by calibration ingest
459 self._metadata["OBSTYPE"] = self._OBSTYPE
461 def updateMetadata(self, date=None, detectorId=None, detectorName=None, instrumentName=None, calibId=None,
462 serial=None):
463 """Update metadata keywords with new values.
465 Parameters
466 ----------
467 date : `datetime.datetime`, optional
468 detectorId : `int`, optional
469 detectorName: `str`, optional
470 instrumentName : `str`, optional
471 calibId: `str`, optional
472 serial: detector serial, `str`, optional
474 """
475 mdOriginal = self.getMetadata()
476 mdSupplemental = dict()
478 if date:
479 mdSupplemental['CALIBDATE'] = date.isoformat()
480 mdSupplemental['CALIB_CREATION_DATE'] = date.date().isoformat(),
481 mdSupplemental['CALIB_CREATION_TIME'] = date.time().isoformat(),
482 if detectorId:
483 mdSupplemental['DETECTOR'] = f"{detectorId}"
484 if detectorName:
485 mdSupplemental['DETECTOR_NAME'] = detectorName
486 if instrumentName:
487 mdSupplemental['INSTRUME'] = instrumentName
488 if calibId:
489 mdSupplemental['CALIB_ID'] = calibId
490 if serial:
491 mdSupplemental['DETECTOR_SERIAL'] = serial
493 mdOriginal.update(mdSupplemental)
495 def getLinearityTypeByName(self, linearityTypeName):
496 """Determine the linearity class to use from the type name.
498 Parameters
499 ----------
500 linearityTypeName : str
501 String name of the linearity type that is needed.
503 Returns
504 -------
505 linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
506 The appropriate linearity class to use. If no matching class
507 is found, `None` is returned.
508 """
509 for t in [LinearizeLookupTable,
510 LinearizeSquared,
511 LinearizePolynomial,
512 LinearizeProportional,
513 LinearizeNone]:
514 if t.LinearityType == linearityTypeName:
515 return t
516 return None
518 def validate(self, detector=None, amplifier=None):
519 """Validate linearity for a detector/amplifier.
521 Parameters
522 ----------
523 detector : `lsst.afw.cameraGeom.Detector`, optional
524 Detector to validate, along with its amplifiers.
525 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
526 Single amplifier to validate.
528 Raises
529 ------
530 RuntimeError :
531 Raised if there is a mismatch in linearity parameters, and
532 the cameraGeom parameters are not being overridden.
533 """
534 amplifiersToCheck = []
535 if detector:
536 if self._detectorName != detector.getName():
537 raise RuntimeError("Detector names don't match: %s != %s" %
538 (self._detectorName, detector.getName()))
539 if int(self._detectorId) != int(detector.getId()):
540 raise RuntimeError("Detector IDs don't match: %s != %s" %
541 (int(self._detectorId), int(detector.getId())))
542 if self._detectorSerial != detector.getSerial():
543 raise RuntimeError("Detector serial numbers don't match: %s != %s" %
544 (self._detectorSerial, detector.getSerial()))
545 if len(detector.getAmplifiers()) != len(self.linearityCoeffs.keys()):
546 raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
547 (len(detector.getAmplifiers()),
548 len(self.linearityCoeffs.keys())))
549 amplifiersToCheck.extend(detector.getAmplifiers())
551 if amplifier:
552 amplifiersToCheck.extend(amplifier)
554 for amp in amplifiersToCheck:
555 ampName = amp.getName()
556 if ampName not in self.linearityCoeffs.keys():
557 raise RuntimeError("Amplifier %s is not in linearity data" %
558 (ampName, ))
559 if amp.getLinearityType() != self.linearityType[ampName]:
560 if self.override:
561 self.log.warn("Overriding amplifier defined linearityType (%s) for %s",
562 self.linearityType[ampName], ampName)
563 else:
564 raise RuntimeError("Amplifier %s type %s does not match saved value %s" %
565 (ampName, amp.getLinearityType(), self.linearityType[ampName]))
566 if (amp.getLinearityCoeffs().shape != self.linearityCoeffs[ampName].shape or not
567 np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffs[ampName], equal_nan=True)):
568 if self.override:
569 self.log.warn("Overriding amplifier defined linearityCoeffs (%s) for %s",
570 self.linearityCoeffs[ampName], ampName)
571 else:
572 raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" %
573 (ampName, amp.getLinearityCoeffs(), self.linearityCoeffs[ampName]))
575 def applyLinearity(self, image, detector=None, log=None):
576 """Apply the linearity to an image.
578 If the linearity parameters are populated, use those,
579 otherwise use the values from the detector.
581 Parameters
582 ----------
583 image : `~lsst.afw.image.image`
584 Image to correct.
585 detector : `~lsst.afw.cameraGeom.detector`
586 Detector to use for linearity parameters if not already
587 populated.
588 log : `~lsst.log.Log`, optional
589 Log object to use for logging.
590 """
591 if log is None:
592 log = self.log
593 if detector and not self.populated:
594 self.fromDetector(detector)
596 self.validate(detector)
598 numAmps = 0
599 numLinearized = 0
600 numOutOfRange = 0
601 for ampName in self.linearityType.keys():
602 linearizer = self.getLinearityTypeByName(self.linearityType[ampName])
603 numAmps += 1
604 if linearizer is not None:
605 ampView = image.Factory(image, self.linearityBBox[ampName])
606 success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffs[ampName],
607 'table': self.tableData,
608 'log': self.log})
609 numOutOfRange += outOfRange
610 if success:
611 numLinearized += 1
612 elif log is not None:
613 log.warn("Amplifier %s did not linearize.",
614 ampName)
615 return Struct(
616 numAmps=numAmps,
617 numLinearized=numLinearized,
618 numOutOfRange=numOutOfRange
619 )
622class LinearizeBase(metaclass=abc.ABCMeta):
623 """Abstract base class functor for correcting non-linearity.
625 Subclasses must define __call__ and set class variable
626 LinearityType to a string that will be used for linearity type in
627 the cameraGeom.Amplifier.linearityType field.
629 All linearity corrections should be defined in terms of an
630 additive correction, such that:
632 corrected_value = uncorrected_value + f(uncorrected_value)
633 """
634 LinearityType = None # linearity type, a string used for AmpInfoCatalogs
636 @abc.abstractmethod
637 def __call__(self, image, **kwargs):
638 """Correct non-linearity.
640 Parameters
641 ----------
642 image : `lsst.afw.image.Image`
643 Image to be corrected
644 kwargs : `dict`
645 Dictionary of parameter keywords:
646 ``"coeffs"``
647 Coefficient vector (`list` or `numpy.array`).
648 ``"table"``
649 Lookup table data (`numpy.array`).
650 ``"log"``
651 Logger to handle messages (`lsst.log.Log`).
653 Returns
654 -------
655 output : `bool`
656 If true, a correction was applied successfully.
658 Raises
659 ------
660 RuntimeError:
661 Raised if the linearity type listed in the
662 detector does not match the class type.
663 """
664 pass
667class LinearizeLookupTable(LinearizeBase):
668 """Correct non-linearity with a persisted lookup table.
670 The lookup table consists of entries such that given
671 "coefficients" c0, c1:
673 for each i,j of image:
674 rowInd = int(c0)
675 colInd = int(c1 + uncorrImage[i,j])
676 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
678 - c0: row index; used to identify which row of the table to use
679 (typically one per amplifier, though one can have multiple
680 amplifiers use the same table)
681 - c1: column index offset; added to the uncorrected image value
682 before truncation; this supports tables that can handle
683 negative image values; also, if the c1 ends with .5 then
684 the nearest index is used instead of truncating to the
685 next smaller index
686 """
687 LinearityType = "LookupTable"
689 def __call__(self, image, **kwargs):
690 """Correct for non-linearity.
692 Parameters
693 ----------
694 image : `lsst.afw.image.Image`
695 Image to be corrected
696 kwargs : `dict`
697 Dictionary of parameter keywords:
698 ``"coeffs"``
699 Columnation vector (`list` or `numpy.array`).
700 ``"table"``
701 Lookup table data (`numpy.array`).
702 ``"log"``
703 Logger to handle messages (`lsst.log.Log`).
705 Returns
706 -------
707 output : `bool`
708 If true, a correction was applied successfully.
710 Raises
711 ------
712 RuntimeError:
713 Raised if the requested row index is out of the table
714 bounds.
715 """
716 numOutOfRange = 0
718 rowInd, colIndOffset = kwargs['coeffs'][0:2]
719 table = kwargs['table']
720 log = kwargs['log']
722 numTableRows = table.shape[0]
723 rowInd = int(rowInd)
724 if rowInd < 0 or rowInd > numTableRows:
725 raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" %
726 (rowInd, numTableRows))
727 tableRow = table[rowInd, :]
728 numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
730 if numOutOfRange > 0 and log is not None:
731 log.warn("%s pixels were out of range of the linearization table",
732 numOutOfRange)
733 if numOutOfRange < image.getArray().size:
734 return True, numOutOfRange
735 else:
736 return False, numOutOfRange
739class LinearizePolynomial(LinearizeBase):
740 """Correct non-linearity with a polynomial mode.
742 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
744 where c_i are the linearity coefficients for each amplifier.
745 Lower order coefficients are not included as they duplicate other
746 calibration parameters:
747 ``"k0"``
748 A coefficient multiplied by uncorrImage**0 is equivalent to
749 bias level. Irrelevant for correcting non-linearity.
750 ``"k1"``
751 A coefficient multiplied by uncorrImage**1 is proportional
752 to the gain. Not necessary for correcting non-linearity.
753 """
754 LinearityType = "Polynomial"
756 def __call__(self, image, **kwargs):
757 """Correct non-linearity.
759 Parameters
760 ----------
761 image : `lsst.afw.image.Image`
762 Image to be corrected
763 kwargs : `dict`
764 Dictionary of parameter keywords:
765 ``"coeffs"``
766 Coefficient vector (`list` or `numpy.array`).
767 If the order of the polynomial is n, this list
768 should have a length of n-1 ("k0" and "k1" are
769 not needed for the correction).
770 ``"log"``
771 Logger to handle messages (`lsst.log.Log`).
773 Returns
774 -------
775 output : `bool`
776 If true, a correction was applied successfully.
777 """
778 if not np.any(np.isfinite(kwargs['coeffs'])):
779 return False, 0
780 if not np.any(kwargs['coeffs']):
781 return False, 0
783 ampArray = image.getArray()
784 correction = np.zeros_like(ampArray)
785 for order, coeff in enumerate(kwargs['coeffs'], start=2):
786 correction += coeff * np.power(ampArray, order)
787 ampArray += correction
789 return True, 0
792class LinearizeSquared(LinearizeBase):
793 """Correct non-linearity with a squared model.
795 corrImage = uncorrImage + c0*uncorrImage^2
797 where c0 is linearity coefficient 0 for each amplifier.
798 """
799 LinearityType = "Squared"
801 def __call__(self, image, **kwargs):
802 """Correct for non-linearity.
804 Parameters
805 ----------
806 image : `lsst.afw.image.Image`
807 Image to be corrected
808 kwargs : `dict`
809 Dictionary of parameter keywords:
810 ``"coeffs"``
811 Coefficient vector (`list` or `numpy.array`).
812 ``"log"``
813 Logger to handle messages (`lsst.log.Log`).
815 Returns
816 -------
817 output : `bool`
818 If true, a correction was applied successfully.
819 """
821 sqCoeff = kwargs['coeffs'][0]
822 if sqCoeff != 0:
823 ampArr = image.getArray()
824 ampArr *= (1 + sqCoeff*ampArr)
825 return True, 0
826 else:
827 return False, 0
830class LinearizeProportional(LinearizeBase):
831 """Do not correct non-linearity.
832 """
833 LinearityType = "Proportional"
835 def __call__(self, image, **kwargs):
836 """Do not correct for non-linearity.
838 Parameters
839 ----------
840 image : `lsst.afw.image.Image`
841 Image to be corrected
842 kwargs : `dict`
843 Dictionary of parameter keywords:
844 ``"coeffs"``
845 Coefficient vector (`list` or `numpy.array`).
846 ``"log"``
847 Logger to handle messages (`lsst.log.Log`).
849 Returns
850 -------
851 output : `bool`
852 If true, a correction was applied successfully.
853 """
854 return True, 0
857class LinearizeNone(LinearizeBase):
858 """Do not correct non-linearity.
859 """
860 LinearityType = "None"
862 def __call__(self, image, **kwargs):
863 """Do not correct for non-linearity.
865 Parameters
866 ----------
867 image : `lsst.afw.image.Image`
868 Image to be corrected
869 kwargs : `dict`
870 Dictionary of parameter keywords:
871 ``"coeffs"``
872 Coefficient vector (`list` or `numpy.array`).
873 ``"log"``
874 Logger to handle messages (`lsst.log.Log`).
876 Returns
877 -------
878 output : `bool`
879 If true, a correction was applied successfully.
880 """
881 return True, 0