Coverage for python/lsst/cp/pipe/ptc/cpExtractPtcTask.py: 15%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

172 statements  

1# This file is part of cp_pipe. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21# 

22import numpy as np 

23 

24import lsst.afw.math as afwMath 

25import lsst.pex.config as pexConfig 

26import lsst.pipe.base as pipeBase 

27from lsst.cp.pipe.utils import (arrangeFlatsByExpTime, arrangeFlatsByExpId, 

28 sigmaClipCorrection) 

29 

30import lsst.pipe.base.connectionTypes as cT 

31 

32from .astierCovPtcUtils import (CovFastFourierTransform, computeCovDirect) 

33from .astierCovPtcFit import makeCovArray 

34 

35from lsst.ip.isr import PhotonTransferCurveDataset 

36from lsst.ip.isr import IsrTask 

37 

38__all__ = ['PhotonTransferCurveExtractConfig', 'PhotonTransferCurveExtractTask'] 

39 

40 

41class PhotonTransferCurveExtractConnections(pipeBase.PipelineTaskConnections, 

42 dimensions=("instrument", "detector")): 

43 

44 inputExp = cT.Input( 

45 name="ptcInputExposurePairs", 

46 doc="Input post-ISR processed exposure pairs (flats) to" 

47 "measure covariances from.", 

48 storageClass="Exposure", 

49 dimensions=("instrument", "exposure", "detector"), 

50 multiple=True, 

51 deferLoad=False, 

52 ) 

53 

54 outputCovariances = cT.Output( 

55 name="ptcCovariances", 

56 doc="Extracted flat (co)variances.", 

57 storageClass="PhotonTransferCurveDataset", 

58 dimensions=("instrument", "exposure", "detector"), 

59 multiple=True, 

60 ) 

61 

62 

63class PhotonTransferCurveExtractConfig(pipeBase.PipelineTaskConfig, 

64 pipelineConnections=PhotonTransferCurveExtractConnections): 

65 """Configuration for the measurement of covariances from flats. 

66 """ 

67 

68 matchByExposureId = pexConfig.Field( 

69 dtype=bool, 

70 doc="Should exposures be matched by ID rather than exposure time?", 

71 default=False, 

72 ) 

73 maximumRangeCovariancesAstier = pexConfig.Field( 

74 dtype=int, 

75 doc="Maximum range of covariances as in Astier+19", 

76 default=8, 

77 ) 

78 covAstierRealSpace = pexConfig.Field( 

79 dtype=bool, 

80 doc="Calculate covariances in real space or via FFT? (see appendix A of Astier+19).", 

81 default=False, 

82 ) 

83 binSize = pexConfig.Field( 

84 dtype=int, 

85 doc="Bin the image by this factor in both dimensions.", 

86 default=1, 

87 ) 

88 minMeanSignal = pexConfig.DictField( 

89 keytype=str, 

90 itemtype=float, 

91 doc="Minimum values (inclusive) of mean signal (in ADU) above which to consider, per amp." 

92 " The same cut is applied to all amps if this dictionary is of the form" 

93 " {'ALL_AMPS': value}", 

94 default={'ALL_AMPS': 0.0}, 

95 ) 

96 maxMeanSignal = pexConfig.DictField( 

97 keytype=str, 

98 itemtype=float, 

99 doc="Maximum values (inclusive) of mean signal (in ADU) below which to consider, per amp." 

100 " The same cut is applied to all amps if this dictionary is of the form" 

101 " {'ALL_AMPS': value}", 

102 default={'ALL_AMPS': 1e6}, 

103 ) 

104 maskNameList = pexConfig.ListField( 

105 dtype=str, 

106 doc="Mask list to exclude from statistics calculations.", 

107 default=['SUSPECT', 'BAD', 'NO_DATA', 'SAT'], 

108 ) 

109 nSigmaClipPtc = pexConfig.Field( 

110 dtype=float, 

111 doc="Sigma cut for afwMath.StatisticsControl()", 

112 default=5.5, 

113 ) 

114 nIterSigmaClipPtc = pexConfig.Field( 

115 dtype=int, 

116 doc="Number of sigma-clipping iterations for afwMath.StatisticsControl()", 

117 default=3, 

118 ) 

