Coverage for python/lsst/meas/deblender/plugins.py: 4%

663 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-28 03:36 -0700

1# This file is part of meas_deblender. 

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__all__ = ["DeblenderPlugin", "fitPsfs", "buildSymmetricTemplates", "rampFluxAtEdge", 

23 "medianSmoothTemplates", "makeTemplatesMonotonic", "clipFootprintsToNonzero", 

24 "weightTemplates", "reconstructTemplates", "apportionFlux"] 

25 

26import numpy as np 

27 

28import lsst.pex.exceptions 

29import lsst.afw.image as afwImage 

30import lsst.afw.detection as afwDet 

31import lsst.afw.geom as afwGeom 

32import lsst.geom as geom 

33 

34# Import C++ routines 

35from .baselineUtils import BaselineUtilsF as bUtils 

36 

37 

38def clipFootprintToNonzeroImpl(foot, image): 

39 """Clips the given *Footprint* to the region in the *Image* 

40 containing non-zero values. 

41 

42 The clipping drops spans that are 

43 totally zero, and moves endpoints to non-zero; it does not 

44 split spans that have internal zeros. 

45 """ 

46 x0 = image.getX0() 

47 y0 = image.getY0() 

48 xImMax = x0 + image.getDimensions().getX() 

49 yImMax = y0 + image.getDimensions().getY() 

50 newSpans = [] 

51 arr = image.getArray() 

52 for span in foot.spans: 

53 y = span.getY() 

54 if y < y0 or y > yImMax: 

55 continue 

56 spanX0 = span.getX0() 

57 spanX1 = span.getX1() 

58 xMin = spanX0 if spanX0 >= x0 else x0 

59 xMax = spanX1 if spanX1 <= xImMax else xImMax 

60 xarray = np.arange(xMin, xMax+1)[arr[y-y0, xMin-x0:xMax-x0+1] != 0] 

61 if len(xarray) > 0: 

62 newSpans.append(afwGeom.Span(y, xarray[0], xarray[-1])) 

63 # Time to update the SpanSet 

64 foot.setSpans(afwGeom.SpanSet(newSpans, normalize=False)) 

65 foot.removeOrphanPeaks() 

66 

67 

68class DeblenderPlugin: 

69 """Class to define plugins for the deblender. 

70 

71 The new deblender executes a series of plugins specified by the user. 

72 Each plugin defines the function to be executed, the keyword arguments 

73 required by the function, and whether or not certain portions of the 

74 deblender might need to be rerun as a result of the function. 

75 """ 

76 def __init__(self, func, onReset=None, maxIterations=50, **kwargs): 

77 """Initialize a deblender plugin 

78 

79 Parameters 

80 ---------- 

81 func: `function` 

82 Function to run when the plugin is executed. 

83 The function should always take 

84 `debResult`, a `DeblenderResult` that stores the deblender result, 

85 and `log`, an `lsst.log`, as the first two arguments, as well as 

86 any additional keyword arguments (that must 

87 be specified in ``kwargs``). The function should also return 

88 ``modified``, a `bool` that tells the deblender whether 

89 or not any templates have been modified by the function. 

90 If ``modified==True``, the deblender will go back to step 

91 ``onReset``, unless the has already been run ``maxIterations``. 

92 onReset: `int` 

93 Index of the deblender plugin to return to if ``func`` modifies 

94 any templates. The default is ``None``, which does not re-run 

95 any plugins. 

96 maxIterations: `int` 

97 Maximum number of times the deblender will reset when 

98 the current plugin 

99 returns ``True``. 

100 """ 

101 self.func = func 

102 self.kwargs = kwargs 

103 self.onReset = onReset 

104 self.maxIterations = maxIterations 

105 self.kwargs = kwargs 

106 self.iterations = 0 

107 

108 def run(self, debResult, log): 

109 """Execute the current plugin 

110 

111 Once the plugin has finished, check to see if part of the deblender 

112 must be executed again. 

113 """ 

114 log.trace("Executing %s", self.func.__name__) 

115 reset = self.func(debResult, log, **self.kwargs) 

116 if reset: 

117 self.iterations += 1 

118 if self.iterations < self.maxIterations: 

119 return self.onReset 

120 return None 

121 

122 def __str__(self): 

123 return ("<Deblender Plugin: func={0}, kwargs={1}".format(self.func.__name__, self.kwargs)) 

124 

125 def __repr__(self): 

126 return self.__str__() 

127 

128 

129def _setPeakError(debResult, log, pk, cx, cy, filters, msg, flag): 

130 """Update the peak in each band with an error 

131 

132 This function logs an error that occurs during deblending and sets the 

133 relevant flag. 

134 

135 Parameters 

136 ---------- 

137 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

138 Container for the final deblender results. 

139 log: `log.Log` 

140 LSST logger for logging purposes. 

141 pk: int 

142 Number of the peak that failed 

143 cx: float 

144 x coordinate of the peak 

145 cy: float 

146 y coordinate of the peak 

147 filters: list of str 

148 List of filter names for the exposures 

149 msg: str 

150 Message to display in log traceback 

151 flag: str 

152 Name of the flag to set 

153 

154 Returns 

155 ------- 

156 None 

157 """ 

158 log.trace("Peak %d at (%f,%f):%s", pk, cx, cy, msg) 

159 for fidx, f in enumerate(filters): 

160 pkResult = debResult.deblendedParents[f].peaks[pk] 

161 getattr(pkResult, flag)() 

162 

163 

164def fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2): 

165 """Fit a PSF + smooth background model (linear) to a small region 

166 around each peak. 

167 

168 This function will iterate over all filters in deblender result but does 

169 not compare results across filters. 

170 DeblendedPeaks that pass the cuts have their templates modified to the 

171 PSF + background model and their ``deblendedAsPsf`` property set 

172 to ``True``. 

173 

174 This will likely be replaced in the future with a function that compares 

175 the psf chi-squared cuts so that peaks flagged as point sources will be 

176 considered point sources in all bands. 

177 

178 Parameters 

179 ---------- 

180 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

181 Container for the final deblender results. 

182 log: `log.Log` 

183 LSST logger for logging purposes. 

184 psfChisqCut*: `float`, optional 

185 ``psfChisqCut1`` is the maximum chi-squared-per-degree-of-freedom 

186 allowed for a peak to be considered a PSF match without recentering. 

187 A fit is also made that includes terms to recenter the PSF. 

188 ``psfChisqCut2`` is the same as ``psfChisqCut1`` except it 

189 determines the restriction on the fit that includes 

190 recentering terms. 

191 If the peak is a match for a re-centered PSF, the PSF is 

192 repositioned at the new center and 

193 the peak footprint is fit again, this time to the new PSF. 

194 If the resulting chi-squared-per-degree-of-freedom is less than 

195 ``psfChisqCut2b`` then it passes the re-centering algorithm. 

196 If the peak passes both the re-centered and fixed position cuts, 

197 the better of the two is accepted, but parameters for all three psf 

198 fits are stored in the ``DebldendedPeak``. 

199 The default for ``psfChisqCut1``, ``psfChisqCut2``, and 

200 ``psfChisqCut2b`` is ``1.5``. 

201 tinyFootprintSize: `float`, optional 

202 The PSF model is shrunk to the size that contains the original 

203 footprint. If the bbox of the clipped PSF model for a peak is 

204 smaller than ``max(tinyFootprintSize,2)`` then ``tinyFootprint`` for 

205 the peak is set to ``True`` and the peak is not fit. The default is 2. 

206 

207 Returns 

208 ------- 

209 modified: `bool` 

210 If any templates have been assigned to PSF point sources then 

211 ``modified`` is ``True``, otherwise it is ``False``. 

212 """ 

213 from .baseline import CachingPsf 

214 modified = False 

215 # Loop over all of the filters to build the PSF 

