31 from .astierCovPtcUtils
import (CovFastFourierTransform, computeCovDirect)
32 from .astierCovPtcFit
import makeCovArray
37 __all__ = [
'PhotonTransferCurveExtractConfig',
'PhotonTransferCurveExtractTask']
41 dimensions=(
"instrument",
"detector")):
44 name=
"ptcInputExposurePairs",
45 doc=
"Input post-ISR processed exposure pairs (flats) to"
46 "measure covariances from.",
47 storageClass=
"Exposure",
48 dimensions=(
"instrument",
"exposure",
"detector"),
53 outputCovariances = cT.Output(
54 name=
"ptcCovariances",
55 doc=
"Extracted flat (co)variances.",
56 storageClass=
"PhotonTransferCurveDataset",
57 dimensions=(
"instrument",
"exposure",
"detector"),
63 pipelineConnections=PhotonTransferCurveExtractConnections):
64 """Configuration for the measurement of covariances from flats.
66 matchByExposureId = pexConfig.Field(
68 doc=
"Should exposures by matched by ID rather than exposure time?",
71 maximumRangeCovariancesAstier = pexConfig.Field(
73 doc=
"Maximum range of covariances as in Astier+19",
76 covAstierRealSpace = pexConfig.Field(
78 doc=
"Calculate covariances in real space or via FFT? (see appendix A of Astier+19).",
81 binSize = pexConfig.Field(
83 doc=
"Bin the image by this factor in both dimensions.",
86 minMeanSignal = pexConfig.DictField(
89 doc=
"Minimum values (inclusive) of mean signal (in ADU) above which to consider, per amp."
90 " The same cut is applied to all amps if this dictionary is of the form"
91 " {'ALL_AMPS': value}",
92 default={
'ALL_AMPS': 0.0},
94 maxMeanSignal = pexConfig.DictField(
97 doc=
"Maximum values (inclusive) of mean signal (in ADU) below which to consider, per amp."
98 " The same cut is applied to all amps if this dictionary is of the form"
99 " {'ALL_AMPS': value}",
100 default={
'ALL_AMPS': 1e6},
102 maskNameList = pexConfig.ListField(
104 doc=
"Mask list to exclude from statistics calculations.",
105 default=[
'SUSPECT',
'BAD',
'NO_DATA'],
107 nSigmaClipPtc = pexConfig.Field(
109 doc=
"Sigma cut for afwMath.StatisticsControl()",
112 nIterSigmaClipPtc = pexConfig.Field(
114 doc=
"Number of sigma-clipping iterations for afwMath.StatisticsControl()",
117 minNumberGoodPixelsForCovariance = pexConfig.Field(
119 doc=
"Minimum number of acceptable good pixels per amp to calculate the covariances (via FFT or"
123 detectorMeasurementRegion = pexConfig.ChoiceField(
125 doc=
"Region of each exposure where to perform the calculations (amplifier or full image).",
128 "AMP":
"Amplifier of the detector.",
129 "FULL":
"Full image."
135 pipeBase.CmdLineTask):
136 """Task to measure covariances from flat fields.
137 This task receives as input a list of flat-field images
138 (flats), and sorts these flats in pairs taken at the
139 same time (if there's a different number of flats,
140 those flats are discarded). The mean, variance, and
141 covariances are measured from the difference of the flat
142 pairs at a given time. The variance is calculated
143 via afwMath, and the covariance via the methods in Astier+19
144 (appendix A). In theory, var = covariance[0,0]. This should
145 be validated, and in the future, we may decide to just keep
148 The measured covariances at a particular time (along with
149 other quantities such as the mean) are stored in a PTC dataset
150 object (`PhotonTransferCurveDataset`), which gets partially
151 filled. The number of partially-filled PTC dataset objects
152 will be less than the number of input exposures, but gen3
153 requires/assumes that the number of input dimensions matches
154 bijectively the number of output dimensions. Therefore, a
155 number of "dummy" PTC dataset are inserted in the output list
156 that has the partially-filled PTC datasets with the covariances.
157 This output list will be used as input of
158 `PhotonTransferCurveSolveTask`, which will assemble the multiple
159 `PhotonTransferCurveDataset`s into a single one in order to fit
160 the measured covariances as a function of flux to a particular
163 Astier+19: "The Shape of the Photon Transfer Curve of CCD
164 sensors", arXiv:1905.08677.
166 ConfigClass = PhotonTransferCurveExtractConfig
167 _DefaultName =
'cpPtcExtract'
170 """Ensure that the input and output dimensions are passed along.
174 butlerQC : `~lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
175 Butler to operate on.
176 inputRefs : `~lsst.pipe.base.connections.InputQuantizedConnection`
177 Input data refs to load.
178 ouptutRefs : `~lsst.pipe.base.connections.OutputQuantizedConnection`
179 Output data refs to persist.
181 inputs = butlerQC.get(inputRefs)
183 if self.config.matchByExposureId:
188 inputs[
'inputDims'] = [expId.dataId[
'exposure']
for expId
in inputRefs.inputExp]
189 outputs = self.
runrun(**inputs)
190 butlerQC.put(outputs, outputRefs)
192 def run(self, inputExp, inputDims):
193 """Measure covariances from difference of flat pairs
197 inputExp : `dict` [`float`,
198 (`~lsst.afw.image.exposure.exposure.ExposureF`,
199 `~lsst.afw.image.exposure.exposure.ExposureF`, ...,
200 `~lsst.afw.image.exposure.exposure.ExposureF`)]
201 Dictionary that groups flat-field exposures that have the same
202 exposure time (seconds).
205 List of exposure IDs.
209 detector =
list(inputExp.values())[0][0].getDetector()
210 detNum = detector.getId()
211 amps = detector.getAmplifiers()
212 ampNames = [amp.getName()
for amp
in amps]
215 maxMeanSignalDict = {ampName: 1e6
for ampName
in ampNames}
216 minMeanSignalDict = {ampName: 0.0
for ampName
in ampNames}
217 for ampName
in ampNames:
218 if 'ALL_AMPS' in self.config.maxMeanSignal:
219 maxMeanSignalDict[ampName] = self.config.maxMeanSignal[
'ALL_AMPS']
220 elif ampName
in self.config.maxMeanSignal:
221 maxMeanSignalDict[ampName] = self.config.maxMeanSignal[ampName]
223 if 'ALL_AMPS' in self.config.minMeanSignal:
224 minMeanSignalDict[ampName] = self.config.minMeanSignal[
'ALL_AMPS']
225 elif ampName
in self.config.minMeanSignal:
226 minMeanSignalDict[ampName] = self.config.minMeanSignal[ampName]
228 tags = [(
'mu',
'<f8'), (
'afwVar',
'<f8'), (
'i',
'<i8'), (
'j',
'<i8'), (
'var',
'<f8'),
229 (
'cov',
'<f8'), (
'npix',
'<i8'), (
'ext',
'<i8'), (
'expTime',
'<f8'), (
'ampName',
'<U3')]
232 self.config.maximumRangeCovariancesAstier)
234 for ampName
in ampNames:
235 dummyPtcDataset.setAmpValues(ampName)
237 partialPtcDatasetList = []
240 for i
in range(len(inputDims)):
241 partialPtcDatasetList.append(dummyPtcDataset)
243 for expTime
in inputExp:
244 exposures = inputExp[expTime]
245 if len(exposures) == 1:
246 self.log.
warn(f
"Only one exposure found at expTime {expTime}. Dropping exposure "
247 f
"{exposures[0].getInfo().getVisitInfo().getExposureId()}.")
251 exp1, exp2 = exposures[0], exposures[1]
252 if len(exposures) > 2:
253 self.log.
warn(f
"Already found 2 exposures at expTime {expTime}. "
254 "Ignoring exposures: "
255 f
"{i.getInfo().getVisitInfo().getExposureId() for i in exposures[2:]}")
260 self.config.maximumRangeCovariancesAstier)
261 for ampNumber, amp
in enumerate(detector):
262 ampName = amp.getName()
264 doRealSpace = self.config.covAstierRealSpace
265 if self.config.detectorMeasurementRegion ==
'AMP':
266 region = amp.getBBox()
267 elif self.config.detectorMeasurementRegion ==
'FULL':
273 muDiff, varDiff, covAstier = self.
measureMeanVarCovmeasureMeanVarCov(exp1, exp2, region=region,
274 covAstierRealSpace=doRealSpace)
276 if np.isnan(muDiff)
or np.isnan(varDiff)
or (covAstier
is None):
277 msg = (f
"NaN mean or var, or None cov in amp {ampName} in exposure pair {expId1},"
278 f
" {expId2} of detector {detNum}.")
282 covArray = np.full((1, self.config.maximumRangeCovariancesAstier,
283 self.config.maximumRangeCovariancesAstier), np.nan)
284 covSqrtWeights = np.full_like(covArray, np.nan)
286 if (muDiff <= minMeanSignalDict[ampName])
or (muDiff >= maxMeanSignalDict[ampName]):
289 if covAstier
is not None:
290 tupleRows = [(muDiff, varDiff) + covRow + (ampNumber, expTime,
291 ampName)
for covRow
in covAstier]
292 tempStructArray = np.array(tupleRows, dtype=tags)
294 self.config.maximumRangeCovariancesAstier)
295 covSqrtWeights = np.nan_to_num(1./np.sqrt(vcov))
297 partialPtcDataset.setAmpValues(ampName, rawExpTime=[expTime], rawMean=[muDiff],
298 rawVar=[varDiff], inputExpIdPair=[(expId1, expId2)],
299 expIdMask=[expIdMask], covArray=covArray,
300 covSqrtWeights=covSqrtWeights)
311 datasetIndex = np.where(expId1 == np.array(inputDims))[0][0]
314 datasetIndex = np.where(expId1//1000 == np.array(inputDims))[0][0]
316 datasetIndex = np.where(expId1//1000 == np.array(inputDims)//1000)[0][0]
317 partialPtcDatasetList[datasetIndex] = partialPtcDataset
318 if nAmpsNan == len(ampNames):
319 msg = f
"NaN mean in all amps of exposure pair {expId1}, {expId2} of detector {detNum}."
321 return pipeBase.Struct(
322 outputCovariances=partialPtcDatasetList,
326 """Calculate the mean of each of two exposures and the variance
327 and covariance of their difference. The variance is calculated
328 via afwMath, and the covariance via the methods in Astier+19
329 (appendix A). In theory, var = covariance[0,0]. This should
330 be validated, and in the future, we may decide to just keep
335 exposure1 : `lsst.afw.image.exposure.exposure.ExposureF`
336 First exposure of flat field pair.
337 exposure2 : `lsst.afw.image.exposure.exposure.ExposureF`
338 Second exposure of flat field pair.
339 region : `lsst.geom.Box2I`, optional
340 Region of each exposure where to perform the calculations (e.g, an amplifier).
341 covAstierRealSpace : `bool`, optional
342 Should the covariannces in Astier+19 be calculated in real space or via FFT?
343 See Appendix A of Astier+19.
347 mu : `float` or `NaN`
348 0.5*(mu1 + mu2), where mu1, and mu2 are the clipped means of the regions in
349 both exposures. If either mu1 or m2 are NaN's, the returned value is NaN.
350 varDiff : `float` or `NaN`
351 Half of the clipped variance of the difference of the regions inthe two input
352 exposures. If either mu1 or m2 are NaN's, the returned value is NaN.
353 covDiffAstier : `list` or `NaN`
354 List with tuples of the form (dx, dy, var, cov, npix), where:
360 Variance at (dx, dy).
362 Covariance at (dx, dy).
364 Number of pixel pairs used to evaluate var and cov.
365 If either mu1 or m2 are NaN's, the returned value is NaN.
368 if region
is not None:
369 im1Area = exposure1.maskedImage[region]
370 im2Area = exposure2.maskedImage[region]
372 im1Area = exposure1.maskedImage
373 im2Area = exposure2.maskedImage
375 if self.config.binSize > 1:
379 im1MaskVal = exposure1.getMask().getPlaneBitMask(self.config.maskNameList)
381 self.config.nIterSigmaClipPtc,
383 im1StatsCtrl.setNanSafe(
True)
384 im1StatsCtrl.setAndMask(im1MaskVal)
386 im2MaskVal = exposure2.getMask().getPlaneBitMask(self.config.maskNameList)
388 self.config.nIterSigmaClipPtc,
390 im2StatsCtrl.setNanSafe(
True)
391 im2StatsCtrl.setAndMask(im2MaskVal)
396 if np.isnan(mu1)
or np.isnan(mu2):
397 self.log.
warn(f
"Mean of amp in image 1 or 2 is NaN: {mu1}, {mu2}.")
398 return np.nan, np.nan,
None
403 temp = im2Area.clone()
405 diffIm = im1Area.clone()
410 diffImMaskVal = diffIm.getMask().getPlaneBitMask(self.config.maskNameList)
412 self.config.nIterSigmaClipPtc,
414 diffImStatsCtrl.setNanSafe(
True)
415 diffImStatsCtrl.setAndMask(diffImMaskVal)
422 w1 = np.where(im1Area.getMask().getArray() == 0, 1, 0)
423 w2 = np.where(im2Area.getMask().getArray() == 0, 1, 0)
426 wDiff = np.where(diffIm.getMask().getArray() == 0, 1, 0)
429 if np.sum(w) < self.config.minNumberGoodPixelsForCovariance:
430 self.log.
warn(f
"Number of good points for covariance calculation ({np.sum(w)}) is less "
431 f
"(than threshold {self.config.minNumberGoodPixelsForCovariance})")
432 return np.nan, np.nan,
None
434 maxRangeCov = self.config.maximumRangeCovariancesAstier
435 if covAstierRealSpace:
440 shapeDiff = np.array(diffIm.image.array.shape)
442 s = shapeDiff + maxRangeCov
443 tempSize = np.array(np.log(s)/np.log(2.)).astype(int)
444 fftSize = np.array(2**(tempSize+1)).astype(int)
445 fftShape = (fftSize[0], fftSize[1])
448 covDiffAstier = c.reportCovFastFourierTransform(maxRangeCov)
450 return mu, varDiff, covDiffAstier
Pass parameters to a Statistics object.
daf::base::PropertyList * list
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
def makeCovArray(inputTuple, maxRangeFromTuple=8)
def computeCovDirect(diffImage, weightImage, maxRange)
def arrangeFlatsByExpId(exposureList)
def arrangeFlatsByExpTime(exposureList)
def getExposureId(self, dataRef)