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

659 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-05 13:22 +0000

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", "evaluateMeanPsfFwhm", "getPsfFwhm"] 

26 

27import itertools 

28import numpy as np 

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.meas.algorithms.testUtils import plantSources 

39from lsst.pex.exceptions import InvalidParameterError 

40from lsst.utils.logging import getLogger 

41from .dipoleFitTask import DipoleFitAlgorithm 

42from . import diffimLib 

43 

44afwDisplay.setDefaultMaskTransparency(75) 

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

46 

47_LOG = getLogger(__name__) 

48 

49 

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

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

52 

53 Image has the given XY0. 

54 """ 

55 disp = afwDisplay.afwDisplay(frame=frame) 

56 with disp.Buffering(): 

57 for s in sSet: 

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

59 

60 if symb == "id": 

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

62 else: 

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

64 

65 

66# Kernel display utilities 

67# 

68 

69 

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

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

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

73 """Show the SpatialCells. 

74 

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

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

77 and size. 

78 """ 

79 disp = afwDisplay.Display(frame=frame) 

80 disp.mtv(maskedIm, title=title) 

81 with disp.Buffering(): 

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

83 for cell in kernelCellSet.getCellList(): 

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

85 

86 goodies = ctypeBad is None 

87 for cand in cell.begin(goodies): 

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

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

90 color = ctypeBad 

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

92 color = ctype 

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

94 color = ctypeUnused 

95 else: 

96 continue 

97 

98 if color: 

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

100 

101 if showChi2: 

102 rchi2 = cand.getChi2() 

103 if rchi2 > 1e100: 

104 rchi2 = np.nan 

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

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

107 

108 

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

110 """Display Dia Sources. 

111 """ 

112 # 

113 # Show us the ccandidates 

114 # 

115 # Too many mask planes in diffims 

116 disp = afwDisplay.Display(frame=frame) 

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

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

119 

120 mos = afwDisplay.utils.Mosaic() 

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

122 source = sources[i] 

123 badFlag = isFlagged[i] 

124 dipoleFlag = isDipole[i] 

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

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

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

128 im.append(stamp.getMaskedImage()) 

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

130 if badFlag: 

131 ctype = afwDisplay.RED 

132 lab += "BAD" 

133 if dipoleFlag: 

134 ctype = afwDisplay.YELLOW 

135 lab += "DIPOLE" 

136 if not badFlag and not dipoleFlag: 

137 ctype = afwDisplay.GREEN 

138 lab += "OK" 

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

140 title = "Dia Sources" 

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

142 return mosaicImage 

143 

144 

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

146 resids=False, kernels=False): 

147 """Display the Kernel candidates. 

148 

149 If kernel is provided include spatial model and residuals; 

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

151 """ 

152 # 

153 # Show us the ccandidates 

154 # 

155 if kernels: 

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

157 else: 

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

159 # 

160 candidateCenters = [] 

161 candidateCentersBad = [] 

162 candidateIndex = 0 

163 for cell in kernelCellSet.getCellList(): 

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

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

166 try: 

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

168 except Exception: 

169 continue 

170 

171 rchi2 = cand.getChi2() 

172 if rchi2 > 1e100: 

173 rchi2 = np.nan 

174 

175 if not showBadCandidates and cand.isBad(): 

176 continue 

177 

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

179 

180 try: 

181 im = cand.getScienceMaskedImage() 

182 im = im.Factory(im, True) 

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

184 except Exception: 

185 continue 

186 if (not resids and not kernels): 

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

188 try: 

189 im = cand.getTemplateMaskedImage() 

190 im = im.Factory(im, True) 

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

192 except Exception: 

193 continue 

194 if (not resids and not kernels): 

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

196 

197 # Difference image with original basis 

198 if resids: 

199 var = resid.variance 

200 var = var.Factory(var, True) 

201 np.sqrt(var.array, var.array) # inplace sqrt 

202 resid = resid.image 

203 resid /= var 

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

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

206 elif kernels: 

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

208 resid = kim.Factory(kim, True) 

209 im_resid.append(resid) 

210 

211 # residuals using spatial model 

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

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

214 sk = afwMath.FixedKernel(ski) 

215 sbg = 0.0 

216 if background: 

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

218 sresid = cand.getDifferenceImage(sk, sbg) 

219 resid = sresid 

220 if resids: 

221 resid = sresid.image 

222 resid /= var 

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

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

225 elif kernels: 

226 kim = ski.convertF() 

227 resid = kim.Factory(kim, True) 

228 im_resid.append(resid) 

229 

230 im = im_resid.makeMosaic() 

231 

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

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

234 

235 mos.append(im, lab, ctype) 

236 

237 if False and np.isnan(rchi2): 

238 disp = afwDisplay.Display(frame=1) 

239 disp.mtv(cand.getScienceMaskedImage.image, title="candidate") 

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

241 

242 im = cand.getScienceMaskedImage() 

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

244 candidateIndex += 1 

245 if cand.isBad(): 

246 candidateCentersBad.append(center) 

247 else: 

248 candidateCenters.append(center) 

249 

250 if resids: 

251 title = "chi Diffim" 

252 elif kernels: 

253 title = "Kernels" 

254 else: 

255 title = "Candidates & residuals" 

256 

257 disp = afwDisplay.Display(frame=frame) 

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

259 

260 return mosaicImage 

261 

262 

263def showKernelBasis(kernel, frame=None): 

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

265 """ 