216 for fidx in debResult.filters: 

217 dp = debResult.deblendedParents[fidx] 

218 peaks = dp.fp.getPeaks() 

219 cpsf = CachingPsf(dp.psf) 

220 

221 # create mask image for pixels within the footprint 

222 fmask = afwImage.Mask(dp.bb) 

223 fmask.setXY0(dp.bb.getMinX(), dp.bb.getMinY()) 

224 dp.fp.spans.setMask(fmask, 1) 

225 

226 # pk.getF() -- retrieving the floating-point location of the peak 

227 # -- actually shows up in the profile if we do it in the loop, so 

228 # grab them all here. 

229 peakF = [pk.getF() for pk in peaks] 

230 

231 for pki, (pk, pkres, pkF) in enumerate(zip(peaks, dp.peaks, peakF)): 

232 log.trace('Filter %s, Peak %i', fidx, pki) 

233 ispsf = _fitPsf(dp.fp, fmask, pk, pkF, pkres, dp.bb, peaks, peakF, log, cpsf, dp.psffwhm, 

234 dp.img, dp.varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, tinyFootprintSize) 

235 modified = modified or ispsf 

236 return modified 

237 

238 

239def _fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peaksF, log, psf, psffwhm, 

240 img, varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, 

241 tinyFootprintSize=2, 

242 ): 

243 r"""Fit a PSF + smooth background model (linear) to a small region 

244 around a peak. 

245 

246 See fitPsfs for a more thorough description, including all 

247 parameters not described below. 

248 

249 Parameters 

250 ---------- 

251 fp: `afw.detection.Footprint` 

252 Footprint containing the Peaks to model. 

253 fmask: `afw.image.Mask` 

254 The Mask plane for pixels in the Footprint 

255 pk: `afw.detection.PeakRecord` 

256 The peak within the Footprint that we are going to fit with PSF model 

257 pkF: `afw.geom.Point2D` 

258 Floating point coordinates of the peak. 

259 pkres: `meas.deblender.DeblendedPeak` 

260 Peak results object that will hold the results. 

261 fbb: `afw.geom.Box2I` 

262 Bounding box of ``fp`` 

263 peaks: `afw.detection.PeakCatalog` 

264 Catalog of peaks contained in the parent footprint. 

265 peaksF: list of `afw.geom.Point2D` 

266 List of floating point coordinates of all of the peaks. 

267 psf: list of `afw.detection.Psf`\ s 

268 Psf of the ``maskedImage`` for each band. 

269 psffwhm: list pf `float`\ s 

270 FWHM of the ``maskedImage``\ 's ``psf`` in each band. 

271 img: `afw.image.ImageF` 

272 The image that contains the footprint. 

273 varimg: `afw.image.ImageF` 

274 The variance of the image that contains the footprint. 

275 

276 Results 

277 ------- 

278 ispsf: `bool` 

279 Whether or not the peak matches a PSF model. 

280 """ 

281 import lsstDebug 

282 

283 # my __name__ is lsst.meas.deblender.baseline 

284 debugPlots = lsstDebug.Info(__name__).plots 

285 debugPsf = lsstDebug.Info(__name__).psf 

286 

287 # The small region is a disk out to R0, plus a ramp with 

288 # decreasing weight down to R1. 

289 R0 = int(np.ceil(psffwhm*1.)) 

290 # ramp down to zero weight at this radius... 

291 R1 = int(np.ceil(psffwhm*1.5)) 

292 cx, cy = pkF.getX(), pkF.getY() 

293 psfimg = psf.computeImage(cx, cy) 

294 # R2: distance to neighbouring peak in order to put it into the model 

295 R2 = R1 + min(psfimg.getWidth(), psfimg.getHeight())/2. 

296 

297 pbb = psfimg.getBBox() 

298 pbb.clip(fbb) 

299 px0, py0 = psfimg.getX0(), psfimg.getY0() 

300 

301 # Make sure we haven't been given a substitute PSF that's nowhere near where we want, as may occur if 

302 # "Cannot compute CoaddPsf at point (xx,yy); no input images at that point." 

303 if not pbb.contains(geom.Point2I(int(cx), int(cy))): 

304 pkres.setOutOfBounds() 

305 return 

306 

307 # The bounding-box of the local region we are going to fit ("stamp") 

308 xlo = int(np.floor(cx - R1)) 

309 ylo = int(np.floor(cy - R1)) 

310 xhi = int(np.ceil(cx + R1)) 

311 yhi = int(np.ceil(cy + R1)) 

312 stampbb = geom.Box2I(geom.Point2I(xlo, ylo), geom.Point2I(xhi, yhi)) 

313 stampbb.clip(fbb) 

314 xlo, xhi = stampbb.getMinX(), stampbb.getMaxX() 

315 ylo, yhi = stampbb.getMinY(), stampbb.getMaxY() 

316 if xlo > xhi or ylo > yhi: 

317 log.trace('Skipping this peak: out of bounds') 

318 pkres.setOutOfBounds() 

319 return 

320 

321 # drop tiny footprints too? 

322 if min(stampbb.getWidth(), stampbb.getHeight()) <= max(tinyFootprintSize, 2): 

323 # Minimum size limit of 2 comes from the "PSF dx" calculation, which involves shifting the PSF 

324 # by one pixel to the left and right. 

325 log.trace('Skipping this peak: tiny footprint / close to edge') 

326 pkres.setTinyFootprint() 

327 return 

328 

329 # find other peaks within range... 

330 otherpeaks = [] 

331 for pk2, pkF2 in zip(peaks, peaksF): 

332 if pk2 == pk: 

333 continue 

334 if pkF.distanceSquared(pkF2) > R2**2: 

335 continue 

336 opsfimg = psf.computeImage(pkF2.getX(), pkF2.getY()) 

337 if not opsfimg.getBBox().overlaps(stampbb): 

338 continue 

339 otherpeaks.append(opsfimg) 

340 log.trace('%i other peaks within range', len(otherpeaks)) 

341 

342 # Now we are going to do a least-squares fit for the flux in this 

343 # PSF, plus a decenter term, a linear sky, and fluxes of nearby 

344 # sources (assumed point sources). Build up the matrix... 

345 # Number of terms -- PSF flux, constant sky, X, Y, + other PSF fluxes 

346 NT1 = 4 + len(otherpeaks) 

347 # + PSF dx, dy 

348 NT2 = NT1 + 2 

349 # Number of pixels -- at most 

350 NP = (1 + yhi - ylo)*(1 + xhi - xlo) 

351 # indices of columns in the "A" matrix. 

352 I_psf = 0 

353 I_sky = 1 

354 I_sky_ramp_x = 2 

355 I_sky_ramp_y = 3 

356 # offset of other psf fluxes: 

357 I_opsf = 4 

358 I_dx = NT1 + 0 

359 I_dy = NT1 + 1 

360 

361 # Build the matrix "A", rhs "b" and weight "w". 

362 ix0, iy0 = img.getX0(), img.getY0() 

363 fx0, fy0 = fbb.getMinX(), fbb.getMinY() 

364 fslice = (slice(ylo-fy0, yhi-fy0+1), slice(xlo-fx0, xhi-fx0+1)) 

365 islice = (slice(ylo-iy0, yhi-iy0+1), slice(xlo-ix0, xhi-ix0+1)) 

366 fmask_sub = fmask .getArray()[fslice] 

367 var_sub = varimg.getArray()[islice] 

368 img_sub = img.getArray()[islice] 

369 

370 # Clip the PSF image to match its bbox 

371 psfarr = psfimg.getArray()[pbb.getMinY()-py0: 1+pbb.getMaxY()-py0, 

372 pbb.getMinX()-px0: 1+pbb.getMaxX()-px0] 

373 px0, px1 = pbb.getMinX(), pbb.getMaxX() 

