Coverage for python/lsst/meas/algorithms/utils.py: 4%

545 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-12 03:05 -0700

1# This file is part of meas_algorithms. 

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__all__ = ["splitId", "showSourceSet", "showPsfSpatialCells", 

25 "showPsfCandidates", "makeSubplots", "plotPsfSpatialModel", 

26 "showPsf", "showPsfMosaic", "showPsfResiduals", "saveSpatialCellSet"] 

27 

28import math 

29import numpy 

30 

31import lsst.log 

32import lsst.pex.exceptions as pexExcept 

33import lsst.daf.base as dafBase 

34import lsst.geom 

35import lsst.afw.geom as afwGeom 

36import lsst.afw.detection as afwDet 

37import lsst.afw.image as afwImage 

38import lsst.afw.math as afwMath 

39import lsst.afw.table as afwTable 

40import lsst.afw.display as afwDisplay 

41import lsst.afw.display.utils as displayUtils 

42import lsst.meas.base as measBase 

43from . import subtractPsf, fitKernelParamsToImage 

44 

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

46 

47afwDisplay.setDefaultMaskTransparency(75) 

48 

49 

50def splitId(oid, asDict=True): 

51 

52 objId = int((oid & 0xffff) - 1) # Should be the value set by apps code 

53 

54 if asDict: 

55 return dict(objId=objId) 

56 else: 

57 return [objId] 

58 

59 

60def showSourceSet(sSet, xy0=(0, 0), display=None, ctype=afwDisplay.GREEN, symb="+", size=2): 

61 """Draw the (XAstrom, YAstrom) positions of a set of Sources. Image has the given XY0""" 

62 

63 if not display: 

64 display = afwDisplay.Display() 

65 with display.Buffering(): 

66 for s in sSet: 

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

68 

69 if symb == "id": 

70 display.dot(str(splitId(s.getId(), True)["objId"]), xc, yc, ctype=ctype, size=size) 

71 else: 

72 display.dot(symb, xc, yc, ctype=ctype, size=size) 

73 

74# 

75# PSF display utilities 

76# 

77 

78 

79def showPsfSpatialCells(exposure, psfCellSet, nMaxPerCell=-1, showChi2=False, showMoments=False, 

80 symb=None, ctype=None, ctypeUnused=None, ctypeBad=None, size=2, display=None): 

81 """Show the SpatialCells. 

82 

83 If symb is something that afwDisplay.Display.dot() understands (e.g. "o"), 

84 the top nMaxPerCell candidates will be indicated with that symbol, using 

85 ctype and size. 

86 """ 

87 

88 if not display: 

89 display = afwDisplay.Display() 

90 with display.Buffering(): 

91 origin = [-exposure.getMaskedImage().getX0(), -exposure.getMaskedImage().getY0()] 

92 for cell in psfCellSet.getCellList(): 

93 displayUtils.drawBBox(cell.getBBox(), origin=origin, display=display) 

94 

95 if nMaxPerCell < 0: 

96 nMaxPerCell = 0 

97 

98 i = 0 

99 goodies = ctypeBad is None 

100 for cand in cell.begin(goodies): 

101 if nMaxPerCell > 0: 

102 i += 1 

103 

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

105 

106 if i > nMaxPerCell: 

107 if not ctypeUnused: 

108 continue 

109 

110 color = ctypeBad if cand.isBad() else ctype 

111 

112 if symb: 

113 if i > nMaxPerCell: 

114 ct = ctypeUnused 

115 else: 

116 ct = ctype 

117 

118 display.dot(symb, xc, yc, ctype=ct, size=size) 

119 

120 source = cand.getSource() 

121 

122 if showChi2: 

123 rchi2 = cand.getChi2() 

124 if rchi2 > 1e100: 

125 rchi2 = numpy.nan 

126 display.dot("%d %.1f" % (splitId(source.getId(), True)["objId"], rchi2), 

127 xc - size, yc - size - 4, ctype=color, size=2) 

