Coverage for tests/utils.py: 34%

690 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-15 09:47 +0000

1# 

2# Developed for the LSST Data Management System. 

3# This product includes software developed by the LSST Project 

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

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

6# for details of code ownership. 

7# 

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

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

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

11# (at your option) any later version. 

12# 

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

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

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

16# GNU General Public License for more details. 

17# 

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

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

20 

21"""Support utilities for Measuring sources""" 

22 

23# Export DipoleTestImage to expose fake image generating funcs 

24__all__ = ["DipoleTestImage"] 

25 

26 

27import astropy.time 

28import numpy as np 

29import lsst.geom as geom 

30import lsst.afw.detection as afwDet 

31import lsst.afw.display as afwDisplay 

32import lsst.afw.detection as afwDetection 

33import lsst.afw.geom as afwGeom 

34import lsst.afw.image as afwImage 

35from lsst.afw.coord import Observatory 

36import lsst.daf.base as dafBase 

37import lsst.afw.math as afwMath 

38import lsst.afw.table as afwTable 

39from lsst.daf.butler import DataCoordinate, DimensionUniverse, Timespan 

40import lsst.meas.algorithms as measAlg 

41import lsst.meas.base as measBase 

42from lsst.meas.algorithms.testUtils import plantSources 

43from lsst.ip.diffim.dipoleFitTask import DipoleFitAlgorithm 

44from lsst.ip.diffim import diffimLib 

45 

46afwDisplay.setDefaultMaskTransparency(75) 

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

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: 

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, edgeWidth=8): 

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: 727 ↛ 728line 727 didn't jump to line 728 because the condition on line 727 was never true

728 self.fluxNeg = self.flux 

729 self.noise = noise 

730 self.gradientParams = gradientParams 

731 self.edgeWidth = edgeWidth 

732 self._makeDipoleImage() 

733 

734 def _makeDipoleImage(self): 

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

736 """ 

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

738 posImage, posCatalog = self._makeStarImage( 

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

740 

741 negImage, negCatalog = self._makeStarImage( 

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

743 

744 dipole = posImage.clone() 

745 di = dipole.getMaskedImage() 

746 di -= negImage.getMaskedImage() 

747 

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

749 = dipole, posImage, posCatalog, negImage, negCatalog 

750 

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

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

753 """ 

754 from lsst.meas.base.tests import TestDataset 

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

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

757 

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

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

760 

761 if schema is None: 761 ↛ 763line 761 didn't jump to line 763 because the condition on line 761 was always true

762 schema = TestDataset.makeMinimalSchema() 

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

764 

765 # set EDGE by masking the whole exposure and un-masking an inner bbox 

766 edgeMask = exposure.mask.getPlaneBitMask('EDGE') 

767 exposure.mask.array |= edgeMask 

768 inner_bbox = exposure.getBBox() 

769 inner_bbox.grow(-self.edgeWidth) 

770 exposure[inner_bbox].mask.array &= ~edgeMask 

771 

772 if self.gradientParams is not None: 772 ↛ 781line 772 didn't jump to line 781 because the condition on line 772 was always true

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

774 gp = self.gradientParams 

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

776 if len(self.gradientParams) > 3: # it includes a set of 2nd-order polynomial params 776 ↛ 777line 776 didn't jump to line 777 because the condition on line 776 was never true

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

778 imgArr = exposure.image.array 

779 imgArr += gradient 

780 

781 return exposure, catalog 

782 

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

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

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

786 return fitResult 

787 

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

789 """Utility function for detecting dipoles. 

790 

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

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

793 helps with dipole measurement for faint dipoles. 

794 

795 Parameters 

796 ---------- 

797 doMerge : `bool` 

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

799 source table. 

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

801 Difference image on which to perform detection. 

802 detectSigma : `float` 

803 Threshold for object detection. 

804 grow : `int` 

805 Number of pixels to grow the footprints before merging. 

806 minBinSize : `int` 

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

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

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

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

811 over and decrease the fit order appropriately.) 

812 

813 Returns 

814 ------- 

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

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

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

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

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

820 returned. 

821 """ 

