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