Coverage for python/lsst/ip/diffim/utils.py: 4%

691 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-27 02:35 -0700

1# This file is part of ip_diffim. 

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 

22"""Support utilities for Measuring sources""" 

23 

24# Export DipoleTestImage to expose fake image generating funcs 

25__all__ = ["DipoleTestImage", "getPsfFwhm"] 

26 

27import numpy as np 

28 

29import lsst.geom as geom 

30import lsst.afw.detection as afwDet 

31import lsst.afw.display as afwDisplay 

32import lsst.afw.geom as afwGeom 

33import lsst.afw.image as afwImage 

34import lsst.afw.math as afwMath 

35import lsst.afw.table as afwTable 

36import lsst.meas.algorithms as measAlg 

37import lsst.meas.base as measBase 

38from lsst.utils.logging import getLogger 

39from .dipoleFitTask import DipoleFitAlgorithm 

40from . import diffimLib 

41from . import diffimTools 

42 

43afwDisplay.setDefaultMaskTransparency(75) 

44keptPlots = False # Have we arranged to keep spatial plots open? 

45 

46 

47def showSourceSet(sSet, xy0=(0, 0), frame=0, ctype=afwDisplay.GREEN, symb="+", size=2): 

48 """Draw the (XAstrom, YAstrom) positions of a set of Sources. 

49 

50 Image has the given XY0. 

51 """ 

52 disp = afwDisplay.afwDisplay(frame=frame) 

53 with disp.Buffering(): 

54 for s in sSet: 

55 xc, yc = s.getXAstrom() - xy0[0], s.getYAstrom() - xy0[1] 

56 

57 if symb == "id": 

58 disp.dot(str(s.getId()), xc, yc, ctype=ctype, size=size) 

59 else: 

60 disp.dot(symb, xc, yc, ctype=ctype, size=size) 

61 

62 

63# Kernel display utilities 

64# 

65 

66 

67def showKernelSpatialCells(maskedIm, kernelCellSet, showChi2=False, symb="o", 

68 ctype=None, ctypeUnused=None, ctypeBad=None, size=3, 

69 frame=None, title="Spatial Cells"): 

70 """Show the SpatialCells. 

71 

72 If symb is something that display.dot understands (e.g. "o"), the top 

73 nMaxPerCell candidates will be indicated with that symbol, using ctype 

74 and size. 

75 """ 

76 disp = afwDisplay.Display(frame=frame) 

77 disp.mtv(maskedIm, title=title) 

78 with disp.Buffering(): 

79 origin = [-maskedIm.getX0(), -maskedIm.getY0()] 

80 for cell in kernelCellSet.getCellList(): 

81 afwDisplay.utils.drawBBox(cell.getBBox(), origin=origin, display=disp) 

82 

83 goodies = ctypeBad is None 

84 for cand in cell.begin(goodies): 

85 xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1] 

86 if cand.getStatus() == afwMath.SpatialCellCandidate.BAD: 

87 color = ctypeBad 

88 elif cand.getStatus() == afwMath.SpatialCellCandidate.GOOD: 

89 color = ctype 

90 elif cand.getStatus() == afwMath.SpatialCellCandidate.UNKNOWN: 

91 color = ctypeUnused 

92 else: 

93 continue 

94 

95 if color: 

96 disp.dot(symb, xc, yc, ctype=color, size=size) 

97 

98 if showChi2: 

99 rchi2 = cand.getChi2() 

100 if rchi2 > 1e100: 

101 rchi2 = np.nan 

102 disp.dot("%d %.1f" % (cand.getId(), rchi2), 

103 xc - size, yc - size - 4, ctype=color, size=size) 

104 

105 

106def showDiaSources(sources, exposure, isFlagged, isDipole, frame=None): 

107 """Display Dia Sources. 

108 """ 

109 # 

110 # Show us the ccandidates 

111 # 

112 # Too many mask planes in diffims 

113 disp = afwDisplay.Display(frame=frame) 

114 for plane in ("BAD", "CR", "EDGE", "INTERPOlATED", "INTRP", "SAT", "SATURATED"): 

115 disp.setMaskPlaneColor(plane, color="ignore") 

116 

117 mos = afwDisplay.utils.Mosaic() 

118 for i in range(len(sources)): 

119 source = sources[i] 

120 badFlag = isFlagged[i] 

121 dipoleFlag = isDipole[i] 

122 bbox = source.getFootprint().getBBox() 

123 stamp = exposure.Factory(exposure, bbox, True) 

124 im = afwDisplay.utils.Mosaic(gutter=1, background=0, mode="x") 

125 im.append(stamp.getMaskedImage()) 

126 lab = "%.1f,%.1f:" % (source.getX(), source.getY()) 

127 if badFlag: 

128 ctype = afwDisplay.RED 

129 lab += "BAD" 

130 if dipoleFlag: 

131 ctype = afwDisplay.YELLOW 

132 lab += "DIPOLE" 

133 if not badFlag and not dipoleFlag: 

134 ctype = afwDisplay.GREEN 

135 lab += "OK" 

136 mos.append(im.makeMosaic(), lab, ctype) 

137 title = "Dia Sources" 

138 mosaicImage = mos.makeMosaic(display=disp, title=title) 

139 return mosaicImage 

140 

141 

142def showKernelCandidates(kernelCellSet, kernel, background, frame=None, showBadCandidates=True, 

143 resids=False, kernels=False): 

144 """Display the Kernel candidates. 

145 

146 If kernel is provided include spatial model and residuals; 

147 If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi. 

148 """ 

149 # 

150 # Show us the ccandidates 

151 # 

152 if kernels: 

153 mos = afwDisplay.utils.Mosaic(gutter=5, background=0) 

154 else: 

155 mos = afwDisplay.utils.Mosaic(gutter=5, background=-1) 

156 # 

157 candidateCenters = [] 

158 candidateCentersBad = [] 

159 candidateIndex = 0 

160 for cell in kernelCellSet.getCellList(): 

161 for cand in cell.begin(False): # include bad candidates 

162 # Original difference image; if does not exist, skip candidate 

163 try: 

164 resid = cand.getDifferenceImage(diffimLib.KernelCandidateF.ORIG) 