822 if diffim is None: 822 ↛ 826line 822 didn't jump to line 826 because the condition on line 822 was always true

823 diffim = self.diffim 

824 

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

826 schema = afwTable.SourceTable.makeMinimalSchema() 

827 

828 # Customize the detection task a bit (optional) 

829 detectConfig = measAlg.SourceDetectionConfig() 

830 detectConfig.returnOriginalFootprints = False # should be the default 

831 

832 # code from imageDifference.py: 

833 detectConfig.thresholdPolarity = "both" 

834 detectConfig.thresholdValue = detectSigma 

835 # detectConfig.nSigmaToGrow = psfSigma 

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

837 detectConfig.thresholdType = "pixel_stdev" 

838 detectConfig.excludeMaskPlanes = ["EDGE"] 

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

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

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

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

843 

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

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

846 

847 table = afwTable.SourceTable.make(schema) 

848 catalog = detectTask.run(table, diffim) 

849 

850 # Now do the merge. 

851 if doMerge: 

852 fpSet = catalog.positive 

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

854 sources = afwTable.SourceCatalog(table) 

855 fpSet.makeSources(sources) 

856 

857 return sources 

858 

859 else: 

860 return detectTask, schema 

861 

862 

863def detectTestSources(exposure, addMaskPlanes=None): 

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

865 

866 Parameters 

867 ---------- 

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

869 Exposure on which to run detection/measurement 

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

871 addMaskPlanes : `list` of `str`, optional 

872 Additional mask planes to add to the maskedImage of the exposure. 

873 

874 Returns 

875 ------- 

876 selectSources 

877 Source catalog containing candidates 

878 """ 

879 if addMaskPlanes is None: 

880 # And add empty INJECTED and INJECTED_TEMPLATE mask planes 

881 addMaskPlanes = ["INJECTED", "INJECTED_TEMPLATE"] 

882 

883 schema = afwTable.SourceTable.makeMinimalSchema() 

884 

885 selectDetection = measAlg.SourceDetectionTask(schema=schema) 

886 selectMeasurement = measBase.SingleFrameMeasurementTask(schema=schema) 

887 table = afwTable.SourceTable.make(schema) 

888 

889 detRet = selectDetection.run( 

890 table=table, 

891 exposure=exposure, 

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

893 doSmooth=True 

894 ) 

895 for mp in addMaskPlanes: 

896 exposure.mask.addMaskPlane(mp) 

897 

898 selectSources = detRet.sources 

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

900 

901 return selectSources 

902 

903 

904def makeFakeWcs(): 

905 """Make a fake, affine Wcs. 

906 """ 

907 crpix = geom.Point2D(123.45, 678.9) 

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

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

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

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

912 

913 

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

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

916 kernelSize=31, templateBorderSize=0, 

917 background=None, 

918 xSize=256, 

919 ySize=256, 

920 x0=12345, 

921 y0=67890, 

922 calibration=1., 

923 doApplyCalibration=False, 

924 xLoc=None, 

925 yLoc=None, 

926 flux=None, 

927 clearEdgeMask=False, 

928 addMaskPlanes=None, 

929 band="g", 

930 physicalFilter="g NotACamera" 

931 ): 

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

933 

934 Parameters 

935 ---------- 

936 seed : `int`, optional 

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

938 nSrc : `int`, optional 

939 Number of sources to simulate. 

940 psfSize : `float`, optional 

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

942 noiseLevel : `float`, optional 

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

944 noiseSeed : `int`, optional 

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

946 fluxLevel : `float`, optional 

947 Reference flux of the simulated sources. 

948 fluxRange : `float`, optional 

949 Range in flux amplitude of the simulated sources. 

950 kernelSize : `int`, optional 

951 Size in pixels of the kernel for simulating sources. Must be odd. 

952 templateBorderSize : `int`, optional 

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

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

955 Optional background to add to the output image. 

956 xSize, ySize : `int`, optional 

957 Size in pixels of the simulated image. 

958 x0, y0 : `int`, optional 

959 Origin of the image. 

960 calibration : `float`, optional 

961 Conversion factor between instFlux and nJy. 

962 doApplyCalibration : `bool`, optional 

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

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

965 User-specified coordinates of the simulated sources. 

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

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

968 User-specified fluxes of the simulated sources. 

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

970 clearEdgeMask : `bool`, optional 

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

972 addMaskPlanes : `list` of `str`, optional 

973 Mask plane names to add to the image. 

974 

975 Returns 

976 ------- 

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

978 The model image, with the mask and variance planes. The DETECTED 

979 plane is filled in for the injected source footprints. 

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

981 Catalog of sources inserted in the model image. 

982 

983 Raises 

984 ------ 

985 ValueError 

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

987 or if ``kernelSize`` is even. 

988 """ 