374 py0, py1 = pbb.getMinY(), pbb.getMaxY() 

375 

376 # Compute the "valid" pixels within our region-of-interest 

377 valid = (fmask_sub > 0) 

378 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1) 

379 RR = ((xx - cx)**2)[np.newaxis, :] + ((yy - cy)**2)[:, np.newaxis] 

380 valid *= (RR <= R1**2) 

381 valid *= (var_sub > 0) 

382 NP = valid.sum() 

383 

384 if NP == 0: 

385 log.warning('Skipping peak at (%.1f, %.1f): no unmasked pixels nearby', cx, cy) 

386 pkres.setNoValidPixels() 

387 return 

388 

389 # pixel coords of valid pixels 

390 XX, YY = np.meshgrid(xx, yy) 

391 ipixes = np.vstack((XX[valid] - xlo, YY[valid] - ylo)).T 

392 

393 inpsfx = (xx >= px0)*(xx <= px1) 

394 inpsfy = (yy >= py0)*(yy <= py1) 

395 inpsf = np.outer(inpsfy, inpsfx) 

396 indx = np.outer(inpsfy, (xx > px0)*(xx < px1)) 

397 indy = np.outer((yy > py0)*(yy < py1), inpsfx) 

398 

399 del inpsfx 

400 del inpsfy 

401 

402 def _overlap(xlo, xhi, xmin, xmax): 

403 assert((xlo <= xmax) and (xhi >= xmin) 

404 and (xlo <= xhi) and (xmin <= xmax)) 

405 xloclamp = max(xlo, xmin) 

406 Xlo = xloclamp - xlo 

407 xhiclamp = min(xhi, xmax) 

408 Xhi = Xlo + (xhiclamp - xloclamp) 

409 assert(xloclamp >= 0) 

410 assert(Xlo >= 0) 

411 return (xloclamp, xhiclamp+1, Xlo, Xhi+1) 

412 

413 A = np.zeros((NP, NT2)) 

414 # Constant term 

415 A[:, I_sky] = 1. 

416 # Sky slope terms: dx, dy 

417 A[:, I_sky_ramp_x] = ipixes[:, 0] + (xlo-cx) 

418 A[:, I_sky_ramp_y] = ipixes[:, 1] + (ylo-cy) 

419 

420 # whew, grab the valid overlapping PSF pixels 

421 px0, px1 = pbb.getMinX(), pbb.getMaxX() 

422 py0, py1 = pbb.getMinY(), pbb.getMaxY() 

423 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1) 

424 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1) 

425 dpx0, dpy0 = px0 - xlo, py0 - ylo 

426 psf_y_slice = slice(sy3 - dpy0, sy4 - dpy0) 

427 psf_x_slice = slice(sx3 - dpx0, sx4 - dpx0) 

428 psfsub = psfarr[psf_y_slice, psf_x_slice] 

429 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

430 A[inpsf[valid], I_psf] = psfsub[vsub] 

431 

432 # PSF dx -- by taking the half-difference of shifted-by-one and 

433 # shifted-by-minus-one. 

434 oldsx = (sx1, sx2, sx3, sx4) 

435 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0+1, px1-1) 

436 psfsub = (psfarr[psf_y_slice, sx3 - dpx0 + 1: sx4 - dpx0 + 1] 

437 - psfarr[psf_y_slice, sx3 - dpx0 - 1: sx4 - dpx0 - 1])/2. 

438 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

439 A[indx[valid], I_dx] = psfsub[vsub] 

440 # revert x indices... 

441 (sx1, sx2, sx3, sx4) = oldsx 

442 

443 # PSF dy 

444 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0+1, py1-1) 

445 psfsub = (psfarr[sy3 - dpy0 + 1: sy4 - dpy0 + 1, psf_x_slice] 

446 - psfarr[sy3 - dpy0 - 1: sy4 - dpy0 - 1, psf_x_slice])/2. 

447 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

448 A[indy[valid], I_dy] = psfsub[vsub] 

449 

450 # other PSFs... 

451 for j, opsf in enumerate(otherpeaks): 

452 obb = opsf.getBBox() 

453 ino = np.outer((yy >= obb.getMinY())*(yy <= obb.getMaxY()), 

454 (xx >= obb.getMinX())*(xx <= obb.getMaxX())) 

455 dpx0, dpy0 = obb.getMinX() - xlo, obb.getMinY() - ylo 

456 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, obb.getMinX(), obb.getMaxX()) 

457 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, obb.getMinY(), obb.getMaxY()) 

458 opsfarr = opsf.getArray() 

459 psfsub = opsfarr[sy3 - dpy0: sy4 - dpy0, sx3 - dpx0: sx4 - dpx0] 

460 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

461 A[ino[valid], I_opsf + j] = psfsub[vsub] 

462 

463 b = img_sub[valid] 

464 

465 # Weights -- from ramp and image variance map. 

466 # Ramp weights -- from 1 at R0 down to 0 at R1. 

467 rw = np.ones_like(RR) 

468 ii = (RR > R0**2) 

469 rr = np.sqrt(RR[ii]) 

470 rw[ii] = np.maximum(0, 1. - ((rr - R0)/(R1 - R0))) 

471 w = np.sqrt(rw[valid]/var_sub[valid]) 

472 # save the effective number of pixels 

473 sumr = np.sum(rw[valid]) 

474 log.debug('sumr = %g', sumr) 

475 

476 del ii 

477 

478 Aw = A*w[:, np.newaxis] 

479 bw = b*w 

480 

481 if debugPlots: 

482 import pylab as plt 

483 plt.clf() 

484 N = NT2 + 2 

485 R, C = 2, (N+1)/2 

486 for i in range(NT2): 

487 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo)) 

488 im1[ipixes[:, 1], ipixes[:, 0]] = A[:, i] 

489 plt.subplot(R, C, i+1) 

490 plt.imshow(im1, interpolation='nearest', origin='lower') 

491 plt.subplot(R, C, NT2+1) 

492 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo)) 

493 im1[ipixes[:, 1], ipixes[:, 0]] = b 

494 plt.imshow(im1, interpolation='nearest', origin='lower') 

495 plt.subplot(R, C, NT2+2) 

496 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo)) 

497 im1[ipixes[:, 1], ipixes[:, 0]] = w 

498 plt.imshow(im1, interpolation='nearest', origin='lower') 

499 plt.savefig('A.png') 

500 

501 # We do fits with and without the decenter (dx,dy) terms. 

502 # Since the dx,dy terms are at the end of the matrix, 

503 # we can do that just by trimming off those elements. 

504 # 

505 # The SVD can fail if there are NaNs in the matrices; this should 

506 # really be handled upstream 

507 try: 

508 # NT1 is number of terms without dx,dy; 

509 # X1 is the result without decenter 

510 X1, r1, rank1, s1 = np.linalg.lstsq(Aw[:, :NT1], bw, rcond=-1) 

511 # X2 is with decenter 

512 X2, r2, rank2, s2 = np.linalg.lstsq(Aw, bw, rcond=-1) 

513 except np.linalg.LinAlgError as e: 

514 log.warning("Failed to fit PSF to child: %s", e) 

515 pkres.setPsfFitFailed() 

516 return 

517 

518 log.debug('r1 r2 %s %s', r1, r2) 

519 

520 # r is weighted chi-squared = sum over pixels: ramp * (model - 

521 # data)**2/sigma**2 

522 if len(r1) > 0: 

523 chisq1 = r1[0] 

524 else: 

525 chisq1 = 1e30 

526 if len(r2) > 0: 

527 chisq2 = r2[0] 

528 else: 

529 chisq2 = 1e30 

530 dof1 = sumr - len(X1) 

531 dof2 = sumr - len(X2) 

532 log.debug('dof1, dof2 %g %g', dof1, dof2) 

533 

534 # This can happen if we're very close to the edge (?) 