266 mos = afwDisplay.utils.Mosaic() 

267 

268 for k in kernel.getKernelList(): 

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

270 k.computeImage(im, False) 

271 mos.append(im) 

272 

273 disp = afwDisplay.Display(frame=frame) 

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

275 

276 return mos 

277 

278############### 

279 

280 

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

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

283 """Plot the Kernel spatial model. 

284 """ 

285 try: 

286 import matplotlib.pyplot as plt 

287 import matplotlib.colors 

288 except ImportError as e: 

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

290 return 

291 

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

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

294 

295 candPos = list() 

296 candFits = list() 

297 badPos = list() 

298 badFits = list() 

299 candAmps = list() 

300 badAmps = list() 

301 for cell in kernelCellSet.getCellList(): 

302 for cand in cell.begin(False): 

303 if not showBadCandidates and cand.isBad(): 

304 continue 

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

306 try: 

307 im = cand.getTemplateMaskedImage() 

308 except Exception: 

309 continue 

310 

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

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

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

314 

315 # compare original and spatial kernel coefficients 

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

317 amp = cand.getCandidateRating() 

318 

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

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

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

322 

323 targetFits.append(kp0) 

324 targetPos.append(candCenter) 

325 targetAmps.append(amp) 

326 

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

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

329 zGood = np.array(candFits) 

330 

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

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

333 zBad = np.array(badFits) 

334 numBad = len(badPos) 

335 

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

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

338 

339 if maxCoeff: 

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

341 else: 

342 maxCoeff = kernel.getNKernelParameters() 

343 

344 for k in range(maxCoeff): 

345 func = kernel.getSpatialFunction(k) 

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

347 yMin = dfGood.min() 

348 yMax = dfGood.max() 

349 if numBad > 0: 

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

351 # Can really screw up the range... 

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

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

354 yMin -= 0.05*(yMax - yMin) 

355 yMax += 0.05*(yMax - yMin) 

356 

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

358 for j, yVal in enumerate(yRange): 

359 for i, xVal in enumerate(xRange): 

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

361 

362 fig = plt.figure(k) 

363 

364 fig.clf() 

365 try: 

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

367 except Exception: # protect against API changes 

368 pass 

369 

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

371 

372 # LL 

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

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

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

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

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

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

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

380 ax.set_title('Spatial polynomial') 

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

382 

383 # UL 

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

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

386 if numBad > 0: 

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

388 ax.set_title("Basis Coefficients") 

389 ax.set_xlabel("Instr mag") 

390 ax.set_ylabel("Coeff") 

391 

392 # LR 

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

394 ax.set_autoscale_on(False) 

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

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

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

398 if numBad > 0: 

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

400 ax.axhline(0.0) 

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

402 

403 # UR 

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

405 ax.set_autoscale_on(False) 

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

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

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

409 if numBad > 0: 

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

411 ax.axhline(0.0) 

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

413 

414 fig.show() 

415 

416 global keptPlots 

417 if keepPlots and not keptPlots: 

418 # Keep plots open when done 

419 def show(): 

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

421 try: 

422 plt.show() 

423 except Exception: 

424 pass 

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

426 import atexit 

427 atexit.register(show) 

428 keptPlots = True 

429 

430 

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

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

433 

434 Parameters 

