lsst.ip.isr  21.0.0-14-g07a2928+57ef95bf7c
brighterFatterKernel.py
Go to the documentation of this file.
1 # This file is part of ip_isr.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 #
22 """Brighter Fatter Kernel calibration definition."""
23 
24 
25 __all__ = ['BrighterFatterKernel']
26 
27 
28 import numpy as np
29 from astropy.table import Table
30 import lsst.afw.math as afwMath
31 from . import IsrCalib
32 
33 
34 class BrighterFatterKernel(IsrCalib):
35  """Calibration of brighter-fatter kernels for an instrument.
36 
37  ampKernels are the kernels for each amplifier in a detector, as
38  generated by having level == 'AMP'
39 
40  detectorKernel is the kernel generated for a detector as a
41  whole, as generated by having level == 'DETECTOR'
42 
43  makeDetectorKernelFromAmpwiseKernels is a method to generate the
44  kernel for a detector, constructed by averaging together the
45  ampwise kernels in the detector. The existing application code is
46  only defined for kernels with level == 'DETECTOR', so this method
47  is used if the supplied kernel was built with level == 'AMP'.
48 
49  Parameters
50  ----------
51  level : `str`
52  Level the kernels will be generated for.
53  log : `lsst.log.Log`, optional
54  Log to write messages to.
55  **kwargs :
56  Parameters to pass to parent constructor.
57 
58  """
59  _OBSTYPE = 'BFK'
60  _SCHEMA = 'Brighter-fatter kernel'
61  _VERSION = 1.0
62 
63  def __init__(self, camera=None, level=None, **kwargs):
64  self.levellevel = level
65 
66  # Things inherited from the PTC
67  self.meansmeans = dict()
68  self.variancesvariances = dict()
69  self.rawXcorrsrawXcorrs = dict()
70  self.badAmpsbadAmps = list()
71  self.shapeshape = (17, 17)
72  self.gaingain = dict()
73  self.noisenoise = dict()
74 
75  # Things calculated from the PTC
76  self.meanXcorrsmeanXcorrs = dict()
77  self.validvalid = dict()
78 
79  # Things that are used downstream
80  self.ampKernelsampKernels = dict()
81  self.detKernelsdetKernels = dict()
82 
83  if camera:
84  self.initFromCamerainitFromCamera(camera, detectorId=kwargs.get('detectorId', None))
85 
86  super().__init__(**kwargs)
87  self.requiredAttributes.update(['level', 'means', 'variances', 'rawXcorrs',
88  'badAmps', 'gain', 'noise', 'meanXcorrs', 'valid',
89  'ampKernels', 'detKernels'])
90 
91  def __eq__(self, other):
92  """Calibration equivalence
93  """
94  if not isinstance(other, self.__class__):
95  return False
96 
97  for attr in self._requiredAttributes:
98  attrSelf = getattr(self, attr)
99  attrOther = getattr(other, attr)
100  if isinstance(attrSelf, dict) and isinstance(attrOther, dict):
101  for ampName in attrSelf:
102  if not np.allclose(attrSelf[ampName], attrOther[ampName], equal_nan=True):
103  return False
104  else:
105  if attrSelf != attrOther:
106  return False
107  return True
108 
109  def updateMetadata(self, setDate=False, **kwargs):
110  """Update calibration metadata.
111 
112  This calls the base class's method after ensuring the required
113  calibration keywords will be saved.
114 
115  Parameters
116  ----------
117  setDate : `bool`, optional
118  Update the CALIBDATE fields in the metadata to the current
119  time. Defaults to False.
120  kwargs :
121  Other keyword parameters to set in the metadata.
122  """
123  kwargs['LEVEL'] = self.levellevel
124  kwargs['KERNEL_DX'] = self.shapeshape[0]
125  kwargs['KERNEL_DY'] = self.shapeshape[1]
126 
127  super().updateMetadata(setDate=setDate, **kwargs)
128 
129  def initFromCamera(self, camera, detectorId=None):
130  """Initialize kernel structure from camera.
131 
132  Parameters
133  ----------
134  camera : `lsst.afw.cameraGeom.Camera`
135  Camera to use to define geometry.
136  detectorId : `int`, optional
137  Index of the detector to generate.
138 
139  Returns
140  -------
141  calib : `lsst.ip.isr.BrighterFatterKernel`
142  The initialized calibration.
143 
144  Raises
145  ------
146  RuntimeError :
147  Raised if no detectorId is supplied for a calibration with
148  level='AMP'.
149  """
150  self._instrument_instrument = camera.getName()
151 
152  if self.levellevel == 'AMP':
153  if detectorId is None:
154  raise RuntimeError("A detectorId must be supplied if level='AMP'.")
155 
156  detector = camera[detectorId]
157  self._detectorId_detectorId = detectorId
158  self._detectorName_detectorName = detector.getName()
159  self._detectorSerial_detectorSerial = detector.getSerial()
160  self.badAmpsbadAmps = []
161 
162  for amp in detector:
163  ampName = amp.getName()
164  self.meansmeans[ampName] = []
165  self.variancesvariances[ampName] = []
166  self.rawXcorrsrawXcorrs[ampName] = []
167  self.gaingain[ampName] = amp.getGain()
168  self.noisenoise[ampName] = amp.getReadNoise()
169  self.meanXcorrsmeanXcorrs[ampName] = []
170  self.ampKernelsampKernels[ampName] = []
171  self.validvalid[ampName] = []
172  elif self.levellevel == 'DETECTOR':
173  for det in camera:
174  detName = det.getName()
175  self.detKernelsdetKernels[detName] = []
176 
177  return self
178 
179  def getLengths(self):
180  """Return the set of lengths needed for reshaping components.
181 
182  Returns
183  -------
184  kernelLength : `int`
185  Product of the elements of self.shape.
186  smallLength : `int`
187  Size of an untiled covariance.
188  nObs : `int`
189  Number of observation pairs used in the kernel.
190  """
191  kernelLength = self.shapeshape[0] * self.shapeshape[1]
192  smallLength = int((self.shapeshape[0] - 1)*(self.shapeshape[1] - 1)/4)
193  nObservations = set([len(self.meansmeans[amp]) for amp in self.meansmeans])
194  if len(nObservations) != 1:
195  raise RuntimeError("Inconsistent number of observations found.")
196  nObs = nObservations.pop()
197 
198  return (kernelLength, smallLength, nObs)
199 
200  @classmethod
201  def fromDict(cls, dictionary):
202  """Construct a calibration from a dictionary of properties.
203 
204  Parameters
205  ----------
206  dictionary : `dict`
207  Dictionary of properties.
208 
209  Returns
210  -------
211  calib : `lsst.ip.isr.BrighterFatterKernel
212  Constructed calibration.
213 
214  Raises
215  ------
216  RuntimeError :
217  Raised if the supplied dictionary is for a different
218  calibration.
219  """
220  calib = cls()
221 
222  if calib._OBSTYPE != (found := dictionary['metadata']['OBSTYPE']):
223  raise RuntimeError(f"Incorrect brighter-fatter kernel supplied. Expected {calib._OBSTYPE}, "
224  f"found {found}")
225 
226  calib.setMetadata(dictionary['metadata'])
227  calib.calibInfoFromDict(dictionary)
228 
229  calib.level = dictionary['metadata'].get('LEVEL', 'AMP')
230  calib.shape = (dictionary['metadata'].get('KERNEL_DX', 0),
231  dictionary['metadata'].get('KERNEL_DY', 0))
232 
233  calib.means = {amp: np.array(dictionary['means'][amp]) for amp in dictionary['means']}
234  calib.variances = {amp: np.array(dictionary['variances'][amp]) for amp in dictionary['variances']}
235 
236  # Lengths for reshape:
237  _, smallLength, nObs = calib.getLengths()
238  smallShapeSide = int(np.sqrt(smallLength))
239 
240  calib.rawXcorrs = {amp: np.array(dictionary['rawXcorrs'][amp]).reshape((nObs,
241  smallShapeSide,
242  smallShapeSide))
243  for amp in dictionary['rawXcorrs']}
244 
245  calib.gain = dictionary['gain']
246  calib.noise = dictionary['noise']
247 
248  calib.meanXcorrs = {amp: np.array(dictionary['meanXcorrs'][amp]).reshape(calib.shape)
249  for amp in dictionary['rawXcorrs']}
250  calib.ampKernels = {amp: np.array(dictionary['ampKernels'][amp]).reshape(calib.shape)
251  for amp in dictionary['ampKernels']}
252  calib.valid = {amp: bool(value) for amp, value in dictionary['valid'].items()}
253  calib.badAmps = [amp for amp, valid in dictionary['valid'].items() if valid is False]
254 
255  calib.detKernels = {det: np.array(dictionary['detKernels'][det]).reshape(calib.shape)
256  for det in dictionary['detKernels']}
257 
258  calib.updateMetadata()
259  return calib
260 
261  def toDict(self):
262  """Return a dictionary containing the calibration properties.
263 
264  The dictionary should be able to be round-tripped through
265  `fromDict`.
266 
267  Returns
268  -------
269  dictionary : `dict`
270  Dictionary of properties.
271  """
272  self.updateMetadataupdateMetadata()
273 
274  outDict = {}
275  metadata = self.getMetadata()
276  outDict['metadata'] = metadata
277 
278  # Lengths for ravel:
279  kernelLength, smallLength, nObs = self.getLengthsgetLengths()
280 
281  outDict['means'] = {amp: np.array(self.meansmeans[amp]).tolist() for amp in self.meansmeans}
282  outDict['variances'] = {amp: np.array(self.variancesvariances[amp]).tolist() for amp in self.variancesvariances}
283  outDict['rawXcorrs'] = {amp: np.array(self.rawXcorrsrawXcorrs[amp]).reshape(nObs*smallLength).tolist()
284  for amp in self.rawXcorrsrawXcorrs}
285  outDict['badAmps'] = self.badAmpsbadAmps
286  outDict['gain'] = self.gaingain
287  outDict['noise'] = self.noisenoise
288 
289  outDict['meanXcorrs'] = {amp: self.meanXcorrsmeanXcorrs[amp].reshape(kernelLength).tolist()
290  for amp in self.meanXcorrsmeanXcorrs}
291  outDict['ampKernels'] = {amp: self.ampKernelsampKernels[amp].reshape(kernelLength).tolist()
292  for amp in self.ampKernelsampKernels}
293  outDict['valid'] = self.validvalid
294  outDict['detKernels'] = {det: self.detKernelsdetKernels[det].reshape(kernelLength).tolist()
295  for det in self.detKernelsdetKernels}
296  return outDict
297 
298  @classmethod
299  def fromTable(cls, tableList):
300  """Construct calibration from a list of tables.
301 
302  This method uses the `fromDict` method to create the
303  calibration, after constructing an appropriate dictionary from
304  the input tables.
305 
306  Parameters
307  ----------
308  tableList : `list` [`astropy.table.Table`]
309  List of tables to use to construct the brighter-fatter
310  calibration.
311 
312  Returns
313  -------
314  calib : `lsst.ip.isr.BrighterFatterKernel`
315  The calibration defined in the tables.
316  """
317  ampTable = tableList[0]
318 
319  metadata = ampTable.meta
320  inDict = dict()
321  inDict['metadata'] = metadata
322 
323  amps = ampTable['AMPLIFIER']
324 
325  meanList = ampTable['MEANS']
326  varianceList = ampTable['VARIANCES']
327 
328  rawXcorrs = ampTable['RAW_XCORRS']
329  gainList = ampTable['GAIN']
330  noiseList = ampTable['NOISE']
331 
332  meanXcorrs = ampTable['MEAN_XCORRS']
333  ampKernels = ampTable['KERNEL']
334  validList = ampTable['VALID']
335 
336  inDict['means'] = {amp: mean for amp, mean in zip(amps, meanList)}
337  inDict['variances'] = {amp: var for amp, var in zip(amps, varianceList)}
338  inDict['rawXcorrs'] = {amp: kernel for amp, kernel in zip(amps, rawXcorrs)}
339  inDict['gain'] = {amp: gain for amp, gain in zip(amps, gainList)}
340  inDict['noise'] = {amp: noise for amp, noise in zip(amps, noiseList)}
341  inDict['meanXcorrs'] = {amp: kernel for amp, kernel in zip(amps, meanXcorrs)}
342  inDict['ampKernels'] = {amp: kernel for amp, kernel in zip(amps, ampKernels)}
343  inDict['valid'] = {amp: bool(valid) for amp, valid in zip(amps, validList)}
344 
345  inDict['badAmps'] = [amp for amp, valid in inDict['valid'].items() if valid is False]
346 
347  if len(tableList) > 1:
348  detTable = tableList[1]
349  inDict['detKernels'] = {det: kernel for det, kernel
350  in zip(detTable['DETECTOR'], detTable['KERNEL'])}
351  else:
352  inDict['detKernels'] = {}
353 
354  return cls.fromDictfromDict(inDict)
355 
356  def toTable(self):
357  """Construct a list of tables containing the information in this calibration.
358 
359  The list of tables should create an identical calibration
360  after being passed to this class's fromTable method.
361 
362  Returns
363  -------
364  tableList : `list` [`lsst.afw.table.Table`]
365  List of tables containing the crosstalk calibration
366  information.
367 
368  """
369  tableList = []
370  self.updateMetadataupdateMetadata()
371 
372  # Lengths
373  kernelLength, smallLength, nObs = self.getLengthsgetLengths()
374 
375  ampList = []
376  meanList = []
377  varianceList = []
378  rawXcorrs = []
379  gainList = []
380  noiseList = []
381 
382  meanXcorrsList = []
383  kernelList = []
384  validList = []
385 
386  for amp in self.meansmeans.keys():
387  ampList.append(amp)
388  meanList.append(self.meansmeans[amp])
389  varianceList.append(self.variancesvariances[amp])
390  rawXcorrs.append(np.array(self.rawXcorrsrawXcorrs[amp]).reshape(nObs*smallLength).tolist())
391  gainList.append(self.gaingain[amp])
392  noiseList.append(self.noisenoise[amp])
393 
394  meanXcorrsList.append(self.meanXcorrsmeanXcorrs[amp].reshape(kernelLength).tolist())
395  kernelList.append(self.ampKernelsampKernels[amp].reshape(kernelLength).tolist())
396  validList.append(int(self.validvalid[amp] and not (amp in self.badAmpsbadAmps)))
397 
398  ampTable = Table({'AMPLIFIER': ampList,
399  'MEANS': meanList,
400  'VARIANCES': varianceList,
401  'RAW_XCORRS': rawXcorrs,
402  'GAIN': gainList,
403  'NOISE': noiseList,
404  'MEAN_XCORRS': meanXcorrsList,
405  'KERNEL': kernelList,
406  'VALID': validList,
407  })
408 
409  ampTable.meta = self.getMetadata().toDict()
410  tableList.append(ampTable)
411 
412  if len(self.detKernelsdetKernels):
413  detList = []
414  kernelList = []
415  for det in self.detKernelsdetKernels.keys():
416  detList.append(det)
417  kernelList.append(self.detKernelsdetKernels[det].reshape(kernelLength).tolist())
418 
419  detTable = Table({'DETECTOR': detList,
420  'KERNEL': kernelList})
421  detTable.meta = self.getMetadata().toDict()
422  tableList.append(detTable)
423 
424  return tableList
425 
426  # Implementation methods
427  def makeDetectorKernelFromAmpwiseKernels(self, detectorName, ampsToExclude=[]):
428  """Average the amplifier level kernels to create a detector level kernel.
429  """
430  inKernels = np.array([self.ampKernelsampKernels[amp] for amp in
431  self.ampKernelsampKernels if amp not in ampsToExclude])
432  averagingList = np.transpose(inKernels)
433  avgKernel = np.zeros_like(inKernels[0])
434  sctrl = afwMath.StatisticsControl()
435  sctrl.setNumSigmaClip(5.0)
436  for i in range(np.shape(avgKernel)[0]):
437  for j in range(np.shape(avgKernel)[1]):
438  avgKernel[i, j] = afwMath.makeStatistics(averagingList[i, j],
439  afwMath.MEANCLIP, sctrl).getValue()
440 
441  self.detKernelsdetKernels[detectorName] = avgKernel
442 
443  def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName):
444  self.detKernel[detectorName] = self.ampKernel[ampName]
def __init__(self, camera=None, level=None, **kwargs)
def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName)
def makeDetectorKernelFromAmpwiseKernels(self, detectorName, ampsToExclude=[])