119 minNumberGoodPixelsForCovariance = pexConfig.Field( 

120 dtype=int, 

121 doc="Minimum number of acceptable good pixels per amp to calculate the covariances (via FFT or" 

122 " direclty).", 

123 default=10000, 

124 ) 

125 thresholdDiffAfwVarVsCov00 = pexConfig.Field( 

126 dtype=float, 

127 doc="If the absolute fractional differece between afwMath.VARIANCECLIP and Cov00 " 

128 "for a region of a difference image is greater than this threshold (percentage), " 

129 "a warning will be issued.", 

130 default=1., 

131 ) 

132 detectorMeasurementRegion = pexConfig.ChoiceField( 

133 dtype=str, 

134 doc="Region of each exposure where to perform the calculations (amplifier or full image).", 

135 default='AMP', 

136 allowed={ 

137 "AMP": "Amplifier of the detector.", 

138 "FULL": "Full image." 

139 } 

140 ) 

141 numEdgeSuspect = pexConfig.Field( 

142 dtype=int, 

143 doc="Number of edge pixels to be flagged as untrustworthy.", 

144 default=0, 

145 ) 

146 edgeMaskLevel = pexConfig.ChoiceField( 

147 dtype=str, 

148 doc="Mask edge pixels in which coordinate frame: DETECTOR or AMP?", 

149 default="DETECTOR", 

150 allowed={ 

151 'DETECTOR': 'Mask only the edges of the full detector.', 

152 'AMP': 'Mask edges of each amplifier.', 

153 }, 

154 ) 

155 

156 

157class PhotonTransferCurveExtractTask(pipeBase.PipelineTask, 

158 pipeBase.CmdLineTask): 

159 """Task to measure covariances from flat fields. 

160 

161 This task receives as input a list of flat-field images 

162 (flats), and sorts these flats in pairs taken at the 

163 same time (if there's a different number of flats, 

164 those flats are discarded). The mean, variance, and 

165 covariances are measured from the difference of the flat 

166 pairs at a given time. The variance is calculated 

167 via afwMath, and the covariance via the methods in Astier+19 

168 (appendix A). In theory, var = covariance[0,0]. This should 

169 be validated, and in the future, we may decide to just keep 

170 one (covariance). 

171 

172 The measured covariances at a particular time (along with other 

173 quantities such as the mean) are stored in a PTC dataset object 

174 (`~lsst.ip.isr.PhotonTransferCurveDataset`), which gets 

175 partially filled. The number of partially-filled PTC dataset 

176 objects will be less than the number of input exposures, but gen3 

177 requires/assumes that the number of input dimensions matches 

178 bijectively the number of output dimensions. Therefore, a number 

179 of "dummy" PTC dataset are inserted in the output list that has 

180 the partially-filled PTC datasets with the covariances. This 

181 output list will be used as input of 

182 ``PhotonTransferCurveSolveTask``, which will assemble the multiple 

183 ``PhotonTransferCurveDataset`` into a single one in order to fit 

184 the measured covariances as a function of flux to a particular 

185 model. 

186 

187 Astier+19: "The Shape of the Photon Transfer Curve of CCD 

188 sensors", arXiv:1905.08677. 

189 """ 

190 

191 ConfigClass = PhotonTransferCurveExtractConfig 

192 _DefaultName = 'cpPtcExtract' 

193 

194 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

195 """Ensure that the input and output dimensions are passed along. 

196 

197 Parameters 

198 ---------- 

199 butlerQC : `~lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext` 

200 Butler to operate on. 

201 inputRefs : `~lsst.pipe.base.connections.InputQuantizedConnection` 

202 Input data refs to load. 

203 ouptutRefs : `~lsst.pipe.base.connections.OutputQuantizedConnection` 

204 Output data refs to persist. 

205 """ 

206 inputs = butlerQC.get(inputRefs) 

207 # Ids of input list of exposures 

208 inputs['inputDims'] = [expId.dataId['exposure'] for expId in inputRefs.inputExp] 

209 

210 # Dictionary, keyed by expTime, with tuples containing flat 

211 # exposures and their IDs. 

212 if self.config.matchByExposureId: 