989 if kernelSize % 2 == 0: 

990 raise ValueError(f"kernelSize must be odd, got {kernelSize}.") 

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

992 # sources in the model images. 

993 bufferSize = kernelSize//2 + templateBorderSize + 1 

994 

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

996 if templateBorderSize > 0: 

997 bbox.grow(templateBorderSize) 

998 

999 rng = np.random.RandomState(seed) 

1000 rngNoise = np.random.RandomState(noiseSeed) 

1001 x0, y0 = bbox.getBegin() 

1002 xSize, ySize = bbox.getDimensions() 

1003 if xLoc is None: 

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

1005 else: 

1006 if len(xLoc) != nSrc: 1006 ↛ 1007line 1006 didn't jump to line 1007 because the condition on line 1006 was never true

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

1008 if yLoc is None: 

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

1010 else: 

1011 if len(yLoc) != nSrc: 1011 ↛ 1012line 1011 didn't jump to line 1012 because the condition on line 1011 was never true

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

1013 

1014 if flux is None: 1014 ↛ 1017line 1014 didn't jump to line 1017 because the condition on line 1014 was always true

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

1016 else: 

1017 if len(flux) != nSrc: 

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

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

1020 injectList = list(zip(xLoc, yLoc, flux, sigmas)) 

1021 skyLevel = 0 

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

1023 modelExposure = plantSources(bbox, kernelSize, skyLevel, injectList, addPoissonNoise=False) 

1024 modelExposure.setWcs(makeFakeWcs()) 

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

1026 noise -= np.mean(noise) 

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

1028 modelExposure.image.array += noise 

1029 

1030 # Run source detection to set up the mask plane 

1031 detectTestSources(modelExposure, addMaskPlanes=addMaskPlanes) 

1032 if clearEdgeMask: 

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

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

1035 if background is not None: 

1036 modelExposure.image += background 

1037 modelExposure.maskedImage /= calibration 

1038 modelExposure.setFilter(afwImage.FilterLabel(band, physicalFilter)) 

1039 modelExposure.info.setId(seed) 

1040 if doApplyCalibration: 

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

1042 

1043 truth = _fillTruthCatalog(injectList) 

1044 

1045 return modelExposure, truth 

1046 

1047 

1048def _makeTruthSchema(): 

1049 """Make a schema for the truth catalog produced by `makeTestImage`. 

1050 

1051 Returns 

1052 ------- 

1053 keys : `dict` [`str`] 

1054 Fields added to the catalog, to make it easier to set them. 

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

1056 Schema to use to make a "truth" SourceCatalog. 

1057 Calib, Ap, and Psf flux slots all are set to ``truth_instFlux``. 

1058 """ 

1059 schema = afwTable.SourceTable.makeMinimalSchema() 

1060 keys = {} 

1061 # Don't use a FluxResultKey so we can manage the flux and err separately. 

1062 keys["instFlux"] = schema.addField("truth_instFlux", type=np.float64, 

1063 doc="true instFlux", units="count") 

