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
46 """Config class for bright-fatter effect coefficient calculation.""" 48 isr = pexConfig.ConfigurableField(
50 doc=
"""Task to perform instrumental signature removal""",
52 isrMandatorySteps = pexConfig.ListField(
54 doc=
"isr operations that must be performed for valid results. Raises if any of these are False",
55 default=[
'doAssembleCcd']
57 isrForbiddenSteps = pexConfig.ListField(
59 doc=
"isr operations that must NOT be performed for valid results. Raises if any of these are True",
60 default=[
'doFlat',
'doFringe',
'doAddDistortionModel',
'doBrighterFatter',
'doUseOpticsTransmission',
61 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
63 isrDesirableSteps = pexConfig.ListField(
65 doc=
"isr operations that it is advisable to perform, but are not mission-critical." +
66 " WARNs are logged for any of these found to be False.",
67 default=[
'doBias',
'doDark',
'doCrosstalk',
'doDefect',
'doLinearize']
69 doCalcGains = pexConfig.Field(
71 doc=
"Measure the per-amplifier gains (using the photon transfer curve method)?",
74 ccdKey = pexConfig.Field(
76 doc=
"The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'",
79 maxIterRegression = pexConfig.Field(
81 doc=
"Maximum number of iterations for the regression fitter",
84 nSigmaClipGainCalc = pexConfig.Field(
86 doc=
"Number of sigma to clip the pixel value distribution to during gain calculation",
89 nSigmaClipRegression = pexConfig.Field(
91 doc=
"Number of sigma to clip outliers from the line of best fit to during iterative regression",
94 xcorrCheckRejectLevel = pexConfig.Field(
96 doc=
"Sanity check level for the sum of the input cross-correlations. Arrays which " +
97 "sum to greater than this are discarded before the clipped mean is calculated.",
100 maxIterSuccessiveOverRelaxation = pexConfig.Field(
102 doc=
"The maximum number of iterations allowed for the successive over-relaxation method",
105 eLevelSuccessiveOverRelaxation = pexConfig.Field(
107 doc=
"The target residual error for the successive over-relaxation method",
110 nSigmaClipKernelGen = pexConfig.Field(
112 doc=
"Number of sigma to clip to during pixel-wise clipping when generating the kernel. See " +
113 "the generateKernel docstring for more info.",
116 nSigmaClipXCorr = pexConfig.Field(
118 doc=
"Number of sigma to clip when calculating means for the cross-correlation",
121 maxLag = pexConfig.Field(
123 doc=
"The maximum lag (in pixels) to use when calculating the cross-correlation/kernel",
126 nPixBorderGainCalc = pexConfig.Field(
128 doc=
"The number of border pixels to exclude when calculating the gain",
131 nPixBorderXCorr = pexConfig.Field(
133 doc=
"The number of border pixels to exclude when calculating the cross-correlation and kernel",
136 biasCorr = pexConfig.Field(
138 doc=
"An empirically determined correction factor, used to correct for the sigma-clipping of" +
139 " a non-Gaussian distribution. Post DM-15277, code will exist here to calulate appropriate values",
142 backgroundBinSize = pexConfig.Field(
144 doc=
"Size of the background bins",
147 fixPtcThroughOrigin = pexConfig.Field(
149 doc=
"Constrain the fit of the photon transfer curve to go through the origin when measuring" +
153 level = pexConfig.ChoiceField(
154 doc=
"The level at which to calculate the brighter-fatter kernels",
155 dtype=str, default=
"DETECTOR",
157 "AMP":
"Every amplifier treated separately",
158 "DETECTOR":
"One kernel per detector",
161 backgroundWarnLevel = pexConfig.Field(
163 doc=
"Log warnings if the mean of the fitted background is found to be above this level after " +
164 "differencing image pair.",
170 """Subclass of TaskRunner for the MakeBrighterFatterKernelTask. 172 This transforms the processed arguments generated by the ArgumentParser 173 into the arguments expected by makeBrighterFatterKernelTask.run(). 175 makeBrighterFatterKernelTask.run() takes a two arguments, 176 one of which is the dataRef (as usual), and the other is the list 177 of visit-pairs, in the form of a list of tuples. 178 This list is supplied on the command line as documented, 179 and this class parses that, and passes the parsed version 182 See pipeBase.TaskRunner for more information. 187 """Parse the visit list and pass through explicitly.""" 189 for visitStringPair
in parsedCmd.visitPairs:
190 visitStrings = visitStringPair.split(
",")
191 if len(visitStrings) != 2:
192 raise RuntimeError(
"Found {} visits in {} instead of 2".
format(len(visitStrings),
195 visits = [
int(visit)
for visit
in visitStrings]
197 raise RuntimeError(
"Could not parse {} as two integer visit numbers".
format(visitStringPair))
198 visitPairs.append(visits)
200 return pipeBase.TaskRunner.getTargetList(parsedCmd, visitPairs=visitPairs, **kwargs)
204 """A DataIdContainer for the MakeBrighterFatterKernelTask.""" 207 """Compute refList based on idList. 209 This method must be defined as the dataset does not exist before this 215 Results of parsing the command-line. 219 Not called if ``add_id_argument`` called 220 with ``doMakeDataRefList=False``. 221 Note that this is almost a copy-and-paste of the vanilla implementation, 222 but without checking if the datasets already exist, 223 as this task exists to make them. 225 if self.datasetType
is None:
226 raise RuntimeError(
"Must call setDatasetType first")
227 butler = namespace.butler
228 for dataId
in self.idList:
229 refList =
list(butler.subset(datasetType=self.datasetType, level=self.level, dataId=dataId))
233 namespace.log.warn(
"No data found for dataId=%s", dataId)
235 self.refList += refList
239 """A (very) simple class to hold the kernel(s) generated. 241 The kernel.kernel is a dictionary holding the kernels themselves. 242 One kernel if the level is 'DETECTOR' or, 243 nAmps in length, if level is 'AMP'. 244 The dict is keyed by either the detector ID or the amplifier IDs. 246 The level is the level for which the kernel(s) were generated so that one 247 can know how to access the kernels without having to query the shape of 248 the dictionary holding the kernel(s). 252 assert type(level) == str
253 assert type(kernelDict) == dict
254 if level ==
'DETECTOR':
255 assert len(kernelDict.keys()) == 1
257 assert len(kernelDict.keys()) > 1
264 """Brighter-fatter effect correction-kernel calculation task. 266 A command line task for calculating the brighter-fatter correction 267 kernel from pairs of flat-field images (with the same exposure length). 269 The following operations are performed: 271 - The configurable isr task is called, which unpersists and assembles the 272 raw images, and performs the selected instrument signature removal tasks. 273 For the purpose of brighter-fatter coefficient calculation is it 274 essential that certain components of isr are *not* performed, and 275 recommended that certain others are. The task checks the selected isr 276 configuration before it is run, and if forbidden components have been 277 selected task will raise, and if recommended ones have not been selected, 280 - The gain of the each amplifier in the detector is calculated using 281 the photon transfer curve (PTC) method and used to correct the images 282 so that all calculations are done in units of electrons, and so that the 283 level across amplifier boundaries is continuous. 284 Outliers in the PTC are iteratively rejected 285 before fitting, with the nSigma rejection level set by 286 config.nSigmaClipRegression. Individual pixels are ignored in the input 287 images the image based on config.nSigmaClipGainCalc. 289 - Each image is then cross-correlated with the one it's paired with 290 (with the pairing defined by the --visit-pairs command line argument), 291 which is done either the whole-image to whole-image, 292 or amplifier-by-amplifier, depending on config.level. 294 - Once the cross-correlations have been calculated for each visit pair, 295 these are used to generate the correction kernel. 296 The maximum lag used, in pixels, and hence the size of the half-size 297 of the kernel generated, is given by config.maxLag, 298 i.e. a value of 10 will result in a kernel of size 2n-1 = 19x19 pixels. 299 Outlier values in these cross-correlations are rejected by using a 300 pixel-wise sigma-clipped thresholding to each cross-correlation in 301 the visit-pairs-length stack of cross-correlations. 302 The number of sigma clipped to is set by config.nSigmaClipKernelGen. 304 - Once DM-15277 has been completed, a method will exist to calculate the 305 empirical correction factor, config.biasCorr. 306 TODO: DM-15277 update this part of the docstring once the ticket is done. 309 RunnerClass = MakeBrighterFatterKernelTaskRunner
310 ConfigClass = MakeBrighterFatterKernelTaskConfig
311 _DefaultName =
"makeBrighterFatterKernel" 314 pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
315 self.makeSubtask(
"isr")
318 if self.
debug.enabled:
319 self.log.
info(
"Running with debug enabled...")
324 if self.
debug.display:
326 afwDisp.setDefaultBackend(self.
debug.displayBackend)
327 afwDisp.Display.delAllDisplays()
328 self.
disp1 = afwDisp.Display(0, open=
True)
329 self.
disp2 = afwDisp.Display(1, open=
True)
331 im = afwImage.ImageF(1, 1)
336 self.
debug.display =
False 337 self.log.
warn(
'Failed to setup/connect to display! Debug display has been disabled')
339 plt.interactive(
False)
345 def _makeArgumentParser(cls):
346 """Augment argument parser for the MakeBrighterFatterKernelTask.""" 348 parser.add_argument(
"--visit-pairs", dest=
"visitPairs", nargs=
"*",
349 help=
"Visit pairs to use. Each pair must be of the form INT,INT e.g. 123,456")
350 parser.add_id_argument(
"--id", datasetType=
"brighterFatterKernel",
351 ContainerClass=BrighterFatterKernelTaskDataIdContainer,
352 help=
"The ccds to use, e.g. --id ccd=0..100")
356 """Check that appropriate ISR settings are being used 357 for brighter-fatter kernel calculation.""" 367 configDict = self.config.isr.toDict()
369 for configParam
in self.config.isrMandatorySteps:
370 if configDict[configParam]
is False:
371 raise RuntimeError(
'Must set config.isr.%s to True ' 372 'for brighter-fatter kernel calulation' % configParam)
374 for configParam
in self.config.isrForbiddenSteps:
375 if configDict[configParam]
is True:
376 raise RuntimeError(
'Must set config.isr.%s to False ' 377 'for brighter-fatter kernel calulation' % configParam)
379 for configParam
in self.config.isrDesirableSteps:
380 if configParam
not in configDict:
381 self.log.
info(
'Failed to find key %s in the isr config dict. You probably want ' +
382 'to set the equivalent for your obs_package to True.' % configParam)
384 if configDict[configParam]
is False:
385 self.log.
warn(
'Found config.isr.%s set to False for brighter-fatter kernel calulation. ' 386 'It is probably desirable to have this set to True' % configParam)
389 if not self.config.isr.assembleCcd.doTrim:
390 raise RuntimeError(
'Must trim when assembling CCDs. Set config.isr.assembleCcd.doTrim to True')
394 """Run the brighter-fatter measurement task. 396 For a dataRef (which is each detector here), 397 and given a list of visit pairs, calulate the 398 brighter-fatter kernel for the detector. 402 dataRef : list of lsst.daf.persistence.ButlerDataRef 403 dataRef for the detector for the visits to be fit. 404 visitPairs : `iterable` of `tuple` of `int` 405 Pairs of visit numbers to be processed together 412 detNum = dataRef.dataId[self.config.ccdKey]
413 if self.config.level ==
'DETECTOR':
414 xcorrs = {detNum: []}
416 elif self.config.level ==
'AMP':
422 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
423 ampInfoCat = detector.getAmpInfoCatalog()
424 ampNames = [amp.getName()
for amp
in ampInfoCat]
425 xcorrs = {key: []
for key
in ampNames}
426 means = {key: []
for key
in ampNames}
428 raise RuntimeError(
"Unsupported level: {}".
format(self.config.level))
431 if self.config.doCalcGains:
432 self.log.
info(
'Compute gains for detector %s' % detNum)
434 dataRef.put(gains, datasetType=
'brighterFatterGain')
435 self.log.
debug(
'Finished gain estimation for detector %s' % detNum)
437 gains = dataRef.get(
'brighterFatterGain')
439 raise RuntimeError(
'Failed to retrieved gains for detector %s' % detNum)
440 self.log.
info(
'Retrieved stored gain for detector %s' % detNum)
441 self.log.
debug(
'Detector %s has gains %s' % (detNum, gains))
445 for (v1, v2)
in visitPairs:
447 dataRef.dataId[
'visit'] = v1
449 dataRef.dataId[
'visit'] = v2
451 del dataRef.dataId[
'visit']
454 self.log.
info(
'Preparing images for cross-correlation calculation for detector %s' % detNum)
464 for det_object
in _scaledMaskedIms1.keys():
466 _scaledMaskedIms2[det_object])
467 xcorrs[det_object].
append(_xcorr)
468 means[det_object].
append([_means1[det_object], _means2[det_object]])
474 self.log.
info(
'Generating kernel(s) for %s' % detNum)
475 for det_object
in xcorrs.keys():
476 if self.config.level ==
'DETECTOR':
477 objId =
'detector %s' % det_object
478 elif self.config.level ==
'AMP':
479 objId =
'detector %s AMP %s' % (detNum, det_object)
480 kernels[det_object] = self.
generateKernel(xcorrs[det_object], means[det_object], objId)
483 self.log.
info(
'Finished generating kernel(s) for %s' % detNum)
484 return pipeBase.Struct(exitStatus=0)
486 def _makeCroppedExposures(self, exp, gains, level):
487 """Prepare exposure for cross-correlation calculation. 489 For each amp, crop by the border amount, specified by 490 config.nPixBorderXCorr, then rescale by the gain 491 and subtract the sigma-clipped mean. 492 If the level is 'DETECTOR' then this is done 493 to the whole image so that it can be cross-correlated, with a copy 495 If the level is 'AMP' then this is done per-amplifier, 496 and a copy of each prepared amp-image returned. 500 exp : `lsst.afw.image.exposure.ExposureF` 501 The exposure to prepare 502 gains : `dict` of `float` 503 Dictionary of the amplifier gain values, keyed by amplifier name 505 Either `AMP` or `DETECTOR` 509 scaledMaskedIms : `dict` of `lsst.afw.image.maskedImage.MaskedImageF` 510 Depending on level, this is either one item, or n_amp items, 511 keyed by detectorId or ampName 515 This function is controlled by the following config parameters: 516 nPixBorderXCorr : `int` 517 The number of border pixels to exclude 518 nSigmaClipXCorr : `float` 519 The number of sigma to be clipped to 521 assert(isinstance(exp, afwImage.ExposureF))
523 local_exp = exp.clone()
526 border = self.config.nPixBorderXCorr
527 sigma = self.config.nSigmaClipXCorr
530 sctrl.setNumSigmaClip(sigma)
535 detector = local_exp.getDetector()
536 ampInfoCat = detector.getAmpInfoCatalog()
538 mi = local_exp.getMaskedImage()
543 for amp
in ampInfoCat:
544 ampName = amp.getName()
545 rescaleIm = mi[amp.getBBox()]
546 rescaleTemp = temp[amp.getBBox()]
548 gain = gains[ampName]
551 self.log.
debug(
"mean*gain = %s, clipped mean = %s" %
554 rescaleIm -= mean*gain
558 afwImage.LOCAL], afwMath.MEANCLIP, sctrl).getValue()
559 returnAreas[ampName] = rescaleIm
561 if level ==
'DETECTOR':
562 detName = local_exp.getDetector().getId()
564 afwMath.MEANCLIP, sctrl).getValue()
565 returnAreas[detName] = rescaleIm
567 return returnAreas, means
569 def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None):
570 """Calculate the cross-correlation of an area. 572 If the area in question contains multiple amplifiers then they must 573 have been gain corrected. 577 maskedIm0 : `lsst.afw.image.MaskedImageF` 579 maskedIm1 : `lsst.afw.image.MaskedImageF` 581 frameId : `str`, optional 582 The frame identifier for use in the filename 583 if writing debug outputs. 584 detId : `str`, optional 585 The detector identifier (detector, or detector+amp, 586 depending on config.level) for use in the filename 587 if writing debug outputs. 588 runningBiasCorrSim : `bool` 589 Set to true when using this function to calculate the amount of bias 590 introduced by the sigma clipping. If False, the biasCorr parameter 591 is divided by to remove the bias, but this is, of course, not 592 appropriate when this is the parameter being measured. 597 The quarter-image cross-correlation 599 The sum of the means of the input images, 600 sigma-clipped, and with borders applied. 601 This is used when using this function with simulations to calculate 602 the biasCorr parameter. 606 This function is controlled by the following config parameters: 608 The maximum lag to use in the cross-correlation calculation 609 nPixBorderXCorr : `int` 610 The number of border pixels to exclude 611 nSigmaClipXCorr : `float` 612 The number of sigma to be clipped to 614 Parameter used to correct from the bias introduced 617 maxLag = self.config.maxLag
618 border = self.config.nPixBorderXCorr
619 sigma = self.config.nSigmaClipXCorr
620 biasCorr = self.config.biasCorr
623 sctrl.setNumSigmaClip(sigma)
626 afwMath.MEANCLIP, sctrl).getValue()
628 afwMath.MEANCLIP, sctrl).getValue()
631 diff = maskedIm0.clone()
632 diff -= maskedIm1.getImage()
633 diff = diff[border: -border, border: -border, afwImage.LOCAL]
635 if self.
debug.writeDiffImages:
636 filename =
'_'.join([
'diff',
'detector', detId, frameId,
'.fits'])
637 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
640 binsize = self.config.backgroundBinSize
641 nx = diff.getWidth()//binsize
642 ny = diff.getHeight()//binsize
645 bgImg = bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE, afwMath.REDUCE_INTERP_ORDER)
646 bgMean = np.mean(bgImg.getArray())
647 if abs(bgMean) >= self.config.backgroundWarnLevel:
648 self.log.
warn(
'Mean of background = %s > config.maxBackground' % bgMean)
652 if self.
debug.writeDiffImages:
653 filename =
'_'.join([
'bgSub',
'diff',
'detector', detId, frameId,
'.fits'])
654 diff.writeFits(os.path.join(self.
debug.debugDataPath, filename))
655 if self.
debug.display:
658 self.log.
debug(
"Median and variance of diff:")
661 sctrl).getValue(), np.var(diff.getImage().getArray()))
664 dim0 = diff[0: -maxLag, : -maxLag, afwImage.LOCAL]
666 width, height = dim0.getDimensions()
667 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
669 for xlag
in range(maxLag + 1):
670 for ylag
in range(maxLag + 1):
671 dim_xy = diff[xlag:xlag + width, ylag: ylag + height, afwImage.LOCAL].
clone()
675 if not runningBiasCorrSim:
676 xcorr[xlag, ylag] /= biasCorr
684 """Estimate the amplifier gains using the specified visits. 686 Given a dataRef and list of flats of varying intensity, 687 calculate the gain for each amplifier in the detector 688 using the photon transfer curve (PTC) method. 690 The config.fixPtcThroughOrigin option determines whether the iterative 691 fitting is forced to go through the origin or not. 692 This defaults to True, fitting var=1/gain * mean. 693 If set to False then var=1/g * mean + const is fitted. 695 This is really a photo transfer curve (PTC) gain measurement task. 696 See DM-14063 for results from of a comparison between 697 this task's numbers and the gain values in the HSC camera model, 698 and those measured by the PTC task in eotest. 702 dataRef : `lsst.daf.persistence.butler.Butler.dataRef` 703 dataRef for the detector for the flats to be used 704 visitPairs : `list` of `tuple` 705 List of visit-pairs to use, as [(v1,v2), (v3,v4)...] 709 gains : `dict` of `float` 710 Dict of the as-calculated amplifier gain values, 711 keyed by amplifier name 712 nominalGains : `dict` of `float` 713 Dict of the amplifier gains, as reported by the `detector` object, 714 keyed by amplifier name 717 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
718 ampInfoCat = detector.getAmpInfoCatalog()
719 ampNames = [amp.getName()
for amp
in ampInfoCat]
721 ampMeans = {key: []
for key
in ampNames}
722 ampCoVariances = {key: []
for key
in ampNames}
723 ampVariances = {key: []
for key
in ampNames}
729 for visPairNum, visPair
in enumerate(visitPairs):
735 ampName = amp.getName()
736 if _means[ampName]*10 < _vars[ampName]
or _means[ampName]*10 < _covars[ampName]:
737 msg =
'Sanity check failed; check visit pair %s amp %s' % (visPair, ampName)
745 for k
in _means.keys():
746 if _vars[k]*1.3 < _covars[k]
or _vars[k]*0.7 > _covars[k]:
747 self.log.
warn(
'Dropped a value')
749 ampMeans[k].
append(_means[k])
750 ampVariances[k].
append(_vars[k])
751 ampCoVariances[k].
append(_covars[k])
756 ampName = amp.getName()
757 nomGains[ampName] = amp.getGain()
758 slopeRaw, interceptRaw, rVal, pVal, stdErr = \
759 stats.linregress(np.asarray(ampMeans[ampName]), np.asarray(ampCoVariances[ampName]))
761 np.asarray(ampCoVariances[ampName]),
762 fixThroughOrigin=
True)
764 np.asarray(ampCoVariances[ampName]),
765 fixThroughOrigin=
False)
766 self.log.
info(
"Slope of raw fit: %s, intercept: %s p value: %s" % (slopeRaw,
768 self.log.
info(
"slope of fixed fit: %s, difference vs raw:%s" % (slopeFix,
769 slopeFix - slopeRaw))
770 self.log.
info(
"slope of unfixed fit: %s, difference vs fix:%s" % (slopeUnfix,
771 slopeFix - slopeUnfix))
772 if self.config.fixPtcThroughOrigin:
773 slopeToUse = slopeFix
775 slopeToUse = slopeUnfix
777 if self.
debug.enabled:
779 ax = fig.add_subplot(111)
780 ax.plot(np.asarray(ampMeans[ampName]),
781 np.asarray(ampCoVariances[ampName]), linestyle=
'None', marker=
'x', label=
'data')
782 if self.config.fixPtcThroughOrigin:
783 ax.plot(np.asarray(ampMeans[ampName]),
784 np.asarray(ampMeans[ampName])*slopeToUse, label=
'Fit through origin')
786 ax.plot(np.asarray(ampMeans[ampName]),
787 np.asarray(ampMeans[ampName])*slopeToUse + intercept,
788 label=
'Fit (intercept unconstrained')
790 dataRef.put(fig,
"plotBrighterFatterPtc", amp=ampName)
791 self.log.
info(
'Saved PTC for detector %s amp %s' % (detector.getId(), ampName))
792 gains[ampName] = 1.0/slopeToUse
793 return gains, nomGains
796 def _checkExpLengthEqual(exp1, exp2, v1=None, v2=None):
797 """Check the exposure lengths of two exposures are equal. 801 exp1 : `lsst.afw.image.exposure.ExposureF` 802 First exposure to check 803 exp2 : `lsst.afw.image.exposure.ExposureF` 804 Second exposure to check 805 v1 : `int` or `str`, optional 806 First visit of the visit pair 807 v2 : `int` or `str`, optional 808 Second visit of the visit pair 813 Raised if the exposure lengths of the two exposures are not equal 815 expTime1 = exp1.getInfo().getVisitInfo().getExposureTime()
816 expTime2 = exp2.getInfo().getVisitInfo().getExposureTime()
817 if expTime1 != expTime2:
818 msg =
"Exposure lengths for visit pairs must be equal. " + \
819 "Found %s and %s" % (expTime1, expTime2)
821 msg +=
" for visit pair %s, %s" % (v1, v2)
822 raise RuntimeError(msg)
824 def _calcMeansAndVars(self, dataRef, v1, v2):
825 """Calculate the means, vars, covars, and retrieve the nominal gains, 826 for each amp in each detector. 828 This code runs using two visit numbers, and for the detector specified. 829 It calculates the correlations in the individual amps without 830 rescaling any gains. This allows a photon transfer curve 831 to be generated and the gains measured. 833 Images are assembled with use the isrTask, and basic isr is performed. 837 dataRef : `lsst.daf.persistence.butler.Butler.dataRef` 838 dataRef for the detector for the repo containg the flats to be used 840 First visit of the visit pair 842 Second visit of the visit pair 846 means, vars, covars : `tuple` of `dicts` 847 Three dicts, keyed by ampName, 848 containing the sum of the image-means, 849 the variance, and the quarter-image of the xcorr. 851 sigma = self.config.nSigmaClipGainCalc
852 maxLag = self.config.maxLag
853 border = self.config.nPixBorderGainCalc
854 biasCorr = self.config.biasCorr
857 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
863 originalDataId = dataRef.dataId.copy()
864 dataRef.dataId[
'visit'] = v1
866 dataRef.dataId[
'visit'] = v2
868 dataRef.dataId = originalDataId
872 detector = exps[0].getDetector()
875 if self.
debug.display:
880 sctrl.setNumSigmaClip(sigma)
881 for imNum, im
in enumerate(ims):
888 ampName = amp.getName()
889 ampIm = im[amp.getBBox()]
891 afwMath.MEANCLIP, sctrl).getValue()
892 if ampName
not in ampMeans.keys():
893 ampMeans[ampName] = []
894 ampMeans[ampName].
append(mean)
897 diff = ims[0].
clone()
900 temp = diff[border: -border, border: -border, afwImage.LOCAL]
905 binsize = self.config.backgroundBinSize
906 nx = temp.getWidth()//binsize
907 ny = temp.getHeight()//binsize
913 diff[box, afwImage.LOCAL] -= bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE,
914 afwMath.REDUCE_INTERP_ORDER)
919 ampName = amp.getName()
921 diffAmpIm = diff[amp.getBBox()].
clone()
922 diffAmpImCrop = diffAmpIm[border: -border - maxLag, border: -border - maxLag, afwImage.LOCAL]
924 w, h = diffAmpImCrop.getDimensions()
925 xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
928 for xlag
in range(maxLag + 1):
929 for ylag
in range(maxLag + 1):
930 dim_xy = diffAmpIm[border + xlag: border + xlag + w,
931 border + ylag: border + ylag + h,
932 afwImage.LOCAL].
clone()
934 dim_xy *= diffAmpImCrop
936 afwMath.MEANCLIP, sctrl).getValue()/(biasCorr)
938 variances[ampName] = xcorr[0, 0]
940 coVars[ampName] = np.sum(xcorr_full)
942 msg =
"M1: " +
str(ampMeans[ampName][0])
943 msg +=
" M2 " +
str(ampMeans[ampName][1])
944 msg +=
" M_sum: " +
str((ampMeans[ampName][0]) + ampMeans[ampName][1])
945 msg +=
" Var " +
str(variances[ampName])
946 msg +=
" coVar: " +
str(coVars[ampName])
951 ampName = amp.getName()
952 means[ampName] = ampMeans[ampName][0] + ampMeans[ampName][1]
954 return means, variances, coVars
956 def _plotXcorr(self, xcorr, mean, zmax=0.05, title=None, fig=None, saveToFileName=None):
957 """Plot the correlation functions.""" 959 xcorr = xcorr.getArray()
971 ax = fig.add_subplot(111, projection=
'3d')
975 nx, ny = np.shape(xcorr)
977 xpos, ypos = np.meshgrid(np.arange(nx), np.arange(ny))
978 xpos = xpos.flatten()
979 ypos = ypos.flatten()
980 zpos = np.zeros(nx*ny)
984 ax.bar3d(xpos, ypos, zpos, 1, 1, dz, color=
'b', zsort=
'max', sort_zpos=100)
985 if xcorr[0, 0] > zmax:
986 ax.bar3d([0], [0], [zmax], 1, 1, 1e-4, color=
'c')
989 ax.set_ylabel(
"column")
990 ax.set_zlabel(
r"$\langle{(F_i - \bar{F})(F_i - \bar{F})}\rangle/\bar{F}$")
995 fig.savefig(saveToFileName)
997 def _iterativeRegression(self, x, y, fixThroughOrigin=False, nSigmaClip=None, maxIter=None):
998 """Use linear regression to fit a line, iteratively removing outliers. 1000 Useful when you have a sufficiently large numbers of points on your PTC. 1001 This function iterates until either there are no outliers of 1002 config.nSigmaClip magnitude, or until the specified maximum number 1003 of iterations has been performed. 1008 The independent variable. Must be a numpy array, not a list. 1010 The dependent variable. Must be a numpy array, not a list. 1011 fixThroughOrigin : `bool`, optional 1012 Whether to fix the PTC through the origin or allow an y-intercept. 1013 nSigmaClip : `float`, optional 1014 The number of sigma to clip to. 1015 Taken from the task config if not specified. 1016 maxIter : `int`, optional 1017 The maximum number of iterations allowed. 1018 Taken from the task config if not specified. 1023 The slope of the line of best fit 1025 The y-intercept of the line of best fit 1028 maxIter = self.config.maxIterRegression
1030 nSigmaClip = self.config.nSigmaClipRegression
1034 sctrl.setNumSigmaClip(nSigmaClip)
1036 if fixThroughOrigin:
1037 while nIter < maxIter:
1039 self.log.
debug(
"Origin fixed, iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1040 TEST = x[:, np.newaxis]
1041 slope, _, _, _ = np.linalg.lstsq(TEST, y)
1046 index = np.where((res > (resMean + nSigmaClip*resStd)) |
1047 (res < (resMean - nSigmaClip*resStd)))
1048 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1049 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1051 x = np.delete(x, index)
1052 y = np.delete(y, index)
1056 while nIter < maxIter:
1058 self.log.
debug(
"Iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1059 xx = np.vstack([x, np.ones(len(x))]).T
1060 ret, _, _, _ = np.linalg.lstsq(xx, y)
1061 slope, intercept = ret
1062 res = y - slope*x - intercept
1065 index = np.where((res > (resMean + nSigmaClip * resStd)) | (res < resMean - nSigmaClip * resStd))
1066 self.log.
debug(
"%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1067 if np.shape(np.where(index))[1] == 0
or (nIter >= maxIter):
1069 x = np.delete(x, index)
1070 y = np.delete(y, index)
1072 return slope, intercept
1075 """Generate the full kernel from a list of cross-correlations and means. 1077 Taking a list of quarter-image, gain-corrected cross-correlations, 1078 do a pixel-wise sigma-clipped mean of each, 1079 and tile into the full-sized kernel image. 1081 Each corr in corrs is one quarter of the full cross-correlation, 1082 and has been gain-corrected. Each mean in means is a tuple of the means 1083 of the two individual images, corresponding to that corr. 1087 corrs : `list` of `numpy.ndarray`, (Ny, Nx) 1088 A list of the quarter-image cross-correlations 1089 means : `dict` of `tuples` of `floats` 1090 The means of the input images for each corr in corrs 1091 rejectLevel : `float`, optional 1092 This is essentially is a sanity check parameter. 1093 If this condition is violated there is something unexpected 1094 going on in the image, and it is discarded from the stack before 1095 the clipped-mean is calculated. 1096 If not provided then config.xcorrCheckRejectLevel is used 1100 kernel : `numpy.ndarray`, (Ny, Nx) 1104 rejectLevel = self.config.xcorrCheckRejectLevel
1111 sctrl.setNumSigmaClip(self.config.nSigmaClipKernelGen)
1113 for corrNum, ((mean1, mean2), corr)
in enumerate(zip(means, corrs)):
1114 corr[0, 0] -= (mean1 + mean2)
1116 self.log.
warn(
'Skipped item %s due to unexpected value of (variance-mean)' % corrNum)
1118 corr /= -1.0*(mean1**2 + mean2**2)
1122 xcorrCheck = np.abs(np.sum(fullCorr))/np.sum(np.abs(fullCorr))
1123 if xcorrCheck > rejectLevel:
1124 self.log.
warn(
"Sum of the xcorr is unexpectedly high. Investigate item num %s for %s. \n" 1125 "value = %s" % (corrNum, objId, xcorrCheck))
1127 xcorrList.append(fullCorr)
1130 raise RuntimeError(
"Cannot generate kernel because all inputs were discarded. " 1131 "Either the data is bad, or config.xcorrCheckRejectLevel is too low")
1134 meanXcorr = np.zeros_like(fullCorr)
1135 xcorrList = np.transpose(xcorrList)
1136 for i
in range(np.shape(meanXcorr)[0]):
1137 for j
in range(np.shape(meanXcorr)[1]):
1143 """An implementation of the successive over relaxation (SOR) method. 1145 A numerical method for solving a system of linear equations 1146 with faster convergence than the Gauss-Seidel method. 1150 source : `numpy.ndarray` 1152 maxIter : `int`, optional 1153 Maximum number of iterations to attempt before aborting 1154 eLevel : `float`, optional 1155 The target error level at which we deem convergence to have occured 1159 output : `numpy.ndarray` 1163 maxIter = self.config.maxIterSuccessiveOverRelaxation
1165 eLevel = self.config.eLevelSuccessiveOverRelaxation
1167 assert source.shape[0] == source.shape[1],
"Input array must be square" 1169 func = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1170 resid = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1171 rhoSpe = np.cos(np.pi/source.shape[0])
1174 for i
in range(1, func.shape[0] - 1):
1175 for j
in range(1, func.shape[1] - 1):
1176 resid[i, j] = (func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1177 func[i + 1, j] - 4*func[i, j] - source[i - 1, j - 1])
1178 inError = np.sum(np.abs(resid))
1186 while nIter < maxIter*2:
1189 for i
in range(1, func.shape[0] - 1, 2):
1190 for j
in range(1, func.shape[1] - 1, 2):
1191 resid[i, j] =
float(func[i, j-1] + func[i, j + 1] + func[i - 1, j] +
1192 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1193 func[i, j] += omega*resid[i, j]*.25
1194 for i
in range(2, func.shape[0] - 1, 2):
1195 for j
in range(2, func.shape[1] - 1, 2):
1196 resid[i, j] =
float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1197 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1198 func[i, j] += omega*resid[i, j]*.25
1200 for i
in range(1, func.shape[0] - 1, 2):
1201 for j
in range(2, func.shape[1] - 1, 2):
1202 resid[i, j] =
float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1203 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1204 func[i, j] += omega*resid[i, j]*.25
1205 for i
in range(2, func.shape[0] - 1, 2):
1206 for j
in range(1, func.shape[1] - 1, 2):
1207 resid[i, j] =
float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1208 func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1209 func[i, j] += omega*resid[i, j]*.25
1210 outError = np.sum(np.abs(resid))
1211 if outError < inError*eLevel:
1214 omega = 1.0/(1 - rhoSpe*rhoSpe/2.0)
1216 omega = 1.0/(1 - rhoSpe*rhoSpe*omega/4.0)
1219 if nIter >= maxIter*2:
1220 self.log.
warn(
"Failure: SuccessiveOverRelaxation did not converge in %s iterations." 1221 "\noutError: %s, inError: %s," % (nIter//2, outError, inError*eLevel))
1223 self.log.
info(
"Success: SuccessiveOverRelaxation converged in %s iterations." 1224 "\noutError: %s, inError: %s", nIter//2, outError, inError*eLevel)
1225 return func[1: -1, 1: -1]
1228 def _tileArray(in_array):
1229 """Given an input quarter-image, tile/mirror it and return full image. 1231 Given a square input of side-length n, of the form 1233 input = array([[1, 2, 3], 1237 return an array of size 2n-1 as 1239 output = array([[ 9, 8, 7, 8, 9], 1248 The square input quarter-array 1253 The full, tiled array 1255 assert(in_array.shape[0] == in_array.shape[1])
1256 length = in_array.shape[0] - 1
1257 output = np.zeros((2*length + 1, 2*length + 1))
1259 for i
in range(length + 1):
1260 for j
in range(length + 1):
1261 output[i + length, j + length] = in_array[i, j]
1262 output[-i + length, j + length] = in_array[i, j]
1263 output[i + length, -j + length] = in_array[i, j]
1264 output[-i + length, -j + length] = in_array[i, j]
1268 def _convertImagelikeToFloatImage(imagelikeObject):
1269 """Turn an exposure or masked image of any type into an ImageF.""" 1270 for attr
in (
"getMaskedImage",
"getImage"):
1271 if hasattr(imagelikeObject, attr):
1272 imagelikeObject = getattr(imagelikeObject, attr)()
1274 floatImage = imagelikeObject.convertF()
1275 except AttributeError:
1276 raise RuntimeError(
"Failed to convert image to float")
1280 def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False,
1281 correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10):
1282 """Calculate the bias induced when sigma-clipping non-Gassian distributions. 1284 Fill image-pairs of the specified size with Poisson-distributed values, 1285 adding correlations as necessary. Then calculate the cross correlation, 1286 and calculate the bias induced using the cross-correlation image 1287 and the image means. 1291 fluxLevels : `list` of `int` 1292 The mean flux levels at which to simiulate. 1293 Nominal values might be something like [70000, 90000, 110000] 1294 imageShape : `tuple` of `int` 1295 The shape of the image array to simulate, nx by ny pixels. 1296 repeats : `int`, optional 1297 Number of repeats to perform so that results 1298 can be averaged to improve SNR. 1299 seed : `int`, optional 1300 The random seed to use for the Poisson points. 1301 addCorrelations : `bool`, optional 1302 Whether to add brighter-fatter-like correlations to the simulated images 1303 If true, a correlation between x_{i,j} and x_{i+1,j+1} is introduced 1304 by adding a*x_{i,j} to x_{i+1,j+1} 1305 correlationStrength : `float`, optional 1306 The strength of the correlations. 1307 This is the value of the coefficient `a` in the above definition. 1308 maxLag : `int`, optional 1309 The maximum lag to work to in pixels 1310 nSigmaClip : `float`, optional 1311 Number of sigma to clip to when calculating the sigma-clipped mean. 1312 border : `int`, optional 1313 Number of border pixels to mask 1317 biases : `dict` of `list` of `float` 1318 A dictionary, keyed by flux level, containing a list of the biases 1319 for each repeat at that flux level 1320 means : `dict` of `list` of `float` 1321 A dictionary, keyed by flux level, containing a list of the average mean 1322 fluxes (average of the mean of the two images) 1323 for the image pairs at that flux level 1324 xcorrs : `dict` of `list` of `np.ndarray` 1325 A dictionary, keyed by flux level, containing a list of the xcorr 1326 images for the image pairs at that flux level 1328 means = {f: []
for f
in fluxLevels}
1329 xcorrs = {f: []
for f
in fluxLevels}
1330 biases = {f: []
for f
in fluxLevels}
1333 config.isrMandatorySteps = []
1334 config.isrForbiddenSteps = []
1335 config.nSigmaClipXCorr = nSigmaClip
1336 config.nPixBorderXCorr = border
1337 config.maxLag = maxLag
1340 im0 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1341 im1 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1343 random = np.random.RandomState(seed)
1345 for rep
in range(repeats):
1346 for flux
in fluxLevels:
1347 data0 = random.poisson(flux, (imageShape)).astype(float)
1348 data1 = random.poisson(flux, (imageShape)).astype(float)
1350 data0[1:, 1:] += correlationStrength*data0[: -1, : -1]
1351 data1[1:, 1:] += correlationStrength*data1[: -1, : -1]
1352 im0.image.array[:, :] = data0
1353 im1.image.array[:, :] = data1
1355 _xcorr, _means = task._crossCorrelate(im0, im1, runningBiasCorrSim=
True)
1357 means[flux].
append(_means)
1358 xcorrs[flux].
append(_xcorr)
1360 bias = xcorrs[flux][-1][1, 1]/means[flux][-1]*(1 + correlationStrength)/correlationStrength
1361 print(
"Simulated/expected avg. flux: %.1f, %.1f" % (flux, means[flux][-1]/2))
1362 print(
"Bias: %.6f" % bias)
1364 bias = xcorrs[flux][-1][0, 0]/means[flux][-1]
1365 print(
"Simulated/expected avg. flux: %.1f, %.1f" % (flux, means[flux][-1]/2))
1366 print(
"Bias: %.6f" % bias)
1367 biases[flux].
append(bias)
1369 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 getTargetList(parsedCmd, kwargs)
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)
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)