213 inputs['inputExp'] = arrangeFlatsByExpId(inputs['inputExp'], inputs['inputDims']) 

214 else: 

215 inputs['inputExp'] = arrangeFlatsByExpTime(inputs['inputExp'], inputs['inputDims']) 

216 

217 outputs = self.run(**inputs) 

218 butlerQC.put(outputs, outputRefs) 

219 

220 def run(self, inputExp, inputDims): 

221 """Measure covariances from difference of flat pairs 

222 

223 Parameters 

224 ---------- 

225 inputExp : `dict` [`float`, `list` [`~lsst.afw.image.ExposureF`]] 

226 Dictionary that groups flat-field exposures that have the same 

227 exposure time (seconds). 

228 

229 inputDims : `list` 

230 List of exposure IDs. 

231 

232 Returns 

233 ------- 

234 results : `lsst.pipe.base.Struct` 

235 The results struct containing: 

236 

237 ``outputCovariances`` 

238 A list containing the per-pair PTC measurements (`list` 

239 [`lsst.ip.isr.PhotonTransferCurveDataset`]) 

240 """ 

241 # inputExp.values() returns a view, which we turn into a list. We then 

242 # access the first exposure-ID tuple to get the detector. 

243 detector = list(inputExp.values())[0][0][0].getDetector() 

244 detNum = detector.getId() 

245 amps = detector.getAmplifiers() 

246 ampNames = [amp.getName() for amp in amps] 

247 

248 # Each amp may have a different min and max ADU signal 

249 # specified in the config. 

250 maxMeanSignalDict = {ampName: 1e6 for ampName in ampNames} 

251 minMeanSignalDict = {ampName: 0.0 for ampName in ampNames} 

252 for ampName in ampNames: 

253 if 'ALL_AMPS' in self.config.maxMeanSignal: 

254 maxMeanSignalDict[ampName] = self.config.maxMeanSignal['ALL_AMPS'] 

255 elif ampName in self.config.maxMeanSignal: 

256 maxMeanSignalDict[ampName] = self.config.maxMeanSignal[ampName] 

257 

258 if 'ALL_AMPS' in self.config.minMeanSignal: 

259 minMeanSignalDict[ampName] = self.config.minMeanSignal['ALL_AMPS'] 

260 elif ampName in self.config.minMeanSignal: 

261 minMeanSignalDict[ampName] = self.config.minMeanSignal[ampName] 

262 # These are the column names for `tupleRows` below. 

263 tags = [('mu', '<f8'), ('afwVar', '<f8'), ('i', '<i8'), ('j', '<i8'), ('var', '<f8'), 

264 ('cov', '<f8'), ('npix', '<i8'), ('ext', '<i8'), ('expTime', '<f8'), ('ampName', '<U3')] 

265 # Create a dummy ptcDataset 

266 dummyPtcDataset = PhotonTransferCurveDataset(ampNames, 'DUMMY', 

267 self.config.maximumRangeCovariancesAstier) 

268 # Initialize amps of `dummyPtcDatset`. 

269 for ampName in ampNames: 

270 dummyPtcDataset.setAmpValues(ampName) 

271 # Output list with PTC datasets. 

272 partialPtcDatasetList = [] 

273 # The number of output references needs to match that of input 

274 # references: initialize outputlist with dummy PTC datasets. 

275 for i in range(len(inputDims)): 

276 partialPtcDatasetList.append(dummyPtcDataset) 

277 

278 if self.config.numEdgeSuspect > 0: 

279 isrTask = IsrTask() 

280 self.log.info("Masking %d pixels from the edges of all exposures as SUSPECT.", 

281 self.config.numEdgeSuspect) 

282 

283 for expTime in inputExp: 

284 exposures = inputExp[expTime] 

285 if len(exposures) == 1: 

286 self.log.warning("Only one exposure found at expTime %f. Dropping exposure %d.", 

287 expTime, exposures[0][1]) 

288 continue 

289 else: 

290 # Only use the first two exposures at expTime. Each 

291 # elements is a tuple (exposure, expId) 

292 exp1, expId1 = exposures[0] 

293 exp2, expId2 = exposures[1] 

294 if len(exposures) > 2: 