535 if dof1 <= 0 or dof2 <= 0: 

536 log.trace('Skipping this peak: bad DOF %g, %g', dof1, dof2) 

537 pkres.setBadPsfDof() 

538 return 

539 

540 q1 = chisq1/dof1 

541 q2 = chisq2/dof2 

542 log.trace('PSF fits: chisq/dof = %g, %g', q1, q2) 

543 ispsf1 = (q1 < psfChisqCut1) 

544 ispsf2 = (q2 < psfChisqCut2) 

545 

546 pkres.psfFit1 = (chisq1, dof1) 

547 pkres.psfFit2 = (chisq2, dof2) 

548 

549 # check that the fit PSF spatial derivative terms aren't too big 

550 if ispsf2: 

551 fdx, fdy = X2[I_dx], X2[I_dy] 

552 f0 = X2[I_psf] 

553 # as a fraction of the PSF flux 

554 dx = fdx/f0 

555 dy = fdy/f0 

556 ispsf2 = ispsf2 and (abs(dx) < 1. and abs(dy) < 1.) 

557 log.trace('isPSF2 -- checking derivatives: dx,dy = %g, %g -> %s', dx, dy, str(ispsf2)) 

558 if not ispsf2: 

559 pkres.psfFitBigDecenter = True 

560 

561 # Looks like a shifted PSF: try actually shifting the PSF by that amount 

562 # and re-evaluate the fit. 

563 if ispsf2: 

564 psfimg2 = psf.computeImage(cx + dx, cy + dy) 

565 # clip 

566 pbb2 = psfimg2.getBBox() 

567 pbb2.clip(fbb) 

568 

569 # Make sure we haven't been given a substitute PSF that's nowhere near where we want, as may occur if 

570 # "Cannot compute CoaddPsf at point (xx,yy); no input images at that point." 

571 if not pbb2.contains(geom.Point2I(int(cx + dx), int(cy + dy))): 

572 ispsf2 = False 

573 else: 

574 # clip image to bbox 

575 px0, py0 = psfimg2.getX0(), psfimg2.getY0() 

576 psfarr = psfimg2.getArray()[pbb2.getMinY()-py0:1+pbb2.getMaxY()-py0, 

577 pbb2.getMinX()-px0:1+pbb2.getMaxX()-px0] 

578 px0, py0 = pbb2.getMinX(), pbb2.getMinY() 

579 px1, py1 = pbb2.getMaxX(), pbb2.getMaxY() 

580 

581 # yuck! Update the PSF terms in the least-squares fit matrix. 

582 Ab = A[:, :NT1] 

583 

584 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1) 

585 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1) 

586 dpx0, dpy0 = px0 - xlo, py0 - ylo 

587 psfsub = psfarr[sy3-dpy0:sy4-dpy0, sx3-dpx0:sx4-dpx0] 

588 vsub = valid[sy1-ylo:sy2-ylo, sx1-xlo:sx2-xlo] 

589 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1) 

590 inpsf = np.outer((yy >= py0)*(yy <= py1), (xx >= px0)*(xx <= px1)) 

591 Ab[inpsf[valid], I_psf] = psfsub[vsub] 

592 

593 Aw = Ab*w[:, np.newaxis] 

594 # re-solve... 

595 Xb, rb, rankb, sb = np.linalg.lstsq(Aw, bw, rcond=-1) 

596 if len(rb) > 0: 

597 chisqb = rb[0] 

598 else: 

599 chisqb = 1e30 

600 dofb = sumr - len(Xb) 

601 qb = chisqb/dofb 

602 ispsf2 = (qb < psfChisqCut2b) 

603 q2 = qb 

604 X2 = Xb 

605 log.trace('shifted PSF: new chisq/dof = %g; good? %s', qb, ispsf2) 

606 pkres.psfFit3 = (chisqb, dofb) 

607 

608 # Which one do we keep? 

609 if (((ispsf1 and ispsf2) and (q2 < q1)) 

610 or (ispsf2 and not ispsf1)): 

611 Xpsf = X2 

612 chisq = chisq2 

613 dof = dof2 

614 log.debug('dof %g', dof) 

615 log.trace('Keeping shifted-PSF model') 

616 cx += dx 

617 cy += dy 

618 pkres.psfFitWithDecenter = True 

619 else: 

620 # (arbitrarily set to X1 when neither fits well) 

621 Xpsf = X1 

622 chisq = chisq1 

623 dof = dof1 

624 log.debug('dof %g', dof) 

625 log.trace('Keeping unshifted PSF model') 

626 

627 ispsf = (ispsf1 or ispsf2) 

628 

629 # Save the PSF models in images for posterity. 

630 if debugPsf: 

631 SW, SH = 1+xhi-xlo, 1+yhi-ylo 

632 psfmod = afwImage.ImageF(SW, SH) 

633 psfmod.setXY0(xlo, ylo) 

634 psfderivmodm = afwImage.MaskedImageF(SW, SH) 

635 psfderivmod = psfderivmodm.getImage() 

636 psfderivmod.setXY0(xlo, ylo) 

637 model = afwImage.ImageF(SW, SH) 

638 model.setXY0(xlo, ylo) 

639 for i in range(len(Xpsf)): 

640 for (x, y), v in zip(ipixes, A[:, i]*Xpsf[i]): 

641 ix, iy = int(x), int(y) 

642 model.set(ix, iy, model.get(ix, iy) + float(v)) 

643 if i in [I_psf, I_dx, I_dy]: 

644 psfderivmod.set(ix, iy, psfderivmod.get(ix, iy) + float(v)) 

645 for ii in range(NP): 

646 x, y = ipixes[ii, :] 

647 psfmod.set(int(x), int(y), float(A[ii, I_psf]*Xpsf[I_psf])) 

648 modelfp = afwDet.Footprint(fp.getPeaks().getSchema()) 

649 for (x, y) in ipixes: 

650 modelfp.addSpan(int(y+ylo), int(x+xlo), int(x+xlo)) 

651 modelfp.normalize() 

652 

653 pkres.psfFitDebugPsf0Img = psfimg 

654 pkres.psfFitDebugPsfImg = psfmod 

655 pkres.psfFitDebugPsfDerivImg = psfderivmod 

656 pkres.psfFitDebugPsfModel = model 

657 pkres.psfFitDebugStamp = img.Factory(img, stampbb, True) 

658 pkres.psfFitDebugValidPix = valid # numpy array 

659 pkres.psfFitDebugVar = varimg.Factory(varimg, stampbb, True) 

660 ww = np.zeros(valid.shape, np.float) 

661 ww[valid] = w 

662 pkres.psfFitDebugWeight = ww # numpy 

663 pkres.psfFitDebugRampWeight = rw 

664 

665 # Save things we learned about this peak for posterity... 

666 pkres.psfFitR0 = R0 

667 pkres.psfFitR1 = R1 

668 pkres.psfFitStampExtent = (xlo, xhi, ylo, yhi) 

669 pkres.psfFitCenter = (cx, cy) 

670 log.debug('saving chisq,dof %g %g', chisq, dof) 

671 pkres.psfFitBest = (chisq, dof) 

672 pkres.psfFitParams = Xpsf 

673 pkres.psfFitFlux = Xpsf[I_psf] 

674 pkres.psfFitNOthers = len(otherpeaks) 

675 

676 if ispsf: 

677 pkres.setDeblendedAsPsf() 

678 

679 # replace the template image by the PSF + derivatives 

680 # image. 

681 log.trace('Deblending as PSF; setting template to PSF model') 

682 

683 # Instantiate the PSF model and clip it to the footprint 

684 psfimg = psf.computeImage(cx, cy) 

685 # Scale by fit flux. 

686 psfimg *= Xpsf[I_psf] 

687 psfimg = psfimg.convertF() 

688 