435 ---------- 

436 

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

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

439 of the spatialKernel basis functions. 

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

441 

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

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

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

445 

446 showBadCandidates : `bool`, optional 

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

448 bad by the numerical algorithm. Defaults to False. 

449 

450 keepPlots: `bool`, optional 

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

452 can be explored interactively. Defaults to True. 

453 

454 Notes 

455 ----- 

456 This function produces 3 figures per image subtraction operation. 

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

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

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

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

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

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

463 center of the image. 

464 

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

466 function was implemented as part of DM-17825. 

467 """ 

468 try: 

469 import matplotlib.pyplot as plt 

470 except ImportError as e: 

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

472 return 

473 

474 # Image dimensions 

475 imgBBox = kernelCellSet.getBBox() 

476 x0 = imgBBox.getBeginX() 

477 y0 = imgBBox.getBeginY() 

478 wImage = imgBBox.getWidth() 

479 hImage = imgBBox.getHeight() 

480 imgCenterX = imgBBox.getCenterX() 

481 imgCenterY = imgBBox.getCenterY() 

482 

483 # Plot the local solutions 

484 # ---- 

485 

486 # Grid size 

487 nX = 8 

488 nY = 8 

489 wCell = wImage / nX 

490 hCell = hImage / nY 

491 

492 fig = plt.figure() 

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

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

495 wspace=0, hspace=0)) 

496 

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

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

499 

500 allParams = [] 

501 for cell in kernelCellSet.getCellList(): 

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

503 # Determine which panel this spatial cell belongs to 

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

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

506 

507 for cand in cell.begin(False): 

508 try: 

509 kernel = cand.getKernel(cand.ORIG) 

510 except Exception: 

511 continue 

512 

513 if not showBadCandidates and cand.isBad(): 

514 continue 

515 

516 nKernelParams = kernel.getNKernelParameters() 

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

518 allParams.append(kernelParams) 

519 

520 if cand.isBad(): 

521 color = 'red' 

522 else: 

523 color = None 

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

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

526 for ax in arrAx.ravel(): 

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

528 

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

530 # ---- 

531 

532 spatialFuncs = spatialKernel.getSpatialFunctionList() 

533 nKernelParams = spatialKernel.getNKernelParameters() 

534 nX = 8 

535 fig = plt.figure() 

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

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

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

539 allParams = np.array(allParams) 

540 for k in range(nKernelParams): 

541 ax = arrAx.ravel()[k] 

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

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

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

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

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

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

548 

549 # Plot grid of the spatial solution 

550 # ---- 

551 

552 nX = 8 

553 nY = 8 

554 wCell = wImage / nX 

555 hCell = hImage / nY 

556 x0 += wCell / 2 

557 y0 += hCell / 2 

558 

559 fig = plt.figure() 

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

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

562 wspace=0, hspace=0)) 

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

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

565 

566 for iX in range(nX): 

567 for iY in range(nY): 

568 x = x0 + iX * wCell 

569 y = y0 + iY * hCell 

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

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

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

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

574 

575 global keptPlots 

576 if keepPlots and not keptPlots: 

577 # Keep plots open when done 

578 def show(): 

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

580 try: 

581 plt.show() 

582 except Exception: 

583 pass 

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

585 import atexit 

586 atexit.register(show) 

587 keptPlots = True 

588 

589 

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

591 showCenter=True, showEllipticity=True): 

592 """Show a mosaic of Kernel images. 