128 

129 if showMoments: 

130 display.dot("%.2f %.2f %.2f" % (source.getIxx(), source.getIxy(), source.getIyy()), 

131 xc-size, yc + size + 4, ctype=color, size=size) 

132 return display 

133 

134 

135def showPsfCandidates(exposure, psfCellSet, psf=None, display=None, normalize=True, showBadCandidates=True, 

136 fitBasisComponents=False, variance=None, chi=None): 

137 """Display the PSF candidates. 

138 

139 If psf is provided include PSF model and residuals; if normalize is true normalize the PSFs 

140 (and residuals) 

141 

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

143 

144 If fitBasisComponents is true, also find the best linear combination of the PSF's components 

145 (if they exist) 

146 """ 

147 if not display: 

148 display = afwDisplay.Display() 

149 

150 if chi is None: 

151 if variance is not None: # old name for chi 

152 chi = variance 

153 # 

154 # Show us the ccandidates 

155 # 

156 mos = displayUtils.Mosaic() 

157 # 

158 candidateCenters = [] 

159 candidateCentersBad = [] 

160 candidateIndex = 0 

161 

162 for cell in psfCellSet.getCellList(): 

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

164 rchi2 = cand.getChi2() 

165 if rchi2 > 1e100: 

166 rchi2 = numpy.nan 

167 

168 if not showBadCandidates and cand.isBad(): 

169 continue 

170 

171 if psf: 

172 im_resid = displayUtils.Mosaic(gutter=0, background=-5, mode="x") 

173 

174 try: 

175 im = cand.getMaskedImage() # copy of this object's image 

176 xc, yc = cand.getXCenter(), cand.getYCenter() 

177 

178 margin = 0 if True else 5 

179 w, h = im.getDimensions() 

180 bbox = lsst.geom.BoxI(lsst.geom.PointI(margin, margin), im.getDimensions()) 

181 

182 if margin > 0: 

183 bim = im.Factory(w + 2*margin, h + 2*margin) 

184 

185 stdev = numpy.sqrt(afwMath.makeStatistics(im.getVariance(), afwMath.MEAN).getValue()) 

186 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random()) 

187 bim.getVariance().set(stdev**2) 

188 

189 bim.assign(im, bbox) 

190 im = bim 

191 xc += margin 

192 yc += margin 

193 

194 im = im.Factory(im, True) 

195 im.setXY0(cand.getMaskedImage().getXY0()) 

196 except Exception: 

197 continue 

198 

199 if not variance: 

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

201 

202 if True: # tweak up centroids 

203 mi = im 

204 psfIm = mi.getImage() 

205 config = measBase.SingleFrameMeasurementTask.ConfigClass() 

206 config.slots.centroid = "base_SdssCentroid" 

207 

208 schema = afwTable.SourceTable.makeMinimalSchema() 

209 measureSources = measBase.SingleFrameMeasurementTask(schema, config=config) 

210 catalog = afwTable.SourceCatalog(schema) 

211 

212 extra = 10 # enough margin to run the sdss centroider 

213 miBig = mi.Factory(im.getWidth() + 2*extra, im.getHeight() + 2*extra) 

214 miBig[extra:-extra, extra:-extra, afwImage.LOCAL] = mi 

215 miBig.setXY0(mi.getX0() - extra, mi.getY0() - extra) 

216 mi = miBig 

217 del miBig 

218 

219 exp = afwImage.makeExposure(mi) 

220 exp.setPsf(psf) 

221 

222 footprintSet = afwDet.FootprintSet(mi, 

223 afwDet.Threshold(0.5*numpy.max(psfIm.getArray())), 

224 "DETECTED") 

225 footprintSet.makeSources(catalog) 

226 

227 if len(catalog) == 0: 

228 raise RuntimeError("Failed to detect any objects") 

229 

230 measureSources.run(catalog, exp) 

231 if len(catalog) == 1: 

232 source = catalog[0] 