295 self.log.warning("Already found 2 exposures at expTime %f. Ignoring exposures: %s", 

296 expTime, ", ".join(str(i[1]) for i in exposures[2:])) 

297 # Mask pixels at the edge of the detector or of each amp 

298 if self.config.numEdgeSuspect > 0: 

299 isrTask.maskEdges(exp1, numEdgePixels=self.config.numEdgeSuspect, 

300 maskPlane="SUSPECT", level=self.config.edgeMaskLevel) 

301 isrTask.maskEdges(exp2, numEdgePixels=self.config.numEdgeSuspect, 

302 maskPlane="SUSPECT", level=self.config.edgeMaskLevel) 

303 

304 nAmpsNan = 0 

305 partialPtcDataset = PhotonTransferCurveDataset(ampNames, 'PARTIAL', 

306 self.config.maximumRangeCovariancesAstier) 

307 for ampNumber, amp in enumerate(detector): 

308 ampName = amp.getName() 

309 # covAstier: [(i, j, var (cov[0,0]), cov, npix) for 

310 # (i,j) in {maxLag, maxLag}^2] 

311 doRealSpace = self.config.covAstierRealSpace 

312 if self.config.detectorMeasurementRegion == 'AMP': 

313 region = amp.getBBox() 

314 elif self.config.detectorMeasurementRegion == 'FULL': 

315 region = None 

316 # `measureMeanVarCov` is the function that measures 

317 # the variance and covariances from the difference 

318 # image of two flats at the same exposure time. The 

319 # variable `covAstier` is of the form: [(i, j, var 

320 # (cov[0,0]), cov, npix) for (i,j) in {maxLag, 

321 # maxLag}^2] 

322 muDiff, varDiff, covAstier = self.measureMeanVarCov(exp1, exp2, region=region, 

323 covAstierRealSpace=doRealSpace) 

324 # Correction factor for sigma clipping. Function 

325 # returns 1/sqrt(varFactor), so it needs to be 

326 # squared. varDiff is calculated via 

327 # afwMath.VARIANCECLIP. 

328 varFactor = sigmaClipCorrection(self.config.nSigmaClipPtc)**2 

329 varDiff *= varFactor 

330 

331 expIdMask = True 

332 if np.isnan(muDiff) or np.isnan(varDiff) or (covAstier is None): 

333 msg = ("NaN mean or var, or None cov in amp %s in exposure pair %d, %d of detector %d.", 

334 ampName, expId1, expId2, detNum) 

335 self.log.warning(msg) 

336 nAmpsNan += 1 

337 expIdMask = False 

338 covArray = np.full((1, self.config.maximumRangeCovariancesAstier, 

339 self.config.maximumRangeCovariancesAstier), np.nan) 

340 covSqrtWeights = np.full_like(covArray, np.nan) 

341 

342 if (muDiff <= minMeanSignalDict[ampName]) or (muDiff >= maxMeanSignalDict[ampName]): 

343 expIdMask = False 

344 

345 if covAstier is not None: 

346 tupleRows = [(muDiff, varDiff) + covRow + (ampNumber, expTime, 

347 ampName) for covRow in covAstier] 

348 tempStructArray = np.array(tupleRows, dtype=tags) 

349 covArray, vcov, _ = makeCovArray(tempStructArray, 

350 self.config.maximumRangeCovariancesAstier) 

351 covSqrtWeights = np.nan_to_num(1./np.sqrt(vcov)) 

352 

353 # Correct covArray for sigma clipping: 

354 # 1) Apply varFactor twice for the whole covariance matrix 

355 covArray *= varFactor**2 

356 # 2) But, only once for the variance element of the 

357 # matrix, covArray[0,0] 

358 covArray[0, 0] /= varFactor 

359 

360 partialPtcDataset.setAmpValues(ampName, rawExpTime=[expTime], rawMean=[muDiff], 

361 rawVar=[varDiff], inputExpIdPair=[(expId1, expId2)], 

362 expIdMask=[expIdMask], covArray=covArray, 

363 covSqrtWeights=covSqrtWeights) 

364 # Use location of exp1 to save PTC dataset from (exp1, exp2) pair. 

365 # Below, np.where(expId1 == np.array(inputDims)) returns a tuple 

366 # with a single-element array, so [0][0] 