689 # Clip the Footprint to the PSF model image bbox. 

690 fpcopy = afwDet.Footprint(fp) 

691 psfbb = psfimg.getBBox() 

692 fpcopy.clipTo(psfbb) 

693 bb = fpcopy.getBBox() 

694 

695 # Copy the part of the PSF model within the clipped footprint. 

696 psfmod = afwImage.ImageF(bb) 

697 fpcopy.spans.copyImage(psfimg, psfmod) 

698 # Save it as our template. 

699 clipFootprintToNonzeroImpl(fpcopy, psfmod) 

700 pkres.setTemplate(psfmod, fpcopy) 

701 

702 # DEBUG 

703 pkres.setPsfTemplate(psfmod, fpcopy) 

704 

705 return ispsf 

706 

707 

708def buildSymmetricTemplates(debResult, log, patchEdges=False, setOrigTemplate=True): 

709 """Build a symmetric template for each peak in each filter 

710 

711 Given ``maskedImageF``, ``footprint``, and a ``DebldendedPeak``, creates 

712 a symmetric template (``templateImage`` and ``templateFootprint``) around 

713 the peak for all peaks not flagged as ``skip`` or ``deblendedAsPsf``. 

714 

715 Parameters 

716 ---------- 

717 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

718 Container for the final deblender results. 

719 log: `log.Log` 

720 LSST logger for logging purposes. 

721 patchEdges: `bool`, optional 

722 If True and if the parent Footprint touches pixels with the 

723 ``EDGE`` bit set, then grow the parent Footprint to include 

724 all symmetric templates. 

725 

726 Returns 

727 ------- 

728 modified: `bool` 

729 If any peaks are not skipped or marked as point sources, 

730 ``modified`` is ``True. Otherwise ``modified`` is ``False``. 

731 """ 

732 modified = False 

733 # Create the Templates for each peak in each filter 

734 for fidx in debResult.filters: 

735 dp = debResult.deblendedParents[fidx] 

736 imbb = dp.img.getBBox() 

737 log.trace('Creating templates for footprint at x0,y0,W,H = %i, %i, %i, %i)', dp.x0, dp.y0, dp.W, dp.H) 

738 

739 for peaki, pkres in enumerate(dp.peaks): 

740 log.trace('Deblending peak %i of %i', peaki, len(dp.peaks)) 

741 # TODO: Check debResult to see if the peak is deblended as a point source 

742 # when comparing all bands, not just a single band 

743 if pkres.skip or pkres.deblendedAsPsf: 

744 continue 

745 modified = True 

746 pk = pkres.peak 

747 cx, cy = pk.getIx(), pk.getIy() 

748 if not imbb.contains(geom.Point2I(cx, cy)): 

749 log.trace('Peak center is not inside image; skipping %i', pkres.pki) 

750 pkres.setOutOfBounds() 

751 continue 

752 log.trace('computing template for peak %i at (%i, %i)', pkres.pki, cx, cy) 

753 timg, tfoot, patched = bUtils.buildSymmetricTemplate(dp.maskedImage, dp.fp, pk, dp.avgNoise, 

754 True, patchEdges) 

755 if timg is None: 

756 log.trace('Peak %i at (%i, %i): failed to build symmetric template', pkres.pki, cx, cy) 

757 pkres.setFailedSymmetricTemplate() 

758 continue 

759 

760 if patched: 

761 pkres.setPatched() 

762 

763 # possibly save the original symmetric template 

764 if setOrigTemplate: 

765 pkres.setOrigTemplate(timg, tfoot) 

766 pkres.setTemplate(timg, tfoot) 

767 return modified 

768 

769 

770def rampFluxAtEdge(debResult, log, patchEdges=False): 

771 r"""Adjust flux on the edges of the template footprints. 

772 

773 Using the PSF, a peak ``~afw.detection.Footprint`` with pixels on the edge 

774 of ``footprint`` is grown by the ``psffwhm*1.5`` and filled in 

775 with ramped pixels. The result is a new symmetric footprint 

776 template for the peaks near the edge. 

777 

778 Parameters 

779 ---------- 

780 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

781 Container for the final deblender results. 

782 log: `log.Log` 

783 LSST logger for logging purposes. 

784 patchEdges: `bool`, optional 

785 If True and if the parent Footprint touches pixels with the 

786 ``EDGE`` bit set, then grow the parent Footprint to include 

787 all symmetric templates. 

788 

789 Returns 

790 ------- 

791 modified: `bool` 

792 If any peaks have their templates modified to include flux at the 

793 edges, ``modified`` is ``True``. 

794 """ 

795 modified = False 

796 # Loop over all filters 

797 for fidx in debResult.filters: 

798 dp = debResult.deblendedParents[fidx] 

799 log.trace('Checking for significant flux at edge: sigma1=%g', dp.avgNoise) 

800 

801 for peaki, pkres in enumerate(dp.peaks): 

802 if pkres.skip or pkres.deblendedAsPsf: 

803 continue 

804 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

805 if bUtils.hasSignificantFluxAtEdge(timg, tfoot, 3*dp.avgNoise): 

806 log.trace("Template %i has significant flux at edge: ramping", pkres.pki) 

807 try: 

808 (timg2, tfoot2, patched) = _handle_flux_at_edge(log, dp.psffwhm, timg, tfoot, dp.fp, 

809 dp.maskedImage, dp.x0, dp.x1, 

810 dp.y0, dp.y1, dp.psf, pkres.peak, 

811 dp.avgNoise, patchEdges) 

812 except lsst.pex.exceptions.Exception as exc: 

813 if (isinstance(exc, lsst.pex.exceptions.InvalidParameterError) 

814 and "CoaddPsf" in str(exc)): 

815 pkres.setOutOfBounds() 

816 continue 

817 raise 

818 pkres.setRampedTemplate(timg2, tfoot2) 

819 if patched: 

820 pkres.setPatched() 

821 pkres.setTemplate(timg2, tfoot2) 

822 modified = True 

823 return modified 

824 

825 

826def _handle_flux_at_edge(log, psffwhm, t1, tfoot, fp, maskedImage, 

827 x0, x1, y0, y1, psf, pk, sigma1, patchEdges): 

828 """Extend a template by the PSF to fill in the footprint. 

829 

830 Using the PSF, a footprint that touches the edge is passed to the 

831 function and is grown by the psffwhm*1.5 and filled in with 

832 ramped pixels. 

833 

834 Parameters 

835 ---------- 

836 log: `log.Log` 

837 LSST logger for logging purposes. 

838 psffwhm: `float` 

839 PSF FWHM in pixels. 

840 t1: `afw.image.ImageF` 

841 The image template that contains the footprint to extend. 

842 tfoot: `afw.detection.Footprint` 

843 Symmetric Footprint to extend. 

844 fp: `afw.detection.Footprint` 

845 Parent Footprint that is being deblended. 

846 maskedImage: `afw.image.MaskedImageF` 

847 Full MaskedImage containing the parent footprint ``fp``. 

848 x0,y0: `init` 

849 Minimum x,y for the bounding box of the footprint ``fp``. 

850 x1,y1: `int` 

851 Maximum x,y for the bounding box of the footprint ``fp``. 

852 psf: `afw.detection.Psf` 

853 PSF of the image. 

854 pk: `afw.detection.PeakRecord` 

855 The peak within the Footprint whose footprint is being extended. 

856 sigma1: `float` 

857 Estimated noise level in the image. 

858 patchEdges: `bool` 

859 If ``patchEdges==True`` and if the footprint touches pixels with the 

860 ``EDGE`` bit set, then for spans whose symmetric mirror are outside 

861 the image, the symmetric footprint is grown to include them and their 

862 pixel values are stored. 

863 

864 Results 

865 ------- 

866 t2: `afw.image.ImageF` 

867 Image of the extended footprint. 

868 tfoot2: `afw.detection.Footprint` 

869 Extended Footprint. 

870 patched: `bool` 

871 If the footprint touches an edge pixel, ``patched`` will be set to 

872 ``True``. Otherwise ``patched`` is ``False``. 

873 """ 