233 else: # more than one source; find the once closest to (xc, yc) 

234 dmin = None # an invalid value to catch logic errors 

235 for i, s in enumerate(catalog): 

236 d = numpy.hypot(xc - s.getX(), yc - s.getY()) 

237 if i == 0 or d < dmin: 

238 source, dmin = s, d 

239 xc, yc = source.getCentroid() 

240 

241 # residuals using spatial model 

242 try: 

243 subtractPsf(psf, im, xc, yc) 

244 except Exception: 

245 continue 

246 

247 resid = im 

248 if variance: 

249 resid = resid.getImage() 

250 var = im.getVariance() 

251 var = var.Factory(var, True) 

252 numpy.sqrt(var.getArray(), var.getArray()) # inplace sqrt 

253 resid /= var 

254 

255 im_resid.append(resid) 

256 

257 # Fit the PSF components directly to the data (i.e. ignoring the spatial model) 

258 if fitBasisComponents: 

259 im = cand.getMaskedImage() 

260 

261 im = im.Factory(im, True) 

262 im.setXY0(cand.getMaskedImage().getXY0()) 

263 

264 try: 

265 noSpatialKernel = psf.getKernel() 

266 except Exception: 

267 noSpatialKernel = None 

268 

269 if noSpatialKernel: 

270 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter()) 

271 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter) 

272 params = fit[0] 

273 kernels = afwMath.KernelList(fit[1]) 

274 outputKernel = afwMath.LinearCombinationKernel(kernels, params) 

275 

276 outImage = afwImage.ImageD(outputKernel.getDimensions()) 

277 outputKernel.computeImage(outImage, False) 

278 

279 im -= outImage.convertF() 

280 resid = im 

281 

282 if margin > 0: 

283 bim = im.Factory(w + 2*margin, h + 2*margin) 

284 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random()) 

285 bim *= stdev 

286 

287 bim.assign(resid, bbox) 

288 resid = bim 

289 

290 if variance: 

291 resid = resid.getImage() 

292 resid /= var 

293 

294 im_resid.append(resid) 

295 

296 im = im_resid.makeMosaic() 

297 else: 

298 im = cand.getMaskedImage() 

299 

300 if normalize: 

301 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue() 

302 

303 objId = splitId(cand.getSource().getId(), True)["objId"] 

304 if psf: 

305 lab = "%d chi^2 %.1f" % (objId, rchi2) 

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

307 else: 

308 lab = "%d flux %8.3g" % (objId, cand.getSource().getPsfInstFlux()) 

309 ctype = afwDisplay.GREEN 

310 

311 mos.append(im, lab, ctype) 

312 

313 if False and numpy.isnan(rchi2): 

314 display.mtv(cand.getMaskedImage().getImage(), title="showPsfCandidates: candidate") 

315 print("amp", cand.getAmplitude()) 

316 

317 im = cand.getMaskedImage() 

318 center = (candidateIndex, xc - im.getX0(), yc - im.getY0()) 

319 candidateIndex += 1 

320 if cand.isBad(): 

321 candidateCentersBad.append(center) 

322 else: 

323 candidateCenters.append(center) 

324 

325 if variance: 

326 title = "chi(Psf fit)" 

327 else: 

328 title = "Stars & residuals" 

329 mosaicImage = mos.makeMosaic(display=display, title=title) 

330 

331 with display.Buffering(): 

332 for centers, color in ((candidateCenters, afwDisplay.GREEN), (candidateCentersBad, afwDisplay.RED)): 

333 for cen in centers: 

334 bbox = mos.getBBox(cen[0]) 

335 display.dot("+", cen[1] + bbox.getMinX(), cen[2] + bbox.getMinY(), ctype=color) 

336 

337 return mosaicImage 

338 

339 

340def makeSubplots(fig, nx=2, ny=2, Nx=1, Ny=1, plottingArea=(0.1, 0.1, 0.85, 0.80), 

341 pxgutter=0.05, pygutter=0.05, xgutter=0.04, ygutter=0.04, 

342 headroom=0.0, panelBorderWeight=0, panelColor='black'): 