1064 keys["instFluxErr"] = schema.addField("truth_instFluxErr", type=np.float64, 

1065 doc="true instFluxErr", units="count") 

1066 keys["centroid"] = afwTable.Point2DKey.addFields(schema, "truth", "true simulated centroid", "pixel") 

1067 schema.addField("truth_flag", "Flag", "truth flux failure flag.") 

1068 # Add the flag fields a source selector would need. 

1069 schema.addField("sky_source", "Flag", "testing flag.") 

1070 schema.addField("base_PixelFlags_flag_interpolated", "Flag", "testing flag.") 

1071 schema.addField("base_PixelFlags_flag_saturated", "Flag", "testing flag.") 

1072 schema.addField("base_PixelFlags_flag_bad", "Flag", "testing flag.") 

1073 schema.addField("base_PixelFlags_flag_edge", "Flag", "testing flag.") 

1074 schema.addField("base_PixelFlags_flag_nodata", "Flag", "testing flag.") 

1075 schema.addField("base_PsfFlux_flag", "Flag", "testing flag.") 

1076 schema.addField("base_ClassificationSizeExtendedness_value", "Flag", "testing flag.") 

1077 schema.addField("deblend_nChild", "Flag", "testing flag.") 

1078 schema.addField("detect_isPrimary", "Flag", "testing flag.") 

1079 schema.addField("calib_psf_used", "Flag", "testing flag.") 

1080 schema.getAliasMap().set("slot_Centroid", "truth") 

1081 schema.getAliasMap().set("slot_CalibFlux", "truth") 

1082 schema.getAliasMap().set("slot_ApFlux", "truth") 

1083 schema.getAliasMap().set("slot_PsfFlux", "truth") 

1084 schema.addField("glint_trail", "Flag", "testing flag.") 

1085 return keys, schema 

1086 

1087 

1088def _fillTruthCatalog(injectList): 

1089 """Add injected sources to the truth catalog. 

1090 

1091 Parameters 

1092 ---------- 

1093 injectList : `list` [`float`] 

1094 Sources that were injected; tuples of (x, y, flux, size). 

1095 

1096 Returns 

1097 ------- 

1098 catalog : `lsst.afw.table.SourceCatalog` 

1099 Catalog with centroids and instFlux/instFluxErr values filled in and 

1100 appropriate slots set. 

1101 """ 

1102 keys, schema = _makeTruthSchema() 

1103 catalog = afwTable.SourceCatalog(schema) 

1104 catalog.reserve(len(injectList)) 

1105 for x, y, flux, size in injectList: 

1106 record = catalog.addNew() 

1107 keys["centroid"].set(record, geom.PointD(x, y)) 

1108 keys["instFlux"].set(record, flux) 

1109 # Approximate injected errors 

1110 keys["instFluxErr"].set(record, 20) 

1111 # 5-sigma effective source width 

1112 circle = afwGeom.Ellipse(afwGeom.ellipses.Axes(5*size, 5*size, 0), geom.Point2D(x, y)) 

1113 footprint = afwDetection.Footprint(afwGeom.SpanSet.fromShape(circle)) 

1114 footprint.addPeak(x, y, flux) 

1115 record.setFootprint(footprint) 

1116 

1117 # Set source records for isolated stars 

1118 record["base_ClassificationSizeExtendedness_value"] = 0 

1119 record["deblend_nChild"] = 0 

1120 record["detect_isPrimary"] = True 

1121 record["calib_psf_used"] = True 

1122 

1123 return catalog 

1124 

1125 

1126def makeStats(badMaskPlanes=None): 

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

1128 

1129 Parameters 

1130 ---------- 

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

1132 List of mask planes to exclude from calculations. 

1133 

1134 Returns 

1135 ------- 

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

1137 Statistics control object for configuring calculations on images. 