367 # is necessary to extract the required index. 

368 datasetIndex = np.where(expId1 == np.array(inputDims))[0][0] 

369 partialPtcDatasetList[datasetIndex] = partialPtcDataset 

370 

371 if nAmpsNan == len(ampNames): 

372 msg = f"NaN mean in all amps of exposure pair {expId1}, {expId2} of detector {detNum}." 

373 self.log.warning(msg) 

374 return pipeBase.Struct( 

375 outputCovariances=partialPtcDatasetList, 

376 ) 

377 

378 def measureMeanVarCov(self, exposure1, exposure2, region=None, covAstierRealSpace=False): 

379 """Calculate the mean of each of two exposures and the variance 

380 and covariance of their difference. The variance is calculated 

381 via afwMath, and the covariance via the methods in Astier+19 

382 (appendix A). In theory, var = covariance[0,0]. This should 

383 be validated, and in the future, we may decide to just keep 

384 one (covariance). 

385 

386 Parameters 

387 ---------- 

388 exposure1 : `lsst.afw.image.exposure.ExposureF` 

389 First exposure of flat field pair. 

390 exposure2 : `lsst.afw.image.exposure.ExposureF` 

391 Second exposure of flat field pair. 

392 region : `lsst.geom.Box2I`, optional 

393 Region of each exposure where to perform the calculations 

394 (e.g, an amplifier). 

395 covAstierRealSpace : `bool`, optional 

396 Should the covariannces in Astier+19 be calculated in real 

397 space or via FFT? See Appendix A of Astier+19. 

398 

399 Returns 

400 ------- 

401 mu : `float` or `NaN` 

402 0.5*(mu1 + mu2), where mu1, and mu2 are the clipped means 

403 of the regions in both exposures. If either mu1 or m2 are 

404 NaN's, the returned value is NaN. 

405 varDiff : `float` or `NaN` 

406 Half of the clipped variance of the difference of the 

407 regions inthe two input exposures. If either mu1 or m2 are 

408 NaN's, the returned value is NaN. 

409 covDiffAstier : `list` or `NaN` 

410 List with tuples of the form (dx, dy, var, cov, npix), where: 

411 dx : `int` 

412 Lag in x 

413 dy : `int` 

414 Lag in y 

415 var : `float` 

416 Variance at (dx, dy). 

417 cov : `float` 

418 Covariance at (dx, dy). 

419 nPix : `int` 

420 Number of pixel pairs used to evaluate var and cov. 

421 

422 If either mu1 or m2 are NaN's, the returned value is NaN. 

423 """ 

424 if region is not None: 

425 im1Area = exposure1.maskedImage[region] 

426 im2Area = exposure2.maskedImage[region] 

427 else: 

428 im1Area = exposure1.maskedImage 

429 im2Area = exposure2.maskedImage 

430 

431 if self.config.binSize > 1: 

432 im1Area = afwMath.binImage(im1Area, self.config.binSize) 

433 im2Area = afwMath.binImage(im2Area, self.config.binSize) 

434 

435 im1MaskVal = exposure1.getMask().getPlaneBitMask(self.config.maskNameList) 

436 im1StatsCtrl = afwMath.StatisticsControl(self.config.nSigmaClipPtc, 

437 self.config.nIterSigmaClipPtc, 

438 im1MaskVal) 

439 im1StatsCtrl.setNanSafe(True) 

440 im1StatsCtrl.setAndMask(im1MaskVal) 

441 

442 im2MaskVal = exposure2.getMask().getPlaneBitMask(self.config.maskNameList) 

443 im2StatsCtrl = afwMath.StatisticsControl(self.config.nSigmaClipPtc, 

444 self.config.nIterSigmaClipPtc, 

445 im2MaskVal) 

446 im2StatsCtrl.setNanSafe(True) 

447 im2StatsCtrl.setAndMask(im2MaskVal) 

448 

449 # Clipped mean of images; then average of mean. 

450 mu1 = afwMath.makeStatistics(im1Area, afwMath.MEANCLIP, im1StatsCtrl).getValue() 

451 mu2 = afwMath.makeStatistics(im2Area, afwMath.MEANCLIP, im2StatsCtrl).getValue() 

