22 """Calculation of brighter-fatter effect correlations and kernels.""" 24 __all__ = [
'MakeBrighterFatterKernelTaskConfig',
25 'MakeBrighterFatterKernelTask',
30 from scipy
import stats
32 import matplotlib.pyplot
as plt
34 from mpl_toolkits.mplot3d
import axes3d
35 from matplotlib.backends.backend_pdf
import PdfPages
36 from dataclasses
import dataclass
44 import lsst.pex.config
as pexConfig
46 from .utils
import PairedVisitListTaskRunner, checkExpLengthEqual
48 from lsst.cp.pipe.ptc import (MeasurePhotonTransferCurveTaskConfig, MeasurePhotonTransferCurveTask,
49 PhotonTransferCurveDataset)
53 """Config class for bright-fatter effect coefficient calculation.""" 55 isr = pexConfig.ConfigurableField(
57 doc=
"""Task to perform instrumental signature removal""",
59 isrMandatorySteps = pexConfig.ListField(
61 doc=
"isr operations that must be performed for valid results. Raises if any of these are False",
62 default=[
'doAssembleCcd']
64 isrForbiddenSteps = pexConfig.ListField(
66 doc=
"isr operations that must NOT be performed for valid results. Raises if any of these are True",
67 default=[
'doFlat',
'doFringe',
'doAddDistortionModel',
'doBrighterFatter',
'doUseOpticsTransmission',
68 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
70 isrDesirableSteps = pexConfig.ListField(
72 doc=
"isr operations that it is advisable to perform, but are not mission-critical." +
73 " WARNs are logged for any of these found to be False.",
74 default=[
'doBias',
'doDark',
'doCrosstalk',
'doDefect',
'doLinearize']
76 doCalcGains = pexConfig.Field(
78 doc=
"Measure the per-amplifier gains (using the photon transfer curve method)?",
81 doPlotPtcs = pexConfig.Field(
83 doc=
"Plot the PTCs and butler.put() them as defined by the plotBrighterFatterPtc template",
86 forceZeroSum = pexConfig.Field(
88 doc=
"Force the correlation matrix to have zero sum by adjusting the (0,0) value?",
91 correlationQuadraticFit = pexConfig.Field(
93 doc=
"Use a quadratic fit to find the correlations instead of simple averaging?",
96 correlationModelRadius = pexConfig.Field(
98 doc=
"Build a model of the correlation coefficients for radii larger than this value in pixels?",
101 correlationModelSlope = pexConfig.Field(
103 doc=
"Slope of the correlation model for radii larger than correlationModelRadius",
106 ccdKey = pexConfig.Field(
108 doc=
"The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'",
111 minMeanSignal = pexConfig.Field(
113 doc=
"Minimum value of mean signal (in ADU) to consider.",
116 maxMeanSignal = pexConfig.Field(
118 doc=
"Maximum value to of mean signal (in ADU) to consider.",
121 maxIterRegression = pexConfig.Field(
123 doc=
"Maximum number of iterations for the regression fitter",
126 nSigmaClipGainCalc = pexConfig.Field(
128 doc=
"Number of sigma to clip the pixel value distribution to during gain calculation",
131 nSigmaClipRegression = pexConfig.Field(
133 doc=
"Number of sigma to clip outliers from the line of best fit to during iterative regression",
136 xcorrCheckRejectLevel = pexConfig.Field(
138 doc=
"Sanity check level for the sum of the input cross-correlations. Arrays which " +
139 "sum to greater than this are discarded before the clipped mean is calculated.",
142 maxIterSuccessiveOverRelaxation = pexConfig.Field(
144 doc=
"The maximum number of iterations allowed for the successive over-relaxation method",
147 eLevelSuccessiveOverRelaxation = pexConfig.Field(
149 doc=
"The target residual error for the successive over-relaxation method",
152 nSigmaClipKernelGen = pexConfig.Field(
154 doc=
"Number of sigma to clip to during pixel-wise clipping when generating the kernel. See " +
155 "the generateKernel docstring for more info.",
158 nSigmaClipXCorr = pexConfig.Field(
160 doc=
"Number of sigma to clip when calculating means for the cross-correlation",
163 maxLag = pexConfig.Field(
165 doc=
"The maximum lag (in pixels) to use when calculating the cross-correlation/kernel",
168 nPixBorderGainCalc = pexConfig.Field(
170 doc=
"The number of border pixels to exclude when calculating the gain",
173 nPixBorderXCorr = pexConfig.Field(
175 doc=
"The number of border pixels to exclude when calculating the cross-correlation and kernel",
178 biasCorr = pexConfig.Field(
180 doc=
"An empirically determined correction factor, used to correct for the sigma-clipping of" +
181 " a non-Gaussian distribution. Post DM-15277, code will exist here to calculate appropriate values",
184 backgroundBinSize = pexConfig.Field(
186 doc=
"Size of the background bins",
189 fixPtcThroughOrigin = pexConfig.Field(
191 doc=
"Constrain the fit of the photon transfer curve to go through the origin when measuring" +
195 level = pexConfig.ChoiceField(
196 doc=
"The level at which to calculate the brighter-fatter kernels",
200 "AMP":
"Every amplifier treated separately",
201 "DETECTOR":
"One kernel per detector",
204 ignoreAmpsForAveraging = pexConfig.ListField(
206 doc=
"List of amp names to ignore when averaging the amplifier kernels into the detector" +
207 " kernel. Only relevant for level = AMP",
210 backgroundWarnLevel = pexConfig.Field(
212 doc=
"Log warnings if the mean of the fitted background is found to be above this level after " +
213 "differencing image pair.",
219 """A DataIdContainer for the MakeBrighterFatterKernelTask.""" 222 """Compute refList based on idList. 224 This method must be defined as the dataset does not exist before this 230 Results of parsing the command-line. 234 Not called if ``add_id_argument`` called 235 with ``doMakeDataRefList=False``. 236 Note that this is almost a copy-and-paste of the vanilla implementation, 237 but without checking if the datasets already exist, 238 as this task exists to make them. 240 if self.datasetType
is None:
241 raise RuntimeError(
"Must call setDatasetType first")
242 butler = namespace.butler
243 for dataId
in self.idList:
244 refList =
list(butler.subset(datasetType=self.datasetType, level=self.level, dataId=dataId))
248 namespace.log.warn(
"No data found for dataId=%s", dataId)
250 self.refList += refList
254 """A simple class to hold the kernel(s) generated and the intermediate 257 kernel.ampwiseKernels are the kernels for each amplifier in the detector, 258 as generated by having LEVEL == 'AMP' 260 kernel.detectorKernel is the kernel generated for the detector as a whole, 261 as generated by having LEVEL == 'DETECTOR' 263 kernel.detectorKernelFromAmpKernels is the kernel for the detector, 264 generated by averaging together the amps in the detector 266 The originalLevel is the level for which the kernel(s) were generated, 267 i.e. the level at which the task was originally run. 271 self.__dict__[
"originalLevel"] = originalLevel
272 self.__dict__[
"ampwiseKernels"] = {}
273 self.__dict__[
"detectorKernel"] = {}
274 self.__dict__[
"detectorKernelFromAmpKernels"] = {}
275 self.__dict__[
"means"] = []
276 self.__dict__[
"rawMeans"] = []
277 self.__dict__[
"rawXcorrs"] = []
278 self.__dict__[
"xCorrs"] = []
279 self.__dict__[
"meanXcorrs"] = []
280 self.__dict__[
"gain"] =
None 281 self.__dict__[
"gainErr"] =
None 282 self.__dict__[
"noise"] =
None 283 self.__dict__[
"noiseErr"] =
None 285 for key, value
in kwargs.items():
286 if hasattr(self, key):
287 setattr(self, key, value)
290 """Protect class attributes""" 291 if attribute
not in self.__dict__:
292 print(f
"Cannot set {attribute}")
294 self.__dict__[attribute] = value
297 self.detectorKernel[detectorName] = self.ampwiseKernels[ampName]
300 if detectorName
not in self.detectorKernelFromAmpKernels.
keys():
301 self.detectorKernelFromAmpKernels[detectorName] = {}
303 if self.detectorKernelFromAmpKernels[detectorName] != {}
and overwrite
is False:
304 raise RuntimeError(
'Was told to replace existing detector kernel with overwrite==False')
306 ampNames = self.ampwiseKernels.
keys()
307 ampsToAverage = [amp
for amp
in ampNames
if amp
not in ampsToExclude]
308 avgKernel = np.zeros_like(self.ampwiseKernels[ampsToAverage[0]])
309 for ampName
in ampsToAverage:
310 avgKernel += self.ampwiseKernels[ampName]
311 avgKernel /= len(ampsToAverage)
313 self.detectorKernelFromAmpKernels[detectorName] = avgKernel
318 """The gains and the results of the PTC fits.""" 324 """Brighter-fatter effect correction-kernel calculation task. 326 A command line task for calculating the brighter-fatter correction 327 kernel from pairs of flat-field images (with the same exposure length). 329 The following operations are performed: 331 - The configurable isr task is called, which unpersists and assembles the 332 raw images, and performs the selected instrument signature removal tasks. 333 For the purpose of brighter-fatter coefficient calculation is it 334 essential that certain components of isr are *not* performed, and 335 recommended that certain others are. The task checks the selected isr 336 configuration before it is run, and if forbidden components have been 337 selected task will raise, and if recommended ones have not been selected, 340 - The gain of the each amplifier in the detector is calculated using 341 the photon transfer curve (PTC) method and used to correct the images 342 so that all calculations are done in units of electrons, and so that the 343 level across amplifier boundaries is continuous. 344 Outliers in the PTC are iteratively rejected 345 before fitting, with the nSigma rejection level set by 346 config.nSigmaClipRegression. Individual pixels are ignored in the input 347 images the image based on config.nSigmaClipGainCalc. 349 - Each image is then cross-correlated with the one it's paired with 350 (with the pairing defined by the --visit-pairs command line argument), 351 which is done either the whole-image to whole-image, 352 or amplifier-by-amplifier, depending on config.level. 354 - Once the cross-correlations have been calculated for each visit pair, 355 these are used to generate the correction kernel. 356 The maximum lag used, in pixels, and hence the size of the half-size 357 of the kernel generated, is given by config.maxLag, 358 i.e. a value of 10 will result in a kernel of size 2n-1 = 19x19 pixels. 359 Outlier values in these cross-correlations are rejected by using a 360 pixel-wise sigma-clipped thresholding to each cross-correlation in 361 the visit-pairs-length stack of cross-correlations. 362 The number of sigma clipped to is set by config.nSigmaClipKernelGen. 364 - Once DM-15277 has been completed, a method will exist to calculate the 365 empirical correction factor, config.biasCorr. 366 TODO: DM-15277 update this part of the docstring once the ticket is done. 369 RunnerClass = PairedVisitListTaskRunner
370 ConfigClass = MakeBrighterFatterKernelTaskConfig
371 _DefaultName =
"makeBrighterFatterKernel" 374 pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
375 self.makeSubtask(
"isr")
378 if self.
debug.enabled:
379 self.log.
info(
"Running with debug enabled...")
384 if self.
debug.display:
386 afwDisp.setDefaultBackend(self.
debug.displayBackend)
387 afwDisp.Display.delAllDisplays()
388 self.
disp1 = afwDisp.Display(0, open=
True)
389 self.
disp2 = afwDisp.Display(1, open=
True)
391 im = afwImage.ImageF(1, 1)
396 self.
debug.display =
False 397 self.log.
warn(
'Failed to setup/connect to display! Debug display has been disabled')
399 plt.interactive(
False)
405 def _makeArgumentParser(cls):
406 """Augment argument parser for the MakeBrighterFatterKernelTask.""" 408 parser.add_argument(
"--visit-pairs", dest=
"visitPairs", nargs=
"*",
409 help=
"Visit pairs to use. Each pair must be of the form INT,INT e.g. 123,456")
410 parser.add_id_argument(
"--id", datasetType=
"brighterFatterKernel",
411 ContainerClass=BrighterFatterKernelTaskDataIdContainer,
412 help=
"The ccds to use, e.g. --id ccd=0..100")
416 """Check that appropriate ISR settings are being used 417 for brighter-fatter kernel calculation.""" 427 configDict = self.config.isr.toDict()
429 for configParam
in self.config.isrMandatorySteps:
430 if configDict[configParam]
is False:
431 raise RuntimeError(
'Must set config.isr.%s to True ' 432 'for brighter-fatter kernel calculation' % configParam)
434 for configParam
in self.config.isrForbiddenSteps:
435 if configDict[configParam]
is True:
436 raise RuntimeError(
'Must set config.isr.%s to False ' 437 'for brighter-fatter kernel calculation' % configParam)
439 for configParam
in self.config.isrDesirableSteps:
440 if configParam
not in configDict:
441 self.log.
info(
'Failed to find key %s in the isr config dict. You probably want ' +
442 'to set the equivalent for your obs_package to True.' % configParam)
444 if configDict[configParam]
is False:
445 self.log.
warn(
'Found config.isr.%s set to False for brighter-fatter kernel calculation. ' 446 'It is probably desirable to have this set to True' % configParam)
449 if not self.config.isr.assembleCcd.doTrim:
450 raise RuntimeError(
'Must trim when assembling CCDs. Set config.isr.assembleCcd.doTrim to True')
454 """Run the brighter-fatter measurement task. 456 For a dataRef (which is each detector here), 457 and given a list of visit pairs, calculate the 458 brighter-fatter kernel for the detector. 462 dataRef : `list` of `lsst.daf.persistence.ButlerDataRef` 463 dataRef for the detector for the visits to be fit. 464 visitPairs : `iterable` of `tuple` of `int` 465 Pairs of visit numbers to be processed together 475 detNum = dataRef.dataId[self.config.ccdKey]
476 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
477 amps = detector.getAmplifiers()
478 ampNames = [amp.getName()
for amp
in amps]
480 if self.config.level ==
'DETECTOR':
481 kernels = {detNum: []}
483 xcorrs = {detNum: []}
484 meanXcorrs = {detNum: []}
485 elif self.config.level ==
'AMP':
486 kernels = {key: []
for key
in ampNames}
487 means = {key: []
for key
in ampNames}
488 xcorrs = {key: []
for key
in ampNames}
489 meanXcorrs = {key: []
for key
in ampNames}
491 raise RuntimeError(
"Unsupported level: {}".
format(self.config.level))
494 if not self.config.doCalcGains:
497 deleteMe = dataRef.get(
'photonTransferCurveDataset')
498 except butlerExceptions.NoResults:
500 deleteMe = dataRef.get(
'brighterFatterGain')
501 except butlerExceptions.NoResults:
504 raise RuntimeError(
"doCalcGains == False and gains could not be got from butler")
from None 512 if self.config.level ==
'DETECTOR':
513 if self.config.doCalcGains:
514 self.log.
info(
'Computing gains for detector %s' % detNum)
516 dataRef.put(gains, datasetType=
'brighterFatterGain')
517 self.log.
debug(
'Finished gain estimation for detector %s' % detNum)
519 gains = dataRef.get(
'brighterFatterGain')
521 raise RuntimeError(
'Failed to retrieved gains for detector %s' % detNum)
522 self.log.
info(
'Retrieved stored gain for detector %s' % detNum)
523 self.log.
debug(
'Detector %s has gains %s' % (detNum, gains))
526 for ampName
in ampNames:
527 gains.gains[ampName] = 1.0
530 ptcConfig.isrForbiddenSteps = []
531 ptcConfig.doFitBootstrap =
True 532 ptcConfig.minMeanSignal = self.config.minMeanSignal
533 ptcConfig.maxMeanSignal = self.config.maxMeanSignal
539 for (v1, v2)
in visitPairs:
540 dataRef.dataId[
'visit'] = v1
542 dataRef.dataId[
'visit'] = v2
544 del dataRef.dataId[
'visit']
547 self.log.
info(
'Preparing images for cross-correlation calculation for detector %s' % detNum)
557 for det_object
in _scaledMaskedIms1.keys():
558 self.log.
debug(
"Calculating correlations for %s" % det_object)
560 _scaledMaskedIms2[det_object])
561 xcorrs[det_object].
append(_xcorr)
562 means[det_object].
append([_means1[det_object], _means2[det_object]])
563 if self.config.level !=
'DETECTOR':
565 expTime = exp1.getInfo().getVisitInfo().getExposureTime()
566 ptcDataset.rawExpTimes[det_object].
append(expTime)
567 ptcDataset.rawMeans[det_object].
append((_means1[det_object] + _means2[det_object]) / 2.0)
568 ptcDataset.rawVars[det_object].
append(_xcorr[0, 0] / 2.0)
574 rawMeans = copy.deepcopy(means)
575 rawXcorrs = copy.deepcopy(xcorrs)
580 if self.config.level !=
'DETECTOR':
581 if self.config.doCalcGains:
582 self.log.
info(
'Calculating gains for detector %s using PTC task' % detNum)
583 ptcDataset = ptcTask.fitPtcAndNl(ptcDataset, ptcConfig.ptcFitType)
584 dataRef.put(ptcDataset, datasetType=
'photonTransferCurveDataset')
585 self.log.
debug(
'Finished gain estimation for detector %s' % detNum)
587 ptcDataset = dataRef.get(
'photonTransferCurveDataset')
591 if self.config.doPlotPtcs:
592 dirname = dataRef.getUri(datasetType=
'cpPipePlotRoot', write=
True)
593 if not os.path.exists(dirname):
595 detNum = dataRef.dataId[self.config.ccdKey]
596 filename = f
"PTC_det{detNum}.pdf" 597 filenameFull = os.path.join(dirname, filename)
598 with PdfPages(filenameFull)
as pdfPages:
599 ptcTask._plotPtc(ptcDataset, ptcConfig.ptcFitType, pdfPages)
603 self.log.
info(
'Generating kernel(s) for %s' % detNum)
604 for det_object
in xcorrs.keys():
605 if self.config.level ==
'DETECTOR':
606 objId =
'detector %s' % det_object
607 elif self.config.level ==
'AMP':
608 objId =
'detector %s AMP %s' % (detNum, det_object)
611 meanXcorr, kernel = self.
generateKernel(xcorrs[det_object], means[det_object], objId)
612 kernels[det_object] = kernel
613 meanXcorrs[det_object] = meanXcorr
616 self.log.
warn(
'RuntimeError during kernel generation for %s' % objId)
620 bfKernel.means = means
621 bfKernel.rawMeans = rawMeans
622 bfKernel.rawXcorrs = rawXcorrs
623 bfKernel.xCorrs = xcorrs
624 bfKernel.meanXcorrs = meanXcorrs
625 bfKernel.originalLevel = self.config.level
627 bfKernel.gain = ptcDataset.gain
628 bfKernel.gainErr = ptcDataset.gainErr
629 bfKernel.noise = ptcDataset.noise
630 bfKernel.noiseErr = ptcDataset.noiseErr
634 if self.config.level ==
'AMP':
635 bfKernel.ampwiseKernels = kernels
636 ex = self.config.ignoreAmpsForAveraging
637 bfKernel.detectorKernel = bfKernel.makeDetectorKernelFromAmpwiseKernels(detNum, ampsToExclude=ex)
639 elif self.config.level ==
'DETECTOR':
640 bfKernel.detectorKernel = kernels
642 raise RuntimeError(
'Invalid level for kernel calculation; this should not be possible.')
644 dataRef.put(bfKernel)
646 self.log.
info(
'Finished generating kernel(s) for %s' % detNum)
647 return pipeBase.Struct(exitStatus=0)
649 def _applyGains(self, means, xcorrs, ptcData):
650 """Apply the gains calculated by the PtcTask. 652 It also removes datapoints that were thrown out in the PTC algorithm. 656 means : `dict` [`str`, `list` of `tuple`] 657 Dictionary, keyed by ampName, containing a list of the means for 660 xcorrs : `dict` [`str`, `list` of `np.array`] 661 Dictionary, keyed by ampName, containing a list of the 662 cross-correlations for each visit pair. 664 ptcDataset : `lsst.cp.pipe.ptc.PhotonTransferCurveDataset` 665 The results of running the ptcTask. 667 ampNames = means.keys()
668 assert set(xcorrs.keys()) ==
set(ampNames) ==
set(ptcData.ampNames)
670 for ampName
in ampNames:
671 mask = ptcData.visitMask[ampName]
672 gain = ptcData.gain[ampName]
674 fitType = ptcData.ptcFitType
675 if fitType !=
'POLYNOMIAL':
676 raise RuntimeError(
"Only polynomial fit types supported currently")
677 ptcFitPars = ptcData.ptcFitPars[ampName]
683 for i
in range(len(means[ampName])):
684 ampMean = np.mean(means[ampName][i])
685 xcorrs[ampName][i][0, 0] -= 2.0 * (ampMean * ptcFitPars[1] + ptcFitPars[0])
688 means[ampName] = [[value*gain
for value
in pair]
for pair
in np.array(means[ampName])[mask]]
689 xcorrs[ampName] = [arr*gain*gain
for arr
in np.array(xcorrs[ampName])[mask]]
692 def _makeCroppedExposures(self, exp, gains, level):
693 """Prepare exposure for cross-correlation calculation. 695 For each amp, crop by the border amount, specified by 696 config.nPixBorderXCorr, then rescale by the gain 697 and subtract the sigma-clipped mean. 698 If the level is 'DETECTOR' then this is done 699 to the whole image so that it can be cross-correlated, with a copy 701 If the level is 'AMP' then this is done per-amplifier, 702 and a copy of each prepared amp-image returned. 706 exp : `lsst.afw.image.exposure.ExposureF` 707 The exposure to prepare 708 gains : `lsst.cp.pipe.makeBrighterFatterKernel.BrighterFatterGain` 709 The object holding the amplifier gains, essentially a 710 dictionary of the amplifier gain values, keyed by amplifier name 712 Either `AMP` or `DETECTOR` 716 scaledMaskedIms : `dict` [`str`, `lsst.afw.image.maskedImage.MaskedImageF`] 717 Depending on level, this is either one item, or n_amp items, 718 keyed by detectorId or ampName 722 This function is controlled by the following config parameters: 723 nPixBorderXCorr : `int` 724 The number of border pixels to exclude 725 nSigmaClipXCorr : `float` 726 The number of sigma to be clipped to 728 assert(isinstance(exp, afwImage.ExposureF))
730 local_exp = exp.clone()
733 border = self.config.nPixBorderXCorr
734 sigma = self.config.nSigmaClipXCorr
737 sctrl.setNumSigmaClip(sigma)
742 detector = local_exp.getDetector()
743 amps = detector.getAmplifiers()
745 mi = local_exp.getMaskedImage()
751 ampName = amp.getName()
752 rescaleIm = mi[amp.getBBox()]
753 rescaleTemp = temp[amp.getBBox()]
755 gain = gains.gains[ampName]
758 self.log.
debug(
"mean*gain = %s, clipped mean = %s" %
761 rescaleIm -= mean*gain
765 afwImage.LOCAL], afwMath.MEANCLIP, sctrl).getValue()
766 returnAreas[ampName] = rescaleIm
768 if level ==
'DETECTOR':
769 detName = local_exp.getDetector().getId()
771 afwMath.MEANCLIP, sctrl).getValue()
772 returnAreas[detName] = rescaleIm
774 return returnAreas, means
776 def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None):
777 """Calculate the cross-correlation of an area. 779 If the area in question contains multiple amplifiers then they must 780 have been gain corrected. 784 maskedIm0 : `lsst.afw.image.MaskedImageF` 786 maskedIm1 : `lsst.afw.image.MaskedImageF` 788 frameId : `str`, optional 789 The frame identifier for use in the filename 790 if writing debug outputs. 791 detId : `str`, optional 792 The detector identifier (detector, or detector+amp, 793 depending on config.level) for use in the filename 794 if writing debug outputs. 795 runningBiasCorrSim : `bool` 796 Set to true when using this function to calculate the amount of bias 797 introduced by the sigma clipping. If False, the biasCorr parameter 798 is divided by to remove the bias, but this is, of course, not 799 appropriate when this is the parameter being measured. 804 The quarter-image cross-correlation 806 The sum of the means of the input images, 807 sigma-clipped, and with borders applied. 808 This is used when using this function with simulations to calculate 809 the biasCorr parameter. 813 This function is controlled by the following config parameters: 815 The maximum lag to use in the cross-correlation calculation 816 nPixBorderXCorr : `int` 817 The number of border pixels to exclude 818 nSigmaClipXCorr : `float` 819 The number of sigma to be clipped to 821 Parameter used to correct from the bias introduced 824 maxLag = self.config.maxLag
825 border = self.config.nPixBorderXCorr
826 sigma = self.config.nSigmaClipXCorr
827 biasCorr = self.config.biasCorr
830 sctrl.setNumSigmaClip(sigma)
833 afwMath.MEANCLIP, sctrl).getValue()
835 afwMath.MEANCLIP, sctrl).getValue()
838 diff = maskedIm0.clone()
839 diff -= maskedIm1.getImage()
840 diff = diff[border: -border, border: -border, afwImage.LOCAL]
842 if self.
debug.writeDiffImages:
843 filename =
'_'.join([
'diff',
'detector', detId, frameId,
'.fits'])
844 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
847 binsize = self.config.backgroundBinSize
848 nx = diff.getWidth()//binsize
849 ny = diff.getHeight()//binsize
852 bgImg = bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE, afwMath.REDUCE_INTERP_ORDER)
853 bgMean = np.mean(bgImg.getArray())
854 if abs(bgMean) >= self.config.backgroundWarnLevel:
855 self.log.
warn(
'Mean of background = %s > config.maxBackground' % bgMean)
859 if self.
debug.writeDiffImages:
860 filename =
'_'.join([
'bgSub',
'diff',
'detector', detId, frameId,
'.fits'])
861 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
862 if self.
debug.display:
865 self.log.
debug(
"Median and variance of diff:")
868 np.var(diff.getImage().getArray())))
871 dim0 = diff[0: -maxLag, : -maxLag, afwImage.LOCAL]
873 width, height = dim0.getDimensions()
874 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
876 for xlag
in range(maxLag + 1):
877 for ylag
in range(maxLag + 1):
878 dim_xy = diff[xlag:xlag + width, ylag: ylag + height, afwImage.LOCAL].
clone()
882 if not runningBiasCorrSim:
883 xcorr[xlag, ylag] /= biasCorr
891 """Estimate the amplifier gains using the specified visits. 893 Given a dataRef and list of flats of varying intensity, 894 calculate the gain for each amplifier in the detector 895 using the photon transfer curve (PTC) method. 897 The config.fixPtcThroughOrigin option determines whether the iterative 898 fitting is forced to go through the origin or not. 899 This defaults to True, fitting var=1/gain * mean. 900 If set to False then var=1/g * mean + const is fitted. 902 This is really a photo transfer curve (PTC) gain measurement task. 903 See DM-14063 for results from of a comparison between 904 this task's numbers and the gain values in the HSC camera model, 905 and those measured by the PTC task in eotest. 909 dataRef : `lsst.daf.persistence.butler.Butler.dataRef` 910 dataRef for the detector for the flats to be used 911 visitPairs : `list` of `tuple` 912 List of visit-pairs to use, as [(v1,v2), (v3,v4)...] 916 gains : `lsst.cp.pipe.makeBrighterFatterKernel.BrighterFatterGain` 917 Object holding the per-amplifier gains, essentially a 918 dict of the as-calculated amplifier gain values, keyed by amp name 919 nominalGains : `dict` [`str`, `float`] 920 Dict of the amplifier gains, as reported by the `detector` object, 921 keyed by amplifier name 924 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
925 amps = detector.getAmplifiers()
926 ampNames = [amp.getName()
for amp
in amps]
928 ampMeans = {key: []
for key
in ampNames}
929 ampCoVariances = {key: []
for key
in ampNames}
930 ampVariances = {key: []
for key
in ampNames}
936 for visPairNum, visPair
in enumerate(visitPairs):
942 ampName = amp.getName()
943 if _means[ampName]*10 < _vars[ampName]
or _means[ampName]*10 < _covars[ampName]:
944 msg =
'Sanity check failed; check visit pair %s amp %s' % (visPair, ampName)
952 for k
in _means.keys():
953 if _vars[k]*1.3 < _covars[k]
or _vars[k]*0.7 > _covars[k]:
954 self.log.
warn(
'Dropped a value')
956 ampMeans[k].
append(_means[k])
957 ampVariances[k].
append(_vars[k])
958 ampCoVariances[k].
append(_covars[k])
964 ampName = amp.getName()
965 if ampMeans[ampName] == []:
967 ptcResults[ampName] = (0, 0, 1, 0)
970 nomGains[ampName] = amp.getGain()
971 slopeRaw, interceptRaw, rVal, pVal, stdErr = \
972 stats.linregress(np.asarray(ampMeans[ampName]), np.asarray(ampCoVariances[ampName]))
974 np.asarray(ampCoVariances[ampName]),
975 fixThroughOrigin=
True)
977 np.asarray(ampCoVariances[ampName]),
978 fixThroughOrigin=
False)
979 self.log.
info(
"Slope of raw fit: %s, intercept: %s p value: %s" % (slopeRaw,
981 self.log.
info(
"slope of fixed fit: %s, difference vs raw:%s" % (slopeFix,
982 slopeFix - slopeRaw))
983 self.log.
info(
"slope of unfixed fit: %s, difference vs fix:%s" % (slopeUnfix,
984 slopeFix - slopeUnfix))
985 if self.config.fixPtcThroughOrigin:
986 slopeToUse = slopeFix
988 slopeToUse = slopeUnfix
990 if self.
debug.enabled:
992 ax = fig.add_subplot(111)
993 ax.plot(np.asarray(ampMeans[ampName]),
994 np.asarray(ampCoVariances[ampName]), linestyle=
'None', marker=
'x', label=
'data')
995 if self.config.fixPtcThroughOrigin:
996 ax.plot(np.asarray(ampMeans[ampName]),
997 np.asarray(ampMeans[ampName])*slopeToUse, label=
'Fit through origin')
999 ax.plot(np.asarray(ampMeans[ampName]),
1000 np.asarray(ampMeans[ampName])*slopeToUse + intercept,
1001 label=
'Fit (intercept unconstrained')
1003 dataRef.put(fig,
"plotBrighterFatterPtc", amp=ampName)
1004 self.log.
info(
'Saved PTC for detector %s amp %s' % (detector.getId(), ampName))
1005 gains[ampName] = 1.0/slopeToUse
1008 ptcResults[ampName] = (0, 0, 1, 0)
1012 def _calcMeansAndVars(self, dataRef, v1, v2):
1013 """Calculate the means, vars, covars, and retrieve the nominal gains, 1014 for each amp in each detector. 1016 This code runs using two visit numbers, and for the detector specified. 1017 It calculates the correlations in the individual amps without 1018 rescaling any gains. This allows a photon transfer curve 1019 to be generated and the gains measured. 1021 Images are assembled with use the isrTask, and basic isr is performed. 1025 dataRef : `lsst.daf.persistence.butler.Butler.dataRef` 1026 dataRef for the detector for the repo containing the flats to be used 1028 First visit of the visit pair 1030 Second visit of the visit pair 1034 means, vars, covars : `tuple` of `dict` 1035 Three dicts, keyed by ampName, 1036 containing the sum of the image-means, 1037 the variance, and the quarter-image of the xcorr. 1039 sigma = self.config.nSigmaClipGainCalc
1040 maxLag = self.config.maxLag
1041 border = self.config.nPixBorderGainCalc
1042 biasCorr = self.config.biasCorr
1045 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
1051 originalDataId = dataRef.dataId.copy()
1052 dataRef.dataId[
'visit'] = v1
1054 dataRef.dataId[
'visit'] = v2
1056 dataRef.dataId = originalDataId
1060 detector = exps[0].getDetector()
1063 if self.
debug.display:
1064 self.
disp1.
mtv(ims[0], title=str(v1))
1065 self.
disp2.
mtv(ims[1], title=str(v2))
1068 sctrl.setNumSigmaClip(sigma)
1069 for imNum, im
in enumerate(ims):
1075 for amp
in detector:
1076 ampName = amp.getName()
1077 ampIm = im[amp.getBBox()]
1079 afwMath.MEANCLIP, sctrl).getValue()
1080 if ampName
not in ampMeans.keys():
1081 ampMeans[ampName] = []
1082 ampMeans[ampName].
append(mean)
1085 diff = ims[0].
clone()
1088 temp = diff[border: -border, border: -border, afwImage.LOCAL]
1093 binsize = self.config.backgroundBinSize
1094 nx = temp.getWidth()//binsize
1095 ny = temp.getHeight()//binsize
1099 box = diff.getBBox()
1101 diff[box, afwImage.LOCAL] -= bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE,
1102 afwMath.REDUCE_INTERP_ORDER)
1106 for amp
in detector:
1107 ampName = amp.getName()
1108 diffAmpIm = diff[amp.getBBox()].
clone()
1109 diffAmpImCrop = diffAmpIm[border: -border - maxLag, border: -border - maxLag, afwImage.LOCAL]
1111 w, h = diffAmpImCrop.getDimensions()
1112 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
1115 for xlag
in range(maxLag + 1):
1116 for ylag
in range(maxLag + 1):
1117 dim_xy = diffAmpIm[border + xlag: border + xlag + w,
1118 border + ylag: border + ylag + h,
1119 afwImage.LOCAL].
clone()
1121 dim_xy *= diffAmpImCrop
1123 afwMath.MEANCLIP, sctrl).getValue()/(biasCorr)
1125 variances[ampName] = xcorr[0, 0]
1127 coVars[ampName] = np.sum(xcorr_full)
1129 msg =
"M1: " + str(ampMeans[ampName][0])
1130 msg +=
" M2 " + str(ampMeans[ampName][1])
1131 msg +=
" M_sum: " + str((ampMeans[ampName][0]) + ampMeans[ampName][1])
1132 msg +=
" Var " + str(variances[ampName])
1133 msg +=
" coVar: " + str(coVars[ampName])
1137 for amp
in detector:
1138 ampName = amp.getName()
1139 means[ampName] = ampMeans[ampName][0] + ampMeans[ampName][1]
1141 return means, variances, coVars
1143 def _plotXcorr(self, xcorr, mean, zmax=0.05, title=None, fig=None, saveToFileName=None):
1144 """Plot the correlation functions.""" 1146 xcorr = xcorr.getArray()
1150 xcorr /= float(mean)
1158 ax = fig.add_subplot(111, projection=
'3d')
1162 nx, ny = np.shape(xcorr)
1164 xpos, ypos = np.meshgrid(np.arange(nx), np.arange(ny))
1165 xpos = xpos.flatten()
1166 ypos = ypos.flatten()
1167 zpos = np.zeros(nx*ny)
1168 dz = xcorr.flatten()
1169 dz[dz > zmax] = zmax
1171 ax.bar3d(xpos, ypos, zpos, 1, 1, dz, color=
'b', zsort=
'max', sort_zpos=100)
1172 if xcorr[0, 0] > zmax:
1173 ax.bar3d([0], [0], [zmax], 1, 1, 1e-4, color=
'c')
1175 ax.set_xlabel(
"row")
1176 ax.set_ylabel(
"column")
1177 ax.set_zlabel(
r"$\langle{(F_i - \bar{F})(F_i - \bar{F})}\rangle/\bar{F}$")
1182 fig.savefig(saveToFileName)
1184 def _iterativeRegression(self, x, y, fixThroughOrigin=False, nSigmaClip=None, maxIter=None):
1185 """Use linear regression to fit a line, iteratively removing outliers. 1187 Useful when you have a sufficiently large numbers of points on your PTC. 1188 This function iterates until either there are no outliers of 1189 config.nSigmaClip magnitude, or until the specified maximum number 1190 of iterations has been performed. 1195 The independent variable. Must be a numpy array, not a list. 1197 The dependent variable. Must be a numpy array, not a list. 1198 fixThroughOrigin : `bool`, optional 1199 Whether to fix the PTC through the origin or allow an y-intercept. 1200 nSigmaClip : `float`, optional 1201 The number of sigma to clip to. 1202 Taken from the task config if not specified. 1203 maxIter : `int`, optional 1204 The maximum number of iterations allowed. 1205 Taken from the task config if not specified. 1210 The slope of the line of best fit 1212 The y-intercept of the line of best fit 1215 maxIter = self.config.maxIterRegression
1217 nSigmaClip = self.config.nSigmaClipRegression
1221 sctrl.setNumSigmaClip(nSigmaClip)
1223 if fixThroughOrigin:
1224 while nIter < maxIter:
1226 self.log.
debug(
"Origin fixed, iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1227 TEST = x[:, np.newaxis]
1228 slope, _, _, _ = np.linalg.lstsq(TEST, y)
1230 res = (y - slope * x) / x
1233 index = np.where((res > (resMean + nSigmaClip*resStd)) |
1234 (res < (resMean - nSigmaClip*resStd)))
1235 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1236 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1238 x = np.delete(x, index)
1239 y = np.delete(y, index)
1243 while nIter < maxIter:
1245 self.log.
debug(
"Iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1246 xx = np.vstack([x, np.ones(len(x))]).T
1247 ret, _, _, _ = np.linalg.lstsq(xx, y)
1248 slope, intercept = ret
1249 res = y - slope*x - intercept
1252 index = np.where((res > (resMean + nSigmaClip * resStd)) | (res < resMean - nSigmaClip * resStd))
1253 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1254 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1256 x = np.delete(x, index)
1257 y = np.delete(y, index)
1259 return slope, intercept
1262 """Generate the full kernel from a list of cross-correlations and means. 1264 Taking a list of quarter-image, gain-corrected cross-correlations, 1265 do a pixel-wise sigma-clipped mean of each, 1266 and tile into the full-sized kernel image. 1268 Each corr in corrs is one quarter of the full cross-correlation, 1269 and has been gain-corrected. Each mean in means is a tuple of the means 1270 of the two individual images, corresponding to that corr. 1274 corrs : `list` of `numpy.ndarray`, (Ny, Nx) 1275 A list of the quarter-image cross-correlations 1276 means : `list` of `tuples` of `floats` 1277 The means of the input images for each corr in corrs 1278 rejectLevel : `float`, optional 1279 This is essentially is a sanity check parameter. 1280 If this condition is violated there is something unexpected 1281 going on in the image, and it is discarded from the stack before 1282 the clipped-mean is calculated. 1283 If not provided then config.xcorrCheckRejectLevel is used 1287 kernel : `numpy.ndarray`, (Ny, Nx) 1290 self.log.
info(
'Calculating kernel for %s'%objId)
1293 rejectLevel = self.config.xcorrCheckRejectLevel
1295 if self.config.correlationQuadraticFit:
1299 for corrNum, ((mean1, mean2), corr)
in enumerate(zip(means, corrs)):
1300 msg =
'For item %s, initial corr[0,0] = %g, corr[1,0] = %g'%(corrNum, corr[0, 0], corr[1, 0])
1302 if self.config.level ==
'DETECTOR':
1304 corr[0, 0] -= (mean1 + mean2)
1308 xcorrList.append(-fullCorr / 2.0)
1309 flux = (mean1 + mean2) / 2.0
1310 fluxList.append(flux * flux)
1316 corr /= -1.0*(mean1**2 + mean2**2)
1319 raise RuntimeError(
"Cannot generate kernel because all inputs were discarded. " 1320 "Either the data is bad, or config.xcorrCheckRejectLevel is too low")
1323 meanXcorr = np.zeros_like(fullCorr)
1324 xcorrList = np.asarray(xcorrList)
1326 for i
in range(np.shape(meanXcorr)[0]):
1327 for j
in range(np.shape(meanXcorr)[1]):
1330 slopeRaw, interceptRaw, rVal, pVal, stdErr = stats.linregress(np.asarray(fluxList),
1335 fixThroughOrigin=
True)
1336 msg =
"(%s,%s):Slope of raw fit: %s, intercept: %s p value: %s" % (i, j, slopeRaw,
1339 self.log.
debug(
"(%s,%s):Slope of fixed fit: %s" % (i, j, slope))
1341 meanXcorr[i, j] = slope
1343 meanXcorr[i, j] = slopeRaw
1345 msg = f
"i={i}, j={j}, slope = {slope:.6g}, slopeRaw = {slopeRaw:.6g}" 1347 self.log.
info(
'Quad Fit meanXcorr[0,0] = %g, meanXcorr[1,0] = %g'%(meanXcorr[8, 8],
1356 sctrl.setNumSigmaClip(self.config.nSigmaClipKernelGen)
1358 for corrNum, ((mean1, mean2), corr)
in enumerate(zip(means, corrs)):
1359 corr[0, 0] -= (mean1 + mean2)
1361 self.log.
warn(
'Skipped item %s due to unexpected value of (variance-mean)' % corrNum)
1363 corr /= -1.0*(mean1**2 + mean2**2)
1367 xcorrCheck = np.abs(np.sum(fullCorr))/np.sum(np.abs(fullCorr))
1368 if xcorrCheck > rejectLevel:
1369 self.log.
warn(
"Sum of the xcorr is unexpectedly high. Investigate item num %s for %s. \n" 1370 "value = %s" % (corrNum, objId, xcorrCheck))
1372 xcorrList.append(fullCorr)
1375 raise RuntimeError(
"Cannot generate kernel because all inputs were discarded. " 1376 "Either the data is bad, or config.xcorrCheckRejectLevel is too low")
1379 meanXcorr = np.zeros_like(fullCorr)
1380 xcorrList = np.transpose(xcorrList)
1381 for i
in range(np.shape(meanXcorr)[0]):
1382 for j
in range(np.shape(meanXcorr)[1]):
1384 afwMath.MEANCLIP, sctrl).getValue()
1386 if self.config.correlationModelRadius < (meanXcorr.shape[0] - 1) / 2:
1388 self.config.correlationModelSlope)
1389 self.log.
info(
"SumToInfinity = %s" % sumToInfinity)
1392 if self.config.forceZeroSum:
1393 self.log.
info(
"Forcing sum of correlation matrix to zero")
1399 """An implementation of the successive over relaxation (SOR) method. 1401 A numerical method for solving a system of linear equations 1402 with faster convergence than the Gauss-Seidel method. 1406 source : `numpy.ndarray` 1408 maxIter : `int`, optional 1409 Maximum number of iterations to attempt before aborting. 1410 eLevel : `float`, optional 1411 The target error level at which we deem convergence to have 1416 output : `numpy.ndarray` 1420 maxIter = self.config.maxIterSuccessiveOverRelaxation
1422 eLevel = self.config.eLevelSuccessiveOverRelaxation
1424 assert source.shape[0] == source.shape[1],
"Input array must be square" 1426 func = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1427 resid = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1428 rhoSpe = np.cos(np.pi/source.shape[0])
1431 for i
in range(1, func.shape[0] - 1):
1432 for j
in range(1, func.shape[1] - 1):
1433 resid[i, j] = (func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1434 func[i + 1, j] - 4*func[i, j] - source[i - 1, j - 1])
1435 inError = np.sum(np.abs(resid))
1443 while nIter < maxIter*2:
1446 for i
in range(1, func.shape[0] - 1, 2):
1447 for j
in range(1, func.shape[1] - 1, 2):
1448 resid[i, j] = float(func[i, j-1] + func[i, j + 1] + func[i - 1, j] +
1449 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1450 func[i, j] += omega*resid[i, j]*.25
1451 for i
in range(2, func.shape[0] - 1, 2):
1452 for j
in range(2, func.shape[1] - 1, 2):
1453 resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1454 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1455 func[i, j] += omega*resid[i, j]*.25
1457 for i
in range(1, func.shape[0] - 1, 2):
1458 for j
in range(2, func.shape[1] - 1, 2):
1459 resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1460 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1461 func[i, j] += omega*resid[i, j]*.25
1462 for i
in range(2, func.shape[0] - 1, 2):
1463 for j
in range(1, func.shape[1] - 1, 2):
1464 resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1465 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1466 func[i, j] += omega*resid[i, j]*.25
1467 outError = np.sum(np.abs(resid))
1468 if outError < inError*eLevel:
1471 omega = 1.0/(1 - rhoSpe*rhoSpe/2.0)
1473 omega = 1.0/(1 - rhoSpe*rhoSpe*omega/4.0)
1476 if nIter >= maxIter*2:
1477 self.log.
warn(
"Failure: SuccessiveOverRelaxation did not converge in %s iterations." 1478 "\noutError: %s, inError: %s," % (nIter//2, outError, inError*eLevel))
1480 self.log.
info(
"Success: SuccessiveOverRelaxation converged in %s iterations." 1481 "\noutError: %s, inError: %s", nIter//2, outError, inError*eLevel)
1482 return func[1: -1, 1: -1]
1485 def _tileArray(in_array):
1486 """Given an input quarter-image, tile/mirror it and return full image. 1488 Given a square input of side-length n, of the form 1490 input = array([[1, 2, 3], 1494 return an array of size 2n-1 as 1496 output = array([[ 9, 8, 7, 8, 9], 1505 The square input quarter-array 1510 The full, tiled array 1512 assert(in_array.shape[0] == in_array.shape[1])
1513 length = in_array.shape[0] - 1
1514 output = np.zeros((2*length + 1, 2*length + 1))
1516 for i
in range(length + 1):
1517 for j
in range(length + 1):
1518 output[i + length, j + length] = in_array[i, j]
1519 output[-i + length, j + length] = in_array[i, j]
1520 output[i + length, -j + length] = in_array[i, j]
1521 output[-i + length, -j + length] = in_array[i, j]
1525 def _forceZeroSum(inputArray, sumToInfinity):
1526 """Given an array of correlations, adjust the 1527 central value to force the sum of the array to be zero. 1532 The square input array, assumed square and with 1533 shape (2n+1) x (2n+1) 1538 The same array, with the value of the central value 1539 inputArray[n,n] adjusted to force the array sum to be zero. 1541 assert(inputArray.shape[0] == inputArray.shape[1])
1542 assert(inputArray.shape[0] % 2 == 1)
1543 center = int((inputArray.shape[0] - 1) / 2)
1544 outputArray = np.copy(inputArray)
1545 outputArray[center, center] -= inputArray.sum() - sumToInfinity
1549 def _buildCorrelationModel(array, replacementRadius, slope):
1550 """Given an array of correlations, build a model 1551 for correlations beyond replacementRadius pixels from the center 1552 and replace the measured values with the model. 1557 The square input array, assumed square and with 1558 shape (2n+1) x (2n+1) 1563 The same array, with the outer values 1564 replaced with a smoothed model. 1566 assert(array.shape[0] == array.shape[1])
1567 assert(array.shape[0] % 2 == 1)
1568 assert(replacementRadius > 1)
1569 center = int((array.shape[0] - 1) / 2)
1573 if (array[center, center + 1] >= 0.0)
or (array[center + 1, center] >= 0.0):
1576 intercept = (np.log10(-array[center, center + 1]) + np.log10(-array[center + 1, center])) / 2.0
1577 preFactor = 10**intercept
1578 slopeFactor = 2.0*
abs(slope) - 2.0
1579 sumToInfinity = 2.0*np.pi*preFactor / (slopeFactor*(float(center)+0.5)**slopeFactor)
1584 for i
in range(array.shape[0]):
1585 for j
in range(array.shape[1]):
1586 r2 = float((i-center)*(i-center) + (j-center)*(j-center))
1587 if abs(i-center) < replacementRadius
and abs(j-center) < replacementRadius:
1590 newCvalue = -preFactor * r2**slope
1591 array[i, j] = newCvalue
1592 return sumToInfinity
1595 def _convertImagelikeToFloatImage(imagelikeObject):
1596 """Turn an exposure or masked image of any type into an ImageF.""" 1597 for attr
in (
"getMaskedImage",
"getImage"):
1598 if hasattr(imagelikeObject, attr):
1599 imagelikeObject = getattr(imagelikeObject, attr)()
1601 floatImage = imagelikeObject.convertF()
1602 except AttributeError:
1603 raise RuntimeError(
"Failed to convert image to float")
1607 def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False,
1608 correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10, logger=None):
1609 """Calculate the bias induced when sigma-clipping non-Gaussian distributions. 1611 Fill image-pairs of the specified size with Poisson-distributed values, 1612 adding correlations as necessary. Then calculate the cross correlation, 1613 and calculate the bias induced using the cross-correlation image 1614 and the image means. 1618 fluxLevels : `list` of `int` 1619 The mean flux levels at which to simulate. 1620 Nominal values might be something like [70000, 90000, 110000] 1621 imageShape : `tuple` of `int` 1622 The shape of the image array to simulate, nx by ny pixels. 1623 repeats : `int`, optional 1624 Number of repeats to perform so that results 1625 can be averaged to improve SNR. 1626 seed : `int`, optional 1627 The random seed to use for the Poisson points. 1628 addCorrelations : `bool`, optional 1629 Whether to add brighter-fatter-like correlations to the simulated images 1630 If true, a correlation between x_{i,j} and x_{i+1,j+1} is introduced 1631 by adding a*x_{i,j} to x_{i+1,j+1} 1632 correlationStrength : `float`, optional 1633 The strength of the correlations. 1634 This is the value of the coefficient `a` in the above definition. 1635 maxLag : `int`, optional 1636 The maximum lag to work to in pixels 1637 nSigmaClip : `float`, optional 1638 Number of sigma to clip to when calculating the sigma-clipped mean. 1639 border : `int`, optional 1640 Number of border pixels to mask 1641 logger : `lsst.log.Log`, optional 1642 Logger to use. Instantiated anew if not provided. 1646 biases : `dict` [`float`, `list` of `float`] 1647 A dictionary, keyed by flux level, containing a list of the biases 1648 for each repeat at that flux level 1649 means : `dict` [`float`, `list` of `float`] 1650 A dictionary, keyed by flux level, containing a list of the average 1651 mean fluxes (average of the mean of the two images) 1652 for the image pairs at that flux level 1653 xcorrs : `dict` [`float`, `list` of `np.ndarray`] 1654 A dictionary, keyed by flux level, containing a list of the xcorr 1655 images for the image pairs at that flux level 1658 logger = lsstLog.Log.getDefaultLogger()
1660 means = {f: []
for f
in fluxLevels}
1661 xcorrs = {f: []
for f
in fluxLevels}
1662 biases = {f: []
for f
in fluxLevels}
1665 config.isrMandatorySteps = []
1666 config.isrForbiddenSteps = []
1667 config.nSigmaClipXCorr = nSigmaClip
1668 config.nPixBorderXCorr = border
1669 config.maxLag = maxLag
1672 im0 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1673 im1 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1675 random = np.random.RandomState(seed)
1677 for rep
in range(repeats):
1678 for flux
in fluxLevels:
1679 data0 = random.poisson(flux, (imageShape)).astype(float)
1680 data1 = random.poisson(flux, (imageShape)).astype(float)
1682 data0[1:, 1:] += correlationStrength*data0[: -1, : -1]
1683 data1[1:, 1:] += correlationStrength*data1[: -1, : -1]
1684 im0.image.array[:, :] = data0
1685 im1.image.array[:, :] = data1
1687 _xcorr, _means = task._crossCorrelate(im0, im1, runningBiasCorrSim=
True)
1689 means[flux].
append(_means)
1690 xcorrs[flux].
append(_xcorr)
1692 bias = xcorrs[flux][-1][1, 1]/means[flux][-1]*(1 + correlationStrength)/correlationStrength
1693 msg = f
"Simulated/expected avg. flux: {flux:.1f}, {(means[flux][-1]/2):.1f}" 1695 logger.info(f
"Bias: {bias:.6f}")
1697 bias = xcorrs[flux][-1][0, 0]/means[flux][-1]
1698 msg = f
"Simulated/expected avg. flux: {flux:.1f}, {(means[flux][-1]/2):.1f}" 1700 logger.info(f
"Bias: {bias:.6f}")
1701 biases[flux].
append(bias)
1703 return biases, means, xcorrs
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Angle abs(Angle const &a)
def _iterativeRegression(self, x, y, fixThroughOrigin=False, nSigmaClip=None, maxIter=None)
def makeDataRefList(self, namespace)
def __init__(self, args, kwargs)
def __setattr__(self, attribute, value)
std::shared_ptr< Background > makeBackground(ImageT const &img, BackgroundControl const &bgCtrl)
A convenience function that uses function overloading to make the correct type of Background...
def successiveOverRelax(self, source, maxIter=None, eLevel=None)
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
def checkExpLengthEqual(exp1, exp2, v1=None, v2=None, raiseWithMessage=False)
daf::base::PropertySet * set
Pass parameters to a Background object.
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
def replaceDetectorKernelWithAmpKernel(self, ampName, detectorName)
Pass parameters to a Statistics object.
def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None)
def _forceZeroSum(inputArray, sumToInfinity)
def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False, correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10, logger=None)
def mtv(data, frame=None, title="", wcs=None, args, kwargs)
def generateKernel(self, corrs, means, objId, rejectLevel=None)
def _buildCorrelationModel(array, replacementRadius, slope)
def runDataRef(self, dataRef, visitPairs)
def makeDetectorKernelFromAmpwiseKernels(self, detectorName, ampsToExclude=[], overwrite=False)
def _applyGains(self, means, xcorrs, ptcData)
def _calcMeansAndVars(self, dataRef, v1, v2)
def __init__(self, originalLevel, kwargs)
def _convertImagelikeToFloatImage(imagelikeObject)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def estimateGains(self, dataRef, visitPairs)
daf::base::PropertyList * list
def _makeCroppedExposures(self, exp, gains, level)
def validateIsrConfig(self)