1138 """ 

1139 if badMaskPlanes is None: 

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

1141 "BAD", "NO_DATA", "DETECTED_NEGATIVE") 

1142 statsControl = afwMath.StatisticsControl() 

1143 statsControl.setNumSigmaClip(3.) 

1144 statsControl.setNumIter(3) 

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

1146 return statsControl 

1147 

1148 

1149class CustomCoaddPsf(measAlg.CoaddPsf): 

1150 """A custom CoaddPSF that overrides the getAveragePosition method. 

1151 

1152 It intentionally moves the position off-image to cause a test failure. 

1153 """ 

1154 def getAveragePosition(self): 

1155 return geom.Point2D(-10000, -10000) 

1156 

1157 

1158def makeTestVisitInfo(visit=12345): 

1159 """Return a visit info for the test images. 

1160 

1161 Converting an exposure to an `lsst.images` type reads the observation 

1162 metadata it still carries from its visit info, so an exposure that will 

1163 be converted needs one. The date matches the timespan of 

1164 `makeTestExposureRecord`. 

1165 

1166 Parameters 

1167 ---------- 

1168 visit : `int`, optional 

1169 Id of the visit to record. 

1170 

1171 Returns 

1172 ------- 

1173 visitInfo : `lsst.afw.image.VisitInfo` 

1174 The visit info. 

1175 """ 

1176 return afwImage.VisitInfo( 

1177 id=visit, 

1178 exposureTime=30.0, 

1179 date=dafBase.DateTime(60815.000174, dafBase.DateTime.MJD, dafBase.DateTime.TAI), 

1180 boresightRaDec=geom.SpherePoint(23.1, 73.2, geom.degrees), 

1181 boresightAzAlt=geom.SpherePoint(120.0, 60.0, geom.degrees), 

1182 boresightRotAngle=73.2*geom.degrees, 

1183 rotType=afwImage.RotType.SKY, 

1184 observatory=Observatory(11.1*geom.degrees, 22.2*geom.degrees, 0.333), 

1185 ) 

1186 

1187 

1188def makeTestExposureRecord(universe, instrument="testCam", visit=12345): 

1189 """Return an ``exposure`` dimension record for the test images. 

1190 

1191 The record holds the observation metadata that converting an image to an 

1192 `lsst.images` type needs. 

1193 

1194 Parameters 

1195 ---------- 

1196 universe : `lsst.daf.butler.DimensionUniverse` 

1197 Dimension universe that defines the record's schema. 

1198 instrument : `str`, optional 

1199 Name of the instrument to record. 

1200 visit : `int`, optional 

1201 Id of the visit to record. The conversion takes the visit id of an 

1202 image from the id of its exposure record. 

1203 

1204 Returns 

1205 ------- 

1206 record : `lsst.daf.butler.DimensionRecord` 

1207 The ``exposure`` record. 

1208 """ 

1209 begin = astropy.time.Time(60815.0, format="mjd", scale="tai") 

1210 exposureTime = 30.0 

1211 return universe["exposure"].RecordClass( 

1212 instrument=instrument, 

1213 id=visit, 

1214 obs_id=f"test_{visit}", 

1215 group=str(visit), 

1216 day_obs=20250520, 

1217 physical_filter="test filter", 

1218 exposure_time=exposureTime, 

1219 seq_num=1, 

1220 seq_start=1, 

1221 seq_end=1, 

1222 can_see_sky=True, 

1223 azimuth=120.0, 

1224 zenith_angle=30.0, 

1225 timespan=Timespan(begin=begin, end=begin + astropy.time.TimeDelta(exposureTime, format="sec")), 

1226 ) 

1227 

1228 

1229def generate_data_id(*, 

1230 tract: int = 9813, 

1231 patch: int = 42, 

1232 cell_x: int = 4, 

1233 cell_y: int = 2, 

1234 band: str = "notR", 

1235 ) -> DataCoordinate: 

1236 """Generate a DataCoordinate instance to use as data_id. 

1237 