593 """ 

594 mos = afwDisplay.utils.Mosaic() 

595 

596 x0 = bbox.getBeginX() 

597 y0 = bbox.getBeginY() 

598 width = bbox.getWidth() 

599 height = bbox.getHeight() 

600 

601 if not ny: 

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

603 if not ny: 

604 ny = 1 

605 

606 schema = afwTable.SourceTable.makeMinimalSchema() 

607 centroidName = "base_SdssCentroid" 

608 shapeName = "base_SdssShape" 

609 control = measBase.SdssCentroidControl() 

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

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

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

613 sdssShape = measBase.SdssShapeControl() 

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

615 table = afwTable.SourceTable.make(schema) 

616 table.defineCentroid(centroidName) 

617 table.defineShape(shapeName) 

618 

619 centers = [] 

620 shapes = [] 

621 for iy in range(ny): 

622 for ix in range(nx): 

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

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

625 

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

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

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

629 mos.append(im, lab) 

630 

631 # SdssCentroidAlgorithm.measure requires an exposure of floats 

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

633 

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

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

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

637 src = table.makeRecord() 

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

639 foot = afwDet.Footprint(spans) 

640 foot.addPeak(centerX, centerY, 1) 

641 src.setFootprint(foot) 

642 

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

644 centroider.measure(src, exp) 

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

646 

647 shaper.measure(src, exp) 

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

649 except Exception: 

650 pass 

651 

652 disp = afwDisplay.Display(frame=frame) 

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

654 

655 if centers and frame is not None: 

656 disp = afwDisplay.Display(frame=frame) 

657 i = 0 

658 with disp.Buffering(): 

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

660 bbox = mos.getBBox(i) 

661 i += 1 

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

663 if showCenter: 

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

665 

666 if showEllipticity: 

667 ixx, ixy, iyy = shape 

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

669 

670 return mos 

671 

672 

673def plotWhisker(results, newWcs): 

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

675 """ 

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

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

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

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

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

681 m in results.matches] 

682 import matplotlib.pyplot as plt 

683 fig = plt.figure() 

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

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

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

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

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

689 xidxs = np.isfinite(xpos) 

690 yidxs = np.isfinite(ypos) 

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

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

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

694 distance.append(0.2) 

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

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

697 # From the documentation this is not clear. 

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

699 bearing.append(0) 

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

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

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

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

704 sp.set_title("WCS Residual") 

705 plt.show() 

706 

707 

708class DipoleTestImage(object): 

709 """Utility class for dipole measurement testing. 

710 

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

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

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

714 """ 

715 

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

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

718 self.w = w 

719 self.h = h 

720 self.xcenPos = xcenPos 

721 self.ycenPos = ycenPos 

722 self.xcenNeg = xcenNeg 

723 self.ycenNeg = ycenNeg 

724 self.psfSigma = psfSigma 

725 self.flux = flux 

726 self.fluxNeg = fluxNeg 

727 if fluxNeg is None: 

728 self.fluxNeg = self.flux 

729 self.noise = noise 

730 self.gradientParams = gradientParams 

731 self._makeDipoleImage() 

732 

733 def _makeDipoleImage(self): 

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

735 """ 

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

737 posImage, posCatalog = self._makeStarImage( 

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

739 

740 negImage, negCatalog = self._makeStarImage( 

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

742 

743 dipole = posImage.clone() 

744 di = dipole.getMaskedImage() 

745 di -= negImage.getMaskedImage() 

746 

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

748 = dipole, posImage, posCatalog, negImage, negCatalog 

749 

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

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

752 """ 

753 from lsst.meas.base.tests import TestDataset 

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

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

756 

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

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

759 

760 if schema is None: 

761 schema = TestDataset.makeMinimalSchema() 

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

763 

764 if self.gradientParams is not None: 

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

766 gp = self.gradientParams 

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

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

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

770 imgArr = exposure.image.array 

771 imgArr += gradient 

772 

773 return exposure, catalog 

774 

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

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

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

778 return fitResult 

779 

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

781 """Utility function for detecting dipoles. 

782 

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

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

785 helps with dipole measurement for faint dipoles. 

786 

787 Parameters 

788 ---------- 

789 doMerge : `bool` 

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

791 source table. 

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

793 Difference image on which to perform detection. 

794 detectSigma : `float` 

795 Threshold for object detection. 

796 grow : `int` 

797 Number of pixels to grow the footprints before merging. 

798 minBinSize : `int` 

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

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

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

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

803 over and decrease the fit order appropriately.) 

804 

805 Returns 

806 ------- 

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

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

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

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

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

812 returned. 

813 """ 

814 if diffim is None: 

815 diffim = self.diffim 

816 

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

818 schema = afwTable.SourceTable.makeMinimalSchema() 

819 

820 # Customize the detection task a bit (optional) 

821 detectConfig = measAlg.SourceDetectionConfig() 

822 detectConfig.returnOriginalFootprints = False # should be the default 

823 

824 # code from imageDifference.py: 

825 detectConfig.thresholdPolarity = "both" 

826 detectConfig.thresholdValue = detectSigma 

827 # detectConfig.nSigmaToGrow = psfSigma 

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

829 detectConfig.thresholdType = "pixel_stdev" 

830 detectConfig.excludeMaskPlanes = ["EDGE"] 

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

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

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

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

