LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
linearize.py
Go to the documentation of this file.
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 #
22 import abc
23 import numpy as np
24 
25 from astropy.table import Table
26 
27 import lsst.afw.math as afwMath
28 from lsst.pipe.base import Struct
29 from lsst.geom import Box2I, Point2I, Extent2I
30 from .applyLookupTable import applyLookupTable
31 from .calibType import IsrCalib
32 
33 __all__ = ["Linearizer",
34  "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared",
35  "LinearizeProportional", "LinearizePolynomial", "LinearizeSpline", "LinearizeNone"]
36 
37 
39  """Parameter set for linearization.
40 
41  These parameters are included in cameraGeom.Amplifier, but
42  should be accessible externally to allow for testing.
43 
44  Parameters
45  ----------
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)
52  detector : `lsst.afw.cameraGeom.Detector`, optional
53  Detector object. Passed to self.fromDetector() on init.
54  log : `logging.Logger`, optional
55  Logger to handle messages.
56  kwargs : `dict`, optional
57  Other keyword arguments to pass to the parent init.
58 
59  Raises
60  ------
61  RuntimeError :
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).
64 
65  Notes
66  -----
67  The linearizer attributes stored are:
68 
69  hasLinearity : `bool`
70  Whether a linearity correction is defined for this detector.
71  override : `bool`
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
78  correction to apply.
79  linearityType : `dict` [`str`, `str`]
80  Type of correction to use, indexed by amplifier names.
81  linearityBBox : `dict` [`str`, `lsst.geom.Box2I`]
82  Bounding box the correction is valid over, indexed by
83  amplifier names.
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  tableData : `numpy.array`, optional
93  Lookup table data for the linearity correction.
94  """
95  _OBSTYPE = "LINEARIZER"
96  _SCHEMA = 'Gen3 Linearizer'
97  _VERSION = 1.1
98 
99  def __init__(self, table=None, **kwargs):
100  self.hasLinearityhasLinearity = False
101  self.overrideoverride = False
102 
103  self.ampNamesampNames = list()
104  self.linearityCoeffslinearityCoeffs = dict()
105  self.linearityTypelinearityType = dict()
106  self.linearityBBoxlinearityBBox = dict()
107 
108  self.fitParamsfitParams = dict()
109  self.fitParamsErrfitParamsErr = dict()
110  self.fitChiSqfitChiSq = dict()
111 
112  self.tableDatatableData = None
113  if table is not None:
114  if len(table.shape) != 2:
115  raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
116  if table.shape[1] < table.shape[0]:
117  raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
118  self.tableDatatableData = np.array(table, order="C")
119 
120  super().__init__(**kwargs)
121  self.requiredAttributesrequiredAttributesrequiredAttributesrequiredAttributes.update(['hasLinearity', 'override',
122  'ampNames',
123  'linearityCoeffs', 'linearityType', 'linearityBBox',
124  'fitParams', 'fitParamsErr', 'fitChiSq',
125  'tableData'])
126 
127  def updateMetadata(self, setDate=False, **kwargs):
128  """Update metadata keywords with new values.
129 
130  This calls the base class's method after ensuring the required
131  calibration keywords will be saved.
132 
133  Parameters
134  ----------
135  setDate : `bool`, optional
136  Update the CALIBDATE fields in the metadata to the current
137  time. Defaults to False.
138  kwargs :
139  Other keyword parameters to set in the metadata.
140  """
141  kwargs['HAS_LINEARITY'] = self.hasLinearityhasLinearity
142  kwargs['OVERRIDE'] = self.overrideoverride
143  kwargs['HAS_TABLE'] = self.tableDatatableData is not None
144 
145  super().updateMetadata(setDate=setDate, **kwargs)
146 
147  def fromDetector(self, detector):
148  """Read linearity parameters from a detector.
149 
150  Parameters
151  ----------
152  detector : `lsst.afw.cameraGeom.detector`
153  Input detector with parameters to use.
154 
155  Returns
156  -------
157  calib : `lsst.ip.isr.Linearizer`
158  The calibration constructed from the detector.
159  """
160  self._detectorName_detectorName_detectorName = detector.getName()
161  self._detectorSerial_detectorSerial_detectorSerial = detector.getSerial()
162  self._detectorId_detectorId_detectorId = detector.getId()
163  self.hasLinearityhasLinearity = True
164 
165  # Do not translate Threshold, Maximum, Units.
166  for amp in detector.getAmplifiers():
167  ampName = amp.getName()
168  self.ampNamesampNames.append(ampName)
169  self.linearityTypelinearityType[ampName] = amp.getLinearityType()
170  self.linearityCoeffslinearityCoeffs[ampName] = amp.getLinearityCoeffs()
171  self.linearityBBoxlinearityBBox[ampName] = amp.getBBox()
172 
173  return self
174 
175  @classmethod
176  def fromDict(cls, dictionary):
177  """Construct a calibration from a dictionary of properties
178 
179  Parameters
180  ----------
181  dictionary : `dict`
182  Dictionary of properties
183 
184  Returns
185  -------
186  calib : `lsst.ip.isr.Linearity`
187  Constructed calibration.
188 
189  Raises
190  ------
191  RuntimeError
192  Raised if the supplied dictionary is for a different
193  calibration.
194  """
195 
196  calib = cls()
197 
198  if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
199  raise RuntimeError(f"Incorrect linearity supplied. Expected {calib._OBSTYPE}, "
200  f"found {dictionary['metadata']['OBSTYPE']}")
201 
202  calib.setMetadata(dictionary['metadata'])
203 
204  calib.hasLinearity = dictionary.get('hasLinearity',
205  dictionary['metadata'].get('HAS_LINEARITY', False))
206  calib.override = dictionary.get('override', True)
207 
208  if calib.hasLinearity:
209  for ampName in dictionary['amplifiers']:
210  amp = dictionary['amplifiers'][ampName]
211  calib.ampNames.append(ampName)
212  calib.linearityCoeffs[ampName] = np.array(amp.get('linearityCoeffs', [0.0]))
213  calib.linearityType[ampName] = amp.get('linearityType', 'None')
214  calib.linearityBBox[ampName] = amp.get('linearityBBox', None)
215 
216  calib.fitParams[ampName] = np.array(amp.get('fitParams', [0.0]))
217  calib.fitParamsErr[ampName] = np.array(amp.get('fitParamsErr', [0.0]))
218  calib.fitChiSq[ampName] = amp.get('fitChiSq', np.nan)
219 
220  calib.tableData = dictionary.get('tableData', None)
221  if calib.tableData:
222  calib.tableData = np.array(calib.tableData)
223 
224  return calib
225 
226  def toDict(self):
227  """Return linearity parameters as a dict.
228 
229  Returns
230  -------
231  outDict : `dict`:
232  """
233  self.updateMetadataupdateMetadataupdateMetadata()
234 
235  outDict = {'metadata': self.getMetadatagetMetadata(),
236  'detectorName': self._detectorName_detectorName_detectorName,
237  'detectorSerial': self._detectorSerial_detectorSerial_detectorSerial,
238  'detectorId': self._detectorId_detectorId_detectorId,
239  'hasTable': self.tableDatatableData is not None,
240  'amplifiers': dict(),
241  }
242  for ampName in self.linearityTypelinearityType:
243  outDict['amplifiers'][ampName] = {'linearityType': self.linearityTypelinearityType[ampName],
244  'linearityCoeffs': self.linearityCoeffslinearityCoeffs[ampName].tolist(),
245  'linearityBBox': self.linearityBBoxlinearityBBox[ampName],
246  'fitParams': self.fitParamsfitParams[ampName].tolist(),
247  'fitParamsErr': self.fitParamsErrfitParamsErr[ampName].tolist(),
248  'fitChiSq': self.fitChiSqfitChiSq[ampName]}
249  if self.tableDatatableData is not None:
250  outDict['tableData'] = self.tableDatatableData.tolist()
251 
252  return outDict
253 
254  @classmethod
255  def fromTable(cls, tableList):
256  """Read linearity from a FITS file.
257 
258  This method uses the `fromDict` method to create the
259  calibration, after constructing an appropriate dictionary from
260  the input tables.
261 
262  Parameters
263  ----------
264  tableList : `list` [`astropy.table.Table`]
265  afwTable read from input file name.
266 
267  Returns
268  -------
269  linearity : `~lsst.ip.isr.linearize.Linearizer``
270  Linearity parameters.
271 
272  Notes
273  -----
274  The method reads a FITS file with 1 or 2 extensions. The metadata is read from the header of
275  extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME', 'TYPE',
276  'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to
277  set each dictionary by looping over rows.
278  Eextension 2 is then attempted to read in the try block (which only exists for lookup tables).
279  It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row.
280 
281  """
282  coeffTable = tableList[0]
283 
284  metadata = coeffTable.meta
285  inDict = dict()
286  inDict['metadata'] = metadata
287  inDict['hasLinearity'] = metadata.get('HAS_LINEARITY', False)
288  inDict['amplifiers'] = dict()
289 
290  for record in coeffTable:
291  ampName = record['AMPLIFIER_NAME']
292 
293  fitParams = record['FIT_PARAMS'] if 'FIT_PARAMS' in record.columns else np.array([0.0])
294  fitParamsErr = record['FIT_PARAMS_ERR'] if 'FIT_PARAMS_ERR' in record.columns else np.array([0.0])
295  fitChiSq = record['RED_CHI_SQ'] if 'RED_CHI_SQ' in record.columns else np.nan
296 
297  inDict['amplifiers'][ampName] = {
298  'linearityType': record['TYPE'],
299  'linearityCoeffs': record['COEFFS'],
300  'linearityBBox': Box2I(Point2I(record['BBOX_X0'], record['BBOX_Y0']),
301  Extent2I(record['BBOX_DX'], record['BBOX_DY'])),
302  'fitParams': fitParams,
303  'fitParamsErr': fitParamsErr,
304  'fitChiSq': fitChiSq,
305  }
306 
307  if len(tableList) > 1:
308  tableData = tableList[1]
309  inDict['tableData'] = [record['LOOKUP_VALUES'] for record in tableData]
310 
311  return cls().fromDict(inDict)
312 
313  def toTable(self):
314  """Construct a list of tables containing the information in this calibration
315 
316  The list of tables should create an identical calibration
317  after being passed to this class's fromTable method.
318 
319  Returns
320  -------
321  tableList : `list` [`astropy.table.Table`]
322  List of tables containing the linearity calibration
323  information.
324  """
325 
326  tableList = []
327  self.updateMetadataupdateMetadataupdateMetadata()
328  catalog = Table([{'AMPLIFIER_NAME': ampName,
329  'TYPE': self.linearityTypelinearityType[ampName],
330  'COEFFS': self.linearityCoeffslinearityCoeffs[ampName],
331  'BBOX_X0': self.linearityBBoxlinearityBBox[ampName].getMinX(),
332  'BBOX_Y0': self.linearityBBoxlinearityBBox[ampName].getMinY(),
333  'BBOX_DX': self.linearityBBoxlinearityBBox[ampName].getWidth(),
334  'BBOX_DY': self.linearityBBoxlinearityBBox[ampName].getHeight(),
335  'FIT_PARAMS': self.fitParamsfitParams[ampName],
336  'FIT_PARAMS_ERR': self.fitParamsErrfitParamsErr[ampName],
337  'RED_CHI_SQ': self.fitChiSqfitChiSq[ampName],
338  } for ampName in self.ampNamesampNames])
339  catalog.meta = self.getMetadatagetMetadata().toDict()
340  tableList.append(catalog)
341 
342  if self.tableDatatableData is not None:
343  catalog = Table([{'LOOKUP_VALUES': value} for value in self.tableDatatableData])
344  tableList.append(catalog)
345  return(tableList)
346 
347  def getLinearityTypeByName(self, linearityTypeName):
348  """Determine the linearity class to use from the type name.
349 
350  Parameters
351  ----------
352  linearityTypeName : str
353  String name of the linearity type that is needed.
354 
355  Returns
356  -------
357  linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
358  The appropriate linearity class to use. If no matching class
359  is found, `None` is returned.
360  """
361  for t in [LinearizeLookupTable,
362  LinearizeSquared,
363  LinearizePolynomial,
364  LinearizeProportional,
365  LinearizeSpline,
366  LinearizeNone]:
367  if t.LinearityType == linearityTypeName:
368  return t
369  return None
370 
371  def validate(self, detector=None, amplifier=None):
372  """Validate linearity for a detector/amplifier.
373 
374  Parameters
375  ----------
376  detector : `lsst.afw.cameraGeom.Detector`, optional
377  Detector to validate, along with its amplifiers.
378  amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
379  Single amplifier to validate.
380 
381  Raises
382  ------
383  RuntimeError :
384  Raised if there is a mismatch in linearity parameters, and
385  the cameraGeom parameters are not being overridden.
386  """
387  amplifiersToCheck = []
388  if detector:
389  if self._detectorName_detectorName_detectorName != detector.getName():
390  raise RuntimeError("Detector names don't match: %s != %s" %
391  (self._detectorName_detectorName_detectorName, detector.getName()))
392  if int(self._detectorId_detectorId_detectorId) != int(detector.getId()):
393  raise RuntimeError("Detector IDs don't match: %s != %s" %
394  (int(self._detectorId_detectorId_detectorId), int(detector.getId())))
395  if self._detectorSerial_detectorSerial_detectorSerial != detector.getSerial():
396  raise RuntimeError("Detector serial numbers don't match: %s != %s" %
397  (self._detectorSerial_detectorSerial_detectorSerial, detector.getSerial()))
398  if len(detector.getAmplifiers()) != len(self.linearityCoeffslinearityCoeffs.keys()):
399  raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
400  (len(detector.getAmplifiers()),
401  len(self.linearityCoeffslinearityCoeffs.keys())))
402  amplifiersToCheck.extend(detector.getAmplifiers())
403 
404  if amplifier:
405  amplifiersToCheck.extend(amplifier)
406 
407  for amp in amplifiersToCheck:
408  ampName = amp.getName()
409  if ampName not in self.linearityCoeffslinearityCoeffs.keys():
410  raise RuntimeError("Amplifier %s is not in linearity data" %
411  (ampName, ))
412  if amp.getLinearityType() != self.linearityTypelinearityType[ampName]:
413  if self.overrideoverride:
414  self.loglog.warning("Overriding amplifier defined linearityType (%s) for %s",
415  self.linearityTypelinearityType[ampName], ampName)
416  else:
417  raise RuntimeError("Amplifier %s type %s does not match saved value %s" %
418  (ampName, amp.getLinearityType(), self.linearityTypelinearityType[ampName]))
419  if (amp.getLinearityCoeffs().shape != self.linearityCoeffslinearityCoeffs[ampName].shape or not
420  np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffslinearityCoeffs[ampName], equal_nan=True)):
421  if self.overrideoverride:
422  self.loglog.warning("Overriding amplifier defined linearityCoeffs (%s) for %s",
423  self.linearityCoeffslinearityCoeffs[ampName], ampName)
424  else:
425  raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" %
426  (ampName, amp.getLinearityCoeffs(), self.linearityCoeffslinearityCoeffs[ampName]))
427 
428  def applyLinearity(self, image, detector=None, log=None):
429  """Apply the linearity to an image.
430 
431  If the linearity parameters are populated, use those,
432  otherwise use the values from the detector.
433 
434  Parameters
435  ----------
436  image : `~lsst.afw.image.image`
437  Image to correct.
438  detector : `~lsst.afw.cameraGeom.detector`
439  Detector to use for linearity parameters if not already
440  populated.
441  log : `~logging.Logger`, optional
442  Log object to use for logging.
443  """
444  if log is None:
445  log = self.loglog
446  if detector and not self.hasLinearityhasLinearity:
447  self.fromDetectorfromDetectorfromDetector(detector)
448 
449  self.validatevalidatevalidate(detector)
450 
451  numAmps = 0
452  numLinearized = 0
453  numOutOfRange = 0
454  for ampName in self.linearityTypelinearityType.keys():
455  linearizer = self.getLinearityTypeByNamegetLinearityTypeByName(self.linearityTypelinearityType[ampName])
456  numAmps += 1
457  if linearizer is not None:
458  ampView = image.Factory(image, self.linearityBBoxlinearityBBox[ampName])
459  success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffslinearityCoeffs[ampName],
460  'table': self.tableDatatableData,
461  'log': self.loglog})
462  numOutOfRange += outOfRange
463  if success:
464  numLinearized += 1
465  elif log is not None:
466  log.warning("Amplifier %s did not linearize.",
467  ampName)
468  return Struct(
469  numAmps=numAmps,
470  numLinearized=numLinearized,
471  numOutOfRange=numOutOfRange
472  )
473 
474 
475 class LinearizeBase(metaclass=abc.ABCMeta):
476  """Abstract base class functor for correcting non-linearity.
477 
478  Subclasses must define __call__ and set class variable
479  LinearityType to a string that will be used for linearity type in
480  the cameraGeom.Amplifier.linearityType field.
481 
482  All linearity corrections should be defined in terms of an
483  additive correction, such that:
484 
485  corrected_value = uncorrected_value + f(uncorrected_value)
486  """
487  LinearityType = None # linearity type, a string used for AmpInfoCatalogs
488 
489  @abc.abstractmethod
490  def __call__(self, image, **kwargs):
491  """Correct non-linearity.
492 
493  Parameters
494  ----------
495  image : `lsst.afw.image.Image`
496  Image to be corrected
497  kwargs : `dict`
498  Dictionary of parameter keywords:
499  ``"coeffs"``
500  Coefficient vector (`list` or `numpy.array`).
501  ``"table"``
502  Lookup table data (`numpy.array`).
503  ``"log"``
504  Logger to handle messages (`logging.Logger`).
505 
506  Returns
507  -------
508  output : `bool`
509  If true, a correction was applied successfully.
510 
511  Raises
512  ------
513  RuntimeError:
514  Raised if the linearity type listed in the
515  detector does not match the class type.
516  """
517  pass
518 
519 
520 class LinearizeLookupTable(LinearizeBase):
521  """Correct non-linearity with a persisted lookup table.
522 
523  The lookup table consists of entries such that given
524  "coefficients" c0, c1:
525 
526  for each i,j of image:
527  rowInd = int(c0)
528  colInd = int(c1 + uncorrImage[i,j])
529  corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
530 
531  - c0: row index; used to identify which row of the table to use
532  (typically one per amplifier, though one can have multiple
533  amplifiers use the same table)
534  - c1: column index offset; added to the uncorrected image value
535  before truncation; this supports tables that can handle
536  negative image values; also, if the c1 ends with .5 then
537  the nearest index is used instead of truncating to the
538  next smaller index
539  """
540  LinearityType = "LookupTable"
541 
542  def __call__(self, image, **kwargs):
543  """Correct for non-linearity.
544 
545  Parameters
546  ----------
547  image : `lsst.afw.image.Image`
548  Image to be corrected
549  kwargs : `dict`
550  Dictionary of parameter keywords:
551  ``"coeffs"``
552  Columnation vector (`list` or `numpy.array`).
553  ``"table"``
554  Lookup table data (`numpy.array`).
555  ``"log"``
556  Logger to handle messages (`logging.Logger`).
557 
558  Returns
559  -------
560  output : `tuple` [`bool`, `int`]
561  If true, a correction was applied successfully. The
562  integer indicates the number of pixels that were
563  uncorrectable by being out of range.
564 
565  Raises
566  ------
567  RuntimeError:
568  Raised if the requested row index is out of the table
569  bounds.
570  """
571  numOutOfRange = 0
572 
573  rowInd, colIndOffset = kwargs['coeffs'][0:2]
574  table = kwargs['table']
575  log = kwargs['log']
576 
577  numTableRows = table.shape[0]
578  rowInd = int(rowInd)
579  if rowInd < 0 or rowInd > numTableRows:
580  raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" %
581  (rowInd, numTableRows))
582  tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype)
583 
584  numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
585 
586  if numOutOfRange > 0 and log is not None:
587  log.warning("%s pixels were out of range of the linearization table",
588  numOutOfRange)
589  if numOutOfRange < image.getArray().size:
590  return True, numOutOfRange
591  else:
592  return False, numOutOfRange
593 
594 
596  """Correct non-linearity with a polynomial mode.
597 
598  corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
599 
600  where c_i are the linearity coefficients for each amplifier.
601  Lower order coefficients are not included as they duplicate other
602  calibration parameters:
603  ``"k0"``
604  A coefficient multiplied by uncorrImage**0 is equivalent to
605  bias level. Irrelevant for correcting non-linearity.
606  ``"k1"``
607  A coefficient multiplied by uncorrImage**1 is proportional
608  to the gain. Not necessary for correcting non-linearity.
609  """
610  LinearityType = "Polynomial"
611 
612  def __call__(self, image, **kwargs):
613  """Correct non-linearity.
614 
615  Parameters
616  ----------
617  image : `lsst.afw.image.Image`
618  Image to be corrected
619  kwargs : `dict`
620  Dictionary of parameter keywords:
621  ``"coeffs"``
622  Coefficient vector (`list` or `numpy.array`).
623  If the order of the polynomial is n, this list
624  should have a length of n-1 ("k0" and "k1" are
625  not needed for the correction).
626  ``"log"``
627  Logger to handle messages (`logging.Logger`).
628 
629  Returns
630  -------
631  output : `tuple` [`bool`, `int`]
632  If true, a correction was applied successfully. The
633  integer indicates the number of pixels that were
634  uncorrectable by being out of range.
635  """
636  if not np.any(np.isfinite(kwargs['coeffs'])):
637  return False, 0
638  if not np.any(kwargs['coeffs']):
639  return False, 0
640 
641  ampArray = image.getArray()
642  correction = np.zeros_like(ampArray)
643  for order, coeff in enumerate(kwargs['coeffs'], start=2):
644  correction += coeff * np.power(ampArray, order)
645  ampArray += correction
646 
647  return True, 0
648 
649 
651  """Correct non-linearity with a squared model.
652 
653  corrImage = uncorrImage + c0*uncorrImage^2
654 
655  where c0 is linearity coefficient 0 for each amplifier.
656  """
657  LinearityType = "Squared"
658 
659  def __call__(self, image, **kwargs):
660  """Correct for non-linearity.
661 
662  Parameters
663  ----------
664  image : `lsst.afw.image.Image`
665  Image to be corrected
666  kwargs : `dict`
667  Dictionary of parameter keywords:
668  ``"coeffs"``
669  Coefficient vector (`list` or `numpy.array`).
670  ``"log"``
671  Logger to handle messages (`logging.Logger`).
672 
673  Returns
674  -------
675  output : `tuple` [`bool`, `int`]
676  If true, a correction was applied successfully. The
677  integer indicates the number of pixels that were
678  uncorrectable by being out of range.
679  """
680 
681  sqCoeff = kwargs['coeffs'][0]
682  if sqCoeff != 0:
683  ampArr = image.getArray()
684  ampArr *= (1 + sqCoeff*ampArr)
685  return True, 0
686  else:
687  return False, 0
688 
689 
691  """Correct non-linearity with a spline model.
692 
693  corrImage = uncorrImage - Spline(coeffs, uncorrImage)
694 
695  Notes
696  -----
697 
698  The spline fit calculates a correction as a function of the
699  expected linear flux term. Because of this, the correction needs
700  to be subtracted from the observed flux.
701 
702  """
703  LinearityType = "Spline"
704 
705  def __call__(self, image, **kwargs):
706  """Correct for non-linearity.
707 
708  Parameters
709  ----------
710  image : `lsst.afw.image.Image`
711  Image to be corrected
712  kwargs : `dict`
713  Dictionary of parameter keywords:
714  ``"coeffs"``
715  Coefficient vector (`list` or `numpy.array`).
716  ``"log"``
717  Logger to handle messages (`logging.Logger`).
718 
719  Returns
720  -------
721  output : `tuple` [`bool`, `int`]
722  If true, a correction was applied successfully. The
723  integer indicates the number of pixels that were
724  uncorrectable by being out of range.
725  """
726  splineCoeff = kwargs['coeffs']
727  centers, values = np.split(splineCoeff, 2)
728  interp = afwMath.makeInterpolate(centers.tolist(), values.tolist(),
729  afwMath.stringToInterpStyle("AKIMA_SPLINE"))
730 
731  ampArr = image.getArray()
732  delta = interp.interpolate(ampArr.flatten())
733  ampArr -= np.array(delta).reshape(ampArr.shape)
734 
735  return True, 0
736 
737 
739  """Do not correct non-linearity.
740  """
741  LinearityType = "Proportional"
742 
743  def __call__(self, image, **kwargs):
744  """Do not correct for non-linearity.
745 
746  Parameters
747  ----------
748  image : `lsst.afw.image.Image`
749  Image to be corrected
750  kwargs : `dict`
751  Dictionary of parameter keywords:
752  ``"coeffs"``
753  Coefficient vector (`list` or `numpy.array`).
754  ``"log"``
755  Logger to handle messages (`logging.Logger`).
756 
757  Returns
758  -------
759  output : `tuple` [`bool`, `int`]
760  If true, a correction was applied successfully. The
761  integer indicates the number of pixels that were
762  uncorrectable by being out of range.
763  """
764  return True, 0
765 
766 
768  """Do not correct non-linearity.
769  """
770  LinearityType = "None"
771 
772  def __call__(self, image, **kwargs):
773  """Do not correct for non-linearity.
774 
775  Parameters
776  ----------
777  image : `lsst.afw.image.Image`
778  Image to be corrected
779  kwargs : `dict`
780  Dictionary of parameter keywords:
781  ``"coeffs"``
782  Coefficient vector (`list` or `numpy.array`).
783  ``"log"``
784  Logger to handle messages (`logging.Logger`).
785 
786  Returns
787  -------
788  output : `tuple` [`bool`, `int`]
789  If true, a correction was applied successfully. The
790  integer indicates the number of pixels that were
791  uncorrectable by being out of range.
792  """
793  return True, 0
An integer coordinate rectangle.
Definition: Box.h:55
def validate(self, other=None)
Definition: calibType.py:597
def requiredAttributes(self, value)
Definition: calibType.py:142
def updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition: calibType.py:181
def fromDetector(self, detector)
Definition: calibType.py:495
def __call__(self, image, **kwargs)
Definition: linearize.py:490
def __call__(self, image, **kwargs)
Definition: linearize.py:542
def __call__(self, image, **kwargs)
Definition: linearize.py:772
def __call__(self, image, **kwargs)
Definition: linearize.py:612
def __call__(self, image, **kwargs)
Definition: linearize.py:743
def __call__(self, image, **kwargs)
Definition: linearize.py:705
def __call__(self, image, **kwargs)
Definition: linearize.py:659
def getLinearityTypeByName(self, linearityTypeName)
Definition: linearize.py:347
def validate(self, detector=None, amplifier=None)
Definition: linearize.py:371
def applyLinearity(self, image, detector=None, log=None)
Definition: linearize.py:428
def fromTable(cls, tableList)
Definition: linearize.py:255
def fromDetector(self, detector)
Definition: linearize.py:147
def updateMetadata(self, setDate=False, **kwargs)
Definition: linearize.py:127
def __init__(self, table=None, **kwargs)
Definition: linearize.py:99
def fromDict(cls, dictionary)
Definition: linearize.py:176
daf::base::PropertyList * list
Definition: fits.cc:913
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
Definition: Interpolate.cc:256
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.
Definition: Interpolate.cc:342
Extent< int, 2 > Extent2I
Definition: Extent.h:397
Point< int, 2 > Point2I
Definition: Point.h:321