lsst.ip.isr  21.0.0-17-g535a0ce+6aead16637
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 updateMetadata(self, setDate=False, **kwargs):
92  """Update calibration metadata.
93 
94  This calls the base class's method after ensuring the required
95  calibration keywords will be saved.
96 
97  Parameters
98  ----------
99  setDate : `bool`, optional
100  Update the CALIBDATE fields in the metadata to the current
101  time. Defaults to False.
102  kwargs :
103  Other keyword parameters to set in the metadata.
104  """
105  kwargs['LEVEL'] = self.levellevel
106  kwargs['KERNEL_DX'] = self.shapeshape[0]
107  kwargs['KERNEL_DY'] = self.shapeshape[1]
108 
109  super().updateMetadata(setDate=setDate, **kwargs)
110 
111  def initFromCamera(self, camera, detectorId=None):
112  """Initialize kernel structure from camera.
113 
114  Parameters
115  ----------
116  camera : `lsst.afw.cameraGeom.Camera`
117  Camera to use to define geometry.
118  detectorId : `int`, optional
119  Index of the detector to generate.
120 
121  Returns
122  -------
123  calib : `lsst.ip.isr.BrighterFatterKernel`
124  The initialized calibration.
125 
126  Raises
127  ------
128  RuntimeError :
129  Raised if no detectorId is supplied for a calibration with
130  level='AMP'.
131  """
132  self._instrument_instrument = camera.getName()
133 
134  if self.levellevel == 'AMP':
135  if detectorId is None:
136  raise RuntimeError("A detectorId must be supplied if level='AMP'.")
137 
138  detector = camera[detectorId]
139  self._detectorId_detectorId = detectorId
140  self._detectorName_detectorName = detector.getName()
141  self._detectorSerial_detectorSerial = detector.getSerial()
142  self.badAmpsbadAmps = []
143 
144  for amp in detector:
145  ampName = amp.getName()
146  self.meansmeans[ampName] = []
147  self.variancesvariances[ampName] = []
148  self.rawXcorrsrawXcorrs[ampName] = []
149  self.gaingain[ampName] = amp.getGain()
150  self.noisenoise[ampName] = amp.getReadNoise()
151  self.meanXcorrsmeanXcorrs[ampName] = []
152  self.ampKernelsampKernels[ampName] = []
153  self.validvalid[ampName] = []
154  elif self.levellevel == 'DETECTOR':
155  for det in camera:
156  detName = det.getName()
157  self.detKernelsdetKernels[detName] = []
158 
159  return self
160 
161  def getLengths(self):
162  """Return the set of lengths needed for reshaping components.
163 
164  Returns
165  -------
166  kernelLength : `int`
167  Product of the elements of self.shape.
168  smallLength : `int`
169  Size of an untiled covariance.
170  nObs : `int`
171  Number of observation pairs used in the kernel.
172  """
173  kernelLength = self.shapeshape[0] * self.shapeshape[1]
174  smallLength = int((self.shapeshape[0] - 1)*(self.shapeshape[1] - 1)/4)
175  nObservations = set([len(self.meansmeans[amp]) for amp in self.meansmeans])
176  if len(nObservations) != 1:
177  raise RuntimeError("Inconsistent number of observations found.")
178  nObs = nObservations.pop()
179 
180  return (kernelLength, smallLength, nObs)
181 
182  @classmethod
183  def fromDict(cls, dictionary):
184  """Construct a calibration from a dictionary of properties.
185 
186  Parameters
187  ----------
188  dictionary : `dict`
189  Dictionary of properties.
190 
191  Returns
192  -------
193  calib : `lsst.ip.isr.BrighterFatterKernel
194  Constructed calibration.
195 
196  Raises
197  ------
198  RuntimeError :
199  Raised if the supplied dictionary is for a different
200  calibration.
201  """
202  calib = cls()
203 
204  if calib._OBSTYPE != (found := dictionary['metadata']['OBSTYPE']):
205  raise RuntimeError(f"Incorrect brighter-fatter kernel supplied. Expected {calib._OBSTYPE}, "
206  f"found {found}")
207 
208  calib.setMetadata(dictionary['metadata'])
209  calib.calibInfoFromDict(dictionary)
210 
211  calib.level = dictionary['metadata'].get('LEVEL', 'AMP')
212  calib.shape = (dictionary['metadata'].get('KERNEL_DX', 0),
213  dictionary['metadata'].get('KERNEL_DY', 0))
214 
215  calib.means = {amp: np.array(dictionary['means'][amp]) for amp in dictionary['means']}
216  calib.variances = {amp: np.array(dictionary['variances'][amp]) for amp in dictionary['variances']}
217 
218  # Lengths for reshape:
219  _, smallLength, nObs = calib.getLengths()
220  smallShapeSide = int(np.sqrt(smallLength))
221 
222  calib.rawXcorrs = {amp: np.array(dictionary['rawXcorrs'][amp]).reshape((nObs,
223  smallShapeSide,
224  smallShapeSide))
225  for amp in dictionary['rawXcorrs']}
226 
227  calib.gain = dictionary['gain']
228  calib.noise = dictionary['noise']
229 
230  calib.meanXcorrs = {amp: np.array(dictionary['meanXcorrs'][amp]).reshape(calib.shape)
231  for amp in dictionary['rawXcorrs']}
232  calib.ampKernels = {amp: np.array(dictionary['ampKernels'][amp]).reshape(calib.shape)
233  for amp in dictionary['ampKernels']}
234  calib.valid = {amp: bool(value) for amp, value in dictionary['valid'].items()}
235  calib.badAmps = [amp for amp, valid in dictionary['valid'].items() if valid is False]
236 
237  calib.detKernels = {det: np.array(dictionary['detKernels'][det]).reshape(calib.shape)
238  for det in dictionary['detKernels']}
239 
240  calib.updateMetadata()
241  return calib
242 
243  def toDict(self):
244  """Return a dictionary containing the calibration properties.
245 
246  The dictionary should be able to be round-tripped through
247  `fromDict`.
248 
249  Returns
250  -------
251  dictionary : `dict`
252  Dictionary of properties.
253  """
254  self.updateMetadataupdateMetadata()
255 
256  outDict = {}
257  metadata = self.getMetadata()
258  outDict['metadata'] = metadata
259 
260  # Lengths for ravel:
261  kernelLength, smallLength, nObs = self.getLengthsgetLengths()
262 
263  outDict['means'] = {amp: np.array(self.meansmeans[amp]).tolist() for amp in self.meansmeans}
264  outDict['variances'] = {amp: np.array(self.variancesvariances[amp]).tolist() for amp in self.variancesvariances}
265  outDict['rawXcorrs'] = {amp: np.array(self.rawXcorrsrawXcorrs[amp]).reshape(nObs*smallLength).tolist()
266  for amp in self.rawXcorrsrawXcorrs}
267  outDict['badAmps'] = self.badAmpsbadAmps
268  outDict['gain'] = self.gaingain
269  outDict['noise'] = self.noisenoise
270 
271  outDict['meanXcorrs'] = {amp: self.meanXcorrsmeanXcorrs[amp].reshape(kernelLength).tolist()
272  for amp in self.meanXcorrsmeanXcorrs}
273  outDict['ampKernels'] = {amp: self.ampKernelsampKernels[amp].reshape(kernelLength).tolist()
274  for amp in self.ampKernelsampKernels}
275  outDict['valid'] = self.validvalid
276  outDict['detKernels'] = {det: self.detKernelsdetKernels[det].reshape(kernelLength).tolist()
277  for det in self.detKernelsdetKernels}
278  return outDict
279 
280  @classmethod
281  def fromTable(cls, tableList):
282  """Construct calibration from a list of tables.
283 
284  This method uses the `fromDict` method to create the
285  calibration, after constructing an appropriate dictionary from
286  the input tables.
287 
288  Parameters
289  ----------
290  tableList : `list` [`astropy.table.Table`]
291  List of tables to use to construct the brighter-fatter
292  calibration.
293 
294  Returns
295  -------
296  calib : `lsst.ip.isr.BrighterFatterKernel`
297  The calibration defined in the tables.
298  """
299  ampTable = tableList[0]
300 
301  metadata = ampTable.meta
302  inDict = dict()
303  inDict['metadata'] = metadata
304 
305  amps = ampTable['AMPLIFIER']
306 
307  meanList = ampTable['MEANS']
308  varianceList = ampTable['VARIANCES']
309 
310  rawXcorrs = ampTable['RAW_XCORRS']
311  gainList = ampTable['GAIN']
312  noiseList = ampTable['NOISE']
313 
314  meanXcorrs = ampTable['MEAN_XCORRS']
315  ampKernels = ampTable['KERNEL']
316  validList = ampTable['VALID']
317 
318  inDict['means'] = {amp: mean for amp, mean in zip(amps, meanList)}
319  inDict['variances'] = {amp: var for amp, var in zip(amps, varianceList)}
320  inDict['rawXcorrs'] = {amp: kernel for amp, kernel in zip(amps, rawXcorrs)}
321  inDict['gain'] = {amp: gain for amp, gain in zip(amps, gainList)}
322  inDict['noise'] = {amp: noise for amp, noise in zip(amps, noiseList)}
323  inDict['meanXcorrs'] = {amp: kernel for amp, kernel in zip(amps, meanXcorrs)}
324  inDict['ampKernels'] = {amp: kernel for amp, kernel in zip(amps, ampKernels)}
325  inDict['valid'] = {amp: bool(valid) for amp, valid in zip(amps, validList)}
326 
327  inDict['badAmps'] = [amp for amp, valid in inDict['valid'].items() if valid is False]
328 
329  if len(tableList) > 1:
330  detTable = tableList[1]
331  inDict['detKernels'] = {det: kernel for det, kernel
332  in zip(detTable['DETECTOR'], detTable['KERNEL'])}
333  else:
334  inDict['detKernels'] = {}
335 
336  return cls.fromDictfromDict(inDict)
337 
338  def toTable(self):
339  """Construct a list of tables containing the information in this calibration.
340 
341  The list of tables should create an identical calibration
342  after being passed to this class's fromTable method.
343 
344  Returns
345  -------
346  tableList : `list` [`lsst.afw.table.Table`]
347  List of tables containing the crosstalk calibration
348  information.
349 
350  """
351  tableList = []
352  self.updateMetadataupdateMetadata()
353 
354  # Lengths
355  kernelLength, smallLength, nObs = self.getLengthsgetLengths()
356 
357  ampList = []
358  meanList = []
359  varianceList = []
360  rawXcorrs = []
361  gainList = []
362  noiseList = []
363 
364  meanXcorrsList = []
365  kernelList = []
366  validList = []
367 
368  for amp in self.meansmeans.keys():
369  ampList.append(amp)
370  meanList.append(self.meansmeans[amp])
371  varianceList.append(self.variancesvariances[amp])
372  rawXcorrs.append(np.array(self.rawXcorrsrawXcorrs[amp]).reshape(nObs*smallLength).tolist())
373  gainList.append(self.gaingain[amp])
374  noiseList.append(self.noisenoise[amp])
375 
376  meanXcorrsList.append(self.meanXcorrsmeanXcorrs[amp].reshape(kernelLength).tolist())
377  kernelList.append(self.ampKernelsampKernels[amp].reshape(kernelLength).tolist())
378  validList.append(int(self.validvalid[amp] and not (amp in self.badAmpsbadAmps)))
379 
380  ampTable = Table({'AMPLIFIER': ampList,
381  'MEANS': meanList,
382  'VARIANCES': varianceList,
383  'RAW_XCORRS': rawXcorrs,
384  'GAIN': gainList,
385  'NOISE': noiseList,
386  'MEAN_XCORRS': meanXcorrsList,
387  'KERNEL': kernelList,
388  'VALID': validList,
389  })
390 
391  ampTable.meta = self.getMetadata().toDict()
392  tableList.append(ampTable)
393 
394  if len(self.detKernelsdetKernels):
395  detList = []
396  kernelList = []
397  for det in self.detKernelsdetKernels.keys():
398  detList.append(det)
399  kernelList.append(self.detKernelsdetKernels[det].reshape(kernelLength).tolist())
400 
401  detTable = Table({'DETECTOR': detList,
402  'KERNEL': kernelList})
403  detTable.meta = self.getMetadata().toDict()
404  tableList.append(detTable)
405 
406  return tableList
407 
408  # Implementation methods
409  def makeDetectorKernelFromAmpwiseKernels(self, detectorName, ampsToExclude=[]):
410  """Average the amplifier level kernels to create a detector level kernel.
411  """
412  inKernels = np.array([self.ampKernelsampKernels[amp] for amp in
413  self.ampKernelsampKernels if amp not in ampsToExclude])
414  averagingList = np.transpose(inKernels)
415  avgKernel = np.zeros_like(inKernels[0])
416  sctrl = afwMath.StatisticsControl()
417  sctrl.setNumSigmaClip(5.0)
418  for i in range(np.shape(avgKernel)[0]):
419  for j in range(np.shape(avgKernel)[1]):
420  avgKernel[i, j] = afwMath.makeStatistics(averagingList[i, j],
421  afwMath.MEANCLIP, sctrl).getValue()
422 
423  self.detKernelsdetKernels[detectorName] = avgKernel
424 
425  def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName):
426  self.detKernel[detectorName] = self.ampKernel[ampName]
def __init__(self, camera=None, level=None, **kwargs)
def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName)
def makeDetectorKernelFromAmpwiseKernels(self, detectorName, ampsToExclude=[])