165 except Exception: 

166 continue 

167 

168 rchi2 = cand.getChi2() 

169 if rchi2 > 1e100: 

170 rchi2 = np.nan 

171 

172 if not showBadCandidates and cand.isBad(): 

173 continue 

174 

175 im_resid = afwDisplay.utils.Mosaic(gutter=1, background=-0.5, mode="x") 

176 

177 try: 

178 im = cand.getScienceMaskedImage() 

179 im = im.Factory(im, True) 

180 im.setXY0(cand.getScienceMaskedImage().getXY0()) 

181 except Exception: 

182 continue 

183 if (not resids and not kernels): 

184 im_resid.append(im.Factory(im, True)) 

185 try: 

186 im = cand.getTemplateMaskedImage() 

187 im = im.Factory(im, True) 

188 im.setXY0(cand.getTemplateMaskedImage().getXY0()) 

189 except Exception: 

190 continue 

191 if (not resids and not kernels): 

192 im_resid.append(im.Factory(im, True)) 

193 

194 # Difference image with original basis 

195 if resids: 

196 var = resid.getVariance() 

197 var = var.Factory(var, True) 

198 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt 

199 resid = resid.getImage() 

200 resid /= var 

201 bbox = kernel.shrinkBBox(resid.getBBox()) 

202 resid = resid.Factory(resid, bbox, deep=True) 

203 elif kernels: 

204 kim = cand.getKernelImage(diffimLib.KernelCandidateF.ORIG).convertF() 

205 resid = kim.Factory(kim, True) 

206 im_resid.append(resid) 

207 

208 # residuals using spatial model 

209 ski = afwImage.ImageD(kernel.getDimensions()) 

210 kernel.computeImage(ski, False, int(cand.getXCenter()), int(cand.getYCenter())) 

211 sk = afwMath.FixedKernel(ski) 

212 sbg = 0.0 

213 if background: 

214 sbg = background(int(cand.getXCenter()), int(cand.getYCenter())) 

215 sresid = cand.getDifferenceImage(sk, sbg) 

216 resid = sresid 

217 if resids: 

218 resid = sresid.getImage() 

219 resid /= var 

220 bbox = kernel.shrinkBBox(resid.getBBox()) 

221 resid = resid.Factory(resid, bbox, deep=True) 

222 elif kernels: 

223 kim = ski.convertF() 

224 resid = kim.Factory(kim, True) 

225 im_resid.append(resid) 

226 

227 im = im_resid.makeMosaic() 

228 

229 lab = "%d chi^2 %.1f" % (cand.getId(), rchi2) 

230 ctype = afwDisplay.RED if cand.isBad() else afwDisplay.GREEN 

231 

232 mos.append(im, lab, ctype) 

233 

234 if False and np.isnan(rchi2): 

235 disp = afwDisplay.Display(frame=1) 

236 disp.mtv(cand.getScienceMaskedImage.getImage(), title="candidate") 

237 print("rating", cand.getCandidateRating()) 

238 

239 im = cand.getScienceMaskedImage() 

240 center = (candidateIndex, cand.getXCenter() - im.getX0(), cand.getYCenter() - im.getY0()) 

241 candidateIndex += 1 

242 if cand.isBad(): 

243 candidateCentersBad.append(center) 

244 else: 

245 candidateCenters.append(center) 

246 

247 if resids: 

248 title = "chi Diffim" 

249 elif kernels: 

250 title = "Kernels" 

251 else: 

252 title = "Candidates & residuals" 

253 

254 disp = afwDisplay.Display(frame=frame) 

255 mosaicImage = mos.makeMosaic(display=disp, title=title) 

256 

257 return mosaicImage 

258 

259 

260def showKernelBasis(kernel, frame=None): 

261 """Display a Kernel's basis images. 

262 """ 

263 mos = afwDisplay.utils.Mosaic() 

264 

265 for k in kernel.getKernelList(): 

266 im = afwImage.ImageD(k.getDimensions()) 

267 k.computeImage(im, False) 

268 mos.append(im) 

269 

270 disp = afwDisplay.Display(frame=frame) 

271 mos.makeMosaic(display=disp, title="Kernel Basis Images") 

272 

273 return mos 

274 

275############### 

276 

277 

278def plotKernelSpatialModel(kernel, kernelCellSet, showBadCandidates=True, 

279 numSample=128, keepPlots=True, maxCoeff=10): 

280 """Plot the Kernel spatial model. 

281 """ 

282 try: 

283 import matplotlib.pyplot as plt 

284 import matplotlib.colors 

285 except ImportError as e: 

286 print("Unable to import numpy and matplotlib: %s" % e) 

287 return 

288 

289 x0 = kernelCellSet.getBBox().getBeginX() 

290 y0 = kernelCellSet.getBBox().getBeginY() 

291 

292 candPos = list() 

293 candFits = list() 

294 badPos = list() 

295 badFits = list() 

296 candAmps = list() 

297 badAmps = list() 

298 for cell in kernelCellSet.getCellList(): 

299 for cand in cell.begin(False): 

300 if not showBadCandidates and cand.isBad(): 

301 continue 

302 candCenter = geom.PointD(cand.getXCenter(), cand.getYCenter()) 

303 try: 

304 im = cand.getTemplateMaskedImage() 

305 except Exception: 

306 continue 

307 

308 targetFits = badFits if cand.isBad() else candFits 

309 targetPos = badPos if cand.isBad() else candPos 

310 targetAmps = badAmps if cand.isBad() else candAmps 

311 

312 # compare original and spatial kernel coefficients 

313 kp0 = np.array(cand.getKernel(diffimLib.KernelCandidateF.ORIG).getKernelParameters()) 

314 amp = cand.getCandidateRating() 

315 

316 targetFits = badFits if cand.isBad() else candFits 

317 targetPos = badPos if cand.isBad() else candPos 

318 targetAmps = badAmps if cand.isBad() else candAmps 

319 

320 targetFits.append(kp0) 

321 targetPos.append(candCenter) 

322 targetAmps.append(amp) 

323 

324 xGood = np.array([pos.getX() for pos in candPos]) - x0 

325 yGood = np.array([pos.getY() for pos in candPos]) - y0 

