LSST Applications g00274db5b6+edbf708997,g00d0e8bbd7+edbf708997,g199a45376c+5137f08352,g1fd858c14a+1d4b6db739,g262e1987ae+f4d9505c4f,g29ae962dfc+7156fb1a53,g2cef7863aa+73c82f25e4,g35bb328faa+edbf708997,g3e17d7035e+5b3adc59f5,g3fd5ace14f+852fa6fbcb,g47891489e3+6dc8069a4c,g53246c7159+edbf708997,g64539dfbff+9f17e571f4,g67b6fd64d1+6dc8069a4c,g74acd417e5+ae494d68d9,g786e29fd12+af89c03590,g7ae74a0b1c+a25e60b391,g7aefaa3e3d+536efcc10a,g7cc15d900a+d121454f8d,g87389fa792+a4172ec7da,g89139ef638+6dc8069a4c,g8d7436a09f+28c28d8d6d,g8ea07a8fe4+db21c37724,g92c671f44c+9f17e571f4,g98df359435+b2e6376b13,g99af87f6a8+b0f4ad7b8d,gac66b60396+966efe6077,gb88ae4c679+7dec8f19df,gbaa8f7a6c5+38b34f4976,gbf99507273+edbf708997,gc24b5d6ed1+9f17e571f4,gca7fc764a6+6dc8069a4c,gcc769fe2a4+97d0256649,gd7ef33dd92+6dc8069a4c,gdab6d2f7ff+ae494d68d9,gdbb4c4dda9+9f17e571f4,ge410e46f29+6dc8069a4c,geaed405ab2+e194be0d2b,w.2025.47
LSST Data Management Base Package
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 """
135 _OBSTYPE = 'BF_DISTORTION_MATRIX'
136 _SCHEMA = 'ELectrostatic brighter-fatter pixel distortion model'
137 _VERSION = 1.0
138
139 def __init__(self, camera=None, inputRange=1, fitRange=None, **kwargs):
140 """
141 Filename refers to an input tuple that contains the
142 boundary shifts for one electron. This file is produced by an
143 electrostatic fit to data extracted from flat-field statistics,
144 implemented in https://gitlab.in2p3.fr/astier/bfptc/tools/fit_cov.py
145 """
146 self.ampNames = []
147 self.inputRange = inputRange
148 if fitRange is None:
149 fitRange = inputRange
150 self.fitRange = fitRange
151 self.badAmps = list()
152 self.gain = dict()
153 self.aMatrix = np.full((inputRange, inputRange), np.nan)
154 self.aMatrixSigma = np.full((inputRange, inputRange), np.nan)
155 self.aMatrixModel = np.full((fitRange, fitRange), np.nan)
156 self.aMatrixSum = np.nan
157 self.aMatrixModelSum = np.nan
158 self.modelNormalization = [np.nan, np.nan]
159 self.usedPixels = np.full((inputRange, inputRange), np.nan)
160 self.fitParamNames = list()
161 self.freeFitParamNames = list()
162 self.fitParams = dict()
163 self.fitParamErrors = dict()
164 self.fitChi2 = np.nan
165 self.fitReducedChi2 = np.nan
166 self.fitParamCovMatrix = np.array([])
167 self.ath = np.full((fitRange, fitRange), np.nan)
168 self.athMinusBeta = np.full((fitRange, fitRange), np.nan)
169 self.aN = np.full((fitRange, fitRange), np.nan)
170 self.aS = np.full((fitRange, fitRange), np.nan)
171 self.aE = np.full((fitRange, fitRange), np.nan)
172 self.aW = np.full((fitRange, fitRange), np.nan)
173
174 super().__init__(**kwargs)
175
176 if camera:
177 self.initFromCamera(camera, detectorId=kwargs.get('detectorId', None))
178
179 self.requiredAttributes.update([
180 'inputRange', 'fitRange', 'badAmps', 'gain', 'aMatrix',
181 'aMatrixSigma', 'aMatrixModel', 'aMatrixSum', 'aMatrixModelSum',
182 'modelNormalization', 'usedPixels', 'fitParamNames',
183 'freeFitParamNames', 'fitParams', 'fitParamErrors', 'fitChi2',
184 'fitReducedChi2', 'fitParamCovMatrix', 'ath', 'athMinusBeta',
185 'aN', 'aS', 'aE', 'aW', 'ampNames',
186 ])
187
188 def updateMetadata(self, setDate=False, **kwargs):
189 """Update calibration metadata.
190
191 This calls the base class's method after ensuring the required
192 calibration keywords will be saved.
193
194 Parameters
195 ----------
196 setDate : `bool`, optional
197 Update the CALIBDATE fields in the metadata to the current
198 time. Defaults to False.
199 kwargs :
200 Other keyword parameters to set in the metadata.
201 """
202 kwargs['INPUT_RANGE'] = self.inputRange
203 kwargs['FIT_RANGE'] = self.fitRange
204
205 super().updateMetadata(setDate=setDate, **kwargs)
206
207 def initFromCamera(self, camera, detectorId=None):
208 """Initialize kernel structure from camera.
209
210 Parameters
211 ----------
212 camera : `lsst.afw.cameraGeom.Camera`
213 Camera to use to define geometry.
214 detectorId : `int`, optional
215 Index of the detector to generate.
216
217 Returns
218 -------
219 calib : `lsst.ip.isr.BrighterFatterKernel`
220 The initialized calibration.
221
222 Raises
223 ------
224 RuntimeError
225 Raised if no detectorId is supplied for a calibration with
226 ``level='AMP'``.
227 """
228 self._instrument = camera.getName()
229
230 if detectorId is not None:
231 detector = camera[detectorId]
232 self._detectorId = detectorId
233 self._detectorName = detector.getName()
234 self._detectorSerial = detector.getSerial()
235
236 for amp in detector:
237 ampName = amp.getName()
238 self.ampNames.append(ampName)
239 else:
240 raise RuntimeError("A detectorId must be supplied if "
241 "camera is supplied.")
242
243 return self
244
245 @classmethod
246 def fromDict(cls, dictionary):
247 """Construct a calibration from a dictionary of properties.
248
249 Parameters
250 ----------
251 dictionary : `dict`
252 Dictionary of properties.
253
254 Returns
255 -------
256 calib : `lsst.ip.isr.BrighterFatterKernel`
257 Constructed calibration.
258
259 Raises
260 ------
261 RuntimeError
262 Raised if the supplied dictionary is for a different
263 calibration.
264 Raised if the version of the supplied dictionary is 1.0.
265 """
266 calib = cls()
267
268 if calib._OBSTYPE != (found := dictionary['metadata']['OBSTYPE']):
269 raise RuntimeError(f"Incorrect brighter-fatter calibration supplied. Expected {calib._OBSTYPE}, "
270 f"found {found}")
271 calib.setMetadata(dictionary['metadata'])
272 calib.calibInfoFromDict(dictionary)
273
274 calib.ampNames = dictionary['ampNames']
275 inputRange = dictionary['inputRange']
276 fitRange = dictionary['fitRange']
277 calib.inputRange = inputRange
278 calib.fitRange = fitRange
279 calib.gain = dictionary['gain']
280 calib.badAmps = dictionary['badAmps']
281 calib.fitParamNames = dictionary['fitParamNames']
282 calib.freeFitParamNames = dictionary['freeFitParamNames']
283 calib.aMatrix = np.array(
284 dictionary['aMatrix'],
285 dtype=np.float64,
286 ).reshape(inputRange, inputRange)
287 calib.aMatrixSigma = np.array(
288 dictionary['aMatrixSigma'],
289 dtype=np.float64,
290 ).reshape(inputRange, inputRange)
291 calib.aMatrixModel = np.array(
292 dictionary['aMatrixSigma'],
293 dtype=np.float64,
294 ).reshape(fitRange, fitRange)
295 calib.aMatrixSum = float(dictionary['aMatrixSum'])
296 calib.aMatrixModelSum = float(dictionary['aMatrixModelSum'])
297 calib.modelNormalization = dictionary['modelNormalization']
298 calib.usedPixels = np.array(dictionary['usedPixels'])
299 calib.fitParams = dictionary['fitParams']
300 calib.fitParamErrors = dictionary['fitParamErrors']
301 calib.fitChi2 = float(dictionary['fitChi2'])
302 calib.fitReducedChi2 = float(dictionary['fitReducedChi2'])
303 calib.fitParamCovMatrix = np.array(
304 dictionary['fitParamCovMatrix'],
305 dtype=np.float64,
306 ).reshape(len(calib.freeFitParamNames), len(calib.freeFitParamNames))
307 calib.ath = np.array(
308 dictionary['ath'],
309 dtype=np.float64,
310 ).reshape(fitRange, fitRange)
311 calib.athMinusBeta = np.array(
312 dictionary['athMinusBeta'],
313 dtype=np.float64,
314 ).reshape(fitRange, fitRange)
315 calib.aN = np.array(
316 dictionary['aN'],
317 dtype=np.float64,
318 ).reshape(fitRange, fitRange)
319 calib.aS = np.array(
320 dictionary['aS'],
321 dtype=np.float64,
322 ).reshape(fitRange, fitRange)
323 calib.aE = np.array(
324 dictionary['aE'],
325 dtype=np.float64,
326 ).reshape(fitRange, fitRange)
327 calib.aW = np.array(
328 dictionary['aW'],
329 dtype=np.float64,
330 ).reshape(fitRange, fitRange)
331
332 calib.updateMetadata()
333 return calib
334
335 def toDict(self):
336 """Return a dictionary containing the calibration properties.
337
338 The dictionary should be able to be round-tripped through
339 `fromDict`.
340
341 Returns
342 -------
343 dictionary : `dict`
344 Dictionary of properties.
345 """
346 self.updateMetadata()
347
348 outDict = dict()
349 metadata = self.getMetadata()
350 outDict['metadata'] = metadata
351
352 outDict['ampNames'] = self.ampNames
353 outDict['inputRange'] = self.inputRange
354 outDict['fitRange'] = self.fitRange
355 outDict['badAmps'] = self.badAmps
356 outDict['fitParamNames'] = self.fitParamNames
357 outDict['freeFitParamNames'] = self.freeFitParamNames
358 outDict['gain'] = self.gain
359 outDict['aMatrix'] = self.aMatrix.ravel().tolist()
360 outDict['aMatrixSigma'] = self.aMatrixSigma.ravel().tolist()
361 outDict['aMatrixModel'] = self.aMatrixModel.ravel().tolist()
362 outDict['aMatrixSum'] = self.aMatrixSum
363 outDict['aMatrixModelSum'] = self.aMatrixModelSum
364 outDict['aMatrixModel'] = self.aMatrixModel.ravel().tolist()
365 outDict['modelNormalization'] = self.modelNormalization
366 outDict['fitParamCovMatrix'] = self.fitParamCovMatrix.ravel().tolist()
367 outDict['usedPixels'] = self.usedPixels.ravel().tolist()
368 outDict['fitParams'] = self.fitParams
369 outDict['fitParamErrors'] = self.fitParamErrors
370 outDict['fitChi2'] = self.fitChi2
371 outDict['fitReducedChi2'] = self.fitReducedChi2
372 outDict['ath'] = self.ath.ravel().tolist()
373 outDict['athMinusBeta'] = self.athMinusBeta.ravel().tolist()
374 outDict['aN'] = self.aN.ravel().tolist()
375 outDict['aS'] = self.aS.ravel().tolist()
376 outDict['aE'] = self.aE.ravel().tolist()
377 outDict['aW'] = self.aW.ravel().tolist()
378
379 return outDict
380
381 @classmethod
382 def fromTable(cls, tableList):
383 """Construct calibration from a list of tables.
384
385 This method uses the `fromDict` method to create the
386 calibration, after constructing an appropriate dictionary from
387 the input tables.
388
389 Parameters
390 ----------
391 tableList : `list` [`astropy.table.Table`]
392 List of tables to use to construct the brighter-fatter
393 calibration.
394
395 Returns
396 -------
397 calib : `lsst.ip.isr.BrighterFatterKernel`
398 The calibration defined in the tables.
399 """
400 record = tableList[0][0]
401
402 metadata = record.meta
403 calibVersion = metadata['BF_DISTORTION_MATRIX_VERSION']
404
405 # Initialize inDict with all expected keys
406 # and empty values of the corresponding type
407 inDict = dict()
408 inDict['metadata'] = metadata
409 inDict['ampNames'] = record['AMPNAMES']
410 inDict['inputRange'] = record['INPUT_RANGE']
411 inDict['fitRange'] = record['FIT_RANGE']
412 inDict['badAmps'] = record['BAD_AMPS']
413 inDict['fitParamNames'] = record['FIT_PARAM_NAMES']
414 inDict['freeFitParamNames'] = record['FREE_FIT_PARAM_NAMES']
415 inDict['gain'] = {str(n): v for n, v in zip(record['AMPNAMES'], record['GAIN'])}
416 inDict['aMatrix'] = record['A_MATRIX']
417 inDict['aMatrixSigma'] = record['A_MATRIX_SIGMA']
418 inDict['aMatrixModel'] = record['A_MATRIX_MODEL']
419 inDict['aMatrixSum'] = record['A_MATRIX_SUM']
420 inDict['aMatrixModelSum'] = record['A_MATRIX_MODEL_SUM']
421 inDict['modelNormalization'] = record['MODEL_NORMALIZATION']
422 inDict['usedPixels'] = record['USED_PIXELS']
423 inDict['fitParams'] = {str(n): v for n, v in zip(record['FIT_PARAM_NAMES'], record['FIT_PARAMS'])}
424 inDict['fitParamErrors'] = {str(n): v for n, v in zip(record['FIT_PARAM_NAMES'],
425 record['FIT_PARAM_ERRORS'])}
426 inDict['fitChi2'] = record['FIT_CHI2']
427 inDict['fitReducedChi2'] = record['FIT_REDUCED_CHI2']
428 inDict['fitParamCovMatrix'] = record['FIT_PARAM_COV_MATRIX']
429 inDict['ath'] = record['ATH']
430 inDict['athMinusBeta'] = record['ATH_MINUS_BETA']
431 inDict['aN'] = record['A_N']
432 inDict['aS'] = record['A_S']
433 inDict['aE'] = record['A_E']
434 inDict['aW'] = record['A_W']
435
436 if not calibVersion == 1.0:
437 cls().log.warning("Unkown version found for "
438 f"ElectrostaticBrighterFatterDistortionMatrix: {calibVersion}. ")
439
440 # Check for newer versions, but there are none...
441
442 return cls.fromDict(inDict)
443
444 def toTable(self):
445 """Construct a list of tables containing the information in this
446 calibration.
447
448 The list of tables should create an identical calibration
449 after being passed to this class's fromTable method.
450
451 Returns
452 -------
453 tableList : `list` [`lsst.afw.table.Table`]
454 List of tables containing the crosstalk calibration
455 information.
456
457 """
458 tableList = []
459 self.updateMetadata()
460
461 recordDict = {
462 'AMPNAMES': self.ampNames,
463 'INPUT_RANGE': self.inputRange,
464 'FIT_RANGE': self.fitRange,
465 'BAD_AMPS': self.badAmps,
466 'GAIN': list(self.gain.values()),
467 'A_MATRIX': self.aMatrix.ravel(),
468 'A_MATRIX_SIGMA': self.aMatrixSigma.ravel(),
469 'A_MATRIX_MODEL': self.aMatrixModel.ravel(),
470 'A_MATRIX_SUM': self.aMatrixSum,
471 'A_MATRIX_MODEL_SUM': self.aMatrixModelSum,
472 'MODEL_NORMALIZATION': self.modelNormalization,
473 'USED_PIXELS': self.usedPixels.ravel(),
474 'FIT_PARAM_NAMES': self.fitParamNames,
475 'FREE_FIT_PARAM_NAMES': self.freeFitParamNames,
476 'FIT_PARAMS': list(self.fitParams.values()),
477 'FIT_PARAM_ERRORS': list(self.fitParamErrors.values()),
478 'FIT_CHI2': self.fitChi2,
479 'FIT_REDUCED_CHI2': self.fitReducedChi2,
480 'FIT_PARAM_COV_MATRIX': self.fitParamCovMatrix.ravel(),
481 'ATH': self.ath.ravel(),
482 'ATH_MINUS_BETA': self.athMinusBeta.ravel(),
483 'A_N': self.aN.ravel(),
484 'A_S': self.aS.ravel(),
485 'A_E': self.aE.ravel(),
486 'A_W': self.aW.ravel(),
487 }
488
489 catalogList = [recordDict]
490 catalog = Table(catalogList)
491
492 inMeta = self.getMetadata().toDict()
493 outMeta = {k: v for k, v in inMeta.items() if v is not None}
494 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
495 catalog.meta = outMeta
496 tableList.append(catalog)
497
498 return tableList
499
500
502 """
503 A class that performs image convolutions in Fourier space, using pyfftw.
504 The constructor takes images as arguments and creates the FFTW plans.
505 The convolutions are performed by the __call__ routine.
506 This is faster than scipy.signal.fftconvolve, and it saves some transforms
507 by allowing the same image to be convolved with several kernels.
508 pyfftw does not accommodate float32 images, so everything
509 should be double precision.
510
511 Code adaped from :
512 https://stackoverflow.com/questions/14786920/convolution-of-two-three-dimensional-arrays-with-padding-on-one-side-too-slow
513 Code posted by Henry Gomersal
514 """
515 def __init__(self, im, kernel, threads=1):
516 # Compute the minimum size of the convolution result.
517 shape = (np.array(im.shape) + np.array(kernel.shape)) - 1
518
519 # Find the next larger "fast size" for FFT computation.
520 # This can provide a significant speedup.
521 shape = np.array([pyfftw.next_fast_len(s) for s in shape])
522
523 # Create FFTW building objects for the image and kernel.
524 self.fftImageObj = pyfftw.builders.rfftn(im, s=shape, threads=threads)
525 self.fftKernelObj = pyfftw.builders.rfftn(kernel, s=shape, threads=threads)
526 self.ifftObj = pyfftw.builders.irfftn(
527 self.fftImageObj.get_output_array(),
528 s=shape,
529 threads=threads,
530 )
531
532 def __call__(self, im, kernels):
533 """
534 Perform the convolution and trim the result to the
535 size of the input image. If kernels is a list, then
536 the routine returns a list of corresponding
537 convolutions.
538 """
539 # Handle both a list of kernels and a single kernel.
540 ks = [kernels] if type(kernels) is not list else kernels
541 convolutions = []
542 for k in ks:
543 # Transform the image and the kernel using FFT.
544 tim = self.fftImageObj(im)
545 tk = self.fftKernelObj(k)
546
547 # Multiply in Fourier space and perform the inverse FFT.
548 convolution = self.ifftObj(tim * tk)
549 # Trim the result to match the input image size, following
550 # the 'same' policy of scipy.signal.fftconvolve.
551 oy = k.shape[0] // 2
552 ox = k.shape[1] // 2
553 convolutions.append(convolution[oy:oy + im.shape[0], ox:ox + im.shape[1]].copy())
554 # Return a single convolution if kernels was
555 # not a list, otherwise return the list.
556 return convolutions[0] if type(kernels) is not list else convolutions
557
558
559def electrostaticBrighterFatterCorrection(exposure, electroBfDistortionMatrix, applyGain, gains=None):
560 """
561 Evaluates the correction of CCD images affected by the
562 brighter-fatter effect, as described in
563 https://arxiv.org/abs/2301.03274. Requires as input the result of
564 an electrostatic fit to flat covariance data (or any other
565 determination of pixel boundary shifts under the influence of a
566 single electron).
567
568 The filename refers to an input tuple that contains the
569 boundary shifts for one electron. This file is produced by an
570 electrostatic fit to data extracted from flat-field statistics,
571 implemented in https://gitlab.in2p3.fr/astier/bfptc/tools/fit_cov.py.
572 """
573
574 # Use the symmetrize function to fill the four quadrants for each kernel
575 r = electroBfDistortionMatrix.fitRange - 1
576 aN = electroBfDistortionMatrix.aN
577 aS = electroBfDistortionMatrix.aS
578 aE = electroBfDistortionMatrix.aE
579 aW = electroBfDistortionMatrix.aW
580
581 # Initialize kN and kE arrays
582 kN = np.zeros((2 * r + 1, 2 * r + 1))
583 kE = np.zeros_like(kN)
584
585 # Fill in the 4 quadrants for kN
586 kN[r:, r:] = aN # Quadrant 1 (bottom-right)
587 kN[:r+1, r:] = np.flipud(aN) # Quadrant 2 (top-right)
588 kN[r:, :r+1] = np.fliplr(aS) # Quadrant 3 (bottom-left)
589 kN[:r+1, :r+1] = np.flipud(np.fliplr(aS)) # Quadrant 4 (top-left)
590
591 # Fill in the 4 quadrants for kE
592 kE[r:, r:] = aE # Quadrant 1 (bottom-right)
593 kE[:r+1, r:] = np.flipud(aW) # Quadrant 2 (top-right)
594 kE[r:, :r+1] = np.fliplr(aE) # Quadrant 3 (bottom-left)
595 kE[:r+1, :r+1] = np.flipud(np.fliplr(aW)) # Quadrant 4 (top-left)
596
597 # Tweak the edges so that the sum rule applies.
598 kN[:, 0] = -kN[:, -1]
599 kE[0, :] = -kE[-1, :]
600
601 # We use the normalization of Guyonnet et al. (2015),
602 # which is compatible with the way the input file is produced.
603 # The factor 1/2 is due to the fact that the charge distribution at the end
604 # is twice the average, and the second 1/2 is due to
605 # charge interpolation.
606 kN *= 0.25
607 kE *= 0.25
608
609 # Indeed, i and j in the tuple refer to serial and parallel directions.
610 # In most Python code, the image reads im[j, i], so we transpose:
611 kN = kN.T
612 kE = kE.T
613
614 # Get the image to perform the correction
615 image = exposure.getMaskedImage().getImage()
616
617 # The image needs to be units of electrons/holes
618 with gainContext(exposure, image, applyGain, gains):
619 # Computes the correction and returns the "delta_image",
620 # which should be subtracted from "im" in order to undo the BF effect.
621 # The input image should be expressed in electrons
622 im = image.getArray().copy()
623 convolver = CustomFFTConvolution(im, kN)
624 convolutions = convolver(im, [kN, kE])
625
626 # The convolutions contain the boundary shifts (in pixel size units)
627 # for [horizontal, vertical] boundaries.
628 # We now compute the charge to move around.
629 delta = np.zeros_like(im)
630 boundaryCharge = np.zeros_like(im)
631
632 # Horizontal boundaries (parallel direction).
633 # We could use a more elaborate interpolator for estimating the
634 # charge on the boundary.
635 boundaryCharge[:-1, :] = im[1:, :] + im[:-1, :]
636 # boundaryCharge[1:-2,:] = (9./8.)*(I[2:-1,:]+I[1:-2,:] -
637 # (1./8.)*(I[0:-3,:]+I[3,:])
638
639 # The charge to move around is the
640 # product of the boundary shift (in pixel size units) times the
641 # charge on the boundary (in charge per pixel unit).
642 dq = boundaryCharge * convolutions[0]
643 delta += dq
644
645 # What is gained by a pixel is lost by its neighbor (the right one).
646 delta[1:, :] -= dq[:-1, :]
647
648 # Vertical boundaries.
649 boundaryCharge = np.zeros_like(im) # Reset to zero.
650 boundaryCharge[:, :-1] = im[:, 1:] + im[:, :-1]
651 dq = boundaryCharge * convolutions[1]
652 delta += dq
653
654 # What is gained by a pixel is lost by its neighbor.
655 delta[:, 1:] -= dq[:, :-1]
656
657 # TODO: One might check that delta.sum() ~ 0 (charge conservation).
658
659 # Apply the correction to the original image
660 exposure.image.array -= delta
661
662 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
electrostaticBrighterFatterCorrection(exposure, electroBfDistortionMatrix, applyGain, gains=None)
gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True)