343 """Return a generator of a set of subplots, a set of Nx*Ny panels of nx*ny plots. Each panel is fully 

344 filled by row (starting in the bottom left) before the next panel is started. If panelBorderWidth is 

345 greater than zero a border is drawn around each panel, adjusted to enclose the axis labels. 

346 

347 E.g. 

348 subplots = makeSubplots(fig, 2, 2, Nx=1, Ny=1, panelColor='k') 

349 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,0)') 

350 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,0)') 

351 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,1)') 

352 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,1)') 

353 fig.show() 

354 

355 Parameters 

356 ---------- 

357 fig : `matplotlib.pyplot.figure` 

358 The matplotlib figure to draw 

359 nx : `int` 

360 The number of plots in each row of each panel 

361 ny : `int` 

362 The number of plots in each column of each panel 

363 Nx : `int` 

364 The number of panels in each row of the figure 

365 Ny : `int` 

366 The number of panels in each column of the figure 

367 plottingArea : `tuple` 

368 (x0, y0, x1, y1) for the part of the figure containing all the panels 

369 pxgutter : `float` 

370 Spacing between columns of panels in units of (x1 - x0) 

371 pygutter : `float` 

372 Spacing between rows of panels in units of (y1 - y0) 

373 xgutter : `float` 

374 Spacing between columns of plots within a panel in units of (x1 - x0) 

375 ygutter : `float` 

376 Spacing between rows of plots within a panel in units of (y1 - y0) 

377 headroom : `float` 

378 Extra spacing above each plot for e.g. a title 

379 panelBorderWeight : `int` 

380 Width of border drawn around panels 

381 panelColor : `str` 

382 Colour of border around panels 

383 """ 

384 

385 log = lsst.log.Log.getLogger("utils.makeSubplots") 

386 try: 

387 import matplotlib.pyplot as plt 

388 except ImportError as e: 

389 log.warning("Unable to import matplotlib: %s", e) 

390 return 

391 

392 # Make show() call canvas.draw() too so that we know how large the axis labels are. Sigh 

393 try: 

394 fig.__show 

395 except AttributeError: 

396 fig.__show = fig.show 

397 

398 def myShow(fig): 

399 fig.__show() 

400 fig.canvas.draw() 

401 

402 import types 

403 fig.show = types.MethodType(myShow, fig) 

404 # 

405 # We can't get the axis sizes until after draw()'s been called, so use a callback Sigh^2 

406 # 

407 axes = {} # all axes in all the panels we're drawing: axes[panel][0] etc. 

408 # 

409 

410 def on_draw(event): 

411 """ 

412 Callback to draw the panel borders when the plots are drawn to the canvas 

413 """ 

414 if panelBorderWeight <= 0: 

415 return False 

416 

417 for p in axes.keys(): 

418 bboxes = [] 

419 for ax in axes[p]: 

420 bboxes.append(ax.bbox.union([label.get_window_extent() for label in 

421 ax.get_xticklabels() + ax.get_yticklabels()])) 

422 

423 ax = axes[p][0] 

424 

425 # this is the bbox that bounds all the bboxes, again in relative 

426 # figure coords 

427 

428 bbox = ax.bbox.union(bboxes) 

429 

430 xy0, xy1 = ax.transData.inverted().transform(bbox) 

431 x0, y0 = xy0 

432 x1, y1 = xy1 

433 w, h = x1 - x0, y1 - y0 

434 # allow a little space around BBox 

435 x0 -= 0.02*w 

436 w += 0.04*w 

437 y0 -= 0.02*h 

438 h += 0.04*h 

439 h += h*headroom 

440 # draw BBox 

441 ax.patches = [] # remove old ones 

442 rec = ax.add_patch(plt.Rectangle((x0, y0), w, h, fill=False, 

443 lw=panelBorderWeight, edgecolor=panelColor)) 