835 

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

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

838 

839 table = afwTable.SourceTable.make(schema) 

840 catalog = detectTask.run(table, diffim) 

841 

842 # Now do the merge. 

843 if doMerge: 

844 fpSet = catalog.positive 

845 fpSet.merge(catalog.negative, grow, grow, False) 

846 sources = afwTable.SourceCatalog(table) 

847 fpSet.makeSources(sources) 

848 

849 return sources 

850 

851 else: 

852 return detectTask, schema 

853 

854 

855def _sliceWidth(image, threshold, peaks, axis): 

856 vec = image.take(peaks[1 - axis], axis=axis) 

857 low = np.interp(threshold, vec[:peaks[axis] + 1], np.arange(peaks[axis] + 1)) 

858 high = np.interp(threshold, vec[:peaks[axis] - 1:-1], np.arange(len(vec) - 1, peaks[axis] - 1, -1)) 

859 return high - low 

860 

861 

862def getPsfFwhm(psf, average=True, position=None): 

863 """Directly calculate the horizontal and vertical widths 

864 of a PSF at half its maximum value. 

865 

866 Parameters 

867 ---------- 

868 psf : `~lsst.afw.detection.Psf` 

869 Point spread function (PSF) to evaluate. 

870 average : `bool`, optional 

871 Set to return the average width over Y and X axes. 

872 position : `~lsst.geom.Point2D`, optional 

873 The position at which to evaluate the PSF. If `None`, then the 

874 average position is used. 

875 

876 Returns 

877 ------- 

878 psfSize : `float` | `tuple` [`float`] 

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

880 Returns the widths along the Y and X axes, 

881 or the average of the two if `average` is set. 

882 

883 See Also 

884 -------- 

885 evaluateMeanPsfFwhm 

886 """ 

887 if position is None: 

888 position = psf.getAveragePosition() 

889 image = psf.computeKernelImage(position).array 

890 peak = psf.computePeak(position) 

891 peakLocs = np.unravel_index(np.argmax(image), image.shape) 

892 width = _sliceWidth(image, peak/2., peakLocs, axis=0), _sliceWidth(image, peak/2., peakLocs, axis=1) 

893 return np.nanmean(width) if average else width 

894 

895 

896def evaluateMeanPsfFwhm(exposure: afwImage.Exposure, 

897 fwhmExposureBuffer: float, fwhmExposureGrid: int) -> float: 

898 """Get the mean PSF FWHM by evaluating it on a grid within an exposure. 

899 

900 Parameters 

901 ---------- 

902 exposure : `~lsst.afw.image.Exposure` 

903 The exposure for which the mean FWHM of the PSF is to be computed. 

904 The exposure must contain a `psf` attribute. 

905 fwhmExposureBuffer : `float` 

906 Fractional buffer margin to be left out of all sides of the image 

907 during the construction of the grid to compute mean PSF FWHM in an 

908 exposure. 

909 fwhmExposureGrid : `int` 

910 Grid size to compute the mean FWHM in an exposure. 

911 

912 Returns 

913 ------- 

914 meanFwhm : `float` 

915 The mean PSF FWHM on the exposure. 

916 

917 Raises 

918 ------ 

919 ValueError 

920 Raised if the PSF cannot be computed at any of the grid points. 

921 

922 See Also 

923 -------- 

924 `getPsfFwhm` 

925 `computeAveragePsf` 

926 """ 

927 

928 psf = exposure.psf 

929 

930 bbox = exposure.getBBox() 

931 xmax, ymax = bbox.getMax() 

932 xmin, ymin = bbox.getMin() 

933 

934 xbuffer = fwhmExposureBuffer*(xmax-xmin) 

935 ybuffer = fwhmExposureBuffer*(ymax-ymin) 

936 

937 width = [] 

938 for (x, y) in itertools.product(np.linspace(xmin+xbuffer, xmax-xbuffer, fwhmExposureGrid), 

939 np.linspace(ymin+ybuffer, ymax-ybuffer, fwhmExposureGrid) 

940 ): 

941 pos = geom.Point2D(x, y) 

942 try: 

943 fwhm = getPsfFwhm(psf, average=True, position=pos) 

944 except InvalidParameterError: 

945 _LOG.debug("Unable to compute PSF FWHM at position (%f, %f).", x, y) 

946 continue 

947 