1238 Modified from ``generate_data_id`` in ``lsst.cell_coadds.test_utils`` 

1239 

1240 Parameters 

1241 ---------- 

1242 tract : `int`, optional 

1243 Tract ID for the data_id 

1244 patch : `int`, optional 

1245 Patch ID for the data_id 

1246 cell_x : `int`, optional 

1247 X index of the cell this patch corresponds to 

1248 cell_y : `int`, optional 

1249 Y index of the cell this patch corresponds to 

1250 band : `str`, optional 

1251 Band for the data_id 

1252 

1253 Returns 

1254 ------- 

1255 data_id : `lsst.daf.butler.DataCoordinate` 

1256 An expanded data_id instance. 

1257 """ 

1258 universe = DimensionUniverse() 

1259 

1260 instrument = universe["instrument"] 

1261 instrument_record = instrument.RecordClass( 

1262 name="DummyCam", 

1263 class_name="lsst.obs.base.instrument_tests.DummyCam", 

1264 ) 

1265 

1266 skymap = universe["skymap"] 

1267 skymap_record = skymap.RecordClass(name="test_skymap") 

1268 

1269 band_element = universe["band"] 

1270 band_record = band_element.RecordClass(name=band) 

1271 

1272 physical_filter = universe["physical_filter"] 

1273 physical_filter_record = physical_filter.RecordClass(name=band, instrument="test", band=band) 

1274 

1275 patch_element = universe["patch"] 

1276 patch_record = patch_element.RecordClass( 

1277 skymap="test_skymap", tract=tract, patch=patch, cell_x=cell_x, cell_y=cell_y 

1278 ) 

1279 

1280 # A dictionary with all the relevant records. 

1281 record = { 

1282 "instrument": instrument_record, 

1283 "patch": patch_record, 

1284 "tract": 9813, 

1285 "band": band_record.name, 

1286 "skymap": skymap_record.name, 

1287 "physical_filter": physical_filter_record, 

1288 } 

1289 

1290 # A dictionary with all the relevant recordIds. 

1291 record_id = record.copy() 

1292 for key in ( 

1293 "instrument", 

1294 "physical_filter", 

1295 ): 

1296 record_id[key] = record_id[key].name 

1297 

1298 data_id = DataCoordinate.standardize(record_id, universe=universe) 

1299 return data_id.expanded(record) 

1300 

1301 

1302def checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True): 

1303 """Exclude sources that are located on or adjacent to masked pixels. 

1304 

1305 Parameters 

1306 ---------- 

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

1308 The image mask plane to use to reject sources 

1309 based on the location of their centroid on the ccd. 

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

1311 The source catalog to evaluate. 

1312 excludeMaskPlanes : `list` of `str` 

1313 List of the names of the mask planes to exclude. 

1314 

1315 Returns 

1316 ------- 

1317 flags : `numpy.ndarray` of `bool` 

1318 Array indicating whether each source in the catalog should be 

1319 kept (True) or rejected (False) based on the value of the 

1320 mask plane at its location. 

1321 """ 

1322 setExcludeMaskPlanes = [ 

1323 maskPlane for maskPlane in excludeMaskPlanes if maskPlane in mask.getMaskPlaneDict() 

1324 ] 

1325 

1326 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes) 

1327 

1328 xv = (np.rint(sources.getX() - mask.getX0())).astype(int) 

1329 yv = (np.rint(sources.getY() - mask.getY0())).astype(int) 

1330 

1331 flags = np.ones(len(sources), dtype=bool) 

1332 if checkAdjacent: 1332 ↛ 1335line 1332 didn't jump to line 1335 because the condition on line 1332 was always true

1333 pixRange = (0, -1, 1) 

1334 else: 

1335 pixRange = (0,) 

1336 for j in pixRange: 

1337 for i in pixRange: 

1338 mv = mask.array[yv + j, xv + i] 

1339 flags *= np.bitwise_and(mv, excludePixelMask) == 0 

1340 return flags