444 rec.set_clip_on(False) 

445 

446 return False 

447 

448 fig.canvas.mpl_connect('draw_event', on_draw) 

449 # 

450 # Choose the plotting areas for each subplot 

451 # 

452 x0, y0 = plottingArea[0:2] 

453 W, H = plottingArea[2:4] 

454 w = (W - (Nx - 1)*pxgutter - (nx*Nx - 1)*xgutter)/float(nx*Nx) 

455 h = (H - (Ny - 1)*pygutter - (ny*Ny - 1)*ygutter)/float(ny*Ny) 

456 # 

457 # OK! Time to create the subplots 

458 # 

459 for panel in range(Nx*Ny): 

460 axes[panel] = [] 

461 px = panel%Nx 

462 py = panel//Nx 

463 for window in range(nx*ny): 

464 x = nx*px + window%nx 

465 y = ny*py + window//nx 

466 ax = fig.add_axes((x0 + xgutter + pxgutter + x*w + (px - 1)*pxgutter + (x - 1)*xgutter, 

467 y0 + ygutter + pygutter + y*h + (py - 1)*pygutter + (y - 1)*ygutter, 

468 w, h), frame_on=True, facecolor='w') 

469 axes[panel].append(ax) 

470 yield ax 

471 

472 

473def plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, numSample=128, 

474 matchKernelAmplitudes=False, keepPlots=True): 

475 """Plot the PSF spatial model.""" 

476 

477 log = lsst.log.Log.getLogger("utils.plotPsfSpatialModel") 

478 try: 

479 import matplotlib.pyplot as plt 

480 import matplotlib as mpl 

481 except ImportError as e: 

482 log.warning("Unable to import matplotlib: %s", e) 

483 return 

484 

485 noSpatialKernel = psf.getKernel() 

486 candPos = list() 

487 candFits = list() 

488 badPos = list() 

489 badFits = list() 

490 candAmps = list() 

491 badAmps = list() 

492 for cell in psfCellSet.getCellList(): 

493 for cand in cell.begin(False): 

494 if not showBadCandidates and cand.isBad(): 

495 continue 

496 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter()) 

497 try: 

498 im = cand.getMaskedImage() 

499 except Exception: 

500 continue 

501 

502 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter) 

503 params = fit[0] 

504 kernels = fit[1] 

505 amp = 0.0 

506 for p, k in zip(params, kernels): 

507 amp += p * k.getSum() 

508 

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

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

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

512 

513 targetFits.append([x / amp for x in params]) 

514 targetPos.append(candCenter) 

515 targetAmps.append(amp) 

516 

517 xGood = numpy.array([pos.getX() for pos in candPos]) - exposure.getX0() 

518 yGood = numpy.array([pos.getY() for pos in candPos]) - exposure.getY0() 

519 zGood = numpy.array(candFits) 

520 

521 xBad = numpy.array([pos.getX() for pos in badPos]) - exposure.getX0() 

522 yBad = numpy.array([pos.getY() for pos in badPos]) - exposure.getY0() 

523 zBad = numpy.array(badFits) 

524 numBad = len(badPos) 

525 

526 xRange = numpy.linspace(0, exposure.getWidth(), num=numSample) 

527 yRange = numpy.linspace(0, exposure.getHeight(), num=numSample) 

528 

529 kernel = psf.getKernel() 

530 nKernelComponents = kernel.getNKernelParameters() 

531 # 

532 # Figure out how many panels we'll need 

533 # 

534 nPanelX = int(math.sqrt(nKernelComponents)) 

535 nPanelY = nKernelComponents//nPanelX 

536 while nPanelY*nPanelX < nKernelComponents: 

537 nPanelX += 1 

538 

539 fig = plt.figure(1) 

540 fig.clf() 

541 try: 

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

543 except Exception: # protect against API changes 

544 pass 

545 # 

546 # Generator for axes arranged in panels 