874 log.trace('Found significant flux at template edge.') 

875 # Compute the max of: 

876 # -symmetric-template-clipped image * PSF 

877 # -footprint-clipped image 

878 # Ie, extend the template by the PSF and "fill in" the footprint. 

879 # Then find the symmetric template of that image. 

880 

881 # The size we'll grow by 

882 S = psffwhm*1.5 

883 # make it an odd integer 

884 S = int((S + 0.5)/2)*2 + 1 

885 

886 tbb = tfoot.getBBox() 

887 tbb.grow(S) 

888 

889 # (footprint+margin)-clipped image; 

890 # we need the pixels OUTSIDE the footprint to be 0. 

891 fpcopy = afwDet.Footprint(fp) 

892 fpcopy.dilate(S) 

893 fpcopy.setSpans(fpcopy.spans.clippedTo(tbb)) 

894 fpcopy.removeOrphanPeaks() 

895 padim = maskedImage.Factory(tbb) 

896 fpcopy.spans.clippedTo(maskedImage.getBBox()).copyMaskedImage(maskedImage, padim) 

897 

898 # find pixels on the edge of the template 

899 edgepix = bUtils.getSignificantEdgePixels(t1, tfoot, -1e6) 

900 

901 # instantiate PSF image 

902 xc = int((x0 + x1)/2) 

903 yc = int((y0 + y1)/2) 

904 psfim = psf.computeImage(geom.Point2D(xc, yc)) 

905 pbb = psfim.getBBox() 

906 # shift PSF image to be centered on zero 

907 lx, ly = pbb.getMinX(), pbb.getMinY() 

908 psfim.setXY0(lx - xc, ly - yc) 

909 pbb = psfim.getBBox() 

910 # clip PSF to S, if necessary 

911 Sbox = geom.Box2I(geom.Point2I(-S, -S), geom.Extent2I(2*S+1, 2*S+1)) 

912 if not Sbox.contains(pbb): 

913 # clip PSF image 

914 psfim = psfim.Factory(psfim, Sbox, afwImage.PARENT, True) 

915 pbb = psfim.getBBox() 

916 px0 = pbb.getMinX() 

917 px1 = pbb.getMaxX() 

918 py0 = pbb.getMinY() 

919 py1 = pbb.getMaxY() 

920 

921 # Compute the ramped-down edge pixels 

922 ramped = t1.Factory(tbb) 

923 Tout = ramped.getArray() 

924 Tin = t1.getArray() 

925 tx0, ty0 = t1.getX0(), t1.getY0() 

926 ox0, oy0 = ramped.getX0(), ramped.getY0() 

927 P = psfim.getArray() 

928 P /= P.max() 

929 # For each edge pixel, Tout = max(Tout, edgepix * PSF) 

930 for span in edgepix.getSpans(): 

931 y = span.getY() 

932 for x in range(span.getX0(), span.getX1()+1): 

933 slc = (slice(y+py0 - oy0, y+py1+1 - oy0), 

934 slice(x+px0 - ox0, x+px1+1 - ox0)) 

935 Tout[slc] = np.maximum(Tout[slc], Tin[y-ty0, x-tx0]*P) 

936 

937 # Fill in the "padim" (which has the right variance and 

938 # mask planes) with the ramped pixels, outside the footprint 

939 imZeros = (padim.getImage().getArray() == 0) 

940 padim.getImage().getArray()[imZeros] = ramped.getArray()[imZeros] 

941 

942 t2, tfoot2, patched = bUtils.buildSymmetricTemplate(padim, fpcopy, pk, sigma1, True, patchEdges) 

943 

944 # This template footprint may extend outside the parent 

945 # footprint -- or the image. Clip it. 

946 # NOTE that this may make it asymmetric, unlike normal templates. 

947 imbb = maskedImage.getBBox() 

948 tfoot2.clipTo(imbb) 

949 tbb = tfoot2.getBBox() 

950 # clip template image to bbox 

951 t2 = t2.Factory(t2, tbb, afwImage.PARENT, True) 

952 

953 return t2, tfoot2, patched 

954 

955 

956def medianSmoothTemplates(debResult, log, medianFilterHalfsize=2): 

957 """Applying median smoothing filter to the template images for every 

958 peak in every filter. 

959 

960 Parameters 

961 ---------- 

962 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

963 Container for the final deblender results. 

964 log: `log.Log` 

965 LSST logger for logging purposes. 

966 medianFilterHalfSize: `int`, optional 

967 Half the box size of the median filter, i.e. a 

968 ``medianFilterHalfSize`` of 50 means that each output pixel will 

969 be the median of the pixels in a 101 x 101-pixel box in the input 

970 image. This parameter is only used when 

971 ``medianSmoothTemplate==True``, otherwise it is ignored. 

972 

973 Returns 

974 ------- 

975 modified: `bool` 

976 Whether or not any templates were modified. 

977 This will be ``True`` as long as there is at least one source that 

978 is not flagged as a PSF. 

979 """ 

980 modified = False 

981 # Loop over all filters 

982 for fidx in debResult.filters: 

983 dp = debResult.deblendedParents[fidx] 

984 for peaki, pkres in enumerate(dp.peaks): 

985 if pkres.skip or pkres.deblendedAsPsf: 

986 continue 

987 modified = True 

988 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

989 filtsize = medianFilterHalfsize*2 + 1 

990 if timg.getWidth() >= filtsize and timg.getHeight() >= filtsize: 

991 log.trace('Median filtering template %i', pkres.pki) 

992 # We want the output to go in "t1", so copy it into 

993 # "inimg" for input 

994 inimg = timg.Factory(timg, True) 

995 bUtils.medianFilter(inimg, timg, medianFilterHalfsize) 

996 # possible save this median-filtered template 

997 pkres.setMedianFilteredTemplate(timg, tfoot) 

998 else: 

999 log.trace('Not median-filtering template %i: size %i x %i smaller than required %i x %i', 

1000 pkres.pki, timg.getWidth(), timg.getHeight(), filtsize, filtsize) 

1001 pkres.setTemplate(timg, tfoot) 

1002 return modified 

1003 

1004 

1005def makeTemplatesMonotonic(debResult, log): 

1006 """Make the templates monotonic. 

1007 

1008 The pixels in the templates are modified such that pixels further 

1009 from the peak will have values smaller than those closer to the peak. 

1010 

1011 Parameters 

1012 ---------- 

1013 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1014 Container for the final deblender results. 

1015 log: `log.Log` 

1016 LSST logger for logging purposes. 

1017 

1018 Returns 

1019 ------- 

1020 modified: `bool` 

1021 Whether or not any templates were modified. 

1022 This will be ``True`` as long as there is at least one source that 

1023 is not flagged as a PSF. 

1024 """ 

1025 modified = False 

1026 # Loop over all filters 

1027 for fidx in debResult.filters: 

1028 dp = debResult.deblendedParents[fidx] 

1029 for peaki, pkres in enumerate(dp.peaks): 

1030 if pkres.skip or pkres.deblendedAsPsf: 

1031 continue 

1032 modified = True 

1033 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

1034 pk = pkres.peak 

1035 log.trace('Making template %i monotonic', pkres.pki) 

1036 bUtils.makeMonotonic(timg, pk) 

1037 pkres.setTemplate(timg, tfoot) 

1038 return modified 

1039 

1040 

1041def clipFootprintsToNonzero(debResult, log): 

