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 updateMetadata(self, setDate=False, **kwargs):
92 """Update calibration metadata.
94 This calls the base class's method after ensuring the required
95 calibration keywords will be saved.
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.level
106 kwargs['KERNEL_DX'] = self.shape[0]
107 kwargs['KERNEL_DY'] = self.shape[1]
109 super().updateMetadata(setDate=setDate, **kwargs)
111 def initFromCamera(self, camera, detectorId=None):
112 """Initialize kernel structure from camera.
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.
121 Returns
122 -------
123 calib : `lsst.ip.isr.BrighterFatterKernel`
124 The initialized calibration.
126 Raises
127 ------
128 RuntimeError :
129 Raised if no detectorId is supplied for a calibration with
130 level='AMP'.
131 """
132 self._instrument = camera.getName()
134 if self.level == 'AMP':
135 if detectorId is None:
136 raise RuntimeError("A detectorId must be supplied if level='AMP'.")
138 detector = camera[detectorId]
139 self._detectorId = detectorId
140 self._detectorName = detector.getName()
141 self._detectorSerial = detector.getSerial()
142 self.badAmps = []
144 for amp in detector:
145 ampName = amp.getName()
146 self.means[ampName] = []
147 self.variances[ampName] = []
148 self.rawXcorrs[ampName] = []
149 self.gain[ampName] = amp.getGain()
150 self.noise[ampName] = amp.getReadNoise()
151 self.meanXcorrs[ampName] = []
152 self.ampKernels[ampName] = []
153 self.valid[ampName] = []
154 elif self.level == 'DETECTOR':
155 for det in camera:
156 detName = det.getName()
157 self.detKernels[detName] = []
159 return self
161 def getLengths(self):
162 """Return the set of lengths needed for reshaping components.
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.shape[0] * self.shape[1]
174 smallLength = int((self.shape[0] - 1)*(self.shape[1] - 1)/4)
175 nObservations = set([len(self.means[amp]) for amp in self.means])
176 if len(nObservations) != 1:
177 raise RuntimeError("Inconsistent number of observations found.")
178 nObs = nObservations.pop()
180 return (kernelLength, smallLength, nObs)
182 @classmethod
183 def fromDict(cls, dictionary):
184 """Construct a calibration from a dictionary of properties.
186 Parameters
187 ----------
188 dictionary : `dict`
189 Dictionary of properties.
191 Returns
192 -------
193 calib : `lsst.ip.isr.BrighterFatterKernel
194 Constructed calibration.
196 Raises
197 ------
198 RuntimeError :
199 Raised if the supplied dictionary is for a different
200 calibration.
201 """
202 calib = cls()
204 if calib._OBSTYPE != (found := dictionary['metadata']['OBSTYPE']):
205 raise RuntimeError(f"Incorrect brighter-fatter kernel supplied. Expected {calib._OBSTYPE}, "
206 f"found {found}")
208 calib.setMetadata(dictionary['metadata'])
209 calib.calibInfoFromDict(dictionary)
211 calib.level = dictionary['metadata'].get('LEVEL', 'AMP')
212 calib.shape = (dictionary['metadata'].get('KERNEL_DX', 0),
213 dictionary['metadata'].get('KERNEL_DY', 0))
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']}
218 # Lengths for reshape:
219 _, smallLength, nObs = calib.getLengths()
220 smallShapeSide = int(np.sqrt(smallLength))
222 calib.rawXcorrs = {amp: np.array(dictionary['rawXcorrs'][amp]).reshape((nObs,
223 smallShapeSide,
224 smallShapeSide))
225 for amp in dictionary['rawXcorrs']}
227 calib.gain = dictionary['gain']
228 calib.noise = dictionary['noise']
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]
237 calib.detKernels = {det: np.array(dictionary['detKernels'][det]).reshape(calib.shape)
238 for det in dictionary['detKernels']}
240 calib.updateMetadata()
241 return calib
243 def toDict(self):
244 """Return a dictionary containing the calibration properties.
246 The dictionary should be able to be round-tripped through
247 `fromDict`.
249 Returns
250 -------
251 dictionary : `dict`
252 Dictionary of properties.
253 """
254 self.updateMetadata()
256 outDict = {}
257 metadata = self.getMetadata()
258 outDict['metadata'] = metadata
260 # Lengths for ravel:
261 kernelLength, smallLength, nObs = self.getLengths()
263 outDict['means'] = {amp: np.array(self.means[amp]).tolist() for amp in self.means}
264 outDict['variances'] = {amp: np.array(self.variances[amp]).tolist() for amp in self.variances}
265 outDict['rawXcorrs'] = {amp: np.array(self.rawXcorrs[amp]).reshape(nObs*smallLength).tolist()
266 for amp in self.rawXcorrs}
267 outDict['badAmps'] = self.badAmps
268 outDict['gain'] = self.gain
269 outDict['noise'] = self.noise
271 outDict['meanXcorrs'] = {amp: self.meanXcorrs[amp].reshape(kernelLength).tolist()
272 for amp in self.meanXcorrs}
273 outDict['ampKernels'] = {amp: self.ampKernels[amp].reshape(kernelLength).tolist()
274 for amp in self.ampKernels}
275 outDict['valid'] = self.valid
276 outDict['detKernels'] = {det: self.detKernels[det].reshape(kernelLength).tolist()
277 for det in self.detKernels}
278 return outDict
280 @classmethod
281 def fromTable(cls, tableList):
282 """Construct calibration from a list of tables.
284 This method uses the `fromDict` method to create the
285 calibration, after constructing an appropriate dictionary from
286 the input tables.
288 Parameters
289 ----------
290 tableList : `list` [`astropy.table.Table`]
291 List of tables to use to construct the brighter-fatter
292 calibration.
294 Returns
295 -------
296 calib : `lsst.ip.isr.BrighterFatterKernel`
297 The calibration defined in the tables.
298 """
299 ampTable = tableList[0]
301 metadata = ampTable.meta
302 inDict = dict()
303 inDict['metadata'] = metadata
305 amps = ampTable['AMPLIFIER']
307 meanList = ampTable['MEANS']
308 varianceList = ampTable['VARIANCES']
310 rawXcorrs = ampTable['RAW_XCORRS']
311 gainList = ampTable['GAIN']
312 noiseList = ampTable['NOISE']
314 meanXcorrs = ampTable['MEAN_XCORRS']
315 ampKernels = ampTable['KERNEL']
316 validList = ampTable['VALID']
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)}
327 inDict['badAmps'] = [amp for amp, valid in inDict['valid'].items() if valid is False]
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'] = {}
336 return cls.fromDict(inDict)
338 def toTable(self):
339 """Construct a list of tables containing the information in this calibration.
341 The list of tables should create an identical calibration
342 after being passed to this class's fromTable method.
344 Returns
345 -------
346 tableList : `list` [`lsst.afw.table.Table`]
347 List of tables containing the crosstalk calibration
348 information.
350 """
351 tableList = []
352 self.updateMetadata()
354 # Lengths
355 kernelLength, smallLength, nObs = self.getLengths()
357 ampList = []
358 meanList = []
359 varianceList = []
360 rawXcorrs = []
361 gainList = []
362 noiseList = []
364 meanXcorrsList = []
365 kernelList = []
366 validList = []
368 for amp in self.means.keys():
369 ampList.append(amp)
370 meanList.append(self.means[amp])
371 varianceList.append(self.variances[amp])
372 rawXcorrs.append(np.array(self.rawXcorrs[amp]).reshape(nObs*smallLength).tolist())
373 gainList.append(self.gain[amp])
374 noiseList.append(self.noise[amp])
376 meanXcorrsList.append(self.meanXcorrs[amp].reshape(kernelLength).tolist())
377 kernelList.append(self.ampKernels[amp].reshape(kernelLength).tolist())
378 validList.append(int(self.valid[amp] and not (amp in self.badAmps)))
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 })
391 ampTable.meta = self.getMetadata().toDict()
392 tableList.append(ampTable)
394 if len(self.detKernels):
395 detList = []
396 kernelList = []
397 for det in self.detKernels.keys():
398 detList.append(det)
399 kernelList.append(self.detKernels[det].reshape(kernelLength).tolist())
401 detTable = Table({'DETECTOR': detList,
402 'KERNEL': kernelList})
403 detTable.meta = self.getMetadata().toDict()
404 tableList.append(detTable)
406 return tableList
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.ampKernels[amp] for amp in
413 self.ampKernels 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()
423 self.detKernels[detectorName] = avgKernel
425 def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName):
426 self.detKernel[detectorName] = self.ampKernel[ampName]