326 zGood = np.array(candFits) 

327 

328 xBad = np.array([pos.getX() for pos in badPos]) - x0 

329 yBad = np.array([pos.getY() for pos in badPos]) - y0 

330 zBad = np.array(badFits) 

331 numBad = len(badPos) 

332 

333 xRange = np.linspace(0, kernelCellSet.getBBox().getWidth(), num=numSample) 

334 yRange = np.linspace(0, kernelCellSet.getBBox().getHeight(), num=numSample) 

335 

336 if maxCoeff: 

337 maxCoeff = min(maxCoeff, kernel.getNKernelParameters()) 

338 else: 

339 maxCoeff = kernel.getNKernelParameters() 

340 

341 for k in range(maxCoeff): 

342 func = kernel.getSpatialFunction(k) 

343 dfGood = zGood[:, k] - np.array([func(pos.getX(), pos.getY()) for pos in candPos]) 

344 yMin = dfGood.min() 

345 yMax = dfGood.max() 

346 if numBad > 0: 

347 dfBad = zBad[:, k] - np.array([func(pos.getX(), pos.getY()) for pos in badPos]) 

348 # Can really screw up the range... 

349 yMin = min([yMin, dfBad.min()]) 

350 yMax = max([yMax, dfBad.max()]) 

351 yMin -= 0.05*(yMax - yMin) 

352 yMax += 0.05*(yMax - yMin) 

353 

354 fRange = np.ndarray((len(xRange), len(yRange))) 

355 for j, yVal in enumerate(yRange): 

356 for i, xVal in enumerate(xRange): 

357 fRange[j][i] = func(xVal, yVal) 

358 

359 fig = plt.figure(k) 

360 

361 fig.clf() 

362 try: 

363 fig.canvas._tkcanvas._root().lift() # == Tk's raise, but raise is a python reserved word 

364 except Exception: # protect against API changes 

365 pass 

366 

367 fig.suptitle('Kernel component %d' % k) 

368 

369 # LL 

370 ax = fig.add_axes((0.1, 0.05, 0.35, 0.35)) 

371 vmin = fRange.min() # - 0.05*np.fabs(fRange.min()) 

372 vmax = fRange.max() # + 0.05*np.fabs(fRange.max()) 

373 norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax) 

374 im = ax.imshow(fRange, aspect='auto', norm=norm, 

375 extent=[0, kernelCellSet.getBBox().getWidth() - 1, 

376 0, kernelCellSet.getBBox().getHeight() - 1]) 

377 ax.set_title('Spatial polynomial') 

378 plt.colorbar(im, orientation='horizontal', ticks=[vmin, vmax]) 

379 

380 # UL 

381 ax = fig.add_axes((0.1, 0.55, 0.35, 0.35)) 

382 ax.plot(-2.5*np.log10(candAmps), zGood[:, k], 'b+') 

383 if numBad > 0: 

384 ax.plot(-2.5*np.log10(badAmps), zBad[:, k], 'r+') 

385 ax.set_title("Basis Coefficients") 

386 ax.set_xlabel("Instr mag") 

387 ax.set_ylabel("Coeff") 

388 

389 # LR 

390 ax = fig.add_axes((0.55, 0.05, 0.35, 0.35)) 

391 ax.set_autoscale_on(False) 

392 ax.set_xbound(lower=0, upper=kernelCellSet.getBBox().getHeight()) 

393 ax.set_ybound(lower=yMin, upper=yMax) 

394 ax.plot(yGood, dfGood, 'b+') 

395 if numBad > 0: 

396 ax.plot(yBad, dfBad, 'r+') 

397 ax.axhline(0.0) 

398 ax.set_title('dCoeff (indiv-spatial) vs. y') 

399 

400 # UR 

401 ax = fig.add_axes((0.55, 0.55, 0.35, 0.35)) 

402 ax.set_autoscale_on(False) 

403 ax.set_xbound(lower=0, upper=kernelCellSet.getBBox().getWidth()) 

404 ax.set_ybound(lower=yMin, upper=yMax) 

405 ax.plot(xGood, dfGood, 'b+') 

406 if numBad > 0: 

407 ax.plot(xBad, dfBad, 'r+') 

408 ax.axhline(0.0) 

409 ax.set_title('dCoeff (indiv-spatial) vs. x') 

410 

411 fig.show() 

412 

413 global keptPlots 

414 if keepPlots and not keptPlots: 

415 # Keep plots open when done 

416 def show(): 

417 print("%s: Please close plots when done." % __name__) 

418 try: 

419 plt.show() 

420 except Exception: 

421 pass 

422 print("Plots closed, exiting...") 

423 import atexit 

424 atexit.register(show) 

425 keptPlots = True 

426 

427 

428def plotKernelCoefficients(spatialKernel, kernelCellSet, showBadCandidates=False, keepPlots=True): 

429 """Plot the individual kernel candidate and the spatial kernel solution coefficients. 

430 

431 Parameters 

432 ---------- 

433 

434 spatialKernel : `lsst.afw.math.LinearCombinationKernel` 

435 The spatial spatialKernel solution model which is a spatially varying linear combination 

436 of the spatialKernel basis functions. 

437 Typically returned by `lsst.ip.diffim.SpatialKernelSolution.getSolutionPair()`. 

438 

439 kernelCellSet : `lsst.afw.math.SpatialCellSet` 

440 The spatial cells that was used for solution for the spatialKernel. They contain the 

441 local solutions of the AL kernel for the selected sources. 

442 

443 showBadCandidates : `bool`, optional 

444 If True, plot the coefficient values for kernel candidates where the solution was marked 

445 bad by the numerical algorithm. Defaults to False. 

446 

447 keepPlots: `bool`, optional 

448 If True, sets ``plt.show()`` to be called before the task terminates, so that the plots 

449 can be explored interactively. Defaults to True. 

450 

451 Notes 

452 ----- 

453 This function produces 3 figures per image subtraction operation. 

454 * A grid plot of the local solutions. Each grid cell corresponds to a proportional area in 

455 the image. In each cell, local kernel solution coefficients are plotted of kernel candidates (color) 

456 that fall into this area as a function of the kernel basis function number. 

457 * A grid plot of the spatial solution. Each grid cell corresponds to a proportional area in 

458 the image. In each cell, the spatial solution coefficients are evaluated for the center of the cell. 

459 * Histogram of the local solution coefficients. Red line marks the spatial solution value at 

460 center of the image. 

461 

462 This function is called if ``lsst.ip.diffim.psfMatch.plotKernelCoefficients==True`` in lsstDebug. This 

463 function was implemented as part of DM-17825. 

464 """ 

