23 """Calculation of brighter-fatter effect correlations and kernels.""" 25 __all__ = [
'MakeBrighterFatterKernelTaskConfig',
26 'MakeBrighterFatterKernelTask',
30 from scipy
import stats
32 import matplotlib.pyplot
as plt
34 from mpl_toolkits.mplot3d
import axes3d
43 from .utils
import PairedVisitTaskRunner
47 """Config class for bright-fatter effect coefficient calculation.""" 49 isr = pexConfig.ConfigurableField(
51 doc=
"""Task to perform instrumental signature removal""",
53 isrMandatorySteps = pexConfig.ListField(
55 doc=
"isr operations that must be performed for valid results. Raises if any of these are False",
56 default=[
'doAssembleCcd']
58 isrForbiddenSteps = pexConfig.ListField(
60 doc=
"isr operations that must NOT be performed for valid results. Raises if any of these are True",
61 default=[
'doFlat',
'doFringe',
'doAddDistortionModel',
'doBrighterFatter',
'doUseOpticsTransmission',
62 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
64 isrDesirableSteps = pexConfig.ListField(
66 doc=
"isr operations that it is advisable to perform, but are not mission-critical." +
67 " WARNs are logged for any of these found to be False.",
68 default=[
'doBias',
'doDark',
'doCrosstalk',
'doDefect',
'doLinearize']
70 doCalcGains = pexConfig.Field(
72 doc=
"Measure the per-amplifier gains (using the photon transfer curve method)?",
75 ccdKey = pexConfig.Field(
77 doc=
"The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'",
80 maxIterRegression = pexConfig.Field(
82 doc=
"Maximum number of iterations for the regression fitter",
85 nSigmaClipGainCalc = pexConfig.Field(
87 doc=
"Number of sigma to clip the pixel value distribution to during gain calculation",
90 nSigmaClipRegression = pexConfig.Field(
92 doc=
"Number of sigma to clip outliers from the line of best fit to during iterative regression",
95 xcorrCheckRejectLevel = pexConfig.Field(
97 doc=
"Sanity check level for the sum of the input cross-correlations. Arrays which " +
98 "sum to greater than this are discarded before the clipped mean is calculated.",
101 maxIterSuccessiveOverRelaxation = pexConfig.Field(
103 doc=
"The maximum number of iterations allowed for the successive over-relaxation method",
106 eLevelSuccessiveOverRelaxation = pexConfig.Field(
108 doc=
"The target residual error for the successive over-relaxation method",
111 nSigmaClipKernelGen = pexConfig.Field(
113 doc=
"Number of sigma to clip to during pixel-wise clipping when generating the kernel. See " +
114 "the generateKernel docstring for more info.",
117 nSigmaClipXCorr = pexConfig.Field(
119 doc=
"Number of sigma to clip when calculating means for the cross-correlation",
122 maxLag = pexConfig.Field(
124 doc=
"The maximum lag (in pixels) to use when calculating the cross-correlation/kernel",
127 nPixBorderGainCalc = pexConfig.Field(
129 doc=
"The number of border pixels to exclude when calculating the gain",
132 nPixBorderXCorr = pexConfig.Field(
134 doc=
"The number of border pixels to exclude when calculating the cross-correlation and kernel",
137 biasCorr = pexConfig.Field(
139 doc=
"An empirically determined correction factor, used to correct for the sigma-clipping of" +
140 " a non-Gaussian distribution. Post DM-15277, code will exist here to calulate appropriate values",
143 backgroundBinSize = pexConfig.Field(
145 doc=
"Size of the background bins",
148 fixPtcThroughOrigin = pexConfig.Field(
150 doc=
"Constrain the fit of the photon transfer curve to go through the origin when measuring" +
154 level = pexConfig.ChoiceField(
155 doc=
"The level at which to calculate the brighter-fatter kernels",
156 dtype=str, default=
"DETECTOR",
158 "AMP":
"Every amplifier treated separately",
159 "DETECTOR":
"One kernel per detector",
162 backgroundWarnLevel = pexConfig.Field(
164 doc=
"Log warnings if the mean of the fitted background is found to be above this level after " +
165 "differencing image pair.",
171 """A DataIdContainer for the MakeBrighterFatterKernelTask.""" 174 """Compute refList based on idList. 176 This method must be defined as the dataset does not exist before this 182 Results of parsing the command-line. 186 Not called if ``add_id_argument`` called 187 with ``doMakeDataRefList=False``. 188 Note that this is almost a copy-and-paste of the vanilla implementation, 189 but without checking if the datasets already exist, 190 as this task exists to make them. 192 if self.datasetType
is None:
193 raise RuntimeError(
"Must call setDatasetType first")
194 butler = namespace.butler
195 for dataId
in self.idList:
196 refList =
list(butler.subset(datasetType=self.datasetType, level=self.level, dataId=dataId))
200 namespace.log.warn(
"No data found for dataId=%s", dataId)
202 self.refList += refList
206 """A (very) simple class to hold the kernel(s) generated. 208 The kernel.kernel is a dictionary holding the kernels themselves. 209 One kernel if the level is 'DETECTOR' or, 210 nAmps in length, if level is 'AMP'. 211 The dict is keyed by either the detector ID or the amplifier IDs. 213 The level is the level for which the kernel(s) were generated so that one 214 can know how to access the kernels without having to query the shape of 215 the dictionary holding the kernel(s). 219 assert type(level) == str
220 assert type(kernelDict) == dict
221 if level ==
'DETECTOR':
222 assert len(kernelDict.keys()) == 1
224 assert len(kernelDict.keys()) > 1
231 """Brighter-fatter effect correction-kernel calculation task. 233 A command line task for calculating the brighter-fatter correction 234 kernel from pairs of flat-field images (with the same exposure length). 236 The following operations are performed: 238 - The configurable isr task is called, which unpersists and assembles the 239 raw images, and performs the selected instrument signature removal tasks. 240 For the purpose of brighter-fatter coefficient calculation is it 241 essential that certain components of isr are *not* performed, and 242 recommended that certain others are. The task checks the selected isr 243 configuration before it is run, and if forbidden components have been 244 selected task will raise, and if recommended ones have not been selected, 247 - The gain of the each amplifier in the detector is calculated using 248 the photon transfer curve (PTC) method and used to correct the images 249 so that all calculations are done in units of electrons, and so that the 250 level across amplifier boundaries is continuous. 251 Outliers in the PTC are iteratively rejected 252 before fitting, with the nSigma rejection level set by 253 config.nSigmaClipRegression. Individual pixels are ignored in the input 254 images the image based on config.nSigmaClipGainCalc. 256 - Each image is then cross-correlated with the one it's paired with 257 (with the pairing defined by the --visit-pairs command line argument), 258 which is done either the whole-image to whole-image, 259 or amplifier-by-amplifier, depending on config.level. 261 - Once the cross-correlations have been calculated for each visit pair, 262 these are used to generate the correction kernel. 263 The maximum lag used, in pixels, and hence the size of the half-size 264 of the kernel generated, is given by config.maxLag, 265 i.e. a value of 10 will result in a kernel of size 2n-1 = 19x19 pixels. 266 Outlier values in these cross-correlations are rejected by using a 267 pixel-wise sigma-clipped thresholding to each cross-correlation in 268 the visit-pairs-length stack of cross-correlations. 269 The number of sigma clipped to is set by config.nSigmaClipKernelGen. 271 - Once DM-15277 has been completed, a method will exist to calculate the 272 empirical correction factor, config.biasCorr. 273 TODO: DM-15277 update this part of the docstring once the ticket is done. 276 RunnerClass = PairedVisitTaskRunner
277 ConfigClass = MakeBrighterFatterKernelTaskConfig
278 _DefaultName =
"makeBrighterFatterKernel" 281 pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
282 self.makeSubtask(
"isr")
285 if self.
debug.enabled:
286 self.log.
info(
"Running with debug enabled...")
291 if self.
debug.display:
293 afwDisp.setDefaultBackend(self.
debug.displayBackend)
294 afwDisp.Display.delAllDisplays()
295 self.
disp1 = afwDisp.Display(0, open=
True)
296 self.
disp2 = afwDisp.Display(1, open=
True)
298 im = afwImage.ImageF(1, 1)
303 self.
debug.display =
False 304 self.log.
warn(
'Failed to setup/connect to display! Debug display has been disabled')
306 plt.interactive(
False)
312 def _makeArgumentParser(cls):
313 """Augment argument parser for the MakeBrighterFatterKernelTask.""" 315 parser.add_argument(
"--visit-pairs", dest=
"visitPairs", nargs=
"*",
316 help=
"Visit pairs to use. Each pair must be of the form INT,INT e.g. 123,456")
317 parser.add_id_argument(
"--id", datasetType=
"brighterFatterKernel",
318 ContainerClass=BrighterFatterKernelTaskDataIdContainer,
319 help=
"The ccds to use, e.g. --id ccd=0..100")
323 """Check that appropriate ISR settings are being used 324 for brighter-fatter kernel calculation.""" 334 configDict = self.config.isr.toDict()
336 for configParam
in self.config.isrMandatorySteps:
337 if configDict[configParam]
is False:
338 raise RuntimeError(
'Must set config.isr.%s to True ' 339 'for brighter-fatter kernel calulation' % configParam)
341 for configParam
in self.config.isrForbiddenSteps:
342 if configDict[configParam]
is True:
343 raise RuntimeError(
'Must set config.isr.%s to False ' 344 'for brighter-fatter kernel calulation' % configParam)
346 for configParam
in self.config.isrDesirableSteps:
347 if configParam
not in configDict:
348 self.log.
info(
'Failed to find key %s in the isr config dict. You probably want ' +
349 'to set the equivalent for your obs_package to True.' % configParam)
351 if configDict[configParam]
is False:
352 self.log.
warn(
'Found config.isr.%s set to False for brighter-fatter kernel calulation. ' 353 'It is probably desirable to have this set to True' % configParam)
356 if not self.config.isr.assembleCcd.doTrim:
357 raise RuntimeError(
'Must trim when assembling CCDs. Set config.isr.assembleCcd.doTrim to True')
361 """Run the brighter-fatter measurement task. 363 For a dataRef (which is each detector here), 364 and given a list of visit pairs, calulate the 365 brighter-fatter kernel for the detector. 369 dataRef : list of lsst.daf.persistence.ButlerDataRef 370 dataRef for the detector for the visits to be fit. 371 visitPairs : `iterable` of `tuple` of `int` 372 Pairs of visit numbers to be processed together 379 detNum = dataRef.dataId[self.config.ccdKey]
380 if self.config.level ==
'DETECTOR':
381 xcorrs = {detNum: []}
383 elif self.config.level ==
'AMP':
389 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
390 ampInfoCat = detector.getAmpInfoCatalog()
391 ampNames = [amp.getName()
for amp
in ampInfoCat]
392 xcorrs = {key: []
for key
in ampNames}
393 means = {key: []
for key
in ampNames}
395 raise RuntimeError(
"Unsupported level: {}".
format(self.config.level))
398 if self.config.doCalcGains:
399 self.log.
info(
'Compute gains for detector %s' % detNum)
401 dataRef.put(gains, datasetType=
'brighterFatterGain')
402 self.log.
debug(
'Finished gain estimation for detector %s' % detNum)
404 gains = dataRef.get(
'brighterFatterGain')
406 raise RuntimeError(
'Failed to retrieved gains for detector %s' % detNum)
407 self.log.
info(
'Retrieved stored gain for detector %s' % detNum)
408 self.log.
debug(
'Detector %s has gains %s' % (detNum, gains))
412 for (v1, v2)
in visitPairs:
414 dataRef.dataId[
'visit'] = v1
416 dataRef.dataId[
'visit'] = v2
418 del dataRef.dataId[
'visit']
421 self.log.
info(
'Preparing images for cross-correlation calculation for detector %s' % detNum)
431 for det_object
in _scaledMaskedIms1.keys():
433 _scaledMaskedIms2[det_object])
434 xcorrs[det_object].
append(_xcorr)
435 means[det_object].
append([_means1[det_object], _means2[det_object]])
441 self.log.
info(
'Generating kernel(s) for %s' % detNum)
442 for det_object
in xcorrs.keys():
443 if self.config.level ==
'DETECTOR':
444 objId =
'detector %s' % det_object
445 elif self.config.level ==
'AMP':
446 objId =
'detector %s AMP %s' % (detNum, det_object)
447 kernels[det_object] = self.
generateKernel(xcorrs[det_object], means[det_object], objId)
450 self.log.
info(
'Finished generating kernel(s) for %s' % detNum)
451 return pipeBase.Struct(exitStatus=0)
453 def _makeCroppedExposures(self, exp, gains, level):
454 """Prepare exposure for cross-correlation calculation. 456 For each amp, crop by the border amount, specified by 457 config.nPixBorderXCorr, then rescale by the gain 458 and subtract the sigma-clipped mean. 459 If the level is 'DETECTOR' then this is done 460 to the whole image so that it can be cross-correlated, with a copy 462 If the level is 'AMP' then this is done per-amplifier, 463 and a copy of each prepared amp-image returned. 467 exp : `lsst.afw.image.exposure.ExposureF` 468 The exposure to prepare 469 gains : `dict` of `float` 470 Dictionary of the amplifier gain values, keyed by amplifier name 472 Either `AMP` or `DETECTOR` 476 scaledMaskedIms : `dict` of `lsst.afw.image.maskedImage.MaskedImageF` 477 Depending on level, this is either one item, or n_amp items, 478 keyed by detectorId or ampName 482 This function is controlled by the following config parameters: 483 nPixBorderXCorr : `int` 484 The number of border pixels to exclude 485 nSigmaClipXCorr : `float` 486 The number of sigma to be clipped to 488 assert(isinstance(exp, afwImage.ExposureF))
490 local_exp = exp.clone()
493 border = self.config.nPixBorderXCorr
494 sigma = self.config.nSigmaClipXCorr
497 sctrl.setNumSigmaClip(sigma)
502 detector = local_exp.getDetector()
503 ampInfoCat = detector.getAmpInfoCatalog()
505 mi = local_exp.getMaskedImage()
510 for amp
in ampInfoCat:
511 ampName = amp.getName()
512 rescaleIm = mi[amp.getBBox()]
513 rescaleTemp = temp[amp.getBBox()]
515 gain = gains[ampName]
518 self.log.
debug(
"mean*gain = %s, clipped mean = %s" %
521 rescaleIm -= mean*gain
525 afwImage.LOCAL], afwMath.MEANCLIP, sctrl).getValue()
526 returnAreas[ampName] = rescaleIm
528 if level ==
'DETECTOR':
529 detName = local_exp.getDetector().getId()
531 afwMath.MEANCLIP, sctrl).getValue()
532 returnAreas[detName] = rescaleIm
534 return returnAreas, means
536 def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None):
537 """Calculate the cross-correlation of an area. 539 If the area in question contains multiple amplifiers then they must 540 have been gain corrected. 544 maskedIm0 : `lsst.afw.image.MaskedImageF` 546 maskedIm1 : `lsst.afw.image.MaskedImageF` 548 frameId : `str`, optional 549 The frame identifier for use in the filename 550 if writing debug outputs. 551 detId : `str`, optional 552 The detector identifier (detector, or detector+amp, 553 depending on config.level) for use in the filename 554 if writing debug outputs. 555 runningBiasCorrSim : `bool` 556 Set to true when using this function to calculate the amount of bias 557 introduced by the sigma clipping. If False, the biasCorr parameter 558 is divided by to remove the bias, but this is, of course, not 559 appropriate when this is the parameter being measured. 564 The quarter-image cross-correlation 566 The sum of the means of the input images, 567 sigma-clipped, and with borders applied. 568 This is used when using this function with simulations to calculate 569 the biasCorr parameter. 573 This function is controlled by the following config parameters: 575 The maximum lag to use in the cross-correlation calculation 576 nPixBorderXCorr : `int` 577 The number of border pixels to exclude 578 nSigmaClipXCorr : `float` 579 The number of sigma to be clipped to 581 Parameter used to correct from the bias introduced 584 maxLag = self.config.maxLag
585 border = self.config.nPixBorderXCorr
586 sigma = self.config.nSigmaClipXCorr
587 biasCorr = self.config.biasCorr
590 sctrl.setNumSigmaClip(sigma)
593 afwMath.MEANCLIP, sctrl).getValue()
595 afwMath.MEANCLIP, sctrl).getValue()
598 diff = maskedIm0.clone()
599 diff -= maskedIm1.getImage()
600 diff = diff[border: -border, border: -border, afwImage.LOCAL]
602 if self.
debug.writeDiffImages:
603 filename =
'_'.join([
'diff',
'detector', detId, frameId,
'.fits'])
604 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
607 binsize = self.config.backgroundBinSize
608 nx = diff.getWidth()//binsize
609 ny = diff.getHeight()//binsize
612 bgImg = bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE, afwMath.REDUCE_INTERP_ORDER)
613 bgMean = np.mean(bgImg.getArray())
614 if abs(bgMean) >= self.config.backgroundWarnLevel:
615 self.log.
warn(
'Mean of background = %s > config.maxBackground' % bgMean)
619 if self.
debug.writeDiffImages:
620 filename =
'_'.join([
'bgSub',
'diff',
'detector', detId, frameId,
'.fits'])
621 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
622 if self.
debug.display:
625 self.log.
debug(
"Median and variance of diff:")
628 sctrl).getValue(), np.var(diff.getImage().getArray()))
631 dim0 = diff[0: -maxLag, : -maxLag, afwImage.LOCAL]
633 width, height = dim0.getDimensions()
634 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
636 for xlag
in range(maxLag + 1):
637 for ylag
in range(maxLag + 1):
638 dim_xy = diff[xlag:xlag + width, ylag: ylag + height, afwImage.LOCAL].
clone()
642 if not runningBiasCorrSim:
643 xcorr[xlag, ylag] /= biasCorr
651 """Estimate the amplifier gains using the specified visits. 653 Given a dataRef and list of flats of varying intensity, 654 calculate the gain for each amplifier in the detector 655 using the photon transfer curve (PTC) method. 657 The config.fixPtcThroughOrigin option determines whether the iterative 658 fitting is forced to go through the origin or not. 659 This defaults to True, fitting var=1/gain * mean. 660 If set to False then var=1/g * mean + const is fitted. 662 This is really a photo transfer curve (PTC) gain measurement task. 663 See DM-14063 for results from of a comparison between 664 this task's numbers and the gain values in the HSC camera model, 665 and those measured by the PTC task in eotest. 669 dataRef : `lsst.daf.persistence.butler.Butler.dataRef` 670 dataRef for the detector for the flats to be used 671 visitPairs : `list` of `tuple` 672 List of visit-pairs to use, as [(v1,v2), (v3,v4)...] 676 gains : `dict` of `float` 677 Dict of the as-calculated amplifier gain values, 678 keyed by amplifier name 679 nominalGains : `dict` of `float` 680 Dict of the amplifier gains, as reported by the `detector` object, 681 keyed by amplifier name 684 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
685 ampInfoCat = detector.getAmpInfoCatalog()
686 ampNames = [amp.getName()
for amp
in ampInfoCat]
688 ampMeans = {key: []
for key
in ampNames}
689 ampCoVariances = {key: []
for key
in ampNames}
690 ampVariances = {key: []
for key
in ampNames}
696 for visPairNum, visPair
in enumerate(visitPairs):
702 ampName = amp.getName()
703 if _means[ampName]*10 < _vars[ampName]
or _means[ampName]*10 < _covars[ampName]:
704 msg =
'Sanity check failed; check visit pair %s amp %s' % (visPair, ampName)
712 for k
in _means.keys():
713 if _vars[k]*1.3 < _covars[k]
or _vars[k]*0.7 > _covars[k]:
714 self.log.
warn(
'Dropped a value')
716 ampMeans[k].
append(_means[k])
717 ampVariances[k].
append(_vars[k])
718 ampCoVariances[k].
append(_covars[k])
723 ampName = amp.getName()
724 nomGains[ampName] = amp.getGain()
725 slopeRaw, interceptRaw, rVal, pVal, stdErr = \
726 stats.linregress(np.asarray(ampMeans[ampName]), np.asarray(ampCoVariances[ampName]))
728 np.asarray(ampCoVariances[ampName]),
729 fixThroughOrigin=
True)
731 np.asarray(ampCoVariances[ampName]),
732 fixThroughOrigin=
False)
733 self.log.
info(
"Slope of raw fit: %s, intercept: %s p value: %s" % (slopeRaw,
735 self.log.
info(
"slope of fixed fit: %s, difference vs raw:%s" % (slopeFix,
736 slopeFix - slopeRaw))
737 self.log.
info(
"slope of unfixed fit: %s, difference vs fix:%s" % (slopeUnfix,
738 slopeFix - slopeUnfix))
739 if self.config.fixPtcThroughOrigin:
740 slopeToUse = slopeFix
742 slopeToUse = slopeUnfix
744 if self.
debug.enabled:
746 ax = fig.add_subplot(111)
747 ax.plot(np.asarray(ampMeans[ampName]),
748 np.asarray(ampCoVariances[ampName]), linestyle=
'None', marker=
'x', label=
'data')
749 if self.config.fixPtcThroughOrigin:
750 ax.plot(np.asarray(ampMeans[ampName]),
751 np.asarray(ampMeans[ampName])*slopeToUse, label=
'Fit through origin')
753 ax.plot(np.asarray(ampMeans[ampName]),
754 np.asarray(ampMeans[ampName])*slopeToUse + intercept,
755 label=
'Fit (intercept unconstrained')
757 dataRef.put(fig,
"plotBrighterFatterPtc", amp=ampName)
758 self.log.
info(
'Saved PTC for detector %s amp %s' % (detector.getId(), ampName))
759 gains[ampName] = 1.0/slopeToUse
760 return gains, nomGains
763 def _checkExpLengthEqual(exp1, exp2, v1=None, v2=None):
764 """Check the exposure lengths of two exposures are equal. 768 exp1 : `lsst.afw.image.exposure.ExposureF` 769 First exposure to check 770 exp2 : `lsst.afw.image.exposure.ExposureF` 771 Second exposure to check 772 v1 : `int` or `str`, optional 773 First visit of the visit pair 774 v2 : `int` or `str`, optional 775 Second visit of the visit pair 780 Raised if the exposure lengths of the two exposures are not equal 782 expTime1 = exp1.getInfo().getVisitInfo().getExposureTime()
783 expTime2 = exp2.getInfo().getVisitInfo().getExposureTime()
784 if expTime1 != expTime2:
785 msg =
"Exposure lengths for visit pairs must be equal. " + \
786 "Found %s and %s" % (expTime1, expTime2)
788 msg +=
" for visit pair %s, %s" % (v1, v2)
789 raise RuntimeError(msg)
791 def _calcMeansAndVars(self, dataRef, v1, v2):
792 """Calculate the means, vars, covars, and retrieve the nominal gains, 793 for each amp in each detector. 795 This code runs using two visit numbers, and for the detector specified. 796 It calculates the correlations in the individual amps without 797 rescaling any gains. This allows a photon transfer curve 798 to be generated and the gains measured. 800 Images are assembled with use the isrTask, and basic isr is performed. 804 dataRef : `lsst.daf.persistence.butler.Butler.dataRef` 805 dataRef for the detector for the repo containg the flats to be used 807 First visit of the visit pair 809 Second visit of the visit pair 813 means, vars, covars : `tuple` of `dicts` 814 Three dicts, keyed by ampName, 815 containing the sum of the image-means, 816 the variance, and the quarter-image of the xcorr. 818 sigma = self.config.nSigmaClipGainCalc
819 maxLag = self.config.maxLag
820 border = self.config.nPixBorderGainCalc
821 biasCorr = self.config.biasCorr
824 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
830 originalDataId = dataRef.dataId.copy()
831 dataRef.dataId[
'visit'] = v1
833 dataRef.dataId[
'visit'] = v2
835 dataRef.dataId = originalDataId
839 detector = exps[0].getDetector()
842 if self.
debug.display:
847 sctrl.setNumSigmaClip(sigma)
848 for imNum, im
in enumerate(ims):
855 ampName = amp.getName()
856 ampIm = im[amp.getBBox()]
858 afwMath.MEANCLIP, sctrl).getValue()
859 if ampName
not in ampMeans.keys():
860 ampMeans[ampName] = []
861 ampMeans[ampName].
append(mean)
864 diff = ims[0].
clone()
867 temp = diff[border: -border, border: -border, afwImage.LOCAL]
872 binsize = self.config.backgroundBinSize
873 nx = temp.getWidth()//binsize
874 ny = temp.getHeight()//binsize
880 diff[box, afwImage.LOCAL] -= bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE,
881 afwMath.REDUCE_INTERP_ORDER)
886 ampName = amp.getName()
888 diffAmpIm = diff[amp.getBBox()].
clone()
889 diffAmpImCrop = diffAmpIm[border: -border - maxLag, border: -border - maxLag, afwImage.LOCAL]
891 w, h = diffAmpImCrop.getDimensions()
892 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
895 for xlag
in range(maxLag + 1):
896 for ylag
in range(maxLag + 1):
897 dim_xy = diffAmpIm[border + xlag: border + xlag + w,
898 border + ylag: border + ylag + h,
899 afwImage.LOCAL].
clone()
901 dim_xy *= diffAmpImCrop
903 afwMath.MEANCLIP, sctrl).getValue()/(biasCorr)
905 variances[ampName] = xcorr[0, 0]
907 coVars[ampName] = np.sum(xcorr_full)
909 msg =
"M1: " +
str(ampMeans[ampName][0])
910 msg +=
" M2 " +
str(ampMeans[ampName][1])
911 msg +=
" M_sum: " +
str((ampMeans[ampName][0]) + ampMeans[ampName][1])
912 msg +=
" Var " +
str(variances[ampName])
913 msg +=
" coVar: " +
str(coVars[ampName])
918 ampName = amp.getName()
919 means[ampName] = ampMeans[ampName][0] + ampMeans[ampName][1]
921 return means, variances, coVars
923 def _plotXcorr(self, xcorr, mean, zmax=0.05, title=None, fig=None, saveToFileName=None):
924 """Plot the correlation functions.""" 926 xcorr = xcorr.getArray()
938 ax = fig.add_subplot(111, projection=
'3d')
942 nx, ny = np.shape(xcorr)
944 xpos, ypos = np.meshgrid(np.arange(nx), np.arange(ny))
945 xpos = xpos.flatten()
946 ypos = ypos.flatten()
947 zpos = np.zeros(nx*ny)
951 ax.bar3d(xpos, ypos, zpos, 1, 1, dz, color=
'b', zsort=
'max', sort_zpos=100)
952 if xcorr[0, 0] > zmax:
953 ax.bar3d([0], [0], [zmax], 1, 1, 1e-4, color=
'c')
956 ax.set_ylabel(
"column")
957 ax.set_zlabel(
r"$\langle{(F_i - \bar{F})(F_i - \bar{F})}\rangle/\bar{F}$")
962 fig.savefig(saveToFileName)
964 def _iterativeRegression(self, x, y, fixThroughOrigin=False, nSigmaClip=None, maxIter=None):
965 """Use linear regression to fit a line, iteratively removing outliers. 967 Useful when you have a sufficiently large numbers of points on your PTC. 968 This function iterates until either there are no outliers of 969 config.nSigmaClip magnitude, or until the specified maximum number 970 of iterations has been performed. 975 The independent variable. Must be a numpy array, not a list. 977 The dependent variable. Must be a numpy array, not a list. 978 fixThroughOrigin : `bool`, optional 979 Whether to fix the PTC through the origin or allow an y-intercept. 980 nSigmaClip : `float`, optional 981 The number of sigma to clip to. 982 Taken from the task config if not specified. 983 maxIter : `int`, optional 984 The maximum number of iterations allowed. 985 Taken from the task config if not specified. 990 The slope of the line of best fit 992 The y-intercept of the line of best fit 995 maxIter = self.config.maxIterRegression
997 nSigmaClip = self.config.nSigmaClipRegression
1001 sctrl.setNumSigmaClip(nSigmaClip)
1003 if fixThroughOrigin:
1004 while nIter < maxIter:
1006 self.log.
debug(
"Origin fixed, iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1007 TEST = x[:, np.newaxis]
1008 slope, _, _, _ = np.linalg.lstsq(TEST, y)
1013 index = np.where((res > (resMean + nSigmaClip*resStd)) |
1014 (res < (resMean - nSigmaClip*resStd)))
1015 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1016 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1018 x = np.delete(x, index)
1019 y = np.delete(y, index)
1023 while nIter < maxIter:
1025 self.log.
debug(
"Iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1026 xx = np.vstack([x, np.ones(len(x))]).T
1027 ret, _, _, _ = np.linalg.lstsq(xx, y)
1028 slope, intercept = ret
1029 res = y - slope*x - intercept
1032 index = np.where((res > (resMean + nSigmaClip * resStd)) | (res < resMean - nSigmaClip * resStd))
1033 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1034 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1036 x = np.delete(x, index)
1037 y = np.delete(y, index)
1039 return slope, intercept
1042 """Generate the full kernel from a list of cross-correlations and means. 1044 Taking a list of quarter-image, gain-corrected cross-correlations, 1045 do a pixel-wise sigma-clipped mean of each, 1046 and tile into the full-sized kernel image. 1048 Each corr in corrs is one quarter of the full cross-correlation, 1049 and has been gain-corrected. Each mean in means is a tuple of the means 1050 of the two individual images, corresponding to that corr. 1054 corrs : `list` of `numpy.ndarray`, (Ny, Nx) 1055 A list of the quarter-image cross-correlations 1056 means : `dict` of `tuples` of `floats` 1057 The means of the input images for each corr in corrs 1058 rejectLevel : `float`, optional 1059 This is essentially is a sanity check parameter. 1060 If this condition is violated there is something unexpected 1061 going on in the image, and it is discarded from the stack before 1062 the clipped-mean is calculated. 1063 If not provided then config.xcorrCheckRejectLevel is used 1067 kernel : `numpy.ndarray`, (Ny, Nx) 1071 rejectLevel = self.config.xcorrCheckRejectLevel
1078 sctrl.setNumSigmaClip(self.config.nSigmaClipKernelGen)
1080 for corrNum, ((mean1, mean2), corr)
in enumerate(zip(means, corrs)):
1081 corr[0, 0] -= (mean1 + mean2)
1083 self.log.
warn(
'Skipped item %s due to unexpected value of (variance-mean)' % corrNum)
1085 corr /= -1.0*(mean1**2 + mean2**2)
1089 xcorrCheck = np.abs(np.sum(fullCorr))/np.sum(np.abs(fullCorr))
1090 if xcorrCheck > rejectLevel:
1091 self.log.
warn(
"Sum of the xcorr is unexpectedly high. Investigate item num %s for %s. \n" 1092 "value = %s" % (corrNum, objId, xcorrCheck))
1094 xcorrList.append(fullCorr)
1097 raise RuntimeError(
"Cannot generate kernel because all inputs were discarded. " 1098 "Either the data is bad, or config.xcorrCheckRejectLevel is too low")
1101 meanXcorr = np.zeros_like(fullCorr)
1102 xcorrList = np.transpose(xcorrList)
1103 for i
in range(np.shape(meanXcorr)[0]):
1104 for j
in range(np.shape(meanXcorr)[1]):
1110 """An implementation of the successive over relaxation (SOR) method. 1112 A numerical method for solving a system of linear equations 1113 with faster convergence than the Gauss-Seidel method. 1117 source : `numpy.ndarray` 1119 maxIter : `int`, optional 1120 Maximum number of iterations to attempt before aborting 1121 eLevel : `float`, optional 1122 The target error level at which we deem convergence to have occured 1126 output : `numpy.ndarray` 1130 maxIter = self.config.maxIterSuccessiveOverRelaxation
1132 eLevel = self.config.eLevelSuccessiveOverRelaxation
1134 assert source.shape[0] == source.shape[1],
"Input array must be square" 1136 func = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1137 resid = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1138 rhoSpe = np.cos(np.pi/source.shape[0])
1141 for i
in range(1, func.shape[0] - 1):
1142 for j
in range(1, func.shape[1] - 1):
1143 resid[i, j] = (func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1144 func[i + 1, j] - 4*func[i, j] - source[i - 1, j - 1])
1145 inError = np.sum(np.abs(resid))
1153 while nIter < maxIter*2:
1156 for i
in range(1, func.shape[0] - 1, 2):
1157 for j
in range(1, func.shape[1] - 1, 2):
1158 resid[i, j] =
float(func[i, j-1] + func[i, j + 1] + func[i - 1, j] +
1159 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1160 func[i, j] += omega*resid[i, j]*.25
1161 for i
in range(2, func.shape[0] - 1, 2):
1162 for j
in range(2, func.shape[1] - 1, 2):
1163 resid[i, j] =
float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1164 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1165 func[i, j] += omega*resid[i, j]*.25
1167 for i
in range(1, func.shape[0] - 1, 2):
1168 for j
in range(2, func.shape[1] - 1, 2):
1169 resid[i, j] =
float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1170 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1171 func[i, j] += omega*resid[i, j]*.25
1172 for i
in range(2, func.shape[0] - 1, 2):
1173 for j
in range(1, func.shape[1] - 1, 2):
1174 resid[i, j] =
float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1175 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1176 func[i, j] += omega*resid[i, j]*.25
1177 outError = np.sum(np.abs(resid))
1178 if outError < inError*eLevel:
1181 omega = 1.0/(1 - rhoSpe*rhoSpe/2.0)
1183 omega = 1.0/(1 - rhoSpe*rhoSpe*omega/4.0)
1186 if nIter >= maxIter*2:
1187 self.log.
warn(
"Failure: SuccessiveOverRelaxation did not converge in %s iterations." 1188 "\noutError: %s, inError: %s," % (nIter//2, outError, inError*eLevel))
1190 self.log.
info(
"Success: SuccessiveOverRelaxation converged in %s iterations." 1191 "\noutError: %s, inError: %s", nIter//2, outError, inError*eLevel)
1192 return func[1: -1, 1: -1]
1195 def _tileArray(in_array):
1196 """Given an input quarter-image, tile/mirror it and return full image. 1198 Given a square input of side-length n, of the form 1200 input = array([[1, 2, 3], 1204 return an array of size 2n-1 as 1206 output = array([[ 9, 8, 7, 8, 9], 1215 The square input quarter-array 1220 The full, tiled array 1222 assert(in_array.shape[0] == in_array.shape[1])
1223 length = in_array.shape[0] - 1
1224 output = np.zeros((2*length + 1, 2*length + 1))
1226 for i
in range(length + 1):
1227 for j
in range(length + 1):
1228 output[i + length, j + length] = in_array[i, j]
1229 output[-i + length, j + length] = in_array[i, j]
1230 output[i + length, -j + length] = in_array[i, j]
1231 output[-i + length, -j + length] = in_array[i, j]
1235 def _convertImagelikeToFloatImage(imagelikeObject):
1236 """Turn an exposure or masked image of any type into an ImageF.""" 1237 for attr
in (
"getMaskedImage",
"getImage"):
1238 if hasattr(imagelikeObject, attr):
1239 imagelikeObject = getattr(imagelikeObject, attr)()
1241 floatImage = imagelikeObject.convertF()
1242 except AttributeError:
1243 raise RuntimeError(
"Failed to convert image to float")
1247 def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False,
1248 correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10):
1249 """Calculate the bias induced when sigma-clipping non-Gassian distributions. 1251 Fill image-pairs of the specified size with Poisson-distributed values, 1252 adding correlations as necessary. Then calculate the cross correlation, 1253 and calculate the bias induced using the cross-correlation image 1254 and the image means. 1258 fluxLevels : `list` of `int` 1259 The mean flux levels at which to simiulate. 1260 Nominal values might be something like [70000, 90000, 110000] 1261 imageShape : `tuple` of `int` 1262 The shape of the image array to simulate, nx by ny pixels. 1263 repeats : `int`, optional 1264 Number of repeats to perform so that results 1265 can be averaged to improve SNR. 1266 seed : `int`, optional 1267 The random seed to use for the Poisson points. 1268 addCorrelations : `bool`, optional 1269 Whether to add brighter-fatter-like correlations to the simulated images 1270 If true, a correlation between x_{i,j} and x_{i+1,j+1} is introduced 1271 by adding a*x_{i,j} to x_{i+1,j+1} 1272 correlationStrength : `float`, optional 1273 The strength of the correlations. 1274 This is the value of the coefficient `a` in the above definition. 1275 maxLag : `int`, optional 1276 The maximum lag to work to in pixels 1277 nSigmaClip : `float`, optional 1278 Number of sigma to clip to when calculating the sigma-clipped mean. 1279 border : `int`, optional 1280 Number of border pixels to mask 1284 biases : `dict` of `list` of `float` 1285 A dictionary, keyed by flux level, containing a list of the biases 1286 for each repeat at that flux level 1287 means : `dict` of `list` of `float` 1288 A dictionary, keyed by flux level, containing a list of the average mean 1289 fluxes (average of the mean of the two images) 1290 for the image pairs at that flux level 1291 xcorrs : `dict` of `list` of `np.ndarray` 1292 A dictionary, keyed by flux level, containing a list of the xcorr 1293 images for the image pairs at that flux level 1295 means = {f: []
for f
in fluxLevels}
1296 xcorrs = {f: []
for f
in fluxLevels}
1297 biases = {f: []
for f
in fluxLevels}
1300 config.isrMandatorySteps = []
1301 config.isrForbiddenSteps = []
1302 config.nSigmaClipXCorr = nSigmaClip
1303 config.nPixBorderXCorr = border
1304 config.maxLag = maxLag
1307 im0 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1308 im1 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1310 random = np.random.RandomState(seed)
1312 for rep
in range(repeats):
1313 for flux
in fluxLevels:
1314 data0 = random.poisson(flux, (imageShape)).astype(float)
1315 data1 = random.poisson(flux, (imageShape)).astype(float)
1317 data0[1:, 1:] += correlationStrength*data0[: -1, : -1]
1318 data1[1:, 1:] += correlationStrength*data1[: -1, : -1]
1319 im0.image.array[:, :] = data0
1320 im1.image.array[:, :] = data1
1322 _xcorr, _means = task._crossCorrelate(im0, im1, runningBiasCorrSim=
True)
1324 means[flux].
append(_means)
1325 xcorrs[flux].
append(_xcorr)
1327 bias = xcorrs[flux][-1][1, 1]/means[flux][-1]*(1 + correlationStrength)/correlationStrength
1328 print(
"Simulated/expected avg. flux: %.1f, %.1f" % (flux, means[flux][-1]/2))
1329 print(
"Bias: %.6f" % bias)
1331 bias = xcorrs[flux][-1][0, 0]/means[flux][-1]
1332 print(
"Simulated/expected avg. flux: %.1f, %.1f" % (flux, means[flux][-1]/2))
1333 print(
"Bias: %.6f" % bias)
1334 biases[flux].
append(bias)
1336 return biases, means, xcorrs
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)
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.
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<>
Pass parameters to a Statistics object.
def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None)
def __init__(self, level, kernelDict)
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
def mtv(data, frame=None, title="", wcs=None, args, kwargs)
def generateKernel(self, corrs, means, objId, rejectLevel=None)
def runDataRef(self, dataRef, visitPairs)
def _checkExpLengthEqual(exp1, exp2, v1=None, v2=None)
def _calcMeansAndVars(self, dataRef, v1, v2)
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)
def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False, correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10)