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.
61 Document what
is stored
in the BFK calibration.
63 Version 1.1 adds the `expIdMask` property,
and substitutes
64 `means`
and `variances`
for `rawMeans`
and `rawVariances`
68 _SCHEMA =
'Brighter-fatter kernel'
71 def __init__(self, camera=None, level=None, **kwargs):
95 self.
initFromCamera(camera, detectorId=kwargs.get(
'detectorId',
None))
97 self.requiredAttributes.update([
'level',
'expIdMask',
'rawMeans',
'rawVariances',
'rawXcorrs',
98 'badAmps',
'gain',
'noise',
'meanXcorrs',
'valid',
99 'ampKernels',
'detKernels'])
102 """Update calibration metadata.
104 This calls the base class's method after ensuring the required
105 calibration keywords will be saved.
109 setDate : `bool`, optional
110 Update the CALIBDATE fields in the metadata to the current
111 time. Defaults to
False.
113 Other keyword parameters to set
in the metadata.
115 kwargs['LEVEL'] = self.
level
116 kwargs[
'KERNEL_DX'] = self.
shape[0]
117 kwargs[
'KERNEL_DY'] = self.
shape[1]
122 """Initialize kernel structure from camera.
127 Camera to use to define geometry.
128 detectorId : `int`, optional
129 Index of the detector to generate.
134 The initialized calibration.
139 Raised if no detectorId
is supplied
for a calibration
with
144 if detectorId
is not None:
145 detector = camera[detectorId]
150 if self.
level ==
'AMP':
151 if detectorId
is None:
152 raise RuntimeError(
"A detectorId must be supplied if level='AMP'.")
157 ampName = amp.getName()
162 self.
gain[ampName] = amp.getGain()
163 self.
noise[ampName] = amp.getReadNoise()
166 self.
valid[ampName] = []
167 elif self.
level ==
'DETECTOR':
168 if detectorId
is None:
170 detName = det.getName()
178 """Return the set of lengths needed for reshaping components.
183 Product of the elements of self.shape.
185 Size of an untiled covariance.
187 Number of observation pairs used in the kernel.
190 smallLength = int((self.shape[0] - 1)*(self.shape[1] - 1)/4)
191 if self.
level ==
'AMP':
193 if len(nObservations) != 1:
194 raise RuntimeError(
"Inconsistent number of observations found.")
195 nObs = nObservations.pop()
199 return (kernelLength, smallLength, nObs)
203 """Construct a calibration from a dictionary of properties.
208 Dictionary of properties.
213 Constructed calibration.
218 Raised if the supplied dictionary
is for a different
220 Raised
if the version of the supplied dictionary
is 1.0.
224 if calib._OBSTYPE != (found := dictionary[
'metadata'][
'OBSTYPE']):
225 raise RuntimeError(f
"Incorrect brighter-fatter kernel supplied. Expected {calib._OBSTYPE}, "
227 calib.setMetadata(dictionary[
'metadata'])
228 calib.calibInfoFromDict(dictionary)
230 calib.level = dictionary[
'metadata'].get(
'LEVEL',
'AMP')
231 calib.shape = (dictionary[
'metadata'].get(
'KERNEL_DX', 0),
232 dictionary[
'metadata'].get(
'KERNEL_DY', 0))
234 calibVersion = dictionary[
'metadata'][
'bfk_VERSION']
235 if calibVersion == 1.0:
236 cls().log.warning(
"Old Version of brightter-fatter kernel found. Current version: "
237 f
"{calib._VERSION}. The new attribute 'expIdMask' will be "
238 "populated with 'True' values, and the new attributes 'rawMeans'"
239 "and 'rawVariances' will be populated with the masked 'means'."
240 "and 'variances' values."
243 calib.expIdMask = {amp: np.repeat(
True, len(dictionary[
'means'][amp]))
for amp
in
245 calib.rawMeans = {amp: np.array(dictionary[
'means'][amp])
for amp
in dictionary[
'means']}
246 calib.rawVariances = {amp: np.array(dictionary[
'variances'][amp])
for amp
in
247 dictionary[
'variances']}
249 calib.expIdMask = {amp: np.array(dictionary[
'expIdMask'][amp])
for amp
in dictionary[
'expIdMask']}
250 calib.rawMeans = {amp: np.array(dictionary[
'rawMeans'][amp])
for amp
in dictionary[
'rawMeans']}
251 calib.rawVariances = {amp: np.array(dictionary[
'rawVariances'][amp])
for amp
in
252 dictionary[
'rawVariances']}
255 _, smallLength, nObs = calib.getLengths()
256 smallShapeSide = int(np.sqrt(smallLength))
258 calib.rawXcorrs = {amp: np.array(dictionary[
'rawXcorrs'][amp]).reshape((nObs,
261 for amp
in dictionary[
'rawXcorrs']}
263 calib.gain = dictionary[
'gain']
264 calib.noise = dictionary[
'noise']
266 calib.meanXcorrs = {amp: np.array(dictionary[
'meanXcorrs'][amp]).reshape(calib.shape)
267 for amp
in dictionary[
'rawXcorrs']}
268 calib.ampKernels = {amp: np.array(dictionary[
'ampKernels'][amp]).reshape(calib.shape)
269 for amp
in dictionary[
'ampKernels']}
270 calib.valid = {amp: bool(value)
for amp, value
in dictionary[
'valid'].items()}
271 calib.badAmps = [amp
for amp, valid
in dictionary[
'valid'].items()
if valid
is False]
273 calib.detKernels = {det: np.array(dictionary[
'detKernels'][det]).reshape(calib.shape)
274 for det
in dictionary[
'detKernels']}
276 calib.updateMetadata()
280 """Return a dictionary containing the calibration properties.
282 The dictionary should be able to be round-tripped through
288 Dictionary of properties.
293 metadata = self.getMetadata()
294 outDict['metadata'] = metadata
297 kernelLength, smallLength, nObs = self.
getLengths()
299 outDict[
'expIdMask'] = {amp: np.array(self.
expIdMask[amp]).tolist()
for amp
in self.
expIdMask}
300 outDict[
'rawMeans'] = {amp: np.array(self.
rawMeans[amp]).tolist()
for amp
in self.
rawMeans}
301 outDict[
'rawVariances'] = {amp: np.array(self.
rawVariances[amp]).tolist()
for amp
in
303 outDict[
'rawXcorrs'] = {amp: np.array(self.
rawXcorrs[amp]).reshape(nObs*smallLength).tolist()
305 outDict[
'badAmps'] = self.
badAmps
306 outDict[
'gain'] = self.
gain
307 outDict[
'noise'] = self.
noise
309 outDict[
'meanXcorrs'] = {amp: self.
meanXcorrs[amp].reshape(kernelLength).tolist()
311 outDict[
'ampKernels'] = {amp: self.
ampKernels[amp].reshape(kernelLength).tolist()
313 outDict[
'valid'] = self.
valid
315 outDict[
'detKernels'] = {det: self.
detKernels[det].reshape(kernelLength).tolist()
321 """Construct calibration from a list of tables.
323 This method uses the `fromDict` method to create the
324 calibration, after constructing an appropriate dictionary from
329 tableList : `list` [`astropy.table.Table`]
330 List of tables to use to construct the brighter-fatter
336 The calibration defined
in the tables.
338 ampTable = tableList[0]
340 metadata = ampTable.meta
342 inDict['metadata'] = metadata
344 amps = ampTable[
'AMPLIFIER']
346 expIdMaskList = ampTable[
'EXP_ID_MASK']
347 rawMeanList = ampTable[
'RAW_MEANS']
348 rawVarianceList = ampTable[
'RAW_VARIANCES']
350 rawXcorrs = ampTable[
'RAW_XCORRS']
351 gainList = ampTable[
'GAIN']
352 noiseList = ampTable[
'NOISE']
354 meanXcorrs = ampTable[
'MEAN_XCORRS']
355 ampKernels = ampTable[
'KERNEL']
356 validList = ampTable[
'VALID']
358 inDict[
'expIdMask'] = {amp: mask
for amp, mask
in zip(amps, expIdMaskList)}
359 inDict[
'rawMeans'] = {amp: mean
for amp, mean
in zip(amps, rawMeanList)}
360 inDict[
'rawVariances'] = {amp: var
for amp, var
in zip(amps, rawVarianceList)}
361 inDict[
'rawXcorrs'] = {amp: kernel
for amp, kernel
in zip(amps, rawXcorrs)}
362 inDict[
'gain'] = {amp: gain
for amp, gain
in zip(amps, gainList)}
363 inDict[
'noise'] = {amp: noise
for amp, noise
in zip(amps, noiseList)}
364 inDict[
'meanXcorrs'] = {amp: kernel
for amp, kernel
in zip(amps, meanXcorrs)}
365 inDict[
'ampKernels'] = {amp: kernel
for amp, kernel
in zip(amps, ampKernels)}
366 inDict[
'valid'] = {amp: bool(valid)
for amp, valid
in zip(amps, validList)}
368 inDict[
'badAmps'] = [amp
for amp, valid
in inDict[
'valid'].items()
if valid
is False]
370 if len(tableList) > 1:
371 detTable = tableList[1]
372 inDict[
'detKernels'] = {det: kernel
for det, kernel
373 in zip(detTable[
'DETECTOR'], detTable[
'KERNEL'])}
375 inDict[
'detKernels'] = {}
380 """Construct a list of tables containing the information in this
383 The list of tables should create an identical calibration
384 after being passed to this class's fromTable method.
388 tableList : `list` [`lsst.afw.table.Table`]
389 List of tables containing the crosstalk calibration
397 kernelLength, smallLength, nObs = self.
getLengths()
411 if self.
level ==
'AMP':
414 expIdMaskList.append(self.
expIdMask[amp])
415 rawMeanList.append(self.
rawMeans[amp])
417 rawXcorrs.append(np.array(self.
rawXcorrs[amp]).reshape(nObs*smallLength).tolist())
418 gainList.append(self.
gain[amp])
419 noiseList.append(self.
noise[amp])
421 meanXcorrsList.append(self.
meanXcorrs[amp].reshape(kernelLength).tolist())
422 kernelList.append(self.
ampKernels[amp].reshape(kernelLength).tolist())
423 validList.append(int(self.
valid[amp]
and not (amp
in self.
badAmps)))
425 ampTable = Table({
'AMPLIFIER': ampList,
426 'EXP_ID_MASK': expIdMaskList,
427 'RAW_MEANS': rawMeanList,
428 'RAW_VARIANCES': rawVarianceList,
429 'RAW_XCORRS': rawXcorrs,
432 'MEAN_XCORRS': meanXcorrsList,
433 'KERNEL': kernelList,
437 ampTable.meta = self.getMetadata().
toDict()
438 tableList.append(ampTable)
445 kernelList.append(self.
detKernels[det].reshape(kernelLength).tolist())
447 detTable = Table({
'DETECTOR': detList,
448 'KERNEL': kernelList})
449 detTable.meta = self.getMetadata().
toDict()
450 tableList.append(detTable)
456 """Average the amplifier level kernels to create a detector level
459 inKernels = np.array([self.ampKernels[amp] for amp
in
461 averagingList = np.transpose(inKernels)
462 avgKernel = np.zeros_like(inKernels[0])
463 sctrl = afwMath.StatisticsControl()
464 sctrl.setNumSigmaClip(5.0)
465 for i
in range(np.shape(avgKernel)[0]):
466 for j
in range(np.shape(avgKernel)[1]):
467 avgKernel[i, j] = afwMath.makeStatistics(averagingList[i, j],
468 afwMath.MEANCLIP, sctrl).getValue()
473 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=[])