465 try: 

466 import matplotlib.pyplot as plt 

467 except ImportError as e: 

468 print("Unable to import matplotlib: %s" % e) 

469 return 

470 

471 # Image dimensions 

472 imgBBox = kernelCellSet.getBBox() 

473 x0 = imgBBox.getBeginX() 

474 y0 = imgBBox.getBeginY() 

475 wImage = imgBBox.getWidth() 

476 hImage = imgBBox.getHeight() 

477 imgCenterX = imgBBox.getCenterX() 

478 imgCenterY = imgBBox.getCenterY() 

479 

480 # Plot the local solutions 

481 # ---- 

482 

483 # Grid size 

484 nX = 8 

485 nY = 8 

486 wCell = wImage / nX 

487 hCell = hImage / nY 

488 

489 fig = plt.figure() 

490 fig.suptitle("Kernel candidate parameters on an image grid") 

491 arrAx = fig.subplots(nrows=nY, ncols=nX, sharex=True, sharey=True, gridspec_kw=dict( 

492 wspace=0, hspace=0)) 

493 

494 # Bottom left panel is for bottom left part of the image 

495 arrAx = arrAx[::-1, :] 

496 

497 allParams = [] 

498 for cell in kernelCellSet.getCellList(): 

499 cellBBox = geom.Box2D(cell.getBBox()) 

500 # Determine which panel this spatial cell belongs to 

