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

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
24import numpy as np
26from lsst.pipe.base import Struct
27from .applyLookupTable import applyLookupTable
29__all__ = ["Linearizer",
30 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared",
31 "LinearizeProportional", "LinearizePolynomial", "LinearizeNone"]
34class Linearizer(abc.ABC):
35 """Parameter set for linearization.
37 These parameters are included in cameraGeom.Amplifier, but
38 should be accessible externally to allow for testing.
40 Parameters
41 ----------
42 table : `numpy.array`, optional
43 Lookup table; a 2-dimensional array of floats:
44 - one row for each row index (value of coef[0] in the amplifier)
45 - one column for each image value
46 To avoid copying the table the last index should vary fastest
47 (numpy default "C" order)
48 override : `bool`, optional
49 Override the parameters defined in the detector/amplifier.
50 log : `lsst.log.Log`, optional
51 Logger to handle messages.
53 Raises
54 ------
55 RuntimeError :
56 Raised if the supplied table is not 2D, or if the table has fewer
57 columns than rows (indicating that the indices are swapped).
58 """
59 def __init__(self, table=None, detector=None, override=False, log=None):
60 self._detectorName = None
61 self._detectorSerial = None
63 self.linearityCoeffs = dict()
64 self.linearityType = dict()
65 self.linearityThreshold = dict()
66 self.linearityMaximum = dict()
67 self.linearityUnits = dict()
68 self.linearityBBox = dict()
70 self.override = override
71 self.populated = False
72 self.log = log
74 self.tableData = None
75 if table is not None:
76 if len(table.shape) != 2:
77 raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
78 if table.shape[1] < table.shape[0]:
79 raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
80 self.tableData = np.array(table, order="C")
82 if detector:
83 self.fromDetector(detector)
85 def __call__(self, exposure):
86 """Apply linearity, setting parameters if necessary.
88 Parameters
89 ----------
90 exposure : `lsst.afw.image.Exposure`
91 Exposure to correct.
93 Returns
94 -------
95 output : `lsst.pipe.base.Struct`
96 Linearization results:
97 ``"numAmps"``
98 Number of amplifiers considered.
99 ``"numLinearized"``
100 Number of amplifiers linearized.
101 """
103 def fromDetector(self, detector):
104 """Read linearity parameters from a detector.
106 Parameters
107 ----------
108 detector : `lsst.afw.cameraGeom.detector`
109 Input detector with parameters to use.
110 """
111 self._detectorName = detector.getName()
112 self._detectorSerial = detector.getSerial()
113 self.populated = True
115 # Do not translate Threshold, Maximum, Units.
116 for amp in detector.getAmplifiers():
117 ampName = amp.getName()
118 self.linearityCoeffs[ampName] = amp.getLinearityCoeffs()
119 self.linearityType[ampName] = amp.getLinearityType()
120 self.linearityBBox[ampName] = amp.getBBox()
122 def fromYaml(self, yamlObject):
123 """Read linearity parameters from a dict.
125 Parameters
126 ----------
127 yamlObject : `dict`
128 Dictionary containing detector and amplifier information.
129 """
130 self._detectorName = yamlObject['detectorName']
131 self._detectorSerial = yamlObject['detectorSerial']
132 self.populated = True
133 self.override = True
135 for amp in yamlObject['amplifiers']:
136 ampName = amp['name']
137 self.linearityCoeffs[ampName] = amp.get('linearityCoeffs', None)
138 self.linearityType[ampName] = amp.get('linearityType', 'None')
139 self.linearityBBox[ampName] = amp.get('linearityBBox', None)
141 def toDict(self):
142 """Return linearity parameters as a dict.
144 Returns
145 -------
146 outDict : `dict`:
147 """
148 outDict = {'detectorName': self._detectorName,
149 'detectorSerial': self._detectorSerial,
150 'hasTable': self._table is not None,
151 'amplifiers': dict()}
152 for ampName in self.linearityType:
153 outDict['amplifiers'][ampName] = {'linearityType': self.linearityType[ampName],
154 'linearityCoeffs': self.linearityCoeffs[ampName],
155 'linearityBBox': self.linearityBBox[ampName]}
157 def getLinearityTypeByName(self, linearityTypeName):
158 """Determine the linearity class to use from the type name.
160 Parameters
161 ----------
162 linearityTypeName : str
163 String name of the linearity type that is needed.
165 Returns
166 -------
167 linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
168 The appropriate linearity class to use. If no matching class
169 is found, `None` is returned.
170 """
171 for t in [LinearizeLookupTable,
172 LinearizeSquared,
173 LinearizePolynomial,
174 LinearizeProportional,
175 LinearizeNone]:
176 if t.LinearityType == linearityTypeName:
177 return t
178 return None
180 def validate(self, detector=None, amplifier=None):
181 """Validate linearity for a detector/amplifier.
183 Parameters
184 ----------
185 detector : `lsst.afw.cameraGeom.Detector`, optional
186 Detector to validate, along with its amplifiers.
187 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
188 Single amplifier to validate.
190 Raises
191 ------
192 RuntimeError :
193 Raised if there is a mismatch in linearity parameters, and
194 the cameraGeom parameters are not being overridden.
195 """
196 amplifiersToCheck = []
197 if detector:
198 if self._detectorName != detector.getName():
199 raise RuntimeError("Detector names don't match: %s != %s" %
200 (self._detectorName, detector.getName()))
201 if self._detectorSerial != detector.getSerial():
202 raise RuntimeError("Detector serial numbers don't match: %s != %s" %
203 (self._detectorSerial, detector.getSerial()))
204 if len(detector.getAmplifiers()) != len(self.linearityCoeffs.keys()):
205 raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
206 (len(detector.getAmplifiers()),
207 len(self.linearityCoeffs.keys())))
208 amplifiersToCheck.extend(detector.getAmplifiers())
210 if amplifier:
211 amplifiersToCheck.extend(amplifier)
213 for amp in amplifiersToCheck:
214 ampName = amp.getName()
215 if ampName not in self.linearityCoeffs.keys():
216 raise RuntimeError("Amplifier %s is not in linearity data" %
217 (ampName, ))
218 if amp.getLinearityType() != self.linearityType[ampName]:
219 if self.override:
220 self.log.warn("Overriding amplifier defined linearityType (%s) for %s",
221 self.linearityType[ampName], ampName)
222 else:
223 raise RuntimeError("Amplifier %s type %s does not match saved value %s" %
224 (ampName, amp.getLinearityType(), self.linearityType[ampName]))
225 if not np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffs[ampName], equal_nan=True):
226 if self.override:
227 self.log.warn("Overriding amplifier defined linearityCoeffs (%s) for %s",
228 self.linearityCoeffs[ampName], ampName)
229 else:
230 raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" %
231 (ampName, amp.getLinearityCoeffs(), self.linearityCoeffs[ampName]))
233 def applyLinearity(self, image, detector=None, log=None):
234 """Apply the linearity to an image.
236 If the linearity parameters are populated, use those,
237 otherwise use the values from the detector.
239 Parameters
240 ----------
241 image : `~lsst.afw.image.image`
242 Image to correct.
243 detector : `~lsst.afw.cameraGeom.detector`
244 Detector to use for linearity parameters if not already
245 populated.
246 log : `~lsst.log.Log`, optional
247 Log object to use for logging.
248 """
249 if log is None:
250 log = self.log
252 if detector and not self.populated:
253 self.fromDetector(detector)
255 self.validate(detector)
257 numAmps = 0
258 numLinearized = 0
259 numOutOfRange = 0
260 for ampName in self.linearityType.keys():
261 linearizer = self.getLinearityTypeByName(self.linearityType[ampName])
262 numAmps += 1
263 if linearizer is not None:
264 ampView = image.Factory(image, self.linearityBBox[ampName])
265 success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffs[ampName],
266 'table': self.tableData,
267 'log': self.log})
268 numOutOfRange += outOfRange
269 if success:
270 numLinearized += 1
271 elif log is not None:
272 log.warn("Amplifier %s did not linearize.",
273 ampName)
274 return Struct(
275 numAmps=numAmps,
276 numLinearized=numLinearized,
277 numOutOfRange=numOutOfRange
278 )
281class LinearizeBase(metaclass=abc.ABCMeta):
282 """Abstract base class functor for correcting non-linearity.
284 Subclasses must define __call__ and set class variable
285 LinearityType to a string that will be used for linearity type in
286 the cameraGeom.Amplifier.linearityType field.
288 All linearity corrections should be defined in terms of an
289 additive correction, such that:
291 corrected_value = uncorrected_value + f(uncorrected_value)
292 """
293 LinearityType = None # linearity type, a string used for AmpInfoCatalogs
295 @abc.abstractmethod
296 def __call__(self, image, **kwargs):
297 """Correct non-linearity.
299 Parameters
300 ----------
301 image : `lsst.afw.image.Image`
302 Image to be corrected
303 kwargs : `dict`
304 Dictionary of parameter keywords:
305 ``"coeffs"``
306 Coefficient vector (`list` or `numpy.array`).
307 ``"table"``
308 Lookup table data (`numpy.array`).
309 ``"log"``
310 Logger to handle messages (`lsst.log.Log`).
312 Returns
313 -------
314 output : `bool`
315 If true, a correction was applied successfully.
317 Raises
318 ------
319 RuntimeError:
320 Raised if the linearity type listed in the
321 detector does not match the class type.
322 """
323 pass
326class LinearizeLookupTable(LinearizeBase):
327 """Correct non-linearity with a persisted lookup table.
329 The lookup table consists of entries such that given
330 "coefficients" c0, c1:
332 for each i,j of image:
333 rowInd = int(c0)
334 colInd = int(c1 + uncorrImage[i,j])
335 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
337 - c0: row index; used to identify which row of the table to use
338 (typically one per amplifier, though one can have multiple
339 amplifiers use the same table)
340 - c1: column index offset; added to the uncorrected image value
341 before truncation; this supports tables that can handle
342 negative image values; also, if the c1 ends with .5 then
343 the nearest index is used instead of truncating to the
344 next smaller index
345 """
346 LinearityType = "LookupTable"
348 def __call__(self, image, **kwargs):
349 """Correct for non-linearity.
351 Parameters
352 ----------
353 image : `lsst.afw.image.Image`
354 Image to be corrected
355 kwargs : `dict`
356 Dictionary of parameter keywords:
357 ``"coeffs"``
358 Columnation vector (`list` or `numpy.array`).
359 ``"table"``
360 Lookup table data (`numpy.array`).
361 ``"log"``
362 Logger to handle messages (`lsst.log.Log`).
364 Returns
365 -------
366 output : `bool`
367 If true, a correction was applied successfully.
369 Raises
370 ------
371 RuntimeError:
372 Raised if the requested row index is out of the table
373 bounds.
374 """
375 numOutOfRange = 0
377 rowInd, colIndOffset = kwargs['coeffs'][0:2]
378 table = kwargs['table']
379 log = kwargs['log']
381 numTableRows = table.shape[0]
382 rowInd = int(rowInd)
383 if rowInd < 0 or rowInd > numTableRows:
384 raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" %
385 (rowInd, numTableRows))
386 tableRow = table[rowInd, :]
387 numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
389 if numOutOfRange > 0 and log is not None:
390 log.warn("%s pixels were out of range of the linearization table",
391 numOutOfRange)
392 if numOutOfRange < image.getArray().size:
393 return True, numOutOfRange
394 else:
395 return False, numOutOfRange
398class LinearizePolynomial(LinearizeBase):
399 """Correct non-linearity with a polynomial mode.
401 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
403 where c_i are the linearity coefficients for each amplifier.
404 Lower order coefficients are not included as they duplicate other
405 calibration parameters:
406 ``"k0"``
407 A coefficient multiplied by uncorrImage**0 is equivalent to
408 bias level. Irrelevant for correcting non-linearity.
409 ``"k1"``
410 A coefficient multiplied by uncorrImage**1 is proportional
411 to the gain. Not necessary for correcting non-linearity.
412 """
413 LinearityType = "Polynomial"
415 def __call__(self, image, **kwargs):
416 """Correct non-linearity.
418 Parameters
419 ----------
420 image : `lsst.afw.image.Image`
421 Image to be corrected
422 kwargs : `dict`
423 Dictionary of parameter keywords:
424 ``"coeffs"``
425 Coefficient vector (`list` or `numpy.array`).
426 ``"log"``
427 Logger to handle messages (`lsst.log.Log`).
429 Returns
430 -------
431 output : `bool`
432 If true, a correction was applied successfully.
433 """
434 if np.any(np.isfinite(kwargs['coeffs'])):
435 return False, 0
436 if not np.any(kwargs['coeffs']):
437 return False, 0
439 ampArray = image.getArray()
440 correction = np.zeroes_like(ampArray)
441 for coeff, order in enumerate(kwargs['coeffs'], start=2):
442 correction += coeff * np.power(ampArray, order)
443 ampArray += correction
445 return True, 0
448class LinearizeSquared(LinearizeBase):
449 """Correct non-linearity with a squared model.
451 corrImage = uncorrImage + c0*uncorrImage^2
453 where c0 is linearity coefficient 0 for each amplifier.
454 """
455 LinearityType = "Squared"
457 def __call__(self, image, **kwargs):
458 """Correct for non-linearity.
460 Parameters
461 ----------
462 image : `lsst.afw.image.Image`
463 Image to be corrected
464 kwargs : `dict`
465 Dictionary of parameter keywords:
466 ``"coeffs"``
467 Coefficient vector (`list` or `numpy.array`).
468 ``"log"``
469 Logger to handle messages (`lsst.log.Log`).
471 Returns
472 -------
473 output : `bool`
474 If true, a correction was applied successfully.
475 """
477 sqCoeff = kwargs['coeffs'][0]
478 if sqCoeff != 0:
479 ampArr = image.getArray()
480 ampArr *= (1 + sqCoeff*ampArr)
481 return True, 0
482 else:
483 return False, 0
486class LinearizeProportional(LinearizeBase):
487 """Do not correct non-linearity.
488 """
489 LinearityType = "Proportional"
491 def __call__(self, image, **kwargs):
492 """Do not correct for non-linearity.
494 Parameters
495 ----------
496 image : `lsst.afw.image.Image`
497 Image to be corrected
498 kwargs : `dict`
499 Dictionary of parameter keywords:
500 ``"coeffs"``
501 Coefficient vector (`list` or `numpy.array`).
502 ``"log"``
503 Logger to handle messages (`lsst.log.Log`).
505 Returns
506 -------
507 output : `bool`
508 If true, a correction was applied successfully.
509 """
510 return True, 0
513class LinearizeNone(LinearizeBase):
514 """Do not correct non-linearity.
515 """
516 LinearityType = "None"
518 def __call__(self, image, **kwargs):
519 """Do not correct for non-linearity.
521 Parameters
522 ----------
523 image : `lsst.afw.image.Image`
524 Image to be corrected
525 kwargs : `dict`
526 Dictionary of parameter keywords:
527 ``"coeffs"``
528 Coefficient vector (`list` or `numpy.array`).
529 ``"log"``
530 Logger to handle messages (`lsst.log.Log`).
532 Returns
533 -------
534 output : `bool`
535 If true, a correction was applied successfully.
536 """
537 return True, 0