948 width.append(fwhm) 

949 

950 if not width: 

951 raise ValueError("Unable to compute PSF FWHM at any position on the exposure.") 

952 

953 return np.nanmean(width) 

954 

955 

956def computeAveragePsf(exposure: afwImage.Exposure, 

957 psfExposureBuffer: float, psfExposureGrid: int) -> afwImage.ImageD: 

958 """Get the average PSF by evaluating it on a grid within an exposure. 

959 

960 Parameters 

961 ---------- 

962 exposure : `~lsst.afw.image.Exposure` 

963 The exposure for which the average PSF is to be computed. 

964 The exposure must contain a `psf` attribute. 

965 psfExposureBuffer : `float` 

966 Fractional buffer margin to be left out of all sides of the image 

967 during the construction of the grid to compute average PSF in an 

968 exposure. 

969 psfExposureGrid : `int` 

970 Grid size to compute the average PSF in an exposure. 

971 

972 Returns 

973 ------- 

974 psfImage : `~lsst.afw.image.Image` 

975 The average PSF across the exposure. 

976 

977 Raises 

978 ------ 

979 ValueError 

980 Raised if the PSF cannot be computed at any of the grid points. 

981 

982 See Also 

983 -------- 

984 `evaluateMeanPsfFwhm` 

985 """ 

986 

987 psf = exposure.psf 

988 

989 bbox = exposure.getBBox() 

990 xmax, ymax = bbox.getMax() 

991 xmin, ymin = bbox.getMin() 

992 

993 xbuffer = psfExposureBuffer*(xmax-xmin) 

994 ybuffer = psfExposureBuffer*(ymax-ymin) 

995 

996 nImg = 0 

997 psfArray = None 

998 for (x, y) in itertools.product(np.linspace(xmin+xbuffer, xmax-xbuffer, psfExposureGrid), 

999 np.linspace(ymin+ybuffer, ymax-ybuffer, psfExposureGrid) 

1000 ): 

1001 pos = geom.Point2D(x, y) 

1002 try: 

1003 singleImage = psf.computeKernelImage(pos) 

1004 except InvalidParameterError: 

1005 _LOG.debug("Unable to compute PSF image at position (%f, %f).", x, y) 

1006 continue 

1007 

1008 if psfArray is None: 

1009 psfArray = singleImage.array 

1010 else: 

1011 psfArray += singleImage.array 

1012 nImg += 1 

1013 

1014 if psfArray is None: 

1015 raise ValueError("Unable to compute PSF image at any position on the exposure.") 

1016 

1017 psfImage = afwImage.ImageD(psfArray/nImg) 

1018 return psfImage 

1019 

1020 

1021def detectTestSources(exposure): 

1022 """Minimal source detection wrapper suitable for unit tests. 

1023 

1024 Parameters 

1025 ---------- 

1026 exposure : `lsst.afw.image.Exposure` 

1027 Exposure on which to run detection/measurement 

1028 The exposure is modified in place to set the 'DETECTED' mask plane. 

1029 

1030 Returns 

1031 ------- 

1032 selectSources : 

1033 Source catalog containing candidates 

1034 """ 

1035 

1036 schema = afwTable.SourceTable.makeMinimalSchema() 

1037 selectDetection = measAlg.SourceDetectionTask(schema=schema) 

1038 selectMeasurement = measBase.SingleFrameMeasurementTask(schema=schema) 

1039 table = afwTable.SourceTable.make(schema) 

1040 

1041 detRet = selectDetection.run( 

1042 table=table, 

1043 exposure=exposure, 

1044 sigma=None, # The appropriate sigma is calculated from the PSF 

1045 doSmooth=True 

1046 ) 

1047 exposure.mask.addMaskPlane("STREAK") # add empty streak mask plane in lieu of maskStreaksTask 

1048 exposure.mask.addMaskPlane("INJECTED") # add empty injected mask plane 

1049 exposure.mask.addMaskPlane("INJECTED_TEMPLATE") # add empty injected template mask plane 

1050 

1051 selectSources = detRet.sources 

1052 selectMeasurement.run(measCat=selectSources, exposure=exposure) 

1053 

1054 return selectSources 

1055 

1056 

1057def makeFakeWcs(): 

1058 """Make a fake, affine Wcs. 

1059 """ 

1060 crpix = geom.Point2D(123.45, 678.9) 

