22"""Brighter Fatter Kernel calibration definition."""
25__all__ = [
'BrighterFatterKernel']
29from astropy.table
import Table
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'.
52 Level the kernels will be generated
for.
53 log : `logging.Logger`, optional
54 Log to write messages to.
56 Parameters to
pass to parent constructor.
60 _SCHEMA =
'Brighter-fatter kernel'
63 def __init__(self, camera=None, level=None, **kwargs):
86 self.
initFromCamerainitFromCamera(camera, detectorId=kwargs.get(
'detectorId',
None))
88 self.requiredAttributes.update([
'level',
'means',
'variances',
'rawXcorrs',
89 'badAmps',
'gain',
'noise',
'meanXcorrs',
'valid',
90 'ampKernels',
'detKernels'])
93 """Update calibration metadata.
95 This calls the base class's method after ensuring the required
96 calibration keywords will be saved.
100 setDate : `bool`, optional
101 Update the CALIBDATE fields in the metadata to the current
102 time. Defaults to
False.
104 Other keyword parameters to set
in the metadata.
106 kwargs['LEVEL'] = self.
levellevel
107 kwargs[
'KERNEL_DX'] = self.
shapeshape[0]
108 kwargs[
'KERNEL_DY'] = self.
shapeshape[1]
113 """Initialize kernel structure from camera.
118 Camera to use to define geometry.
119 detectorId : `int`, optional
120 Index of the detector to generate.
125 The initialized calibration.
130 Raised if no detectorId
is supplied
for a calibration
with
135 if detectorId
is not None:
136 detector = camera[detectorId]
141 if self.
levellevel ==
'AMP':
142 if detectorId
is None:
143 raise RuntimeError(
"A detectorId must be supplied if level='AMP'.")
148 ampName = amp.getName()
149 self.
meansmeans[ampName] = []
152 self.
gaingain[ampName] = amp.getGain()
153 self.
noisenoise[ampName] = amp.getReadNoise()
156 self.
validvalid[ampName] = []
157 elif self.
levellevel ==
'DETECTOR':
158 if detectorId
is None:
160 detName = det.getName()
168 """Return the set of lengths needed for reshaping components.
173 Product of the elements of self.shapeshape.
175 Size of an untiled covariance.
177 Number of observation pairs used in the kernel.
179 kernelLength = self.shapeshape[0] * self.shapeshape[1]
180 smallLength = int((self.shapeshape[0] - 1)*(self.shapeshape[1] - 1)/4)
181 if self.
levellevel ==
'AMP':
182 nObservations = set([len(self.
meansmeans[amp])
for amp
in self.
meansmeans])
183 if len(nObservations) != 1:
184 raise RuntimeError(
"Inconsistent number of observations found.")
185 nObs = nObservations.pop()
189 return (kernelLength, smallLength, nObs)
193 """Construct a calibration from a dictionary of properties.
198 Dictionary of properties.
203 Constructed calibration.
208 Raised if the supplied dictionary
is for a different
213 if calib._OBSTYPE != (found := dictionary[
'metadata'][
'OBSTYPE']):
214 raise RuntimeError(f
"Incorrect brighter-fatter kernel supplied. Expected {calib._OBSTYPE}, "
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']}
228 _, smallLength, nObs = calib.getLengths()
229 smallShapeSide = int(np.sqrt(smallLength))
231 calib.rawXcorrs = {amp: np.array(dictionary[
'rawXcorrs'][amp]).reshape((nObs,
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()
253 """Return a dictionary containing the calibration properties.
255 The dictionary should be able to be round-tripped through
261 Dictionary of properties.
266 metadata = self.getMetadata()
267 outDict['metadata'] = metadata
270 kernelLength, smallLength, nObs = self.
getLengthsgetLengths()
272 outDict[
'means'] = {amp: np.array(self.
meansmeans[amp]).tolist()
for amp
in self.
meansmeans}
273 outDict[
'variances'] = {amp: np.array(self.
variancesvariances[amp]).tolist()
for amp
in self.
variancesvariances}
274 outDict[
'rawXcorrs'] = {amp: np.array(self.
rawXcorrsrawXcorrs[amp]).reshape(nObs*smallLength).tolist()
276 outDict[
'badAmps'] = self.
badAmpsbadAmps
277 outDict[
'gain'] = self.
gaingain
278 outDict[
'noise'] = self.
noisenoise
280 outDict[
'meanXcorrs'] = {amp: self.
meanXcorrsmeanXcorrs[amp].reshape(kernelLength).tolist()
282 outDict[
'ampKernels'] = {amp: self.
ampKernelsampKernels[amp].reshape(kernelLength).tolist()
284 outDict[
'valid'] = self.
validvalid
286 outDict[
'detKernels'] = {det: self.
detKernelsdetKernels[det].reshape(kernelLength).tolist()
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
300 tableList : `list` [`astropy.table.Table`]
301 List of tables to use to construct the brighter-fatter
307 The calibration defined
in the tables.
309 ampTable = tableList[0]
311 metadata = ampTable.meta
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'])}
344 inDict[
'detKernels'] = {}
349 """Construct a list of tables containing the information in this
352 The list of tables should create an identical calibration
353 after being passed to this class's fromTable method.
357 tableList : `list` [`lsst.afw.table.Table`]
358 List of tables containing the crosstalk calibration
366 kernelLength, smallLength, nObs = self.
getLengthsgetLengths()
379 if self.
levellevel ==
'AMP':
380 for amp
in self.
meansmeans.keys():
382 meanList.append(self.
meansmeans[amp])
383 varianceList.append(self.
variancesvariances[amp])
384 rawXcorrs.append(np.array(self.
rawXcorrsrawXcorrs[amp]).reshape(nObs*smallLength).tolist())
385 gainList.append(self.
gaingain[amp])
386 noiseList.append(self.
noisenoise[amp])
388 meanXcorrsList.append(self.
meanXcorrsmeanXcorrs[amp].reshape(kernelLength).tolist())
389 kernelList.append(self.
ampKernelsampKernels[amp].reshape(kernelLength).tolist())
390 validList.append(int(self.
validvalid[amp]
and not (amp
in self.
badAmpsbadAmps)))
392 ampTable = Table({
'AMPLIFIER': ampList,
394 'VARIANCES': varianceList,
395 'RAW_XCORRS': rawXcorrs,
398 'MEAN_XCORRS': meanXcorrsList,
399 'KERNEL': kernelList,
403 ampTable.meta = self.getMetadata().
toDict()
404 tableList.append(ampTable)
411 kernelList.append(self.
detKernelsdetKernels[det].reshape(kernelLength).tolist())
413 detTable = Table({
'DETECTOR': detList,
414 'KERNEL': kernelList})
415 detTable.meta = self.getMetadata().
toDict()
416 tableList.append(detTable)
422 """Average the amplifier level kernels to create a detector level
425 inKernels = np.array([self.ampKernelsampKernels[amp] for amp
in
426 self.
ampKernelsampKernels
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.
detKernelsdetKernels[detectorName] = avgKernel
439 self.detKernel[detectorName] = self.ampKernel[ampName]
def __init__(self, camera=None, level=None, **kwargs)
def fromTable(cls, tableList)
def initFromCamera(self, camera, detectorId=None)
def updateMetadata(self, setDate=False, **kwargs)
def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName)
def fromDict(cls, dictionary)
def makeDetectorKernelFromAmpwiseKernels(self, detectorName, ampsToExclude=[])