547 # 

548 mpl.rcParams["figure.titlesize"] = "x-small" 

549 subplots = makeSubplots(fig, 2, 2, Nx=nPanelX, Ny=nPanelY, xgutter=0.06, ygutter=0.06, pygutter=0.04) 

550 

551 for k in range(nKernelComponents): 

552 func = kernel.getSpatialFunction(k) 

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

554 yMin = dfGood.min() 

555 yMax = dfGood.max() 

556 if numBad > 0: 

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

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

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

560 yMin -= 0.05 * (yMax - yMin) 

561 yMax += 0.05 * (yMax - yMin) 

562 

563 yMin = -0.01 

564 yMax = 0.01 

565 

566 fRange = numpy.ndarray((len(xRange), len(yRange))) 

567 for j, yVal in enumerate(yRange): 

568 for i, xVal in enumerate(xRange): 

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

570 

571 ax = next(subplots) 

572 

573 ax.set_autoscale_on(False) 

574 ax.set_xbound(lower=0, upper=exposure.getHeight()) 

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

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

577 if numBad > 0: 

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

579 ax.axhline(0.0) 

580 ax.set_title('Residuals(y)') 

581 

582 ax = next(subplots) 

583 

584 if matchKernelAmplitudes and k == 0: 

585 vmin = 0.0 

586 vmax = 1.1 

587 else: 

588 vmin = fRange.min() 

589 vmax = fRange.max() 

590 

591 norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax) 

592 im = ax.imshow(fRange, aspect='auto', origin="lower", norm=norm, 

593 extent=[0, exposure.getWidth()-1, 0, exposure.getHeight()-1]) 

594 ax.set_title('Spatial poly') 

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

596 

597 ax = next(subplots) 

598 ax.set_autoscale_on(False) 

599 ax.set_xbound(lower=0, upper=exposure.getWidth()) 

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

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

602 if numBad > 0: 

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

604 ax.axhline(0.0) 

605 ax.set_title('K%d Residuals(x)' % k) 

606 

607 ax = next(subplots) 

608 

609 photoCalib = exposure.getPhotoCalib() 

610 # If there is no calibration factor, use 1.0. 

611 if photoCalib.getCalibrationMean() <= 0: 

612 photoCalib = afwImage.PhotoCalib(1.0) 

613 

614 ampMag = [photoCalib.instFluxToMagnitude(candAmp) for candAmp in candAmps] 

615 ax.plot(ampMag, zGood[:, k], 'b+') 

616 if numBad > 0: 

617 badAmpMag = [photoCalib.instFluxToMagnitude(badAmp) for badAmp in badAmps] 

618 ax.plot(badAmpMag, zBad[:, k], 'r+') 

619 

620 ax.set_title('Flux variation') 

621 

622 fig.show() 

623 

624 global keptPlots 

625 if keepPlots and not keptPlots: 

626 # Keep plots open when done 

627 def show(): 

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

629 try: 

630 plt.show() 

631 except Exception: 

632 pass 

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

634 import atexit 

635 atexit.register(show) 

636 keptPlots = True 

637 

638 

639def showPsf(psf, eigenValues=None, XY=None, normalize=True, display=None): 

640 """Display a PSF's eigen images 

641 

642 If normalize is True, set the largest absolute value of each eigenimage to 1.0 (n.b. sum == 0.0 for i > 0) 

643 """ 

644 

645 if eigenValues: 

646 coeffs = eigenValues 

647 elif XY is not None: 

648 coeffs = psf.getLocalKernel(lsst.geom.PointD(XY[0], XY[1])).getKernelParameters() 

649 else: 

650 coeffs = None 

651 

652 mos = displayUtils.Mosaic(gutter=2, background=-0.1) 

653 for i, k in enumerate(psf.getKernel().getKernelList()): 

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

655 k.computeImage(im, False) 

656 if normalize: 

657 im /= numpy.max(numpy.abs(im.getArray())) 

658 

