lsst.ip.isr gd2a69bfd97+51b480cc68
Loading...
Searching...
No Matches
electrostaticBrighterFatter.py
Go to the documentation of this file.
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."""
23
24
25__all__ = [
26 'ElectrostaticBrighterFatterDistortionMatrix',
27 'electrostaticBrighterFatterCorrection',
28]
29
30
31import pyfftw
32import numpy as np
33from astropy.table import Table
34
35from . import IsrCalib
36from .isrFunctions import gainContext
37
38
40 """Calibration of brighter-fatter kernels for an instrument.
41
42 ampKernels are the kernels for each amplifier in a detector, as
43 generated by having ``level == 'AMP'``.
44
45 detectorKernel is the kernel generated for a detector as a
46 whole, as generated by having ``level == 'DETECTOR'``.
47
48 makeDetectorKernelFromAmpwiseKernels is a method to generate the
49 kernel for a detector, constructed by averaging together the
50 ampwise kernels in the detector. The existing application code is
51 only defined for kernels with ``level == 'DETECTOR'``, so this method
52 is used if the supplied kernel was built with ``level == 'AMP'``.
53
54 Parameters
55 ----------
56 camera : `lsst.afw.cameraGeom.Camera`
57 Camera describing detector geometry.
58 level : `str`
59 Level the kernels will be generated for.
60 log : `logging.Logger`, optional
61 Log to write messages to.
62 **kwargs :
63 Parameters to pass to parent constructor.
64
65 Notes
66 -----
67 Version 1.1 adds the `expIdMask` property, and substitutes
68 `means` and `variances` for `rawMeans` and `rawVariances`
69 from the PTC dataset.
70
71 inputRange : `int`
72 The size of the input aMatrix shape in each dimension.
73 fitRange : `int`
74 The size of the input aMatrix shape in each dimension that is
75 used for fitting the electrostatic model. Must be less than or
76 equal to inputRange.
77 badAmps : `list`, [`str`]
78 List of bad amplifiers names.
79 gain : `dict`, [`str`,`float`]
80 Dictionary keyed by amp names containing the gains inherited
81 from the inputPTC.
82 aMatrix : `numpy.ndarray`
83 The average aMatrix inherited from the inputPTC's good amplifiers.
84 aMatrixSigma : `numpy.ndarray`
85 The uncertainty matrix used to weight the fit.
86 aMatrixModel : `numpy.ndarray`
87 The modeled aMatrix based on the electrostatic fit parameters.
88 aMatrixSum : `float`
89 The sum of the symmetrized aMatrix.
90 aMatrixModelSum : `float`
91 The sum of the symmetrized aMatrixModel.
92 modelNormalization : `list`
93 A two element array of the multiplicative and additive
94 normalization to the aMatrixModel.
95 usedPixels : `numpy.ndarray`, [`bool`]
96 Array of shape like aMatrix containing the mask indicating which
97 elements of the input aMatrix were used to fit the electrostatic
98 model.
99 fitParamNames : `list`, [`str`]
100 List of all the parameter names in the electrostatic fit.
101 freeFitParamNames : `list`, [`str`]
102 List of the parameter names that were allowed to vary during
103 the electrostatic fit.
104 fitParams : `dict`, [`str`, `float`]
105 Dictionary containing each named parameter and its final
106 fitted value.
107 fitParamErrors : `dict`, [`str`, `float`]
108 Dictionary containing each named parameter and its
109 estimated fitting error.
110 fitChi2 : `float`
111 The computed chi squared between the data and the
112 final model.
113 fitReducedChi2 : `float`
114 The computed reduced chi squared between the data
115 and the final model.
116 fitParamCovMatrix : `numpy.ndarray`
117 The estimated covariance matrix between all fit parameters.
118 ath : `numpy.ndarray`
119 something...
120 athMinusBeta : `numpy.ndarray`
121 something...
122 aN : `numpy.ndarray`
123 Array of shape (fitRange, fitRange) containing the
124 computed `North` component of the pixel boundary shift.
125 aS : `numpy.ndarray`
126 Array of shape (fitRange, fitRange) containing the
127 computed `South` component of the pixel boundary shift.
128 aE : `numpy.ndarray`
129 Array of shape (fitRange, fitRange) containing the
130 computed `East` component of the pixel boundary shift.
131 aW : `numpy.ndarray`
132 Array of shape (fitRange, fitRange) containing the
133 computed `West` component of the pixel boundary shift.
134 aMatrixModelByFilter : `dict` [`str`, `numpy.ndarray`]
135 Dict keyed by physical filter name containing the modeled aMatrix
136 arrays (shape (fitRange, fitRange)) per filter. Only populated
137 when color correction is enabled.
138 aNByFilter : `dict` [`str`, `numpy.ndarray`]
139 Dict keyed by physical filter name containing the North pixel
140 boundary shift arrays (shape (fitRange, fitRange)) per filter.
141 Only populated when color correction is enabled.
142 aSByFilter : `dict` [`str`, `numpy.ndarray`]
143 Dict keyed by physical filter name containing the South pixel
144 boundary shift arrays per filter.
145 aEByFilter : `dict` [`str`, `numpy.ndarray`]
146 Dict keyed by physical filter name containing the East pixel
147 boundary shift arrays per filter.
148 aWByFilter : `dict` [`str`, `numpy.ndarray`]
149 Dict keyed by physical filter name containing the West pixel
150 boundary shift arrays per filter.
151 availableFilters : `list` of `str`
152 List of physical filter names for which per-filter data exists;
153 these are the keys used in the ``*ByFilter`` attributes.
154
155 Version 1.1 adds the `aMatrixModelByFilter`, `athByFilter`,
156 `athMinusBetaByFilter`, `aNByFilter`, `aSByFilter`,
157 `aEByFilter`, and `aWByFilter` attributes.
158 """
159 _OBSTYPE = 'BF_DISTORTION_MATRIX'
160 _SCHEMA = 'ELectrostatic brighter-fatter pixel distortion model'
161 _VERSION = 1.1
162
163 def __init__(self, camera=None, inputRange=1, fitRange=None, availableFilters=list(), **kwargs):
164 """
165 Filename refers to an input tuple that contains the
166 boundary shifts for one electron. This file is produced by an
167 electrostatic fit to data extracted from flat-field statistics,
168 implemented in https://gitlab.in2p3.fr/astier/bfptc/tools/fit_cov.py
169 """
170 self.ampNames = []
171 self.inputRange = inputRange
172 if fitRange is None:
173 fitRange = inputRange
174 self.fitRange = fitRange
175 self.badAmps = list()
176 self.gain = dict()
177 self.aMatrix = np.full((inputRange, inputRange), np.nan)
178 self.aMatrixSigma = np.full((inputRange, inputRange), np.nan)
179 self.aMatrixModel = np.full((fitRange, fitRange), np.nan)
180 self.aMatrixSum = np.nan
181 self.aMatrixModelSum = np.nan
182 self.modelNormalization = [np.nan, np.nan]
183 self.usedPixels = np.full((inputRange, inputRange), np.nan)
184 self.fitParamNames = list()
185 self.freeFitParamNames = list()
186 self.fitParams = dict()
187 self.fitParamErrors = dict()
188 self.fitChi2 = np.nan
189 self.fitReducedChi2 = np.nan
190 self.fitParamCovMatrix = np.array([])
191 self.ath = np.full((fitRange, fitRange), np.nan)
192 self.athMinusBeta = np.full((fitRange, fitRange), np.nan)
193 self.aN = np.full((fitRange, fitRange), np.nan)
194 self.aS = np.full((fitRange, fitRange), np.nan)
195 self.aE = np.full((fitRange, fitRange), np.nan)
196 self.aW = np.full((fitRange, fitRange), np.nan)
197 self.availableFilters = availableFilters
198 self.aMatrixModelByFilter = {k: np.full((fitRange, fitRange), np.nan) for k in availableFilters}
199 self.athByFilter = {k: None for k in availableFilters}
200 self.athMinusBetaByFilter = {k: None for k in availableFilters}
201 self.aNByFilter = {k: np.full((fitRange, fitRange), np.nan) for k in availableFilters}
202 self.aSByFilter = {k: np.full((fitRange, fitRange), np.nan) for k in availableFilters}
203 self.aEByFilter = {k: np.full((fitRange, fitRange), np.nan) for k in availableFilters}
204 self.aWByFilter = {k: np.full((fitRange, fitRange), np.nan) for k in availableFilters}
205
206 super().__init__(**kwargs)
207
208 if camera:
209 self.initFromCamera(camera, detectorId=kwargs.get('detectorId', None))
210
211 self.requiredAttributes.update([
212 'inputRange', 'fitRange', 'badAmps', 'gain', 'aMatrix',
213 'aMatrixSigma', 'aMatrixModel', 'aMatrixSum', 'aMatrixModelSum',
214 'modelNormalization', 'usedPixels', 'fitParamNames',
215 'freeFitParamNames', 'fitParams', 'fitParamErrors', 'fitChi2',
216 'fitReducedChi2', 'fitParamCovMatrix', 'ath', 'athMinusBeta',
217 'aN', 'aS', 'aE', 'aW', 'ampNames', 'availableFilters',
218 'aMatrixModelByFilter', 'athByFilter', 'athMinusBetaByFilter',
219 'aNByFilter', 'aSByFilter', 'aEByFilter', 'aWByFilter',
220 ])
221
222 def updateMetadata(self, setDate=False, **kwargs):
223 """Update calibration metadata.
224
225 This calls the base class's method after ensuring the required
226 calibration keywords will be saved.
227
228 Parameters
229 ----------
230 setDate : `bool`, optional
231 Update the CALIBDATE fields in the metadata to the current
232 time. Defaults to False.
233 kwargs :
234 Other keyword parameters to set in the metadata.
235 """
236 kwargs['INPUT_RANGE'] = self.inputRange
237 kwargs['FIT_RANGE'] = self.fitRange
238
239 super().updateMetadata(setDate=setDate, **kwargs)
240
241 def initFromCamera(self, camera, detectorId=None):
242 """Initialize kernel structure from camera.
243
244 Parameters
245 ----------
246 camera : `lsst.afw.cameraGeom.Camera`
247 Camera to use to define geometry.
248 detectorId : `int`, optional
249 Index of the detector to generate.
250
251 Returns
252 -------
253 calib : `lsst.ip.isr.BrighterFatterKernel`
254 The initialized calibration.
255
256 Raises
257 ------
258 RuntimeError
259 Raised if no detectorId is supplied for a calibration with
260 ``level='AMP'``.
261 """
262 self._instrument = camera.getName()
263
264 if detectorId is not None:
265 detector = camera[detectorId]
266 self._detectorId = detectorId
267 self._detectorName = detector.getName()
268 self._detectorSerial = detector.getSerial()
269
270 for amp in detector:
271 ampName = amp.getName()
272 self.ampNames.append(ampName)
273 else:
274 raise RuntimeError("A detectorId must be supplied if "
275 "camera is supplied.")
276
277 return self
278
279 @classmethod
280 def fromDict(cls, dictionary):
281 """Construct a calibration from a dictionary of properties.
282
283 Parameters
284 ----------
285 dictionary : `dict`
286 Dictionary of properties.
287
288 Returns
289 -------
290 calib : `lsst.ip.isr.BrighterFatterKernel`
291 Constructed calibration.
292
293 Raises
294 ------
295 RuntimeError
296 Raised if the supplied dictionary is for a different
297 calibration.
298 Raised if the version of the supplied dictionary is 1.0.
299 """
300 calib = cls()
301
302 if calib._OBSTYPE != (found := dictionary['metadata']['OBSTYPE']):
303 raise RuntimeError(f"Incorrect brighter-fatter calibration supplied. Expected {calib._OBSTYPE}, "
304 f"found {found}")
305 calib.setMetadata(dictionary['metadata'])
306 calib.calibInfoFromDict(dictionary)
307
308 calib.ampNames = dictionary['ampNames']
309 inputRange = dictionary['inputRange']
310 fitRange = dictionary['fitRange']
311 calib.inputRange = inputRange
312 calib.fitRange = fitRange
313 calib.gain = dictionary['gain']
314 calib.badAmps = dictionary['badAmps']
315 calib.fitParamNames = dictionary['fitParamNames']
316 calib.freeFitParamNames = dictionary['freeFitParamNames']
317 calib.aMatrix = np.array(
318 dictionary['aMatrix'],
319 dtype=np.float64,
320 ).reshape(inputRange, inputRange)
321 calib.aMatrixSigma = np.array(
322 dictionary['aMatrixSigma'],
323 dtype=np.float64,
324 ).reshape(inputRange, inputRange)
325 calib.aMatrixModel = np.array(
326 dictionary['aMatrixSigma'],
327 dtype=np.float64,
328 ).reshape(fitRange, fitRange)
329 calib.aMatrixSum = float(dictionary['aMatrixSum'])
330 calib.aMatrixModelSum = float(dictionary['aMatrixModelSum'])
331 calib.modelNormalization = dictionary['modelNormalization']
332 calib.usedPixels = np.array(dictionary['usedPixels'])
333 calib.fitParams = dictionary['fitParams']
334 calib.fitParamErrors = dictionary['fitParamErrors']
335 calib.fitChi2 = float(dictionary['fitChi2'])
336 calib.fitReducedChi2 = float(dictionary['fitReducedChi2'])
337 calib.fitParamCovMatrix = np.array(
338 dictionary['fitParamCovMatrix'],
339 dtype=np.float64,
340 ).reshape(len(calib.freeFitParamNames), len(calib.freeFitParamNames))
341 calib.ath = np.array(
342 dictionary['ath'],
343 dtype=np.float64,
344 ).reshape(fitRange, fitRange)
345 calib.athMinusBeta = np.array(
346 dictionary['athMinusBeta'],
347 dtype=np.float64,
348 ).reshape(fitRange, fitRange)
349 calib.aN = np.array(
350 dictionary['aN'],
351 dtype=np.float64,
352 ).reshape(fitRange, fitRange)
353 calib.aS = np.array(
354 dictionary['aS'],
355 dtype=np.float64,
356 ).reshape(fitRange, fitRange)
357 calib.aE = np.array(
358 dictionary['aE'],
359 dtype=np.float64,
360 ).reshape(fitRange, fitRange)
361 calib.aW = np.array(
362 dictionary['aW'],
363 dtype=np.float64,
364 ).reshape(fitRange, fitRange)
365
366 filters = dictionary['availableFilters']
367 calib.availableFilters = dictionary['availableFilters']
368 calib.aMatrixModelByFilter = {
369 k: np.array(v, dtype=np.float64).reshape(fitRange, fitRange)
370 for k, v in zip(filters, dictionary['aMatrixModelByFilter'])
371 }
372 calib.athByFilter = {
373 k: np.array(v, dtype=np.float64).reshape(fitRange, fitRange)
374 for k, v in zip(filters, dictionary['athByFilter'])
375 }
376 calib.athMinusBetaByFilter = {
377 k: np.array(v, dtype=np.float64).reshape(fitRange, fitRange)
378 for k, v in zip(filters, dictionary['athMinusBetaByFilter'])
379 }
380 calib.aNByFilter = {
381 k: np.array(v, dtype=np.float64).reshape(fitRange, fitRange)
382 for k, v in zip(filters, dictionary['aNByFilter'])
383 }
384 calib.aSByFilter = {
385 k: np.array(v, dtype=np.float64).reshape(fitRange, fitRange)
386 for k, v in zip(filters, dictionary['aSByFilter'])
387 }
388 calib.aEByFilter = {
389 k: np.array(v, dtype=np.float64).reshape(fitRange, fitRange)
390 for k, v in zip(filters, dictionary['aEByFilter'])
391 }
392 calib.aWByFilter = {
393 k: np.array(v, dtype=np.float64).reshape(fitRange, fitRange)
394 for k, v in zip(filters, dictionary['aWByFilter'])
395 }
396
397 calib.updateMetadata()
398 return calib
399
400 def toDict(self):
401 """Return a dictionary containing the calibration properties.
402
403 The dictionary should be able to be round-tripped through
404 `fromDict`.
405
406 Returns
407 -------
408 dictionary : `dict`
409 Dictionary of properties.
410 """
411 self.updateMetadata()
412
413 def _dictOfArraysToDictOfLists(dictOfArrays):
414 dictOfLists = {}
415 for key, value in dictOfArrays.items():
416 dictOfLists[key] = value.ravel().tolist()
417
418 return dictOfLists
419
420 outDict = dict()
421 metadata = self.getMetadata()
422 outDict['metadata'] = metadata
423
424 outDict['ampNames'] = self.ampNames
425 outDict['inputRange'] = self.inputRange
426 outDict['fitRange'] = self.fitRange
427 outDict['badAmps'] = self.badAmps
428 outDict['fitParamNames'] = self.fitParamNames
429 outDict['freeFitParamNames'] = self.freeFitParamNames
430 outDict['gain'] = self.gain
431 outDict['aMatrix'] = self.aMatrix.ravel().tolist()
432 outDict['aMatrixSigma'] = self.aMatrixSigma.ravel().tolist()
433 outDict['aMatrixModel'] = self.aMatrixModel.ravel().tolist()
434 outDict['aMatrixSum'] = self.aMatrixSum
435 outDict['aMatrixModelSum'] = self.aMatrixModelSum
436 outDict['aMatrixModel'] = self.aMatrixModel.ravel().tolist()
437 outDict['modelNormalization'] = self.modelNormalization
438 outDict['fitParamCovMatrix'] = self.fitParamCovMatrix.ravel().tolist()
439 outDict['usedPixels'] = self.usedPixels.ravel().tolist()
440 outDict['fitParams'] = self.fitParams
441 outDict['fitParamErrors'] = self.fitParamErrors
442 outDict['fitChi2'] = self.fitChi2
443 outDict['fitReducedChi2'] = self.fitReducedChi2
444 outDict['ath'] = self.ath.ravel().tolist()
445 outDict['athMinusBeta'] = self.athMinusBeta.ravel().tolist()
446 outDict['aN'] = self.aN.ravel().tolist()
447 outDict['aS'] = self.aS.ravel().tolist()
448 outDict['aE'] = self.aE.ravel().tolist()
449 outDict['aW'] = self.aW.ravel().tolist()
450 outDict['availableFilters'] = self.availableFilters
451 outDict['aMatrixModelByFilter'] = _dictOfArraysToDictOfLists(self.aMatrixModelByFilter)
452 outDict['aNByFilter'] = _dictOfArraysToDictOfLists(self.aNByFilter)
453 outDict['aSByFilter'] = _dictOfArraysToDictOfLists(self.aSByFilter)
454 outDict['aEByFilter'] = _dictOfArraysToDictOfLists(self.aEByFilter)
455 outDict['aWByFilter'] = _dictOfArraysToDictOfLists(self.aWByFilter)
456 outDict['athByFilter'] = _dictOfArraysToDictOfLists(self.athByFilter)
457 outDict['athMinusBetaByFilter'] = _dictOfArraysToDictOfLists(self.athMinusBetaByFilter)
458
459 return outDict
460
461 @classmethod
462 def fromTable(cls, tableList):
463 """Construct calibration from a list of tables.
464
465 This method uses the `fromDict` method to create the
466 calibration, after constructing an appropriate dictionary from
467 the input tables.
468
469 Parameters
470 ----------
471 tableList : `list` [`astropy.table.Table`]
472 List of tables to use to construct the brighter-fatter
473 calibration.
474
475 Returns
476 -------
477 calib : `lsst.ip.isr.BrighterFatterKernel`
478 The calibration defined in the tables.
479 """
480 record = tableList[0][0]
481
482 metadata = record.meta
483 calibVersion = metadata['BF_DISTORTION_MATRIX_VERSION']
484
485 # Initialize inDict with all expected keys
486 # and empty values of the corresponding type
487 inDict = dict()
488 inDict['metadata'] = metadata
489 inDict['ampNames'] = record['AMPNAMES']
490 inDict['inputRange'] = record['INPUT_RANGE']
491 inDict['fitRange'] = record['FIT_RANGE']
492 inDict['badAmps'] = record['BAD_AMPS']
493 inDict['fitParamNames'] = record['FIT_PARAM_NAMES']
494 inDict['freeFitParamNames'] = record['FREE_FIT_PARAM_NAMES']
495 inDict['gain'] = {str(n): v for n, v in zip(record['AMPNAMES'], record['GAIN'])}
496 inDict['aMatrix'] = record['A_MATRIX']
497 inDict['aMatrixSigma'] = record['A_MATRIX_SIGMA']
498 inDict['aMatrixModel'] = record['A_MATRIX_MODEL']
499 inDict['aMatrixSum'] = record['A_MATRIX_SUM']
500 inDict['aMatrixModelSum'] = record['A_MATRIX_MODEL_SUM']
501 inDict['modelNormalization'] = record['MODEL_NORMALIZATION']
502 inDict['usedPixels'] = record['USED_PIXELS']
503 inDict['fitParams'] = {str(n): v for n, v in zip(record['FIT_PARAM_NAMES'], record['FIT_PARAMS'])}
504 inDict['fitParamErrors'] = {str(n): v for n, v in zip(record['FIT_PARAM_NAMES'],
505 record['FIT_PARAM_ERRORS'])}
506 inDict['fitChi2'] = record['FIT_CHI2']
507 inDict['fitReducedChi2'] = record['FIT_REDUCED_CHI2']
508 inDict['fitParamCovMatrix'] = record['FIT_PARAM_COV_MATRIX']
509 inDict['ath'] = record['ATH']
510 inDict['athMinusBeta'] = record['ATH_MINUS_BETA']
511 inDict['aN'] = record['A_N']
512 inDict['aS'] = record['A_S']
513 inDict['aE'] = record['A_E']
514 inDict['aW'] = record['A_W']
515
516 if not calibVersion == 1.0:
517 cls().log.warning("Unkown version found for "
518 f"ElectrostaticBrighterFatterDistortionMatrix: {calibVersion}. ")
519
520 # Check for newer versions
521 if calibVersion < 1.1:
522 inDict['availableFilters'] = list()
523 inDict['aMatrixModelByFilter'] = dict()
524 inDict['athByFilter'] = dict()
525 inDict['athMinusBetaByFilter'] = dict()
526 inDict['aNByFilter'] = dict()
527 inDict['aSByFilter'] = dict()
528 inDict['aEByFilter'] = dict()
529 inDict['aWByFilter'] = dict()
530 else:
531 inDict['availableFilters'] = record['AVAILABLE_FILTERS']
532 inDict['aMatrixModelByFilter'] = record['A_MATRIX_MODEL_BY_FILTER']
533 inDict['athByFilter'] = record['ATH_BY_FILTER']
534 inDict['athMinusBetaByFilter'] = record['ATH_MINUS_BETA_BY_FILTER']
535 inDict['aNByFilter'] = record['A_N_BY_FILTER']
536 inDict['aSByFilter'] = record['A_S_BY_FILTER']
537 inDict['aEByFilter'] = record['A_E_BY_FILTER']
538 inDict['aWByFilter'] = record['A_W_BY_FILTER']
539
540 return cls.fromDict(inDict)
541
542 def toTable(self):
543 """Construct a list of tables containing the information in this
544 calibration.
545
546 The list of tables should create an identical calibration
547 after being passed to this class's fromTable method.
548
549 Returns
550 -------
551 tableList : `list` [`lsst.afw.table.Table`]
552 List of tables containing the crosstalk calibration
553 information.
554
555 """
556 tableList = []
557 self.updateMetadata()
558
559 recordDict = {
560 'AMPNAMES': self.ampNames,
561 'INPUT_RANGE': self.inputRange,
562 'FIT_RANGE': self.fitRange,
563 'BAD_AMPS': self.badAmps,
564 'GAIN': list(self.gain.values()),
565 'A_MATRIX': self.aMatrix.ravel(),
566 'A_MATRIX_SIGMA': self.aMatrixSigma.ravel(),
567 'A_MATRIX_MODEL': self.aMatrixModel.ravel(),
568 'A_MATRIX_SUM': self.aMatrixSum,
569 'A_MATRIX_MODEL_SUM': self.aMatrixModelSum,
570 'MODEL_NORMALIZATION': self.modelNormalization,
571 'USED_PIXELS': self.usedPixels.ravel(),
572 'FIT_PARAM_NAMES': self.fitParamNames,
573 'FREE_FIT_PARAM_NAMES': self.freeFitParamNames,
574 'FIT_PARAMS': list(self.fitParams.values()),
575 'FIT_PARAM_ERRORS': list(self.fitParamErrors.values()),
576 'FIT_CHI2': self.fitChi2,
577 'FIT_REDUCED_CHI2': self.fitReducedChi2,
578 'FIT_PARAM_COV_MATRIX': self.fitParamCovMatrix.ravel(),
579 'ATH': self.ath.ravel(),
580 'ATH_MINUS_BETA': self.athMinusBeta.ravel(),
581 'A_N': self.aN.ravel(),
582 'A_S': self.aS.ravel(),
583 'A_E': self.aE.ravel(),
584 'A_W': self.aW.ravel(),
585 'AVAILABLE_FILTERS': self.availableFilters,
586 'A_MATRIX_MODEL_BY_FILTER': list(self.aMatrixModelByFilter.values()),
587 'ATH_BY_FILTER': list(self.athByFilter.values()),
588 'ATH_MINUS_BETA_BY_FILTER': list(self.athMinusBetaByFilter.values()),
589 'A_N_BY_FILTER': list(self.aNByFilter.values()),
590 'A_S_BY_FILTER': list(self.aSByFilter.values()),
591 'A_E_BY_FILTER': list(self.aEByFilter.values()),
592 'A_W_BY_FILTER': list(self.aWByFilter.values()),
593 }
594
595 catalogList = [recordDict]
596 catalog = Table(catalogList)
597
598 inMeta = self.getMetadata().toDict()
599 outMeta = {k: v for k, v in inMeta.items() if v is not None}
600 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
601 catalog.meta = outMeta
602 tableList.append(catalog)
603
604 return tableList
605
606
608 """
609 A class that performs image convolutions in Fourier space, using pyfftw.
610 The constructor takes images as arguments and creates the FFTW plans.
611 The convolutions are performed by the __call__ routine.
612 This is faster than scipy.signal.fftconvolve, and it saves some transforms
613 by allowing the same image to be convolved with several kernels.
614 pyfftw does not accommodate float32 images, so everything
615 should be double precision.
616
617 Code adaped from :
618 https://stackoverflow.com/questions/14786920/convolution-of-two-three-dimensional-arrays-with-padding-on-one-side-too-slow
619 Code posted by Henry Gomersal
620 """
621 def __init__(self, im, kernel, threads=1):
622 # Compute the minimum size of the convolution result.
623 shape = (np.array(im.shape) + np.array(kernel.shape)) - 1
624
625 # Find the next larger "fast size" for FFT computation.
626 # This can provide a significant speedup.
627 shape = np.array([pyfftw.next_fast_len(s) for s in shape])
628
629 # Create FFTW building objects for the image and kernel.
630 self.fftImageObj = pyfftw.builders.rfftn(im, s=shape, threads=threads)
631 self.fftKernelObj = pyfftw.builders.rfftn(kernel, s=shape, threads=threads)
632 self.ifftObj = pyfftw.builders.irfftn(
633 self.fftImageObj.output_array,
634 s=shape,
635 threads=threads,
636 )
637
638 def __call__(self, im, kernels):
639 """
640 Perform the convolution and trim the result to the
641 size of the input image. If kernels is a list, then
642 the routine returns a list of corresponding
643 convolutions.
644 """
645 # Handle both a list of kernels and a single kernel.
646 ks = [kernels] if type(kernels) is not list else kernels
647 convolutions = []
648 for k in ks:
649 # Transform the image and the kernel using FFT.
650 tim = self.fftImageObj(im)
651 tk = self.fftKernelObj(k)
652
653 # Multiply in Fourier space and perform the inverse FFT.
654 convolution = self.ifftObj(tim * tk)
655 # Trim the result to match the input image size, following
656 # the 'same' policy of scipy.signal.fftconvolve.
657 oy = k.shape[0] // 2
658 ox = k.shape[1] // 2
659 convolutions.append(convolution[oy:oy + im.shape[0], ox:ox + im.shape[1]].copy())
660 # Return a single convolution if kernels was
661 # not a list, otherwise return the list.
662 return convolutions[0] if type(kernels) is not list else convolutions
663
664
665def electrostaticBrighterFatterCorrection(log, exposure, electroBfDistortionMatrix, applyGain,
666 gains=None, applyFilterCorrection=False):
667 """
668 Evaluates the correction of CCD images affected by the
669 brighter-fatter effect, as described in
670 https://arxiv.org/abs/2301.03274. Requires as input the result of
671 an electrostatic fit to flat covariance data (or any other
672 determination of pixel boundary shifts under the influence of a
673 single electron).
674
675 The filename refers to an input tuple that contains the
676 boundary shifts for one electron. This file is produced by an
677 electrostatic fit to data extracted from flat-field statistics,
678 implemented in https://gitlab.in2p3.fr/astier/bfptc/tools/fit_cov.py.
679 """
680
681 # Use the symmetrize function to fill the four quadrants for each kernel
682 r = electroBfDistortionMatrix.fitRange - 1
683 aN = electroBfDistortionMatrix.aN
684 aS = electroBfDistortionMatrix.aS
685 aE = electroBfDistortionMatrix.aE
686 aW = electroBfDistortionMatrix.aW
687
688 # Find the physicalFilter associated with the exposure
689 physicalFilter = exposure.metadata["FILTER"]
690
691 if applyFilterCorrection:
692 if (physicalFilter is not None) and \
693 (physicalFilter in electroBfDistortionMatrix.availableFilters):
694 # We have a pre-computed distortion matrix for
695 # the exposure's filter
696 aN = electroBfDistortionMatrix.aNByFilter[physicalFilter]
697 aS = electroBfDistortionMatrix.aSByFilter[physicalFilter]
698 aE = electroBfDistortionMatrix.aEByFilter[physicalFilter]
699 aW = electroBfDistortionMatrix.aWByFilter[physicalFilter]
700 else:
701 # There is no pre-computed distortion matrix
702 # for the exposure's filter
703 log.warning(f"Filter {exposure.metadata['FILTER']} not found in "
704 "electroBfDistortionMatrix.availableFilters. "
705 "Defaulting to the using the `generic` distortion "
706 "matrix, computed assuming all photons are converted "
707 "to electrons at the surface of the silicon detector.")
708
709 # Initialize kN and kE arrays
710 kN = np.zeros((2 * r + 1, 2 * r + 1))
711 kE = np.zeros_like(kN)
712
713 # Fill in the 4 quadrants for kN
714 kN[r:, r:] = aN # Quadrant 1 (bottom-right)
715 kN[:r+1, r:] = np.flipud(aN) # Quadrant 2 (top-right)
716 kN[r:, :r+1] = np.fliplr(aS) # Quadrant 3 (bottom-left)
717 kN[:r+1, :r+1] = np.flipud(np.fliplr(aS)) # Quadrant 4 (top-left)
718
719 # Fill in the 4 quadrants for kE
720 kE[r:, r:] = aE # Quadrant 1 (bottom-right)
721 kE[:r+1, r:] = np.flipud(aW) # Quadrant 2 (top-right)
722 kE[r:, :r+1] = np.fliplr(aE) # Quadrant 3 (bottom-left)
723 kE[:r+1, :r+1] = np.flipud(np.fliplr(aW)) # Quadrant 4 (top-left)
724
725 # Tweak the edges so that the sum rule applies.
726 kN[:, 0] = -kN[:, -1]
727 kE[0, :] = -kE[-1, :]
728
729 # We use the normalization of Guyonnet et al. (2015),
730 # which is compatible with the way the input file is produced.
731 # The factor 1/2 is due to the fact that the charge distribution at the end
732 # is twice the average, and the second 1/2 is due to
733 # charge interpolation.
734 kN *= 0.25
735 kE *= 0.25
736
737 # Indeed, i and j in the tuple refer to serial and parallel directions.
738 # In most Python code, the image reads im[j, i], so we transpose:
739 kN = kN.T
740 kE = kE.T
741
742 # Get the image to perform the correction
743 image = exposure.getMaskedImage().getImage()
744
745 # The image needs to be units of electrons/holes
746 with gainContext(exposure, image, applyGain, gains):
747 # Computes the correction and returns the "delta_image",
748 # which should be subtracted from "im" in order to undo the BF effect.
749 # The input image should be expressed in electrons
750 im = image.getArray().copy()
751 convolver = CustomFFTConvolution(im, kN)
752 convolutions = convolver(im, [kN, kE])
753
754 # The convolutions contain the boundary shifts (in pixel size units)
755 # for [horizontal, vertical] boundaries.
756 # We now compute the charge to move around.
757 delta = np.zeros_like(im)
758 boundaryCharge = np.zeros_like(im)
759
760 # Horizontal boundaries (parallel direction).
761 # We could use a more elaborate interpolator for estimating the
762 # charge on the boundary.
763 boundaryCharge[:-1, :] = im[1:, :] + im[:-1, :]
764 # boundaryCharge[1:-2,:] = (9./8.)*(I[2:-1,:]+I[1:-2,:] -
765 # (1./8.)*(I[0:-3,:]+I[3,:])
766
767 # The charge to move around is the
768 # product of the boundary shift (in pixel size units) times the
769 # charge on the boundary (in charge per pixel unit).
770 dq = boundaryCharge * convolutions[0]
771 delta += dq
772
773 # What is gained by a pixel is lost by its neighbor (the right one).
774 delta[1:, :] -= dq[:-1, :]
775
776 # Vertical boundaries.
777 boundaryCharge = np.zeros_like(im) # Reset to zero.
778 boundaryCharge[:, :-1] = im[:, 1:] + im[:, :-1]
779 dq = boundaryCharge * convolutions[1]
780 delta += dq
781
782 # What is gained by a pixel is lost by its neighbor.
783 delta[:, 1:] -= dq[:, :-1]
784
785 # TODO: One might check that delta.sum() ~ 0 (charge conservation).
786
787 # Apply the correction to the original image
788 exposure.image.array -= delta
789
790 return exposure
fromDict(cls, dictionary, **kwargs)
Definition calibType.py:606
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:210
__init__(self, camera=None, inputRange=1, fitRange=None, availableFilters=list(), **kwargs)
electrostaticBrighterFatterCorrection(log, exposure, electroBfDistortionMatrix, applyGain, gains=None, applyFilterCorrection=False)
gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True)