501 iX = int((cellBBox.getCenterX() - x0)//wCell) 

502 iY = int((cellBBox.getCenterY() - y0)//hCell) 

503 

504 for cand in cell.begin(False): 

505 try: 

506 kernel = cand.getKernel(cand.ORIG) 

507 except Exception: 

508 continue 

509 

510 if not showBadCandidates and cand.isBad(): 

511 continue 

512 

513 nKernelParams = kernel.getNKernelParameters() 

514 kernelParams = np.array(kernel.getKernelParameters()) 

515 allParams.append(kernelParams) 

516 

517 if cand.isBad(): 

518 color = 'red' 

519 else: 

520 color = None 

521 arrAx[iY, iX].plot(np.arange(nKernelParams), kernelParams, '.-', 

522 color=color, drawstyle='steps-mid', linewidth=0.1) 

523 for ax in arrAx.ravel(): 

524 ax.grid(True, axis='y') 

525 

526 # Plot histogram of the local parameters and the global solution at the image center 

527 # ---- 

528 

529 spatialFuncs = spatialKernel.getSpatialFunctionList() 

530 nKernelParams = spatialKernel.getNKernelParameters() 

531 nX = 8 

532 fig = plt.figure() 

533 fig.suptitle("Hist. of parameters marked with spatial solution at img center") 

534 arrAx = fig.subplots(nrows=int(nKernelParams//nX)+1, ncols=nX) 

535 arrAx = arrAx[::-1, :] 

536 allParams = np.array(allParams) 

537 for k in range(nKernelParams): 

538 ax = arrAx.ravel()[k] 

539 ax.hist(allParams[:, k], bins=20, edgecolor='black') 

540 ax.set_xlabel('P{}'.format(k)) 

541 valueParam = spatialFuncs[k](imgCenterX, imgCenterY) 

542 ax.axvline(x=valueParam, color='red') 

543 ax.text(0.1, 0.9, '{:.1f}'.format(valueParam), 

544 transform=ax.transAxes, backgroundcolor='lightsteelblue') 

545 

546 # Plot grid of the spatial solution 

547 # ---- 

548 

549 nX = 8 

550 nY = 8 

551 wCell = wImage / nX 

552 hCell = hImage / nY 

553 x0 += wCell / 2 

554 y0 += hCell / 2 

555 

556 fig = plt.figure() 

557 fig.suptitle("Spatial solution of kernel parameters on an image grid") 

558 arrAx = fig.subplots(nrows=nY, ncols=nX, sharex=True, sharey=True, gridspec_kw=dict( 

559 wspace=0, hspace=0)) 

560 arrAx = arrAx[::-1, :] 

561 kernelParams = np.zeros(nKernelParams, dtype=float) 

562 

563 for iX in range(nX): 

564 for iY in range(nY): 

565 x = x0 + iX * wCell 

566 y = y0 + iY * hCell 

567 # Evaluate the spatial solution functions for this x,y location 

568 kernelParams = [f(x, y) for f in spatialFuncs] 

569 arrAx[iY, iX].plot(np.arange(nKernelParams), kernelParams, '.-', drawstyle='steps-mid') 

570 arrAx[iY, iX].grid(True, axis='y') 

571 

572 global keptPlots 

573 if keepPlots and not keptPlots: 

574 # Keep plots open when done 

575 def show(): 

576 print("%s: Please close plots when done." % __name__) 

577 try: 

578 plt.show() 

579 except Exception: 

580 pass 

581 print("Plots closed, exiting...") 

582 import atexit 

583 atexit.register(show) 

584 keptPlots = True 

585 

586 

587def showKernelMosaic(bbox, kernel, nx=7, ny=None, frame=None, title=None, 

588 showCenter=True, showEllipticity=True): 

589 """Show a mosaic of Kernel images. 

590 """ 

591 mos = afwDisplay.utils.Mosaic() 

592 

593 x0 = bbox.getBeginX() 

594 y0 = bbox.getBeginY() 

595 width = bbox.getWidth() 

596 height = bbox.getHeight() 

597 

598 if not ny: 

599 ny = int(nx*float(height)/width + 0.5) 

600 if not ny: 

601 ny = 1 

602 

603 schema = afwTable.SourceTable.makeMinimalSchema() 

604 centroidName = "base_SdssCentroid" 

605 shapeName = "base_SdssShape" 

606 control = measBase.SdssCentroidControl() 

607 schema.getAliasMap().set("slot_Centroid", centroidName) 

608 schema.getAliasMap().set("slot_Centroid_flag", centroidName + "_flag") 

609 centroider = measBase.SdssCentroidAlgorithm(control, centroidName, schema) 

610 sdssShape = measBase.SdssShapeControl() 

611 shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema) 

612 table = afwTable.SourceTable.make(schema) 

613 table.defineCentroid(centroidName) 

614 table.defineShape(shapeName) 

615 

616 centers = [] 

617 shapes = [] 

618 for iy in range(ny): 

619 for ix in range(nx): 

620 x = int(ix*(width - 1)/(nx - 1)) + x0 

621 y = int(iy*(height - 1)/(ny - 1)) + y0 

622 

623 im = afwImage.ImageD(kernel.getDimensions()) 

624 ksum = kernel.computeImage(im, False, x, y) 

625 lab = "Kernel(%d,%d)=%.2f" % (x, y, ksum) if False else "" 

626 mos.append(im, lab) 

627 

628 # SdssCentroidAlgorithm.measure requires an exposure of floats 

629 exp = afwImage.makeExposure(afwImage.makeMaskedImage(im.convertF())) 

630 

631 w, h = im.getWidth(), im.getHeight() 

632 centerX = im.getX0() + w//2 

633 centerY = im.getY0() + h//2 

634 src = table.makeRecord() 

635 spans = afwGeom.SpanSet(exp.getBBox()) 

636 foot = afwDet.Footprint(spans) 

637 foot.addPeak(centerX, centerY, 1) 

638 src.setFootprint(foot) 

639 

640 try: # The centroider requires a psf, so this will fail if none is attached to exp 

641 centroider.measure(src, exp) 

642 centers.append((src.getX(), src.getY())) 

643 

644 shaper.measure(src, exp) 

645 shapes.append((src.getIxx(), src.getIxy(), src.getIyy())) 

646 except Exception: 

647 pass 

648 

649 disp = afwDisplay.Display(frame=frame) 

650 mos.makeMosaic(display=disp, title=title if title else "Model Kernel", mode=nx) 

651 

652 if centers and frame is not None: 

653 disp = afwDisplay.Display(frame=frame) 

654 i = 0 

655 with disp.Buffering(): 

656 for cen, shape in zip(centers, shapes): 

657 bbox = mos.getBBox(i) 

658 i += 1 

659 xc, yc = cen[0] + bbox.getMinX(), cen[1] + bbox.getMinY() 

660 if showCenter: 

661 disp.dot("+", xc, yc, ctype=afwDisplay.BLUE) 

662 

663 if showEllipticity: 

664 ixx, ixy, iyy = shape 

665 disp.dot("@:%g,%g,%g" % (ixx, ixy, iyy), xc, yc, ctype=afwDisplay.RED) 

666 

667 return mos 

668 

669 

670def plotPixelResiduals(exposure, warpedTemplateExposure, diffExposure, kernelCellSet, 

671 kernel, background, testSources, config, 

672 origVariance=False, nptsFull=1e6, keepPlots=True, titleFs=14): 

673 """Plot diffim residuals for LOCAL and SPATIAL models. 

674 """ 

675 candidateResids = [] 

676 spatialResids = [] 

677 nonfitResids = [] 

678 

679 for cell in kernelCellSet.getCellList(): 

680 for cand in cell.begin(True): # only look at good ones 

681 # Be sure 

682 if not (cand.getStatus() == afwMath.SpatialCellCandidate.GOOD): 

683 continue 

684 

685 diffim = cand.getDifferenceImage(diffimLib.KernelCandidateF.ORIG) 

686 orig = cand.getScienceMaskedImage() 

687 

688 ski = afwImage.ImageD(kernel.getDimensions()) 

689 kernel.computeImage(ski, False, int(cand.getXCenter()), int(cand.getYCenter())) 

690 sk = afwMath.FixedKernel(ski) 

691 sbg = background(int(cand.getXCenter()), int(cand.getYCenter())) 

692 sdiffim = cand.getDifferenceImage(sk, sbg) 

693 

694 # trim edgs due to convolution 

695 bbox = kernel.shrinkBBox(diffim.getBBox()) 

696 tdiffim = diffim.Factory(diffim, bbox) 

697 torig = orig.Factory(orig, bbox) 

698 tsdiffim = sdiffim.Factory(sdiffim, bbox) 

699 

700 if origVariance: 

701 candidateResids.append(np.ravel(tdiffim.getImage().getArray() 

702 / np.sqrt(torig.getVariance().getArray()))) 

703 spatialResids.append(np.ravel(tsdiffim.getImage().getArray() 

704 / np.sqrt(torig.getVariance().getArray()))) 

705 else: 

706 candidateResids.append(np.ravel(tdiffim.getImage().getArray() 

707 / np.sqrt(tdiffim.getVariance().getArray()))) 

708 spatialResids.append(np.ravel(tsdiffim.getImage().getArray() 

709 / np.sqrt(tsdiffim.getVariance().getArray()))) 

710 

711 fullIm = diffExposure.getMaskedImage().getImage().getArray() 

712 fullMask = diffExposure.getMaskedImage().getMask().getArray() 

713 if origVariance: 

714 fullVar = exposure.getMaskedImage().getVariance().getArray() 

715 else: 

716 fullVar = diffExposure.getMaskedImage().getVariance().getArray() 

717 

718 bitmaskBad = 0 

719 bitmaskBad |= afwImage.Mask.getPlaneBitMask('NO_DATA') 

720 bitmaskBad |= afwImage.Mask.getPlaneBitMask('SAT') 

721 idx = np.where((fullMask & bitmaskBad) == 0) 

722 stride = int(len(idx[0])//nptsFull) 

723 sidx = idx[0][::stride], idx[1][::stride] 

724 allResids = fullIm[sidx]/np.sqrt(fullVar[sidx]) 

725 

726 testFootprints = diffimTools.sourceToFootprintList(testSources, warpedTemplateExposure, 

727 exposure, config, 

728 getLogger(__name__).getChild("plotPixelResiduals")) 

729 for fp in testFootprints: 

730 subexp = diffExposure.Factory(diffExposure, fp["footprint"].getBBox()) 

731 subim = subexp.getMaskedImage().getImage() 

732 if origVariance: 

733 subvar = afwImage.ExposureF(exposure, fp["footprint"].getBBox()).getMaskedImage().getVariance() 

734 else: 

735 subvar = subexp.getMaskedImage().getVariance() 

736 nonfitResids.append(np.ravel(subim.getArray()/np.sqrt(subvar.getArray()))) 

737 

738 candidateResids = np.ravel(np.array(candidateResids)) 

739 spatialResids = np.ravel(np.array(spatialResids)) 

740 nonfitResids = np.ravel(np.array(nonfitResids)) 

741 

742 try: 

743 import pylab 

744 from matplotlib.font_manager import FontProperties 

745 except ImportError as e: 

746 print("Unable to import pylab: %s" % e) 

747 return 

748 

749 fig = pylab.figure() 

750 fig.clf() 

751 try: 

752 fig.canvas._tkcanvas._root().lift() # == Tk's raise, but raise is a python reserved word 

753 except Exception: # protect against API changes 

754 pass 

755 if origVariance: 

756 fig.suptitle("Diffim residuals: Normalized by sqrt(input variance)", fontsize=titleFs) 

757 else: 

758 fig.suptitle("Diffim residuals: Normalized by sqrt(diffim variance)", fontsize=titleFs) 

759 

760 sp1 = pylab.subplot(221) 

761 sp2 = pylab.subplot(222, sharex=sp1, sharey=sp1) 

762 sp3 = pylab.subplot(223, sharex=sp1, sharey=sp1) 

763 sp4 = pylab.subplot(224, sharex=sp1, sharey=sp1) 

764 xs = np.arange(-5, 5.05, 0.1) 

765 ys = 1./np.sqrt(2*np.pi)*np.exp(-0.5*xs**2) 

766 

767 sp1.hist(candidateResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)" 

768 % (np.mean(candidateResids), np.var(candidateResids))) 

769 sp1.plot(xs, ys, "r-", lw=2, label="N(0,1)") 

770 sp1.set_title("Candidates: basis fit", fontsize=titleFs - 2) 

771 sp1.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6)) 

772 

773 sp2.hist(spatialResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)" 

774 % (np.mean(spatialResids), np.var(spatialResids))) 