1061 crval = geom.SpherePoint(0.1, 0.1, geom.degrees) 

1062 cdMatrix = np.array([[5.19513851e-05, -2.81124812e-07], 

1063 [-3.25186974e-07, -5.19112119e-05]]) 

1064 return afwGeom.makeSkyWcs(crpix, crval, cdMatrix) 

1065 

1066 

1067def makeTestImage(seed=5, nSrc=20, psfSize=2., noiseLevel=5., 

1068 noiseSeed=6, fluxLevel=500., fluxRange=2., 

1069 kernelSize=32, templateBorderSize=0, 

1070 background=None, 

1071 xSize=256, 

1072 ySize=256, 

1073 x0=12345, 

1074 y0=67890, 

1075 calibration=1., 

1076 doApplyCalibration=False, 

1077 xLoc=None, 

1078 yLoc=None, 

1079 flux=None, 

1080 clearEdgeMask=False, 

1081 ): 

1082 """Make a reproduceable PSF-convolved exposure for testing. 

1083 

1084 Parameters 

1085 ---------- 

1086 seed : `int`, optional 

1087 Seed value to initialize the random number generator for sources. 

1088 nSrc : `int`, optional 

1089 Number of sources to simulate. 

1090 psfSize : `float`, optional 

1091 Width of the PSF of the simulated sources, in pixels. 

1092 noiseLevel : `float`, optional 

1093 Standard deviation of the noise to add to each pixel. 

1094 noiseSeed : `int`, optional 

1095 Seed value to initialize the random number generator for noise. 

1096 fluxLevel : `float`, optional 

1097 Reference flux of the simulated sources. 

1098 fluxRange : `float`, optional 

1099 Range in flux amplitude of the simulated sources. 

1100 kernelSize : `int`, optional 

1101 Size in pixels of the kernel for simulating sources. 

1102 templateBorderSize : `int`, optional 

1103 Size in pixels of the image border used to pad the image. 

1104 background : `lsst.afw.math.Chebyshev1Function2D`, optional 

1105 Optional background to add to the output image. 

1106 xSize, ySize : `int`, optional 

1107 Size in pixels of the simulated image. 

1108 x0, y0 : `int`, optional 

1109 Origin of the image. 

1110 calibration : `float`, optional 

1111 Conversion factor between instFlux and nJy. 

1112 doApplyCalibration : `bool`, optional 

1113 Apply the photometric calibration and return the image in nJy? 

1114 xLoc, yLoc : `list` of `float`, optional 

1115 User-specified coordinates of the simulated sources. 

1116 If specified, must have length equal to ``nSrc`` 

1117 flux : `list` of `float`, optional 

1118 User-specified fluxes of the simulated sources. 

1119 If specified, must have length equal to ``nSrc`` 

1120 clearEdgeMask : `bool`, optional 

1121 Clear the "EDGE" mask plane after source detection. 

1122 

1123 Returns 

1124 ------- 

1125 modelExposure : `lsst.afw.image.Exposure` 

1126 The model image, with the mask and variance planes. 

1127 sourceCat : `lsst.afw.table.SourceCatalog` 

1128 Catalog of sources detected on the model image. 

1129 

1130 Raises 

1131 ------ 

1132 ValueError 

1133 If `xloc`, `yloc`, or `flux` are supplied with inconsistant lengths. 

1134 """ 

1135 # Distance from the inner edge of the bounding box to avoid placing test 

1136 # sources in the model images. 

1137 bufferSize = kernelSize/2 + templateBorderSize + 1 

1138 

1139 bbox = geom.Box2I(geom.Point2I(x0, y0), geom.Extent2I(xSize, ySize)) 

1140 if templateBorderSize > 0: 

1141 bbox.grow(templateBorderSize) 

1142 

1143 rng = np.random.RandomState(seed) 

1144 rngNoise = np.random.RandomState(noiseSeed) 

1145 x0, y0 = bbox.getBegin() 

1146 xSize, ySize = bbox.getDimensions() 

1147 if xLoc is None: 

1148 xLoc = rng.rand(nSrc)*(xSize - 2*bufferSize) + bufferSize + x0 

1149 else: 

1150 if len(xLoc) != nSrc: 

1151 raise ValueError("xLoc must have length equal to nSrc. %f supplied vs %f", len(xLoc), nSrc) 

1152 if yLoc is None: 