1042 r"""Clip non-zero spans in the template footprints for every peak in each filter. 

1043 

1044 Peak ``Footprint``\ s are clipped to the region in the image containing 

1045 non-zero values by dropping spans that are completely zero and moving 

1046 endpoints to non-zero pixels (but does not split spans that have 

1047 internal zeros). 

1048 

1049 Parameters 

1050 ---------- 

1051 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1052 Container for the final deblender results. 

1053 log: `log.Log` 

1054 LSST logger for logging purposes. 

1055 

1056 Returns 

1057 ------- 

1058 modified: `bool` 

1059 Whether or not any templates were modified. 

1060 This will be ``True`` as long as there is at least one source that 

1061 is not flagged as a PSF. 

1062 """ 

1063 # Loop over all filters 

1064 for fidx in debResult.filters: 

1065 dp = debResult.deblendedParents[fidx] 

1066 for peaki, pkres in enumerate(dp.peaks): 

1067 if pkres.skip or pkres.deblendedAsPsf: 

1068 continue 

1069 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

1070 clipFootprintToNonzeroImpl(tfoot, timg) 

1071 if not tfoot.getBBox().isEmpty() and tfoot.getBBox() != timg.getBBox(afwImage.PARENT): 

1072 timg = timg.Factory(timg, tfoot.getBBox(), afwImage.PARENT, True) 

1073 pkres.setTemplate(timg, tfoot) 

1074 return False 

1075 

1076 

1077def weightTemplates(debResult, log): 

1078 """Weight the templates to best fit the observed image in each filter 

1079 

1080 This function re-weights the templates so that their linear combination 

1081 best represents the observed image in that filter. 

1082 In the future it may be useful to simultaneously weight all of the 

1083 filters together. 

1084 

1085 Parameters 

1086 ---------- 

1087 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1088 Container for the final deblender results. 

1089 log: `log.Log` 

1090 LSST logger for logging purposes. 

1091 

1092 Returns 

1093 ------- 

1094 modified: `bool` 

1095 ``weightTemplates`` does not actually modify the ``Footprint`` 

1096 templates other than to add a weight to them, so ``modified`` 

1097 is always ``False``. 

1098 """ 

1099 # Weight the templates by doing a least-squares fit to the image 

1100 log.trace('Weighting templates') 

1101 for fidx in debResult.filters: 

1102 _weightTemplates(debResult.deblendedParents[fidx]) 

1103 return False 

1104 

1105 

1106def _weightTemplates(dp): 

1107 """Weight the templates to best match the parent Footprint in a single 

1108 filter 

1109 

1110 This includes weighting both regular templates and point source templates 

1111 

1112 Parameter 

1113 --------- 

1114 dp: `DeblendedParent` 

1115 The deblended parent to re-weight 

1116 

1117 Returns 

1118 ------- 

1119 None 

1120 """ 

1121 nchild = np.sum([pkres.skip is False for pkres in dp.peaks]) 

1122 A = np.zeros((dp.W*dp.H, nchild)) 

1123 parentImage = afwImage.ImageF(dp.bb) 

1124 afwDet.copyWithinFootprintImage(dp.fp, dp.img, parentImage) 

1125 b = parentImage.getArray().ravel() 

1126 

1127 index = 0 

1128 for pkres in dp.peaks: 

1129 if pkres.skip: 

1130 continue 

1131 childImage = afwImage.ImageF(dp.bb) 

1132 afwDet.copyWithinFootprintImage(dp.fp, pkres.templateImage, childImage) 

1133 A[:, index] = childImage.getArray().ravel() 

1134 index += 1 

1135 

1136 X1, r1, rank1, s1 = np.linalg.lstsq(A, b, rcond=-1) 

1137 del A 

1138 del b 

1139 

1140 index = 0 

1141 for pkres in dp.peaks: 

1142 if pkres.skip: 

1143 continue 

1144 pkres.templateImage *= X1[index] 

1145 pkres.setTemplateWeight(X1[index]) 

1146 index += 1 

1147 

1148 

1149def reconstructTemplates(debResult, log, maxTempDotProd=0.5): 

1150 """Remove "degenerate templates" 

1151 

1152 If galaxies have substructure, such as face-on spirals, the process of 

1153 identifying peaks can "shred" the galaxy into many pieces. The templates 

1154 of shredded galaxies are typically quite similar because they represent 

1155 the same galaxy, so we try to identify these "degenerate" peaks 

1156 by looking at the inner product (in pixel space) of pairs of templates. 

1157 If they are nearly parallel, we only keep one of the peaks and reject 

1158 the other. If only one of the peaks is a PSF template, the other template 

1159 is used, otherwise the one with the maximum template value is kept. 

1160 

1161 Parameters 

1162 ---------- 

1163 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1164 Container for the final deblender results. 

1165 log: `log.Log` 

1166 LSST logger for logging purposes. 

1167 maxTempDotProd: `float`, optional 

1168 All dot products between templates greater than ``maxTempDotProd`` 

1169 will result in one of the templates removed. 

1170 

1171 Returns 

1172 ------- 

1173 modified: `bool` 

1174 If any degenerate templates are found, ``modified`` is ``True``. 

1175 """ 

1176 log.trace('Looking for degnerate templates') 

1177 

1178 foundReject = False 

1179 for fidx in debResult.filters: 

1180 dp = debResult.deblendedParents[fidx] 

1181 nchild = np.sum([pkres.skip is False for pkres in dp.peaks]) 

1182 indexes = [pkres.pki for pkres in dp.peaks if pkres.skip is False] 

1183 

1184 # We build a matrix that stores the dot product between templates. 

1185 # We convert the template images to HeavyFootprints because they already have a method 

1186 # to compute the dot product. 

1187 A = np.zeros((nchild, nchild)) 

1188 maxTemplate = [] 

1189 heavies = [] 

1190 for pkres in dp.peaks: 

1191 if pkres.skip: 

1192 continue 

1193 heavies.append(afwDet.makeHeavyFootprint(pkres.templateFootprint, 

1194 afwImage.MaskedImageF(pkres.templateImage))) 

1195 maxTemplate.append(np.max(pkres.templateImage.getArray())) 

1196 

1197 for i in range(nchild): 

1198 for j in range(i + 1): 

1199 A[i, j] = heavies[i].dot(heavies[j]) 

1200 

1201 # Normalize the dot products to get the cosine of the angle between templates 

1202 for i in range(nchild): 

1203 for j in range(i): 

1204 norm = A[i, i]*A[j, j] 

1205 if norm <= 0: 

1206 A[i, j] = 0 

1207 else: 

1208 A[i, j] /= np.sqrt(norm) 

1209 

1210 # Iterate over pairs of objects and find the maximum non-diagonal element of the matrix. 

1211 # Exit the loop once we find a single degenerate pair greater than the threshold. 

1212 rejectedIndex = -1 

1213 for i in range(nchild): 

1214 currentMax = 0. 

1215 for j in range(i): 

1216 if A[i, j] > currentMax: 

1217 currentMax = A[i, j] 

1218 if currentMax > maxTempDotProd: 

1219 foundReject = True 

1220 rejectedIndex = j 

1221 

1222 if foundReject: 

1223 break 

1224 

1225 del A 

1226 

1227 # If one of the objects is identified as a PSF keep the other one, otherwise keep the one 

1228 # with the maximum template value 

1229 if foundReject: 

1230 keep = indexes[i] 

1231 reject = indexes[rejectedIndex] 

1232 if dp.peaks[keep].deblendedAsPsf and dp.peaks[reject].deblendedAsPsf is False: 

1233 keep = indexes[rejectedIndex] 

1234 reject = indexes[i] 

1235 elif dp.peaks[keep].deblendedAsPsf is False and dp.peaks[reject].deblendedAsPsf: 

1236 reject = indexes[rejectedIndex] 

1237 keep = indexes[i] 

1238 else: 

1239 if maxTemplate[rejectedIndex] > maxTemplate[i]: 

1240 keep = indexes[rejectedIndex] 