775 sp2.plot(xs, ys, "r-", lw=2, label="N(0,1)") 

776 sp2.set_title("Candidates: spatial fit", fontsize=titleFs - 2) 

777 sp2.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6)) 

778 

779 sp3.hist(nonfitResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)" 

780 % (np.mean(nonfitResids), np.var(nonfitResids))) 

781 sp3.plot(xs, ys, "r-", lw=2, label="N(0,1)") 

782 sp3.set_title("Control sample: spatial fit", fontsize=titleFs - 2) 

783 sp3.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6)) 

784 

785 sp4.hist(allResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)" 

786 % (np.mean(allResids), np.var(allResids))) 

787 sp4.plot(xs, ys, "r-", lw=2, label="N(0,1)") 

788 sp4.set_title("Full image (subsampled)", fontsize=titleFs - 2) 

789 sp4.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6)) 

790 

791 pylab.setp(sp1.get_xticklabels() + sp1.get_yticklabels(), fontsize=titleFs - 4) 

792 pylab.setp(sp2.get_xticklabels() + sp2.get_yticklabels(), fontsize=titleFs - 4) 

793 pylab.setp(sp3.get_xticklabels() + sp3.get_yticklabels(), fontsize=titleFs - 4) 

794 pylab.setp(sp4.get_xticklabels() + sp4.get_yticklabels(), fontsize=titleFs - 4) 

795 

796 sp1.set_xlim(-5, 5) 

797 sp1.set_ylim(0, 0.5) 

798 fig.show() 

799 

800 global keptPlots 

801 if keepPlots and not keptPlots: 

802 # Keep plots open when done 

803 def show(): 

804 print("%s: Please close plots when done." % __name__) 

805 try: 

806 pylab.show() 

807 except Exception: 

808 pass 

809 print("Plots closed, exiting...") 

810 import atexit 

811 atexit.register(show) 

812 keptPlots = True 

813 

814 

815def calcCentroid(arr): 

816 """Calculate first moment of a (kernel) image. 

817 """ 

818 y, x = arr.shape 

819 sarr = arr*arr 

820 xarr = np.asarray([[el for el in range(x)] for el2 in range(y)]) 

821 yarr = np.asarray([[el2 for el in range(x)] for el2 in range(y)]) 

822 narr = xarr*sarr 

823 sarrSum = sarr.sum() 

824 centx = narr.sum()/sarrSum 

825 narr = yarr*sarr 

826 centy = narr.sum()/sarrSum 

827 return centx, centy 

828 

829 

830def calcWidth(arr, centx, centy): 

831 """Calculate second moment of a (kernel) image. 

832 """ 

833 y, x = arr.shape 

834 # Square the flux so we don't have to deal with negatives 

835 sarr = arr*arr 

836 xarr = np.asarray([[el for el in range(x)] for el2 in range(y)]) 

837 yarr = np.asarray([[el2 for el in range(x)] for el2 in range(y)]) 

838 narr = sarr*np.power((xarr - centx), 2.) 

839 sarrSum = sarr.sum() 

840 xstd = np.sqrt(narr.sum()/sarrSum) 

841 narr = sarr*np.power((yarr - centy), 2.) 

842 ystd = np.sqrt(narr.sum()/sarrSum) 

843 return xstd, ystd 

844 

845 

846def printSkyDiffs(sources, wcs): 

847 """Print differences in sky coordinates. 

848 

849 The difference is that between the source Position and its Centroid mapped 

850 through Wcs. 

851 """ 

852 for s in sources: 

853 sCentroid = s.getCentroid() 

854 sPosition = s.getCoord().getPosition(geom.degrees) 

855 dra = 3600*(sPosition.getX() - wcs.pixelToSky(sCentroid).getPosition(geom.degrees).getX())/0.2 

856 ddec = 3600*(sPosition.getY() - wcs.pixelToSky(sCentroid).getPosition(geom.degrees).getY())/0.2 

