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',
'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.ptcFitType =
'POLYNOMIAL'
533 ptcConfig.polynomialFitDegree = 3
534 ptcConfig.minMeanSignal = self.config.minMeanSignal
535 ptcConfig.maxMeanSignal = self.config.maxMeanSignal
541 for (v1, v2)
in visitPairs:
542 dataRef.dataId[
'expId'] = v1
544 dataRef.dataId[
'expId'] = v2
546 del dataRef.dataId[
'expId']
549 self.log.
info(
'Preparing images for cross-correlation calculation for detector %s' % detNum)
559 for det_object
in _scaledMaskedIms1.keys():
560 self.log.
debug(
"Calculating correlations for %s" % det_object)
562 _scaledMaskedIms2[det_object])
563 xcorrs[det_object].
append(_xcorr)
564 means[det_object].
append([_means1[det_object], _means2[det_object]])
565 if self.config.level !=
'DETECTOR':
567 expTime = exp1.getInfo().getVisitInfo().getExposureTime()
568 ptcDataset.rawExpTimes[det_object].
append(expTime)
569 ptcDataset.rawMeans[det_object].
append((_means1[det_object] + _means2[det_object]) / 2.0)
570 ptcDataset.rawVars[det_object].
append(_xcorr[0, 0] / 2.0)
576 rawMeans = copy.deepcopy(means)
577 rawXcorrs = copy.deepcopy(xcorrs)
582 if self.config.level !=
'DETECTOR':
583 if self.config.doCalcGains:
584 self.log.
info(
'Calculating gains for detector %s using PTC task' % detNum)
585 ptcDataset = ptcTask.fitPtcAndNonLinearity(ptcDataset, ptcConfig.ptcFitType)
586 dataRef.put(ptcDataset, datasetType=
'photonTransferCurveDataset')
587 self.log.
debug(
'Finished gain estimation for detector %s' % detNum)
589 ptcDataset = dataRef.get(
'photonTransferCurveDataset')
593 if self.config.doPlotPtcs:
594 dirname = dataRef.getUri(datasetType=
'cpPipePlotRoot', write=
True)
595 if not os.path.exists(dirname):
597 detNum = dataRef.dataId[self.config.ccdKey]
598 filename = f
"PTC_det{detNum}.pdf"
599 filenameFull = os.path.join(dirname, filename)
600 with PdfPages(filenameFull)
as pdfPages:
601 ptcTask._plotPtc(ptcDataset, ptcConfig.ptcFitType, pdfPages)
605 self.log.
info(
'Generating kernel(s) for %s' % detNum)
606 for det_object
in xcorrs.keys():
607 if self.config.level ==
'DETECTOR':
608 objId =
'detector %s' % det_object
609 elif self.config.level ==
'AMP':
610 objId =
'detector %s AMP %s' % (detNum, det_object)
613 meanXcorr, kernel = self.
generateKernel(xcorrs[det_object], means[det_object], objId)
614 kernels[det_object] = kernel
615 meanXcorrs[det_object] = meanXcorr
618 self.log.
warn(
'RuntimeError during kernel generation for %s' % objId)
622 bfKernel.means = means
623 bfKernel.rawMeans = rawMeans
624 bfKernel.rawXcorrs = rawXcorrs
625 bfKernel.xCorrs = xcorrs
626 bfKernel.meanXcorrs = meanXcorrs
627 bfKernel.originalLevel = self.config.level
629 bfKernel.gain = ptcDataset.gain
630 bfKernel.gainErr = ptcDataset.gainErr
631 bfKernel.noise = ptcDataset.noise
632 bfKernel.noiseErr = ptcDataset.noiseErr
636 if self.config.level ==
'AMP':
637 bfKernel.ampwiseKernels = kernels
638 ex = self.config.ignoreAmpsForAveraging
639 bfKernel.detectorKernel = bfKernel.makeDetectorKernelFromAmpwiseKernels(detNum, ampsToExclude=ex)
641 elif self.config.level ==
'DETECTOR':
642 bfKernel.detectorKernel = kernels
644 raise RuntimeError(
'Invalid level for kernel calculation; this should not be possible.')
646 dataRef.put(bfKernel)
648 self.log.
info(
'Finished generating kernel(s) for %s' % detNum)
649 return pipeBase.Struct(exitStatus=0)
651 def _applyGains(self, means, xcorrs, ptcData):
652 """Apply the gains calculated by the PtcTask.
654 It also removes datapoints that were thrown out in the PTC algorithm.
658 means : `dict` [`str`, `list` of `tuple`]
659 Dictionary, keyed by ampName, containing a list of the means for
662 xcorrs : `dict` [`str`, `list` of `np.array`]
663 Dictionary, keyed by ampName, containing a list of the
664 cross-correlations for each visit pair.
666 ptcDataset : `lsst.cp.pipe.ptc.PhotonTransferCurveDataset`
667 The results of running the ptcTask.
669 ampNames = means.keys()
670 assert set(xcorrs.keys()) ==
set(ampNames) ==
set(ptcData.ampNames)
672 for ampName
in ampNames:
673 mask = ptcData.visitMask[ampName]
674 gain = ptcData.gain[ampName]
676 fitType = ptcData.ptcFitType[ampName]
677 if fitType !=
'POLYNOMIAL':
678 raise RuntimeError(f
"Only polynomial fit types supported currently, found {fitType}")
679 ptcFitPars = ptcData.ptcFitPars[ampName]
685 for i
in range(len(means[ampName])):
686 ampMean = np.mean(means[ampName][i])
687 xcorrs[ampName][i][0, 0] -= 2.0 * (ampMean * ptcFitPars[1] + ptcFitPars[0])
690 means[ampName] = [[value*gain
for value
in pair]
for pair
in np.array(means[ampName])[mask]]
691 xcorrs[ampName] = [arr*gain*gain
for arr
in np.array(xcorrs[ampName])[mask]]
694 def _makeCroppedExposures(self, exp, gains, level):
695 """Prepare exposure for cross-correlation calculation.
697 For each amp, crop by the border amount, specified by
698 config.nPixBorderXCorr, then rescale by the gain
699 and subtract the sigma-clipped mean.
700 If the level is 'DETECTOR' then this is done
701 to the whole image so that it can be cross-correlated, with a copy
703 If the level is 'AMP' then this is done per-amplifier,
704 and a copy of each prepared amp-image returned.
708 exp : `lsst.afw.image.exposure.ExposureF`
709 The exposure to prepare
710 gains : `lsst.cp.pipe.makeBrighterFatterKernel.BrighterFatterGain`
711 The object holding the amplifier gains, essentially a
712 dictionary of the amplifier gain values, keyed by amplifier name
714 Either `AMP` or `DETECTOR`
718 scaledMaskedIms : `dict` [`str`, `lsst.afw.image.maskedImage.MaskedImageF`]
719 Depending on level, this is either one item, or n_amp items,
720 keyed by detectorId or ampName
724 This function is controlled by the following config parameters:
725 nPixBorderXCorr : `int`
726 The number of border pixels to exclude
727 nSigmaClipXCorr : `float`
728 The number of sigma to be clipped to
730 assert(isinstance(exp, afwImage.ExposureF))
732 local_exp = exp.clone()
735 border = self.config.nPixBorderXCorr
736 sigma = self.config.nSigmaClipXCorr
739 sctrl.setNumSigmaClip(sigma)
744 detector = local_exp.getDetector()
745 amps = detector.getAmplifiers()
747 mi = local_exp.getMaskedImage()
753 ampName = amp.getName()
754 rescaleIm = mi[amp.getBBox()]
755 rescaleTemp = temp[amp.getBBox()]
757 gain = gains.gains[ampName]
760 self.log.
debug(
"mean*gain = %s, clipped mean = %s" %
763 rescaleIm -= mean*gain
767 afwImage.LOCAL], afwMath.MEANCLIP, sctrl).getValue()
768 returnAreas[ampName] = rescaleIm
770 if level ==
'DETECTOR':
771 detName = local_exp.getDetector().getId()
773 afwMath.MEANCLIP, sctrl).getValue()
774 returnAreas[detName] = rescaleIm
776 return returnAreas, means
778 def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None):
779 """Calculate the cross-correlation of an area.
781 If the area in question contains multiple amplifiers then they must
782 have been gain corrected.
786 maskedIm0 : `lsst.afw.image.MaskedImageF`
788 maskedIm1 : `lsst.afw.image.MaskedImageF`
790 frameId : `str`, optional
791 The frame identifier for use in the filename
792 if writing debug outputs.
793 detId : `str`, optional
794 The detector identifier (detector, or detector+amp,
795 depending on config.level) for use in the filename
796 if writing debug outputs.
797 runningBiasCorrSim : `bool`
798 Set to true when using this function to calculate the amount of bias
799 introduced by the sigma clipping. If False, the biasCorr parameter
800 is divided by to remove the bias, but this is, of course, not
801 appropriate when this is the parameter being measured.
806 The quarter-image cross-correlation
808 The sum of the means of the input images,
809 sigma-clipped, and with borders applied.
810 This is used when using this function with simulations to calculate
811 the biasCorr parameter.
815 This function is controlled by the following config parameters:
817 The maximum lag to use in the cross-correlation calculation
818 nPixBorderXCorr : `int`
819 The number of border pixels to exclude
820 nSigmaClipXCorr : `float`
821 The number of sigma to be clipped to
823 Parameter used to correct from the bias introduced
826 maxLag = self.config.maxLag
827 border = self.config.nPixBorderXCorr
828 sigma = self.config.nSigmaClipXCorr
829 biasCorr = self.config.biasCorr
832 sctrl.setNumSigmaClip(sigma)
835 afwMath.MEANCLIP, sctrl).getValue()
837 afwMath.MEANCLIP, sctrl).getValue()
840 diff = maskedIm0.clone()
841 diff -= maskedIm1.getImage()
842 diff = diff[border: -border, border: -border, afwImage.LOCAL]
844 if self.
debug.writeDiffImages:
845 filename =
'_'.join([
'diff',
'detector', detId, frameId,
'.fits'])
846 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
849 binsize = self.config.backgroundBinSize
850 nx = diff.getWidth()//binsize
851 ny = diff.getHeight()//binsize
854 bgImg = bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE, afwMath.REDUCE_INTERP_ORDER)
855 bgMean = np.mean(bgImg.getArray())
856 if abs(bgMean) >= self.config.backgroundWarnLevel:
857 self.log.
warn(
'Mean of background = %s > config.maxBackground' % bgMean)
861 if self.
debug.writeDiffImages:
862 filename =
'_'.join([
'bgSub',
'diff',
'detector', detId, frameId,
'.fits'])
863 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
864 if self.
debug.display:
867 self.log.
debug(
"Median and variance of diff:")
870 np.var(diff.getImage().getArray())))
873 dim0 = diff[0: -maxLag, : -maxLag, afwImage.LOCAL]
875 width, height = dim0.getDimensions()
876 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
878 for xlag
in range(maxLag + 1):
879 for ylag
in range(maxLag + 1):
880 dim_xy = diff[xlag:xlag + width, ylag: ylag + height, afwImage.LOCAL].
clone()
884 if not runningBiasCorrSim:
885 xcorr[xlag, ylag] /= biasCorr
893 """Estimate the amplifier gains using the specified visits.
895 Given a dataRef and list of flats of varying intensity,
896 calculate the gain for each amplifier in the detector
897 using the photon transfer curve (PTC) method.
899 The config.fixPtcThroughOrigin option determines whether the iterative
900 fitting is forced to go through the origin or not.
901 This defaults to True, fitting var=1/gain * mean.
902 If set to False then var=1/g * mean + const is fitted.
904 This is really a photo transfer curve (PTC) gain measurement task.
905 See DM-14063 for results from of a comparison between
906 this task's numbers and the gain values in the HSC camera model,
907 and those measured by the PTC task in eotest.
911 dataRef : `lsst.daf.persistence.butler.Butler.dataRef`
912 dataRef for the detector for the flats to be used
913 visitPairs : `list` of `tuple`
914 List of visit-pairs to use, as [(v1,v2), (v3,v4)...]
918 gains : `lsst.cp.pipe.makeBrighterFatterKernel.BrighterFatterGain`
919 Object holding the per-amplifier gains, essentially a
920 dict of the as-calculated amplifier gain values, keyed by amp name
921 nominalGains : `dict` [`str`, `float`]
922 Dict of the amplifier gains, as reported by the `detector` object,
923 keyed by amplifier name
926 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
927 amps = detector.getAmplifiers()
928 ampNames = [amp.getName()
for amp
in amps]
930 ampMeans = {key: []
for key
in ampNames}
931 ampCoVariances = {key: []
for key
in ampNames}
932 ampVariances = {key: []
for key
in ampNames}
938 for visPairNum, visPair
in enumerate(visitPairs):
944 ampName = amp.getName()
945 if _means[ampName]*10 < _vars[ampName]
or _means[ampName]*10 < _covars[ampName]:
946 msg =
'Sanity check failed; check visit pair %s amp %s' % (visPair, ampName)
954 for k
in _means.keys():
955 if _vars[k]*1.3 < _covars[k]
or _vars[k]*0.7 > _covars[k]:
956 self.log.
warn(
'Dropped a value')
958 ampMeans[k].
append(_means[k])
959 ampVariances[k].
append(_vars[k])
960 ampCoVariances[k].
append(_covars[k])
966 ampName = amp.getName()
967 if ampMeans[ampName] == []:
969 ptcResults[ampName] = (0, 0, 1, 0)
972 nomGains[ampName] = amp.getGain()
973 slopeRaw, interceptRaw, rVal, pVal, stdErr = \
974 stats.linregress(np.asarray(ampMeans[ampName]), np.asarray(ampCoVariances[ampName]))
976 np.asarray(ampCoVariances[ampName]),
977 fixThroughOrigin=
True)
979 np.asarray(ampCoVariances[ampName]),
980 fixThroughOrigin=
False)
981 self.log.
info(
"Slope of raw fit: %s, intercept: %s p value: %s" % (slopeRaw,
983 self.log.
info(
"slope of fixed fit: %s, difference vs raw:%s" % (slopeFix,
984 slopeFix - slopeRaw))
985 self.log.
info(
"slope of unfixed fit: %s, difference vs fix:%s" % (slopeUnfix,
986 slopeFix - slopeUnfix))
987 if self.config.fixPtcThroughOrigin:
988 slopeToUse = slopeFix
990 slopeToUse = slopeUnfix
992 if self.
debug.enabled:
994 ax = fig.add_subplot(111)
995 ax.plot(np.asarray(ampMeans[ampName]),
996 np.asarray(ampCoVariances[ampName]), linestyle=
'None', marker=
'x', label=
'data')
997 if self.config.fixPtcThroughOrigin:
998 ax.plot(np.asarray(ampMeans[ampName]),
999 np.asarray(ampMeans[ampName])*slopeToUse, label=
'Fit through origin')
1001 ax.plot(np.asarray(ampMeans[ampName]),
1002 np.asarray(ampMeans[ampName])*slopeToUse + intercept,
1003 label=
'Fit (intercept unconstrained')
1005 dataRef.put(fig,
"plotBrighterFatterPtc", amp=ampName)
1006 self.log.
info(
'Saved PTC for detector %s amp %s' % (detector.getId(), ampName))
1007 gains[ampName] = 1.0/slopeToUse
1010 ptcResults[ampName] = (0, 0, 1, 0)
1014 def _calcMeansAndVars(self, dataRef, v1, v2):
1015 """Calculate the means, vars, covars, and retrieve the nominal gains,
1016 for each amp in each detector.
1018 This code runs using two visit numbers, and for the detector specified.
1019 It calculates the correlations in the individual amps without
1020 rescaling any gains. This allows a photon transfer curve
1021 to be generated and the gains measured.
1023 Images are assembled with use the isrTask, and basic isr is performed.
1027 dataRef : `lsst.daf.persistence.butler.Butler.dataRef`
1028 dataRef for the detector for the repo containing the flats to be used
1030 First visit of the visit pair
1032 Second visit of the visit pair
1036 means, vars, covars : `tuple` of `dict`
1037 Three dicts, keyed by ampName,
1038 containing the sum of the image-means,
1039 the variance, and the quarter-image of the xcorr.
1041 sigma = self.config.nSigmaClipGainCalc
1042 maxLag = self.config.maxLag
1043 border = self.config.nPixBorderGainCalc
1044 biasCorr = self.config.biasCorr
1047 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
1053 originalDataId = dataRef.dataId.copy()
1054 dataRef.dataId[
'expId'] = v1
1056 dataRef.dataId[
'expId'] = v2
1058 dataRef.dataId = originalDataId
1062 detector = exps[0].getDetector()
1065 if self.
debug.display:
1066 self.
disp1.
mtv(ims[0], title=str(v1))
1067 self.
disp2.
mtv(ims[1], title=str(v2))
1070 sctrl.setNumSigmaClip(sigma)
1071 for imNum, im
in enumerate(ims):
1077 for amp
in detector:
1078 ampName = amp.getName()
1079 ampIm = im[amp.getBBox()]
1081 afwMath.MEANCLIP, sctrl).getValue()
1082 if ampName
not in ampMeans.keys():
1083 ampMeans[ampName] = []
1084 ampMeans[ampName].
append(mean)
1087 diff = ims[0].
clone()
1090 temp = diff[border: -border, border: -border, afwImage.LOCAL]
1095 binsize = self.config.backgroundBinSize
1096 nx = temp.getWidth()//binsize
1097 ny = temp.getHeight()//binsize
1101 box = diff.getBBox()
1103 diff[box, afwImage.LOCAL] -= bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE,
1104 afwMath.REDUCE_INTERP_ORDER)
1108 for amp
in detector:
1109 ampName = amp.getName()
1110 diffAmpIm = diff[amp.getBBox()].
clone()
1111 diffAmpImCrop = diffAmpIm[border: -border - maxLag, border: -border - maxLag, afwImage.LOCAL]
1113 w, h = diffAmpImCrop.getDimensions()
1114 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
1117 for xlag
in range(maxLag + 1):
1118 for ylag
in range(maxLag + 1):
1119 dim_xy = diffAmpIm[border + xlag: border + xlag + w,
1120 border + ylag: border + ylag + h,
1121 afwImage.LOCAL].
clone()
1123 dim_xy *= diffAmpImCrop
1125 afwMath.MEANCLIP, sctrl).getValue()/(biasCorr)
1127 variances[ampName] = xcorr[0, 0]
1129 coVars[ampName] = np.sum(xcorr_full)
1131 msg =
"M1: " + str(ampMeans[ampName][0])
1132 msg +=
" M2 " + str(ampMeans[ampName][1])
1133 msg +=
" M_sum: " + str((ampMeans[ampName][0]) + ampMeans[ampName][1])
1134 msg +=
" Var " + str(variances[ampName])
1135 msg +=
" coVar: " + str(coVars[ampName])
1139 for amp
in detector:
1140 ampName = amp.getName()
1141 means[ampName] = ampMeans[ampName][0] + ampMeans[ampName][1]
1143 return means, variances, coVars
1145 def _plotXcorr(self, xcorr, mean, zmax=0.05, title=None, fig=None, saveToFileName=None):
1146 """Plot the correlation functions."""
1148 xcorr = xcorr.getArray()
1152 xcorr /= float(mean)
1160 ax = fig.add_subplot(111, projection=
'3d')
1164 nx, ny = np.shape(xcorr)
1166 xpos, ypos = np.meshgrid(np.arange(nx), np.arange(ny))
1167 xpos = xpos.flatten()
1168 ypos = ypos.flatten()
1169 zpos = np.zeros(nx*ny)
1170 dz = xcorr.flatten()
1171 dz[dz > zmax] = zmax
1173 ax.bar3d(xpos, ypos, zpos, 1, 1, dz, color=
'b', zsort=
'max', sort_zpos=100)
1174 if xcorr[0, 0] > zmax:
1175 ax.bar3d([0], [0], [zmax], 1, 1, 1e-4, color=
'c')
1177 ax.set_xlabel(
"row")
1178 ax.set_ylabel(
"column")
1179 ax.set_zlabel(
r"$\langle{(F_i - \bar{F})(F_i - \bar{F})}\rangle/\bar{F}$")
1184 fig.savefig(saveToFileName)
1186 def _iterativeRegression(self, x, y, fixThroughOrigin=False, nSigmaClip=None, maxIter=None):
1187 """Use linear regression to fit a line, iteratively removing outliers.
1189 Useful when you have a sufficiently large numbers of points on your PTC.
1190 This function iterates until either there are no outliers of
1191 config.nSigmaClip magnitude, or until the specified maximum number
1192 of iterations has been performed.
1197 The independent variable. Must be a numpy array, not a list.
1199 The dependent variable. Must be a numpy array, not a list.
1200 fixThroughOrigin : `bool`, optional
1201 Whether to fix the PTC through the origin or allow an y-intercept.
1202 nSigmaClip : `float`, optional
1203 The number of sigma to clip to.
1204 Taken from the task config if not specified.
1205 maxIter : `int`, optional
1206 The maximum number of iterations allowed.
1207 Taken from the task config if not specified.
1212 The slope of the line of best fit
1214 The y-intercept of the line of best fit
1217 maxIter = self.config.maxIterRegression
1219 nSigmaClip = self.config.nSigmaClipRegression
1223 sctrl.setNumSigmaClip(nSigmaClip)
1225 if fixThroughOrigin:
1226 while nIter < maxIter:
1228 self.log.
debug(
"Origin fixed, iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1229 TEST = x[:, np.newaxis]
1230 slope, _, _, _ = np.linalg.lstsq(TEST, y)
1232 res = (y - slope * x) / x
1235 index = np.where((res > (resMean + nSigmaClip*resStd)) |
1236 (res < (resMean - nSigmaClip*resStd)))
1237 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1238 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1240 x = np.delete(x, index)
1241 y = np.delete(y, index)
1245 while nIter < maxIter:
1247 self.log.
debug(
"Iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1248 xx = np.vstack([x, np.ones(len(x))]).T
1249 ret, _, _, _ = np.linalg.lstsq(xx, y)
1250 slope, intercept = ret
1251 res = y - slope*x - intercept
1254 index = np.where((res > (resMean + nSigmaClip * resStd)) | (res < resMean - nSigmaClip * resStd))
1255 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1256 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1258 x = np.delete(x, index)
1259 y = np.delete(y, index)
1261 return slope, intercept
1264 """Generate the full kernel from a list of cross-correlations and means.
1266 Taking a list of quarter-image, gain-corrected cross-correlations,
1267 do a pixel-wise sigma-clipped mean of each,
1268 and tile into the full-sized kernel image.
1270 Each corr in corrs is one quarter of the full cross-correlation,
1271 and has been gain-corrected. Each mean in means is a tuple of the means
1272 of the two individual images, corresponding to that corr.
1276 corrs : `list` of `numpy.ndarray`, (Ny, Nx)
1277 A list of the quarter-image cross-correlations
1278 means : `list` of `tuples` of `floats`
1279 The means of the input images for each corr in corrs
1280 rejectLevel : `float`, optional
1281 This is essentially is a sanity check parameter.
1282 If this condition is violated there is something unexpected
1283 going on in the image, and it is discarded from the stack before
1284 the clipped-mean is calculated.
1285 If not provided then config.xcorrCheckRejectLevel is used
1289 kernel : `numpy.ndarray`, (Ny, Nx)
1292 self.log.
info(
'Calculating kernel for %s'%objId)
1295 rejectLevel = self.config.xcorrCheckRejectLevel
1297 if self.config.correlationQuadraticFit:
1301 for corrNum, ((mean1, mean2), corr)
in enumerate(zip(means, corrs)):
1302 msg =
'For item %s, initial corr[0,0] = %g, corr[1,0] = %g'%(corrNum, corr[0, 0], corr[1, 0])
1304 if self.config.level ==
'DETECTOR':
1306 corr[0, 0] -= (mean1 + mean2)
1310 xcorrList.append(-fullCorr / 2.0)
1311 flux = (mean1 + mean2) / 2.0
1312 fluxList.append(flux * flux)
1318 corr /= -1.0*(mean1**2 + mean2**2)
1321 raise RuntimeError(
"Cannot generate kernel because all inputs were discarded. "
1322 "Either the data is bad, or config.xcorrCheckRejectLevel is too low")
1325 meanXcorr = np.zeros_like(fullCorr)
1326 xcorrList = np.asarray(xcorrList)
1328 for i
in range(np.shape(meanXcorr)[0]):
1329 for j
in range(np.shape(meanXcorr)[1]):
1332 slopeRaw, interceptRaw, rVal, pVal, stdErr = stats.linregress(np.asarray(fluxList),
1337 fixThroughOrigin=
True)
1338 msg =
"(%s,%s):Slope of raw fit: %s, intercept: %s p value: %s" % (i, j, slopeRaw,
1341 self.log.
debug(
"(%s,%s):Slope of fixed fit: %s" % (i, j, slope))
1343 meanXcorr[i, j] = slope
1345 meanXcorr[i, j] = slopeRaw
1347 msg = f
"i={i}, j={j}, slope = {slope:.6g}, slopeRaw = {slopeRaw:.6g}"
1349 self.log.
info(
'Quad Fit meanXcorr[0,0] = %g, meanXcorr[1,0] = %g'%(meanXcorr[8, 8],
1358 sctrl.setNumSigmaClip(self.config.nSigmaClipKernelGen)
1360 for corrNum, ((mean1, mean2), corr)
in enumerate(zip(means, corrs)):
1361 corr[0, 0] -= (mean1 + mean2)
1363 self.log.
warn(
'Skipped item %s due to unexpected value of (variance-mean)' % corrNum)
1365 corr /= -1.0*(mean1**2 + mean2**2)
1369 xcorrCheck = np.abs(np.sum(fullCorr))/np.sum(np.abs(fullCorr))
1370 if xcorrCheck > rejectLevel:
1371 self.log.
warn(
"Sum of the xcorr is unexpectedly high. Investigate item num %s for %s. \n"
1372 "value = %s" % (corrNum, objId, xcorrCheck))
1374 xcorrList.append(fullCorr)
1377 raise RuntimeError(
"Cannot generate kernel because all inputs were discarded. "
1378 "Either the data is bad, or config.xcorrCheckRejectLevel is too low")
1381 meanXcorr = np.zeros_like(fullCorr)
1382 xcorrList = np.transpose(xcorrList)
1383 for i
in range(np.shape(meanXcorr)[0]):
1384 for j
in range(np.shape(meanXcorr)[1]):
1386 afwMath.MEANCLIP, sctrl).getValue()
1388 if self.config.correlationModelRadius < (meanXcorr.shape[0] - 1) / 2:
1390 self.config.correlationModelSlope)
1391 self.log.
info(
"SumToInfinity = %s" % sumToInfinity)
1394 if self.config.forceZeroSum:
1395 self.log.
info(
"Forcing sum of correlation matrix to zero")
1401 """An implementation of the successive over relaxation (SOR) method.
1403 A numerical method for solving a system of linear equations
1404 with faster convergence than the Gauss-Seidel method.
1408 source : `numpy.ndarray`
1410 maxIter : `int`, optional
1411 Maximum number of iterations to attempt before aborting.
1412 eLevel : `float`, optional
1413 The target error level at which we deem convergence to have
1418 output : `numpy.ndarray`
1422 maxIter = self.config.maxIterSuccessiveOverRelaxation
1424 eLevel = self.config.eLevelSuccessiveOverRelaxation
1426 assert source.shape[0] == source.shape[1],
"Input array must be square"
1428 func = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1429 resid = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1430 rhoSpe = np.cos(np.pi/source.shape[0])
1433 for i
in range(1, func.shape[0] - 1):
1434 for j
in range(1, func.shape[1] - 1):
1435 resid[i, j] = (func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1436 func[i + 1, j] - 4*func[i, j] - source[i - 1, j - 1])
1437 inError = np.sum(np.abs(resid))
1445 while nIter < maxIter*2:
1448 for i
in range(1, func.shape[0] - 1, 2):
1449 for j
in range(1, func.shape[1] - 1, 2):
1450 resid[i, j] = float(func[i, j-1] + func[i, j + 1] + func[i - 1, j] +
1451 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1452 func[i, j] += omega*resid[i, j]*.25
1453 for i
in range(2, func.shape[0] - 1, 2):
1454 for j
in range(2, func.shape[1] - 1, 2):
1455 resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1456 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1457 func[i, j] += omega*resid[i, j]*.25
1459 for i
in range(1, func.shape[0] - 1, 2):
1460 for j
in range(2, func.shape[1] - 1, 2):
1461 resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1462 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1463 func[i, j] += omega*resid[i, j]*.25
1464 for i
in range(2, func.shape[0] - 1, 2):
1465 for j
in range(1, func.shape[1] - 1, 2):
1466 resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1467 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1468 func[i, j] += omega*resid[i, j]*.25
1469 outError = np.sum(np.abs(resid))
1470 if outError < inError*eLevel:
1473 omega = 1.0/(1 - rhoSpe*rhoSpe/2.0)
1475 omega = 1.0/(1 - rhoSpe*rhoSpe*omega/4.0)
1478 if nIter >= maxIter*2:
1479 self.log.
warn(
"Failure: SuccessiveOverRelaxation did not converge in %s iterations."
1480 "\noutError: %s, inError: %s," % (nIter//2, outError, inError*eLevel))
1482 self.log.
info(
"Success: SuccessiveOverRelaxation converged in %s iterations."
1483 "\noutError: %s, inError: %s", nIter//2, outError, inError*eLevel)
1484 return func[1: -1, 1: -1]
1487 def _tileArray(in_array):
1488 """Given an input quarter-image, tile/mirror it and return full image.
1490 Given a square input of side-length n, of the form
1492 input = array([[1, 2, 3],
1496 return an array of size 2n-1 as
1498 output = array([[ 9, 8, 7, 8, 9],
1507 The square input quarter-array
1512 The full, tiled array
1514 assert(in_array.shape[0] == in_array.shape[1])
1515 length = in_array.shape[0] - 1
1516 output = np.zeros((2*length + 1, 2*length + 1))
1518 for i
in range(length + 1):
1519 for j
in range(length + 1):
1520 output[i + length, j + length] = in_array[i, j]
1521 output[-i + length, j + length] = in_array[i, j]
1522 output[i + length, -j + length] = in_array[i, j]
1523 output[-i + length, -j + length] = in_array[i, j]
1527 def _forceZeroSum(inputArray, sumToInfinity):
1528 """Given an array of correlations, adjust the
1529 central value to force the sum of the array to be zero.
1534 The square input array, assumed square and with
1535 shape (2n+1) x (2n+1)
1540 The same array, with the value of the central value
1541 inputArray[n,n] adjusted to force the array sum to be zero.
1543 assert(inputArray.shape[0] == inputArray.shape[1])
1544 assert(inputArray.shape[0] % 2 == 1)
1545 center = int((inputArray.shape[0] - 1) / 2)
1546 outputArray = np.copy(inputArray)
1547 outputArray[center, center] -= inputArray.sum() - sumToInfinity
1551 def _buildCorrelationModel(array, replacementRadius, slope):
1552 """Given an array of correlations, build a model
1553 for correlations beyond replacementRadius pixels from the center
1554 and replace the measured values with the model.
1559 The square input array, assumed square and with
1560 shape (2n+1) x (2n+1)
1565 The same array, with the outer values
1566 replaced with a smoothed model.
1568 assert(array.shape[0] == array.shape[1])
1569 assert(array.shape[0] % 2 == 1)
1570 assert(replacementRadius > 1)
1571 center = int((array.shape[0] - 1) / 2)
1575 if (array[center, center + 1] >= 0.0)
or (array[center + 1, center] >= 0.0):
1578 intercept = (np.log10(-array[center, center + 1]) + np.log10(-array[center + 1, center])) / 2.0
1579 preFactor = 10**intercept
1580 slopeFactor = 2.0*
abs(slope) - 2.0
1581 sumToInfinity = 2.0*np.pi*preFactor / (slopeFactor*(float(center)+0.5)**slopeFactor)
1586 for i
in range(array.shape[0]):
1587 for j
in range(array.shape[1]):
1588 r2 = float((i-center)*(i-center) + (j-center)*(j-center))
1589 if abs(i-center) < replacementRadius
and abs(j-center) < replacementRadius:
1592 newCvalue = -preFactor * r2**slope
1593 array[i, j] = newCvalue
1594 return sumToInfinity
1597 def _convertImagelikeToFloatImage(imagelikeObject):
1598 """Turn an exposure or masked image of any type into an ImageF."""
1599 for attr
in (
"getMaskedImage",
"getImage"):
1600 if hasattr(imagelikeObject, attr):
1601 imagelikeObject = getattr(imagelikeObject, attr)()
1603 floatImage = imagelikeObject.convertF()
1604 except AttributeError:
1605 raise RuntimeError(
"Failed to convert image to float")
1609 def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False,
1610 correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10, logger=None):
1611 """Calculate the bias induced when sigma-clipping non-Gaussian distributions.
1613 Fill image-pairs of the specified size with Poisson-distributed values,
1614 adding correlations as necessary. Then calculate the cross correlation,
1615 and calculate the bias induced using the cross-correlation image
1616 and the image means.
1620 fluxLevels : `list` of `int`
1621 The mean flux levels at which to simulate.
1622 Nominal values might be something like [70000, 90000, 110000]
1623 imageShape : `tuple` of `int`
1624 The shape of the image array to simulate, nx by ny pixels.
1625 repeats : `int`, optional
1626 Number of repeats to perform so that results
1627 can be averaged to improve SNR.
1628 seed : `int`, optional
1629 The random seed to use for the Poisson points.
1630 addCorrelations : `bool`, optional
1631 Whether to add brighter-fatter-like correlations to the simulated images
1632 If true, a correlation between x_{i,j} and x_{i+1,j+1} is introduced
1633 by adding a*x_{i,j} to x_{i+1,j+1}
1634 correlationStrength : `float`, optional
1635 The strength of the correlations.
1636 This is the value of the coefficient `a` in the above definition.
1637 maxLag : `int`, optional
1638 The maximum lag to work to in pixels
1639 nSigmaClip : `float`, optional
1640 Number of sigma to clip to when calculating the sigma-clipped mean.
1641 border : `int`, optional
1642 Number of border pixels to mask
1643 logger : `lsst.log.Log`, optional
1644 Logger to use. Instantiated anew if not provided.
1648 biases : `dict` [`float`, `list` of `float`]
1649 A dictionary, keyed by flux level, containing a list of the biases
1650 for each repeat at that flux level
1651 means : `dict` [`float`, `list` of `float`]
1652 A dictionary, keyed by flux level, containing a list of the average
1653 mean fluxes (average of the mean of the two images)
1654 for the image pairs at that flux level
1655 xcorrs : `dict` [`float`, `list` of `np.ndarray`]
1656 A dictionary, keyed by flux level, containing a list of the xcorr
1657 images for the image pairs at that flux level
1660 logger = lsstLog.Log.getDefaultLogger()
1662 means = {f: []
for f
in fluxLevels}
1663 xcorrs = {f: []
for f
in fluxLevels}
1664 biases = {f: []
for f
in fluxLevels}
1667 config.isrMandatorySteps = []
1668 config.isrForbiddenSteps = []
1669 config.nSigmaClipXCorr = nSigmaClip
1670 config.nPixBorderXCorr = border
1671 config.maxLag = maxLag
1674 im0 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1675 im1 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1677 random = np.random.RandomState(seed)
1679 for rep
in range(repeats):
1680 for flux
in fluxLevels:
1681 data0 = random.poisson(flux, (imageShape)).astype(float)
1682 data1 = random.poisson(flux, (imageShape)).astype(float)
1684 data0[1:, 1:] += correlationStrength*data0[: -1, : -1]
1685 data1[1:, 1:] += correlationStrength*data1[: -1, : -1]
1686 im0.image.array[:, :] = data0
1687 im1.image.array[:, :] = data1
1689 _xcorr, _means = task._crossCorrelate(im0, im1, runningBiasCorrSim=
True)
1691 means[flux].
append(_means)
1692 xcorrs[flux].
append(_xcorr)
1694 bias = xcorrs[flux][-1][1, 1]/means[flux][-1]*(1 + correlationStrength)/correlationStrength
1695 msg = f
"Simulated/expected avg. flux: {flux:.1f}, {(means[flux][-1]/2):.1f}"
1697 logger.info(f
"Bias: {bias:.6f}")
1699 bias = xcorrs[flux][-1][0, 0]/means[flux][-1]
1700 msg = f
"Simulated/expected avg. flux: {flux:.1f}, {(means[flux][-1]/2):.1f}"
1702 logger.info(f
"Bias: {bias:.6f}")
1703 biases[flux].
append(bias)
1705 return biases, means, xcorrs