1241 reject = indexes[i] 

1242 log.trace('Removing object with index %d : %f. Degenerate with %d', 

1243 reject, currentMax, keep) 

1244 dp.peaks[reject].skip = True 

1245 dp.peaks[reject].degenerate = True 

1246 

1247 return foundReject 

1248 

1249 

1250def apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak', 

1251 strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001, 

1252 getTemplateSum=False): 

1253 """Apportion flux to all of the peak templates in each filter 

1254 

1255 Divide the ``maskedImage`` flux amongst all of the templates based 

1256 on the fraction of flux assigned to each ``template``. 

1257 Leftover "stray flux" is assigned to peaks based on the other parameters. 

1258 

1259 Parameters 

1260 ---------- 

1261 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1262 Container for the final deblender results. 

1263 log: `log.Log` 

1264 LSST logger for logging purposes. 

1265 assignStrayFlux: `bool`, optional 

1266 If True then flux in the parent footprint that is not covered by any 

1267 of the template footprints is assigned to templates based on 

1268 their 1/(1+r^2) distance. 

1269 How the flux is apportioned is determined by ``strayFluxAssignment``. 

1270 strayFluxAssignment: `string`, optional 

1271 Determines how stray flux is apportioned. 

1272 

1273 * ``trim``: Trim stray flux and do not include in any footprints 

1274 * ``r-to-peak`` (default): Stray flux is assigned based on 

1275 (1/(1+r^2) from the peaks 

1276 * ``r-to-footprint``: Stray flux is distributed to the footprints 

1277 based on 1/(1+r^2) of the minimum distance from the stray flux 

1278 to footprint 

1279 * ``nearest-footprint``: Stray flux is assigned to the footprint 

1280 with lowest L-1 (Manhattan) distance to the stray flux 

1281 

1282 strayFluxToPointSources: `string`, optional 

1283 Determines how stray flux is apportioned to point sources 

1284 

1285 * ``never``: never apportion stray flux to point sources 

1286 * ``necessary`` (default): point sources are included only if there 

1287 are no extended sources nearby 

1288 * ``always``: point sources are always included in 

1289 the 1/(1+r^2) splitting 

1290 

1291 clipStrayFluxFraction: `float`, optional 

1292 Minimum stray-flux portion. 

1293 Any stray-flux portion less than ``clipStrayFluxFraction`` is 

1294 clipped to zero. 

1295 getTemplateSum: `bool`, optional 

1296 As part of the flux calculation, the sum of the templates is 

1297 calculated. If ``getTemplateSum==True`` then the sum of the 

1298 templates is stored in the result (a `DeblendedFootprint`). 

1299 

1300 Returns 

1301 ------- 

1302 modified: `bool` 

1303 Apportion flux always modifies the templates, so ``modified`` is 

1304 always ``True``. However, this should likely be the final step and 

1305 it is unlikely that any deblender plugins will be re-run. 

1306 """ 

1307 validStrayPtSrc = ['never', 'necessary', 'always'] 

1308 validStrayAssign = ['r-to-peak', 'r-to-footprint', 'nearest-footprint', 'trim'] 

1309 if strayFluxToPointSources not in validStrayPtSrc: 

1310 raise ValueError((('strayFluxToPointSources: value \"%s\" not in the set of allowed values: ') % 

1311 strayFluxToPointSources) + str(validStrayPtSrc)) 

1312 if strayFluxAssignment not in validStrayAssign: 

1313 raise ValueError((('strayFluxAssignment: value \"%s\" not in the set of allowed values: ') % 

1314 strayFluxAssignment) + str(validStrayAssign)) 

1315 

1316 for fidx in debResult.filters: 

1317 dp = debResult.deblendedParents[fidx] 

1318 # Prepare inputs to "apportionFlux" call. 

1319 # template maskedImages 

1320 tmimgs = [] 

1321 # template footprints 

1322 tfoots = [] 

1323 # deblended as psf 

1324 dpsf = [] 

1325 # peak x,y 

1326 pkx = [] 

1327 pky = [] 

1328 # indices of valid templates 

1329 ibi = [] 

1330 bb = dp.fp.getBBox() 

1331 

1332 for peaki, pkres in enumerate(dp.peaks): 

1333 if pkres.skip: 

1334 continue 

1335 tmimgs.append(pkres.templateImage) 

1336 tfoots.append(pkres.templateFootprint) 

1337 # for stray flux... 

1338 dpsf.append(pkres.deblendedAsPsf) 

1339 pk = pkres.peak 

1340 pkx.append(pk.getIx()) 

1341 pky.append(pk.getIy()) 

1342 ibi.append(pkres.pki) 

1343 

1344 # Now apportion flux according to the templates 

1345 log.trace('Apportioning flux among %i templates', len(tmimgs)) 

1346 sumimg = afwImage.ImageF(bb) 

1347 # .getDimensions()) 

1348 # sumimg.setXY0(bb.getMinX(), bb.getMinY()) 

1349 

1350 strayopts = 0 

1351 if strayFluxAssignment == 'trim': 

1352 assignStrayFlux = False 

1353 strayopts |= bUtils.STRAYFLUX_TRIM 

1354 if assignStrayFlux: 

1355 strayopts |= bUtils.ASSIGN_STRAYFLUX 

1356 if strayFluxToPointSources == 'necessary': 

1357 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_WHEN_NECESSARY 

1358 elif strayFluxToPointSources == 'always': 

1359 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_ALWAYS 

1360 

1361 if strayFluxAssignment == 'r-to-peak': 

1362 # this is the default 

1363 pass 

1364 elif strayFluxAssignment == 'r-to-footprint': 

1365 strayopts |= bUtils.STRAYFLUX_R_TO_FOOTPRINT 

1366 elif strayFluxAssignment == 'nearest-footprint': 

1367 strayopts |= bUtils.STRAYFLUX_NEAREST_FOOTPRINT 

1368 

1369 portions, strayflux = bUtils.apportionFlux(dp.maskedImage, dp.fp, tmimgs, tfoots, sumimg, dpsf, 

1370 pkx, pky, strayopts, clipStrayFluxFraction) 

1371 

1372 # Shrink parent to union of children 

1373 if strayFluxAssignment == 'trim': 

1374 finalSpanSet = afwGeom.SpanSet() 

1375 for foot in tfoots: 

1376 finalSpanSet = finalSpanSet.union(foot.spans) 

1377 dp.fp.setSpans(finalSpanSet) 

1378 

1379 # Store the template sum in the deblender result 

1380 if getTemplateSum: 

1381 debResult.setTemplateSums(sumimg, fidx) 

1382 

1383 # Save the apportioned fluxes 

1384 ii = 0 

1385 for j, (pk, pkres) in enumerate(zip(dp.fp.getPeaks(), dp.peaks)): 

1386 if pkres.skip: 

1387 continue 

1388 pkres.setFluxPortion(portions[ii]) 

1389 

1390 if assignStrayFlux: 

1391 # NOTE that due to a swig bug (https://github.com/swig/swig/issues/59) 

1392 # we CANNOT iterate over "strayflux", but must index into it. 

1393 stray = strayflux[ii] 

1394 else: 

1395 stray = None 

1396 ii += 1 

1397 

1398 pkres.setStrayFlux(stray) 

1399 

1400 # Set child footprints to contain the right number of peaks. 

1401 for j, (pk, pkres) in enumerate(zip(dp.fp.getPeaks(), dp.peaks)): 

1402 if pkres.skip: 

1403 continue 

1404 

1405 for foot, add in [(pkres.templateFootprint, True), (pkres.origFootprint, True), 

1406 (pkres.strayFlux, False)]: 

1407 if foot is None: 

1408 continue 

1409 pks = foot.getPeaks() 

1410 pks.clear() 

1411 if add: 

1412 pks.append(pk) 

1413 return True