857 if np.isfinite(dra) and np.isfinite(ddec): 

858 print(dra, ddec) 

859 

860 

861def makeRegions(sources, outfilename, wcs=None): 

862 """Create regions file for display from input source list. 

863 """ 

864 fh = open(outfilename, "w") 

865 fh.write("global color=red font=\"helvetica 10 normal\" " 

866 "select=1 highlite=1 edit=1 move=1 delete=1 include=1 fixed=0 source\nfk5\n") 

867 for s in sources: 

868 if wcs: 

869 (ra, dec) = wcs.pixelToSky(s.getCentroid()).getPosition(geom.degrees) 

870 else: 

871 (ra, dec) = s.getCoord().getPosition(geom.degrees) 

872 if np.isfinite(ra) and np.isfinite(dec): 

873 fh.write("circle(%f,%f,2\")\n"%(ra, dec)) 

874 fh.flush() 

875 fh.close() 

876 

877 

878def showSourceSetSky(sSet, wcs, xy0, frame=0, ctype=afwDisplay.GREEN, symb="+", size=2): 

879 """Draw the (RA, Dec) positions of a set of Sources. Image has the XY0. 

880 """ 

881 disp = afwDisplay.Display(frame=frame) 

882 with disp.Buffering(): 

883 for s in sSet: 

884 (xc, yc) = wcs.skyToPixel(s.getCoord().getRa(), s.getCoord().getDec()) 

885 xc -= xy0[0] 

886 yc -= xy0[1] 

887 disp.dot(symb, xc, yc, ctype=ctype, size=size) 

888 

889 

890def plotWhisker(results, newWcs): 

891 """Plot whisker diagram of astromeric offsets between results.matches. 

892 """ 

893 refCoordKey = results.matches[0].first.getTable().getCoordKey() 

894 inCentroidKey = results.matches[0].second.getTable().getCentroidSlot().getMeasKey() 

895 positions = [m.first.get(refCoordKey) for m in results.matches] 

896 residuals = [m.first.get(refCoordKey).getOffsetFrom( 

897 newWcs.pixelToSky(m.second.get(inCentroidKey))) for 

898 m in results.matches] 

899 import matplotlib.pyplot as plt 

900 fig = plt.figure() 

901 sp = fig.add_subplot(1, 1, 0) 

902 xpos = [x[0].asDegrees() for x in positions] 

903 ypos = [x[1].asDegrees() for x in positions] 

904 xpos.append(0.02*(max(xpos) - min(xpos)) + min(xpos)) 

905 ypos.append(0.98*(max(ypos) - min(ypos)) + min(ypos)) 

906 xidxs = np.isfinite(xpos) 

907 yidxs = np.isfinite(ypos) 

908 X = np.asarray(xpos)[xidxs] 

909 Y = np.asarray(ypos)[yidxs] 

910 distance = [x[1].asArcseconds() for x in residuals] 

911 distance.append(0.2) 

912 distance = np.asarray(distance)[xidxs] 

913 # NOTE: This assumes that the bearing is measured positive from +RA through North. 

914 # From the documentation this is not clear. 

915 bearing = [x[0].asRadians() for x in residuals] 

916 bearing.append(0) 

917 bearing = np.asarray(bearing)[xidxs] 

918 U = (distance*np.cos(bearing)) 

919 V = (distance*np.sin(bearing)) 

920 sp.quiver(X, Y, U, V) 

921 sp.set_title("WCS Residual") 

922 plt.show() 

923 

924 

925class DipoleTestImage(object): 

926 """Utility class for dipole measurement testing. 

927 

928 Generate an image with simulated dipoles and noise; store the original 

929 "pre-subtraction" images and catalogs as well. 

930 Used to generate test data for DMTN-007 (http://dmtn-007.lsst.io). 

931 """ 

932 

933 def __init__(self, w=101, h=101, xcenPos=[27.], ycenPos=[25.], xcenNeg=[23.], ycenNeg=[25.], 

934 psfSigma=2., flux=[30000.], fluxNeg=None, noise=10., gradientParams=None): 

935 self.w = w 

936 self.h = h 

937 self.xcenPos = xcenPos 

938 self.ycenPos = ycenPos 

939 self.xcenNeg = xcenNeg 

940 self.ycenNeg = ycenNeg 

941 self.psfSigma = psfSigma 

942 self.flux = flux 

943 self.fluxNeg = fluxNeg 

944 if fluxNeg is None: 

945 self.fluxNeg = self.flux 

946 self.noise = noise 

947 self.gradientParams = gradientParams 

948 self._makeDipoleImage() 

949 

950 def _makeDipoleImage(self): 

951 """Generate an exposure and catalog with the given dipole source(s). 

952 """ 

953 # Must seed the pos/neg images with different values to ensure they get different noise realizations 

954 posImage, posCatalog = self._makeStarImage( 

955 xc=self.xcenPos, yc=self.ycenPos, flux=self.flux, randomSeed=111) 

956 

957 negImage, negCatalog = self._makeStarImage( 

958 xc=self.xcenNeg, yc=self.ycenNeg, flux=self.fluxNeg, randomSeed=222) 

959 

960 dipole = posImage.clone() 

961 di = dipole.getMaskedImage() 

962 di -= negImage.getMaskedImage() 

963 

964 # Carry through pos/neg detection masks to new planes in diffim 

965 dm = di.getMask() 

966 posDetectedBits = posImage.getMaskedImage().getMask().getArray() == dm.getPlaneBitMask("DETECTED") 

967 negDetectedBits = negImage.getMaskedImage().getMask().getArray() == dm.getPlaneBitMask("DETECTED") 

968 pos_det = dm.addMaskPlane("DETECTED_POS") # new mask plane -- different from "DETECTED" 

969 neg_det = dm.addMaskPlane("DETECTED_NEG") # new mask plane -- different from "DETECTED_NEGATIVE" 

970 dma = dm.getArray() 

971 # set the two custom mask planes to these new masks 

972 dma[:, :] = posDetectedBits*pos_det + negDetectedBits*neg_det 

973 self.diffim, self.posImage, self.posCatalog, self.negImage, self.negCatalog \ 