659 if coeffs: 

660 mos.append(im, "%g" % (coeffs[i]/coeffs[0])) 

661 else: 

662 mos.append(im) 

663 

664 if not display: 

665 display = afwDisplay.Display() 

666 mos.makeMosaic(display=display, title="Kernel Basis Functions") 

667 

668 return mos 

669 

670 

671def showPsfMosaic(exposure, psf=None, nx=7, ny=None, showCenter=True, showEllipticity=False, 

672 showFwhm=False, stampSize=0, display=None, title=None): 

673 """Show a mosaic of Psf images. exposure may be an Exposure (optionally with PSF), 

674 or a tuple (width, height) 

675 

676 If stampSize is > 0, the psf images will be trimmed to stampSize*stampSize 

677 """ 

678 

679 scale = 1.0 

680 if showFwhm: 

681 showEllipticity = True 

682 scale = 2*math.log(2) # convert sigma^2 to HWHM^2 for a Gaussian 

683 

684 mos = displayUtils.Mosaic() 

685 

686 try: # maybe it's a real Exposure 

687 width, height = exposure.getWidth(), exposure.getHeight() 

688 x0, y0 = exposure.getXY0() 

689 if not psf: 

690 psf = exposure.getPsf() 

691 except AttributeError: 

692 try: # OK, maybe a list [width, height] 

693 width, height = exposure[0], exposure[1] 

694 x0, y0 = 0, 0 

695 except TypeError: # I guess not 

696 raise RuntimeError("Unable to extract width/height from object of type %s" % type(exposure)) 

697 

698 if not ny: 

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

700 if not ny: 

701 ny = 1 

702 

703 centroidName = "SdssCentroid" 

704 shapeName = "base_SdssShape" 

705 

706 schema = afwTable.SourceTable.makeMinimalSchema() 

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

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

709 

710 control = measBase.SdssCentroidControl() 

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

712 

713 sdssShape = measBase.SdssShapeControl() 

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

715 table = afwTable.SourceTable.make(schema) 

716 

717 table.defineCentroid(centroidName) 

718 table.defineShape(shapeName) 

719 

720 bbox = None 

721 if stampSize > 0: 

722 w, h = psf.computeImage(lsst.geom.PointD(0, 0)).getDimensions() 

723 if stampSize <= w and stampSize <= h: 