452 if np.isnan(mu1) or np.isnan(mu2): 

453 self.log.warning("Mean of amp in image 1 or 2 is NaN: %f, %f.", mu1, mu2) 

454 return np.nan, np.nan, None 

455 mu = 0.5*(mu1 + mu2) 

456 

457 # Take difference of pairs 

458 # symmetric formula: diff = (mu2*im1-mu1*im2)/(0.5*(mu1+mu2)) 

459 temp = im2Area.clone() 

460 temp *= mu1 

461 diffIm = im1Area.clone() 

462 diffIm *= mu2 

463 diffIm -= temp 

464 diffIm /= mu 

465 

466 diffImMaskVal = diffIm.getMask().getPlaneBitMask(self.config.maskNameList) 

467 diffImStatsCtrl = afwMath.StatisticsControl(self.config.nSigmaClipPtc, 

468 self.config.nIterSigmaClipPtc, 

469 diffImMaskVal) 

470 diffImStatsCtrl.setNanSafe(True) 

471 diffImStatsCtrl.setAndMask(diffImMaskVal) 

472 

473 # Variance calculation via afwMath 

474 varDiff = 0.5*(afwMath.makeStatistics(diffIm, afwMath.VARIANCECLIP, diffImStatsCtrl).getValue()) 

475 

476 # Covariances calculations 

477 # Get the pixels that were not clipped 

478 varClip = afwMath.makeStatistics(diffIm, afwMath.VARIANCECLIP, diffImStatsCtrl).getValue() 

479 meanClip = afwMath.makeStatistics(diffIm, afwMath.MEANCLIP, diffImStatsCtrl).getValue() 

480 cut = meanClip + self.config.nSigmaClipPtc*np.sqrt(varClip) 

481 unmasked = np.where(np.fabs(diffIm.image.array) <= cut, 1, 0) 

482 

483 # Get the pixels in the mask planes of the difference image 

484 # that were ignored by the clipping algorithm 

485 wDiff = np.where(diffIm.getMask().getArray() == 0, 1, 0) 

486 # Combine the two sets of pixels ('1': use; '0': don't use) 

487 # into a final weight matrix to be used in the covariance 

488 # calculations below. 

489 w = unmasked*wDiff 

490 

491 if np.sum(w) < self.config.minNumberGoodPixelsForCovariance: 

492 self.log.warning("Number of good points for covariance calculation (%s) is less " 

493 "(than threshold %s)", np.sum(w), self.config.minNumberGoodPixelsForCovariance) 

494 return np.nan, np.nan, None 

495 

496 maxRangeCov = self.config.maximumRangeCovariancesAstier 

497 if covAstierRealSpace: 

498 # Calculate covariances in real space. 

499 covDiffAstier = computeCovDirect(diffIm.image.array, w, maxRangeCov) 

500 else: 

501 # Calculate covariances via FFT (default). 

502 shapeDiff = np.array(diffIm.image.array.shape) 

503 # Calculate the sizes of FFT dimensions. 

504 s = shapeDiff + maxRangeCov 

505 tempSize = np.array(np.log(s)/np.log(2.)).astype(int) 

506 fftSize = np.array(2**(tempSize+1)).astype(int) 

507 fftShape = (fftSize[0], fftSize[1]) 

508 

509 c = CovFastFourierTransform(diffIm.image.array, w, fftShape, maxRangeCov) 

510 covDiffAstier = c.reportCovFastFourierTransform(maxRangeCov) 

511 

512 # Compare Cov[0,0] and afwMath.VARIANCECLIP covDiffAstier[0] 

513 # is the Cov[0,0] element, [3] is the variance, and there's a 

514 # factor of 0.5 difference with afwMath.VARIANCECLIP. 

515 thresholdPercentage = self.config.thresholdDiffAfwVarVsCov00 

516 fractionalDiff = 100*np.fabs(1 - varDiff/(covDiffAstier[0][3]*0.5)) 

517 if fractionalDiff >= thresholdPercentage: 

518 self.log.warning("Absolute fractional difference between afwMatch.VARIANCECLIP and Cov[0,0] " 

519 "is more than %f%%: %f", thresholdPercentage, fractionalDiff) 

520 

521 return mu, varDiff, covDiffAstier