1153 yLoc = rng.rand(nSrc)*(ySize - 2*bufferSize) + bufferSize + y0 

1154 else: 

1155 if len(yLoc) != nSrc: 

1156 raise ValueError("yLoc must have length equal to nSrc. %f supplied vs %f", len(yLoc), nSrc) 

1157 

1158 if flux is None: 

1159 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*fluxLevel 

1160 else: 

1161 if len(flux) != nSrc: 

1162 raise ValueError("flux must have length equal to nSrc. %f supplied vs %f", len(flux), nSrc) 

1163 sigmas = [psfSize for src in range(nSrc)] 

1164 coordList = list(zip(xLoc, yLoc, flux, sigmas)) 

1165 skyLevel = 0 

1166 # Don't use the built in poisson noise: it modifies the global state of numpy random 

1167 modelExposure = plantSources(bbox, kernelSize, skyLevel, coordList, addPoissonNoise=False) 

1168 modelExposure.setWcs(makeFakeWcs()) 

1169 noise = rngNoise.randn(ySize, xSize)*noiseLevel 

1170 noise -= np.mean(noise) 

1171 modelExposure.variance.array = np.sqrt(np.abs(modelExposure.image.array)) + noiseLevel**2 

1172 modelExposure.image.array += noise 

1173 

1174 # Run source detection to set up the mask plane 

1175 sourceCat = detectTestSources(modelExposure) 

1176 if clearEdgeMask: 

1177 modelExposure.mask &= ~modelExposure.mask.getPlaneBitMask("EDGE") 

1178 modelExposure.setPhotoCalib(afwImage.PhotoCalib(calibration, 0., bbox)) 

1179 if background is not None: 

1180 modelExposure.image += background 

1181 modelExposure.maskedImage /= calibration 

1182 modelExposure.info.setId(seed) 

1183 if doApplyCalibration: 

1184 modelExposure.maskedImage = modelExposure.photoCalib.calibrateImage(modelExposure.maskedImage) 

1185 

1186 return modelExposure, sourceCat 

1187 

1188 

1189def makeStats(badMaskPlanes=None): 

1190 """Create a statistics control for configuring calculations on images. 

1191 

1192 Parameters 

1193 ---------- 

1194 badMaskPlanes : `list` of `str`, optional 

1195 List of mask planes to exclude from calculations. 

1196 

1197 Returns 

1198 ------- 

1199 statsControl : ` lsst.afw.math.StatisticsControl` 

1200 Statistics control object for configuring calculations on images. 

1201 """ 

1202 if badMaskPlanes is None: 

1203 badMaskPlanes = ("INTRP", "EDGE", "DETECTED", "SAT", "CR", 

1204 "BAD", "NO_DATA", "DETECTED_NEGATIVE") 

1205 statsControl = afwMath.StatisticsControl() 

1206 statsControl.setNumSigmaClip(3.) 

1207 statsControl.setNumIter(3) 

1208 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(badMaskPlanes)) 

1209 return statsControl 

1210 

1211 

1212def computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP): 

1213 """Calculate a robust mean of the variance plane of an exposure. 

1214 

1215 Parameters 

1216 ---------- 

1217 image : `lsst.afw.image.Image` 

1218 Image or variance plane of an exposure to evaluate. 

1219 mask : `lsst.afw.image.Mask` 

1220 Mask plane to use for excluding pixels. 

1221 statsCtrl : `lsst.afw.math.StatisticsControl` 

1222 Statistics control object for configuring the calculation. 

1223 statistic : `lsst.afw.math.Property`, optional 

1224 The type of statistic to compute. Typical values are 

1225 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``. 

1226 

1227 Returns 

1228 ------- 

1229 value : `float` 

1230 The result of the statistic calculated from the unflagged pixels. 

1231 """ 

1232 statObj = afwMath.makeStatistics(image, mask, statistic, statsCtrl) 

1233 return statObj.getValue(statistic) 

1234 

1235 

1236def computePSFNoiseEquivalentArea(psf): 

1237 """Compute the noise equivalent area for an image psf 

1238 

1239 Parameters 

1240 ---------- 

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

1242 

1243 Returns 

1244 ------- 

1245 nea : `float` 

1246 """ 

1247 psfImg = psf.computeImage(psf.getAveragePosition()) 

1248 nea = 1./np.sum(psfImg.array**2) 

1249 return nea