974 = dipole, posImage, posCatalog, negImage, negCatalog 

975 

976 def _makeStarImage(self, xc=[15.3], yc=[18.6], flux=[2500], schema=None, randomSeed=None): 

977 """Generate an exposure and catalog with the given stellar source(s). 

978 """ 

979 from lsst.meas.base.tests import TestDataset 

980 bbox = geom.Box2I(geom.Point2I(0, 0), geom.Point2I(self.w - 1, self.h - 1)) 

981 dataset = TestDataset(bbox, psfSigma=self.psfSigma, threshold=1.) 

982 

983 for i in range(len(xc)): 

984 dataset.addSource(instFlux=flux[i], centroid=geom.Point2D(xc[i], yc[i])) 

985 

986 if schema is None: 

987 schema = TestDataset.makeMinimalSchema() 

988 exposure, catalog = dataset.realize(noise=self.noise, schema=schema, randomSeed=randomSeed) 

989 

990 if self.gradientParams is not None: 

991 y, x = np.mgrid[:self.w, :self.h] 

992 gp = self.gradientParams 

993 gradient = gp[0] + gp[1]*x + gp[2]*y 

994 if len(self.gradientParams) > 3: # it includes a set of 2nd-order polynomial params 

995 gradient += gp[3]*x*y + gp[4]*x*x + gp[5]*y*y 

996 imgArr = exposure.getMaskedImage().getArrays()[0] 

997 imgArr += gradient 

998 

999 return exposure, catalog 

1000 

1001 def fitDipoleSource(self, source, **kwds): 

1002 alg = DipoleFitAlgorithm(self.diffim, self.posImage, self.negImage) 

1003 fitResult = alg.fitDipole(source, **kwds) 

1004 return fitResult 

1005 

1006 def detectDipoleSources(self, doMerge=True, diffim=None, detectSigma=5.5, grow=3, minBinSize=32): 

1007 """Utility function for detecting dipoles. 

1008 

1009 Detect pos/neg sources in the diffim, then merge them. A 

1010 bigger "grow" parameter leads to a larger footprint which 

1011 helps with dipole measurement for faint dipoles. 

1012 

1013 Parameters 

1014 ---------- 

1015 doMerge : `bool` 

1016 Whether to merge the positive and negagive detections into a single 

1017 source table. 

1018 diffim : `lsst.afw.image.exposure.exposure.ExposureF` 

1019 Difference image on which to perform detection. 

1020 detectSigma : `float` 

1021 Threshold for object detection. 

1022 grow : `int` 

1023 Number of pixels to grow the footprints before merging. 

1024 minBinSize : `int` 

1025 Minimum bin size for the background (re)estimation (only applies if 

1026 the default leads to min(nBinX, nBinY) < fit order so the default 

1027 config parameter needs to be decreased, but not to a value smaller 

1028 than ``minBinSize``, in which case the fitting algorithm will take 

1029 over and decrease the fit order appropriately.) 

1030 

1031 Returns 

1032 ------- 

1033 sources : `lsst.afw.table.SourceCatalog` 

1034 If doMerge=True, the merged source catalog is returned OR 

1035 detectTask : `lsst.meas.algorithms.SourceDetectionTask` 

1036 schema : `lsst.afw.table.Schema` 

1037 If doMerge=False, the source detection task and its schema are 

1038 returned. 

1039 """ 

1040 if diffim is None: 

1041 diffim = self.diffim 

1042 

1043 # Start with a minimal schema - only the fields all SourceCatalogs need 

1044 schema = afwTable.SourceTable.makeMinimalSchema() 

1045 

1046 # Customize the detection task a bit (optional) 

1047 detectConfig = measAlg.SourceDetectionConfig() 

1048 detectConfig.returnOriginalFootprints = False # should be the default 

1049 

1050 diffimPsf = diffim.getPsf() 

1051 psfSigma = diffimPsf.computeShape(diffimPsf.getAveragePosition()).getDeterminantRadius() 

1052 

1053 # code from imageDifference.py: 

1054 detectConfig.thresholdPolarity = "both" 

1055 detectConfig.thresholdValue = detectSigma 

1056 # detectConfig.nSigmaToGrow = psfSigma 

1057 detectConfig.reEstimateBackground = True # if False, will fail often for faint sources on gradients? 

1058 detectConfig.thresholdType = "pixel_stdev" 

1059 # Test images are often quite small, so may need to adjust background binSize 

1060 while ((min(diffim.getWidth(), diffim.getHeight()))//detectConfig.background.binSize 

1061 < detectConfig.background.approxOrderX and detectConfig.background.binSize > minBinSize): 

1062 detectConfig.background.binSize = max(minBinSize, detectConfig.background.binSize//2) 

1063 

1064 # Create the detection task. We pass the schema so the task can declare a few flag fields 

1065 detectTask = measAlg.SourceDetectionTask(schema, config=detectConfig) 

1066 

1067 table = afwTable.SourceTable.make(schema) 

1068 catalog = detectTask.run(table, diffim, sigma=psfSigma) 

1069 

1070 # Now do the merge. 

1071 if doMerge: 

1072 fpSet = catalog.fpSets.positive 

1073 fpSet.merge(catalog.fpSets.negative, grow, grow, False) 

1074 sources = afwTable.SourceCatalog(table) 

1075 fpSet.makeSources(sources) 

1076 

1077 return sources 

1078 

1079 else: 

1080 return detectTask, schema 

1081 

1082 

1083def getPsfFwhm(psf): 

1084 """Calculate the FWHM in pixels of a supplied PSF. 

1085 

1086 Parameters 

1087 ---------- 

1088 psf : `lsst.afw.detection.Psf` 

1089 Point spread function (PSF) to evaluate. 

1090 

1091 Returns 

1092 ------- 

1093 psfSize : `float` 

1094 The FWHM of the PSF computed at its average position. 

1095 """ 

1096 sigma2fwhm = 2.*np.sqrt(2.*np.log(2.)) 

1097 psfAvgPos = psf.getAveragePosition() 

1098 psfSize = psf.computeShape(psfAvgPos).getDeterminantRadius()*sigma2fwhm 

1099 return psfSize