Coverage for python/lsst/ip/isr/brighterFatterKernel.py : 8%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# 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."""
25__all__ = ['BrighterFatterKernel']
28import numpy as np
29from astropy.table import Table
30import lsst.afw.math as afwMath
31from . import IsrCalib
34class BrighterFatterKernel(IsrCalib):
35 """Calibration of brighter-fatter kernels for an instrument.
37 ampKernels are the kernels for each amplifier in a detector, as
38 generated by having level == 'AMP'
40 detectorKernel is the kernel generated for a detector as a
41 whole, as generated by having level == 'DETECTOR'
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'.
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.
58 """
59 _OBSTYPE = 'BFK'
60 _SCHEMA = 'Brighter-fatter kernel'
61 _VERSION = 1.0
63 def __init__(self, camera=None, level=None, **kwargs):
64 self.level = level
66 # Things inherited from the PTC
67 self.means = dict()
68 self.variances = dict()
69 self.rawXcorrs = dict()
70 self.badAmps = list()
71 self.shape = (17, 17)
72 self.gain = dict()
73 self.noise = dict()
75 # Things calculated from the PTC
76 self.meanXcorrs = dict()
77 self.valid = dict()
79 # Things that are used downstream
80 self.ampKernels = dict()
81 self.detKernels = dict()
83 if camera:
84 self.initFromCamera(camera, detectorId=kwargs.get('detectorId', None))
86 super().__init__(**kwargs)
87 self.requiredAttributes.update(['level', 'means', 'variances', 'rawXcorrs',
88 'badAmps', 'gain', 'noise', 'meanXcorrs', 'valid',
89 'ampKernels', 'detKernels'])
91 def __eq__(self, other):
92 """Calibration equivalence
93 """
94 if not isinstance(other, self.__class__):
95 return False
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
109 def updateMetadata(self, setDate=False, **kwargs):
110 """Update calibration metadata.
112 This calls the base class's method after ensuring the required
113 calibration keywords will be saved.
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.level
124 kwargs['KERNEL_DX'] = self.shape[0]
125 kwargs['KERNEL_DY'] = self.shape[1]
127 super().updateMetadata(setDate=setDate, **kwargs)
129 def initFromCamera(self, camera, detectorId=None):
130 """Initialize kernel structure from camera.
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.
139 Returns
140 -------
141 calib : `lsst.ip.isr.BrighterFatterKernel`
142 The initialized calibration.
144 Raises
145 ------
146 RuntimeError :
147 Raised if no detectorId is supplied for a calibration with
148 level='AMP'.
149 """
150 self._instrument = camera.getName()
152 if self.level == 'AMP':
153 if detectorId is None:
154 raise RuntimeError("A detectorId must be supplied if level='AMP'.")
156 detector = camera[detectorId]
157 self._detectorId = detectorId
158 self._detectorName = detector.getName()
159 self._detectorSerial = detector.getSerial()
160 self.badAmps = []
162 for amp in detector:
163 ampName = amp.getName()
164 self.means[ampName] = []
165 self.variances[ampName] = []
166 self.rawXcorrs[ampName] = []
167 self.gain[ampName] = amp.getGain()
168 self.noise[ampName] = amp.getReadNoise()
169 self.meanXcorrs[ampName] = []
170 self.ampKernels[ampName] = []
171 self.valid[ampName] = []
172 elif self.level == 'DETECTOR':
173 for det in camera:
174 detName = det.getName()
175 self.detKernels[detName] = []
177 return self
179 def getLengths(self):
180 """Return the set of lengths needed for reshaping components.
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.shape[0] * self.shape[1]
192 smallLength = int((self.shape[0] - 1)*(self.shape[1] - 1)/4)
193 nObservations = set([len(self.means[amp]) for amp in self.means])
194 if len(nObservations) != 1:
195 raise RuntimeError("Inconsistent number of observations found.")
196 nObs = nObservations.pop()
198 return (kernelLength, smallLength, nObs)
200 @classmethod
201 def fromDict(cls, dictionary):
202 """Construct a calibration from a dictionary of properties.
204 Parameters
205 ----------
206 dictionary : `dict`
207 Dictionary of properties.
209 Returns
210 -------
211 calib : `lsst.ip.isr.BrighterFatterKernel
212 Constructed calibration.
214 Raises
215 ------
216 RuntimeError :
217 Raised if the supplied dictionary is for a different
218 calibration.
219 """
220 calib = cls()
222 if calib._OBSTYPE != (found := dictionary['metadata']['OBSTYPE']):
223 raise RuntimeError(f"Incorrect brighter-fatter kernel supplied. Expected {calib._OBSTYPE}, "
224 f"found {found}")
226 calib.setMetadata(dictionary['metadata'])
227 calib.calibInfoFromDict(dictionary)
229 calib.level = dictionary['metadata'].get('LEVEL', 'AMP')
230 calib.shape = (dictionary['metadata'].get('KERNEL_DX', 0),
231 dictionary['metadata'].get('KERNEL_DY', 0))
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']}
236 # Lengths for reshape:
237 _, smallLength, nObs = calib.getLengths()
238 smallShapeSide = int(np.sqrt(smallLength))
240 calib.rawXcorrs = {amp: np.array(dictionary['rawXcorrs'][amp]).reshape((nObs,
241 smallShapeSide,
242 smallShapeSide))
243 for amp in dictionary['rawXcorrs']}
245 calib.gain = dictionary['gain']
246 calib.noise = dictionary['noise']
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]
255 calib.detKernels = {det: np.array(dictionary['detKernels'][det]).reshape(calib.shape)
256 for det in dictionary['detKernels']}
258 calib.updateMetadata()
259 return calib
261 def toDict(self):
262 """Return a dictionary containing the calibration properties.
264 The dictionary should be able to be round-tripped through
265 `fromDict`.
267 Returns
268 -------
269 dictionary : `dict`
270 Dictionary of properties.
271 """
272 self.updateMetadata()
274 outDict = {}
275 metadata = self.getMetadata()
276 outDict['metadata'] = metadata
278 # Lengths for ravel:
279 kernelLength, smallLength, nObs = self.getLengths()
281 outDict['means'] = {amp: np.array(self.means[amp]).tolist() for amp in self.means}
282 outDict['variances'] = {amp: np.array(self.variances[amp]).tolist() for amp in self.variances}
283 outDict['rawXcorrs'] = {amp: np.array(self.rawXcorrs[amp]).reshape(nObs*smallLength).tolist()
284 for amp in self.rawXcorrs}
285 outDict['badAmps'] = self.badAmps
286 outDict['gain'] = self.gain
287 outDict['noise'] = self.noise
289 outDict['meanXcorrs'] = {amp: self.meanXcorrs[amp].reshape(kernelLength).tolist()
290 for amp in self.meanXcorrs}
291 outDict['ampKernels'] = {amp: self.ampKernels[amp].reshape(kernelLength).tolist()
292 for amp in self.ampKernels}
293 outDict['valid'] = self.valid
294 outDict['detKernels'] = {det: self.detKernels[det].reshape(kernelLength).tolist()
295 for det in self.detKernels}
296 return outDict
298 @classmethod
299 def fromTable(cls, tableList):
300 """Construct calibration from a list of tables.
302 This method uses the `fromDict` method to create the
303 calibration, after constructing an appropriate dictionary from
304 the input tables.
306 Parameters
307 ----------
308 tableList : `list` [`astropy.table.Table`]
309 List of tables to use to construct the brighter-fatter
310 calibration.
312 Returns
313 -------
314 calib : `lsst.ip.isr.BrighterFatterKernel`
315 The calibration defined in the tables.
316 """
317 ampTable = tableList[0]
319 metadata = ampTable.meta
320 inDict = dict()
321 inDict['metadata'] = metadata
323 amps = ampTable['AMPLIFIER']
325 meanList = ampTable['MEANS']
326 varianceList = ampTable['VARIANCES']
328 rawXcorrs = ampTable['RAW_XCORRS']
329 gainList = ampTable['GAIN']
330 noiseList = ampTable['NOISE']
332 meanXcorrs = ampTable['MEAN_XCORRS']
333 ampKernels = ampTable['KERNEL']
334 validList = ampTable['VALID']
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)}
345 inDict['badAmps'] = [amp for amp, valid in inDict['valid'].items() if valid is False]
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'] = {}
354 return cls.fromDict(inDict)
356 def toTable(self):
357 """Construct a list of tables containing the information in this calibration.
359 The list of tables should create an identical calibration
360 after being passed to this class's fromTable method.
362 Returns
363 -------
364 tableList : `list` [`lsst.afw.table.Table`]
365 List of tables containing the crosstalk calibration
366 information.
368 """
369 tableList = []
370 self.updateMetadata()
372 # Lengths
373 kernelLength, smallLength, nObs = self.getLengths()
375 ampList = []
376 meanList = []
377 varianceList = []
378 rawXcorrs = []
379 gainList = []
380 noiseList = []
382 meanXcorrsList = []
383 kernelList = []
384 validList = []
386 for amp in self.means.keys():
387 ampList.append(amp)
388 meanList.append(self.means[amp])
389 varianceList.append(self.variances[amp])
390 rawXcorrs.append(np.array(self.rawXcorrs[amp]).reshape(nObs*smallLength).tolist())
391 gainList.append(self.gain[amp])
392 noiseList.append(self.noise[amp])
394 meanXcorrsList.append(self.meanXcorrs[amp].reshape(kernelLength).tolist())
395 kernelList.append(self.ampKernels[amp].reshape(kernelLength).tolist())
396 validList.append(int(self.valid[amp] and not (amp in self.badAmps)))
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 })
409 ampTable.meta = self.getMetadata().toDict()
410 tableList.append(ampTable)
412 if len(self.detKernels):
413 detList = []
414 kernelList = []
415 for det in self.detKernels.keys():
416 detList.append(det)
417 kernelList.append(self.detKernels[det].reshape(kernelLength).tolist())
419 detTable = Table({'DETECTOR': detList,
420 'KERNEL': kernelList})
421 detTable.meta = self.getMetadata().toDict()
422 tableList.append(detTable)
424 return tableList
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.ampKernels[amp] for amp in
431 self.ampKernels 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()
441 self.detKernels[detectorName] = avgKernel
443 def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName):
444 self.detKernel[detectorName] = self.ampKernel[ampName]