724 bbox = lsst.geom.BoxI(lsst.geom.PointI((w - stampSize)//2, (h - stampSize)//2), 

725 lsst.geom.ExtentI(stampSize, stampSize)) 

726 

727 centers = [] 

728 shapes = [] 

729 for iy in range(ny): 

730 for ix in range(nx): 

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

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

733 

734 im = psf.computeImage(lsst.geom.PointD(x, y)).convertF() 

735 imPeak = psf.computePeak(lsst.geom.PointD(x, y)) 

736 im /= imPeak 

737 if bbox: 

738 im = im.Factory(im, bbox) 

739 lab = "PSF(%d,%d)" % (x, y) if False else "" 

740 mos.append(im, lab) 

741 

742 exp = afwImage.makeExposure(afwImage.makeMaskedImage(im)) 

743 exp.setPsf(psf) 

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

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

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

747 src = table.makeRecord() 

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

749 foot = afwDet.Footprint(spans) 

750 foot.addPeak(centerX, centerY, 1) 

751 src.setFootprint(foot) 

752 

753 try: 

754 centroider.measure(src, exp) 

755 centers.append((src.getX() - im.getX0(), src.getY() - im.getY0())) 

756 

757 shaper.measure(src, exp) 

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

759 except Exception: 

760 pass 

761 

762 if not display: 

763 display = afwDisplay.Display() 

764 mos.makeMosaic(display=display, title=title if title else "Model Psf", mode=nx) 

765 

766 if centers and display: 

767 with display.Buffering(): 

768 for i, (cen, shape) in enumerate(zip(centers, shapes)): 

769 bbox = mos.getBBox(i) 

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

771 if showCenter: 

772 display.dot("+", xc, yc, ctype=afwDisplay.BLUE) 

773 

774 if showEllipticity: 

775 ixx, ixy, iyy = shape 

776 ixx *= scale 

777 ixy *= scale 

778 iyy *= scale 

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

780 

781 return mos 

782 

783 

784def showPsfResiduals(exposure, sourceSet, magType="psf", scale=10, display=None): 

785 mimIn = exposure.getMaskedImage() 

786 mimIn = mimIn.Factory(mimIn, True) # make a copy to subtract from 

787 

788 psf = exposure.getPsf() 

789 psfWidth, psfHeight = psf.getLocalKernel().getDimensions() 

790 # 

791 # Make the image that we'll paste our residuals into. N.b. they can overlap the edges 

792 # 

793 w, h = int(mimIn.getWidth()/scale), int(mimIn.getHeight()/scale) 

794 

795 im = mimIn.Factory(w + psfWidth, h + psfHeight) 

796 

797 cenPos = [] 

798 for s in sourceSet: 

799 x, y = s.getX(), s.getY() 

800 

801 sx, sy = int(x/scale + 0.5), int(y/scale + 0.5) 

802 

803 smim = im.Factory(im, lsst.geom.BoxI(lsst.geom.PointI(sx, sy), 

804 lsst.geom.ExtentI(psfWidth, psfHeight))) 

805 sim = smim.getImage() 

806 

807 try: 

808 if magType == "ap": 

809 flux = s.getApInstFlux() 

810 elif magType == "model": 

811 flux = s.getModelInstFlux() 

812 elif magType == "psf": 

813 flux = s.getPsfInstFlux() 

814 else: 

815 raise RuntimeError("Unknown flux type %s" % magType) 

816 

817 subtractPsf(psf, mimIn, x, y, flux) 

818 except Exception as e: 

819 print(e) 

820 

821 try: 

822 expIm = mimIn.getImage().Factory(mimIn.getImage(), 

823 lsst.geom.BoxI(lsst.geom.PointI(int(x) - psfWidth//2, 

824 int(y) - psfHeight//2), 

825 lsst.geom.ExtentI(psfWidth, psfHeight)), 

826 ) 

827 except pexExcept.Exception: 

828 continue 

829 

830 cenPos.append([x - expIm.getX0() + sx, y - expIm.getY0() + sy]) 

831 

832 sim += expIm 

833 

834 if display: 

835 display = afwDisplay.Display() 

836 display.mtv(im, title="showPsfResiduals: image") 

837 with display.Buffering(): 

838 for x, y in cenPos: 

839 display.dot("+", x, y) 

840 

841 return im 

842 

843 

844def saveSpatialCellSet(psfCellSet, fileName="foo.fits", display=None): 

845 """Write the contents of a SpatialCellSet to a many-MEF fits file""" 

846 

847 mode = "w" 

848 for cell in psfCellSet.getCellList(): 

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

850 dx = afwImage.positionToIndex(cand.getXCenter(), True)[1] 

851 dy = afwImage.positionToIndex(cand.getYCenter(), True)[1] 

852 im = afwMath.offsetImage(cand.getMaskedImage(), -dx, -dy, "lanczos5") 

853 

854 md = dafBase.PropertySet() 

855 md.set("CELL", cell.getLabel()) 

856 md.set("ID", cand.getId()) 

857 md.set("XCENTER", cand.getXCenter()) 

858 md.set("YCENTER", cand.getYCenter()) 

859 md.set("BAD", cand.isBad()) 

860 md.set("AMPL", cand.getAmplitude()) 

861 md.set("FLUX", cand.getSource().getPsfInstFlux()) 

862 md.set("CHI2", cand.getSource().getChi2()) 

863 

864 im.writeFits(fileName, md, mode) 

865 mode = "a" 

866 

867 if display: 

868 display.mtv(im, title="saveSpatialCellSet: image")