lsst.ip.isr  20.0.0-8-gc2abeef+bba7c37fb9
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 copy
24 import datetime
25 
26 import numpy as np
27 import yaml
28 
29 import lsst.afw.table as afwTable
30 from lsst.daf.base import PropertyList
31 from lsst.pipe.base import Struct
32 from lsst.geom import Box2I, Point2I, Extent2I
33 from .applyLookupTable import applyLookupTable
34 
35 __all__ = ["Linearizer",
36  "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared",
37  "LinearizeProportional", "LinearizePolynomial", "LinearizeNone"]
38 
39 
40 class Linearizer(abc.ABC):
41  """Parameter set for linearization.
42 
43  These parameters are included in cameraGeom.Amplifier, but
44  should be accessible externally to allow for testing.
45 
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.
60 
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  """
67 
68  _OBSTYPE = "linearizer"
69  """The dataset type name used for this class"""
70 
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()
76 
77  self.linearityCoeffs = dict()
78  self.linearityType = dict()
79  self.linearityThreshold = dict()
80  self.linearityMaximum = dict()
81  self.linearityUnits = dict()
82  self.linearityBBox = dict()
83 
84  self.fitParams = dict()
85  self.fitParamsErr = dict()
87 
88  self.override = override
89  self.populated = False
90  self.log = log
91 
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")
99 
100  if detector:
101  self.fromDetector(detector)
102 
103  def __call__(self, exposure):
104  """Apply linearity, setting parameters if necessary.
105 
106  Parameters
107  ----------
108  exposure : `lsst.afw.image.Exposure`
109  Exposure to correct.
110 
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  """
120 
121  def fromDetector(self, detector):
122  """Read linearity parameters from a detector.
123 
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
133 
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()
140 
141  def fromYaml(self, yamlObject):
142  """Read linearity parameters from a dict.
143 
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
155 
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)
164 
165  if self.tableData is None:
166  self.tableData = yamlObject.get('tableData', None)
167  if self.tableData:
168  self.tableData = np.array(self.tableData)
169 
170  return self
171 
172  def toDict(self):
173  """Return linearity parameters as a dict.
174 
175  Returns
176  -------
177  outDict : `dict`:
178  """
179  # metadata copied from defects code
180  now = datetime.datetime.utcnow()
181  self.updateMetadata(date=now)
182 
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()
199 
200  return outDict
201 
202  @classmethod
203  def readText(cls, filename):
204  """Read linearity from text file.
205 
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)
219 
220  def writeText(self, filename):
221  """Write the linearity model to a text file.
222 
223  Parameters
224  ----------
225  filename : `str`
226  Name of the file to write.
227 
228  Returns
229  -------
230  used : `str`
231  The name of the file used to write the data.
232 
233  Raises
234  ------
235  RuntimeError :
236  Raised if filename does not end in ".yaml".
237 
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'")
249 
250  return filename
251 
252  @classmethod
253  def fromTable(cls, table, tableExtTwo=None):
254  """Read linearity from a FITS file.
255 
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
262 
263  Returns
264  -------
265  linearity : `~lsst.ip.isr.linearize.Linearizer``
266  Linearity parameters.
267 
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()
279 
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()
289 
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()
301 
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]
312 
313  linDict['amplifiers'][ampName] = ampDict
314 
315  if tableExtTwo is not None:
316  lookupValuesKey = 'LOOKUP_VALUES'
317  linDict["tableData"] = [record[lookupValuesKey] for record in tableExtTwo]
318 
319  return cls().fromYaml(linDict)
320 
321  @classmethod
322  def readFits(cls, filename):
323  """Read linearity from a FITS file.
324 
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.
333 
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)
350 
351  def toAmpTable(self, metadata):
352  """Produce linearity catalog
353 
354  Parameters
355  ----------
356  metadata : `lsst.daf.base.PropertyList`
357  Linearizer metadata
358 
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
366 
367  # Now pack it into a fits table.
368  length = max([len(self.linearityCoeffs[x]) for x in self.linearityCoeffs.keys()])
369 
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")
378 
379  if (self.fitParams):
380  lengthFitParams = max([len(self.fitParams[x]) for x in self.fitParams.keys()])
381 
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")
388 
389  catalog = afwTable.BaseCatalog(schema)
390  catalog.resize(len(self.linearityCoeffs.keys()))
391 
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]
400 
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)
405 
406  return catalog
407 
408  def toTableDataTable(self, metadata):
409  """Produce linearity catalog from table data
410 
411  Parameters
412  ----------
413  metadata : `lsst.daf.base.PropertyList`
414  Linearizer metadata
415 
416  Returns
417  -------
418  catalog : `lsst.afw.table.BaseCatalog`
419  Catalog to write
420  """
421 
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])
428 
429  for ii in range(dimensions[0]):
430  catalog[ii][lut] = np.array(self.tableData[ii], dtype=np.float32)
431 
432  metadata["LINEARITY_LOOKUP"] = True
433  catalog.setMetadata(metadata)
434 
435  return catalog
436 
437  def writeFits(self, filename):
438  """Write the linearity model to a FITS file.
439 
440  Parameters
441  ----------
442  filename : `str`
443  Name of the file to write.
444 
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)
455 
456  if self.tableData is not None:
457  catalog = self.toTableDataTable(metadata)
458  catalog.writeFits(filename, "a")
459 
460  return
461 
462  def getMetadata(self):
463  """Retrieve metadata associated with this `Linearizer`.
464 
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
473 
474  def setMetadata(self, metadata=None):
475  """Store a copy of the supplied metadata with the `Linearizer`.
476 
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)
488 
489  # Ensure that we have the obs type required by calibration ingest
490  self._metadata["OBSTYPE"] = self._OBSTYPE
491 
492  def updateMetadata(self, date=None, detectorId=None, detectorName=None, instrumentName=None, calibId=None,
493  serial=None):
494  """Update metadata keywords with new values.
495 
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
504 
505  """
506  mdOriginal = self.getMetadata()
507  mdSupplemental = dict()
508 
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
523 
524  mdOriginal.update(mdSupplemental)
525 
526  def getLinearityTypeByName(self, linearityTypeName):
527  """Determine the linearity class to use from the type name.
528 
529  Parameters
530  ----------
531  linearityTypeName : str
532  String name of the linearity type that is needed.
533 
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
548 
549  def validate(self, detector=None, amplifier=None):
550  """Validate linearity for a detector/amplifier.
551 
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.
558 
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())
581 
582  if amplifier:
583  amplifiersToCheck.extend(amplifier)
584 
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]))
605 
606  def applyLinearity(self, image, detector=None, log=None):
607  """Apply the linearity to an image.
608 
609  If the linearity parameters are populated, use those,
610  otherwise use the values from the detector.
611 
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)
626 
627  self.validate(detector)
628 
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  )
651 
652 
653 class LinearizeBase(metaclass=abc.ABCMeta):
654  """Abstract base class functor for correcting non-linearity.
655 
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.
659 
660  All linearity corrections should be defined in terms of an
661  additive correction, such that:
662 
663  corrected_value = uncorrected_value + f(uncorrected_value)
664  """
665  LinearityType = None # linearity type, a string used for AmpInfoCatalogs
666 
667  @abc.abstractmethod
668  def __call__(self, image, **kwargs):
669  """Correct non-linearity.
670 
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`).
683 
684  Returns
685  -------
686  output : `bool`
687  If true, a correction was applied successfully.
688 
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
696 
697 
698 class LinearizeLookupTable(LinearizeBase):
699  """Correct non-linearity with a persisted lookup table.
700 
701  The lookup table consists of entries such that given
702  "coefficients" c0, c1:
703 
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]
708 
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"
719 
720  def __call__(self, image, **kwargs):
721  """Correct for non-linearity.
722 
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`).
735 
736  Returns
737  -------
738  output : `bool`
739  If true, a correction was applied successfully.
740 
741  Raises
742  ------
743  RuntimeError:
744  Raised if the requested row index is out of the table
745  bounds.
746  """
747  numOutOfRange = 0
748 
749  rowInd, colIndOffset = kwargs['coeffs'][0:2]
750  table = kwargs['table']
751  log = kwargs['log']
752 
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)
760 
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
768 
769 
771  """Correct non-linearity with a polynomial mode.
772 
773  corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
774 
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"
786 
787  def __call__(self, image, **kwargs):
788  """Correct non-linearity.
789 
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`).
803 
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
813 
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
819 
820  return True, 0
821 
822 
824  """Correct non-linearity with a squared model.
825 
826  corrImage = uncorrImage + c0*uncorrImage^2
827 
828  where c0 is linearity coefficient 0 for each amplifier.
829  """
830  LinearityType = "Squared"
831 
832  def __call__(self, image, **kwargs):
833  """Correct for non-linearity.
834 
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`).
845 
846  Returns
847  -------
848  output : `bool`
849  If true, a correction was applied successfully.
850  """
851 
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
859 
860 
862  """Do not correct non-linearity.
863  """
864  LinearityType = "Proportional"
865 
866  def __call__(self, image, **kwargs):
867  """Do not correct for non-linearity.
868 
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`).
879 
880  Returns
881  -------
882  output : `bool`
883  If true, a correction was applied successfully.
884  """
885  return True, 0
886 
887 
889  """Do not correct non-linearity.
890  """
891  LinearityType = "None"
892 
893  def __call__(self, image, **kwargs):
894  """Do not correct for non-linearity.
895 
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`).
906 
907  Returns
908  -------
909  output : `bool`
910  If true, a correction was applied successfully.
911  """
912  return True, 0
lsst::ip::isr.linearize.Linearizer.override
override
Definition: linearize.py:88
lsst::ip::isr.linearize.Linearizer
Definition: linearize.py:40
lsst::ip::isr.linearize.Linearizer.linearityFitReducedChiSquared
linearityFitReducedChiSquared
Definition: linearize.py:86
lsst::ip::isr.linearize.Linearizer.fromTable
def fromTable(cls, table, tableExtTwo=None)
Definition: linearize.py:253
lsst::ip::isr.linearize.LinearizeBase.__call__
def __call__(self, image, **kwargs)
Definition: linearize.py:668
lsst::ip::isr.linearize.Linearizer.updateMetadata
def updateMetadata(self, date=None, detectorId=None, detectorName=None, instrumentName=None, calibId=None, serial=None)
Definition: linearize.py:492
lsst::ip::isr.linearize.Linearizer.writeFits
def writeFits(self, filename)
Definition: linearize.py:437
lsst::ip::isr.linearize.LinearizeBase
Definition: linearize.py:653
lsst::ip::isr.linearize.Linearizer.__init__
def __init__(self, table=None, detector=None, override=False, log=None)
Definition: linearize.py:71
lsst::ip::isr.linearize.Linearizer._detectorName
_detectorName
Definition: linearize.py:72
lsst::ip::isr.linearize.Linearizer.linearityMaximum
linearityMaximum
Definition: linearize.py:80
lsst::ip::isr.linearize.Linearizer.fromYaml
def fromYaml(self, yamlObject)
Definition: linearize.py:141
lsst::daf::base::PropertyList
lsst::ip::isr.linearize.Linearizer.populated
populated
Definition: linearize.py:89
lsst::ip::isr.linearize.LinearizeSquared.__call__
def __call__(self, image, **kwargs)
Definition: linearize.py:832
lsst::ip::isr.linearize.Linearizer.fitParams
fitParams
Definition: linearize.py:84
lsst::ip::isr.linearize.Linearizer.fromDetector
def fromDetector(self, detector)
Definition: linearize.py:121
lsst::ip::isr.linearize.Linearizer.linearityBBox
linearityBBox
Definition: linearize.py:82
lsst::ip::isr.linearize.Linearizer.toTableDataTable
def toTableDataTable(self, metadata)
Definition: linearize.py:408
lsst::ip::isr.linearize.LinearizeNone.__call__
def __call__(self, image, **kwargs)
Definition: linearize.py:893
lsst::ip::isr.linearize.LinearizeLookupTable.__call__
def __call__(self, image, **kwargs)
Definition: linearize.py:720
lsst::ip::isr.linearize.LinearizePolynomial
Definition: linearize.py:770
lsst::ip::isr.linearize.Linearizer.getMetadata
def getMetadata(self)
Definition: linearize.py:462
lsst::ip::isr.linearize.Linearizer._detectorId
_detectorId
Definition: linearize.py:74
applyLookupTable
lsst::ip::isr.linearize.Linearizer.tableData
tableData
Definition: linearize.py:92
lsst::ip::isr.linearize.Linearizer.linearityType
linearityType
Definition: linearize.py:78
lsst::ip::isr.linearize.Linearizer.linearityUnits
linearityUnits
Definition: linearize.py:81
lsst::ip::isr.linearize.Linearizer._metadata
_metadata
Definition: linearize.py:75
lsst::ip::isr.linearize.LinearizeNone
Definition: linearize.py:888
lsst::afw::table
lsst::ip::isr.linearize.Linearizer._detectorSerial
_detectorSerial
Definition: linearize.py:73
lsst::ip::isr.linearize.Linearizer.readFits
def readFits(cls, filename)
Definition: linearize.py:322
lsst::ip::isr.linearize.Linearizer.toDict
def toDict(self)
Definition: linearize.py:172
lsst::ip::isr.linearize.Linearizer.validate
def validate(self, detector=None, amplifier=None)
Definition: linearize.py:549
lsst::ip::isr.linearize.Linearizer.__call__
def __call__(self, exposure)
Definition: linearize.py:103
lsst::ip::isr.linearize.Linearizer.writeText
def writeText(self, filename)
Definition: linearize.py:220
lsst::ip::isr.linearize.LinearizeProportional
Definition: linearize.py:861
lsst::ip::isr.linearize.Linearizer.linearityThreshold
linearityThreshold
Definition: linearize.py:79
lsst::geom
lsst::daf::base
lsst::ip::isr.linearize.Linearizer.setMetadata
def setMetadata(self, metadata=None)
Definition: linearize.py:474
lsst::ip::isr.linearize.LinearizeProportional.__call__
def __call__(self, image, **kwargs)
Definition: linearize.py:866
lsst::ip::isr.linearize.Linearizer.log
log
Definition: linearize.py:90
Point< int, 2 >
lsst::ip::isr.linearize.LinearizeSquared
Definition: linearize.py:823
lsst::geom::Box2I
lsst::ip::isr.linearize.Linearizer.linearityCoeffs
linearityCoeffs
Definition: linearize.py:77
lsst::ip::isr.linearize.Linearizer.toAmpTable
def toAmpTable(self, metadata)
Definition: linearize.py:351
lsst::ip::isr.linearize.Linearizer.getLinearityTypeByName
def getLinearityTypeByName(self, linearityTypeName)
Definition: linearize.py:526
lsst::ip::isr.linearize.Linearizer._OBSTYPE
string _OBSTYPE
Definition: linearize.py:68
lsst::ip::isr.linearize.Linearizer.fitParamsErr
fitParamsErr
Definition: linearize.py:85
lsst::pipe::base
lsst::ip::isr.linearize.Linearizer.applyLinearity
def applyLinearity(self, image, detector=None, log=None)
Definition: linearize.py:606
Extent< int, 2 >
lsst::ip::isr.linearize.Linearizer.readText
def readText(cls, filename)
Definition: linearize.py:203
lsst::ip::isr.linearize.LinearizePolynomial.__call__
def __call__(self, image, **kwargs)
Definition: linearize.py:787