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

663 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-06 13:13 +0000

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) and (xlo <= xhi) and (xmin <= xmax) 

404 xloclamp = max(xlo, xmin) 

405 Xlo = xloclamp - xlo 

406 xhiclamp = min(xhi, xmax) 

407 Xhi = Xlo + (xhiclamp - xloclamp) 

408 assert xloclamp >= 0 

409 assert Xlo >= 0 

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

411 

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

413 # Constant term 

414 A[:, I_sky] = 1. 

415 # Sky slope terms: dx, dy 

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

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

418 

419 # whew, grab the valid overlapping PSF pixels 

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

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

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

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

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

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

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

427 psfsub = psfarr[psf_y_slice, psf_x_slice] 

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

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

430 

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

432 # shifted-by-minus-one. 

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

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

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

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

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

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

439 # revert x indices... 

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

441 

442 # PSF dy 

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

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

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

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

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

448 

449 # other PSFs... 

450 for j, opsf in enumerate(otherpeaks): 

451 obb = opsf.getBBox() 

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

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

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

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

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

457 opsfarr = opsf.getArray() 

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

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

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

461 

462 b = img_sub[valid] 

463 

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

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

466 rw = np.ones_like(RR) 

467 ii = (RR > R0**2) 

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

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

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

471 # save the effective number of pixels 

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

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

474 

475 del ii 

476 

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

478 bw = b*w 

479 

480 if debugPlots: 

481 import pylab as plt 

482 plt.clf() 

483 N = NT2 + 2 

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

485 for i in range(NT2): 

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

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

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

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

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

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

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

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

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

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

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

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

498 plt.savefig('A.png') 

499 

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

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

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

503 # 

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

505 # really be handled upstream 

506 try: 

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

508 # X1 is the result without decenter 

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

510 # X2 is with decenter 

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

512 except np.linalg.LinAlgError as e: 

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

514 pkres.setPsfFitFailed() 

515 return 

516 

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

518 

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

520 # data)**2/sigma**2 

521 if len(r1) > 0: 

522 chisq1 = r1[0] 

523 else: 

524 chisq1 = 1e30 

525 if len(r2) > 0: 

526 chisq2 = r2[0] 

527 else: 

528 chisq2 = 1e30 

529 dof1 = sumr - len(X1) 

530 dof2 = sumr - len(X2) 

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

532 

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

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

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

536 pkres.setBadPsfDof() 

537 return 

538 

539 q1 = chisq1/dof1 

540 q2 = chisq2/dof2 

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

542 ispsf1 = (q1 < psfChisqCut1) 

543 ispsf2 = (q2 < psfChisqCut2) 

544 

545 pkres.psfFit1 = (chisq1, dof1) 

546 pkres.psfFit2 = (chisq2, dof2) 

547 

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

549 if ispsf2: 

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

551 f0 = X2[I_psf] 

552 # as a fraction of the PSF flux 

553 dx = fdx/f0 

554 dy = fdy/f0 

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

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

557 if not ispsf2: 

558 pkres.psfFitBigDecenter = True 

559 

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

561 # and re-evaluate the fit. 

562 if ispsf2: 

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

564 # clip 

565 pbb2 = psfimg2.getBBox() 

566 pbb2.clip(fbb) 

567 

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

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

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

571 ispsf2 = False 

572 else: 

573 # clip image to bbox 

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

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

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

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

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

579 

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

581 Ab = A[:, :NT1] 

582 

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

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

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

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

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

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

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

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

591 

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

593 # re-solve... 

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

595 if len(rb) > 0: 

596 chisqb = rb[0] 

597 else: 

598 chisqb = 1e30 

599 dofb = sumr - len(Xb) 

600 qb = chisqb/dofb 

601 ispsf2 = (qb < psfChisqCut2b) 

602 q2 = qb 

603 X2 = Xb 

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

605 pkres.psfFit3 = (chisqb, dofb) 

606 

607 # Which one do we keep? 

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

609 or (ispsf2 and not ispsf1)): 

610 Xpsf = X2 

611 chisq = chisq2 

612 dof = dof2 

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

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

615 cx += dx 

616 cy += dy 

617 pkres.psfFitWithDecenter = True 

618 else: 

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

620 Xpsf = X1 

621 chisq = chisq1 

622 dof = dof1 

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

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

625 

626 ispsf = (ispsf1 or ispsf2) 

627 

628 # Save the PSF models in images for posterity. 

629 if debugPsf: 

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

631 psfmod = afwImage.ImageF(SW, SH) 

632 psfmod.setXY0(xlo, ylo) 

633 psfderivmodm = afwImage.MaskedImageF(SW, SH) 

634 psfderivmod = psfderivmodm.getImage() 

635 psfderivmod.setXY0(xlo, ylo) 

636 model = afwImage.ImageF(SW, SH) 

637 model.setXY0(xlo, ylo) 

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

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

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

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

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

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

644 for ii in range(NP): 

645 x, y = ipixes[ii, :] 

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

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

648 for (x, y) in ipixes: 

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

650 modelfp.normalize() 

651 

652 pkres.psfFitDebugPsf0Img = psfimg 

653 pkres.psfFitDebugPsfImg = psfmod 

654 pkres.psfFitDebugPsfDerivImg = psfderivmod 

655 pkres.psfFitDebugPsfModel = model 

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

657 pkres.psfFitDebugValidPix = valid # numpy array 

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

659 ww = np.zeros(valid.shape, np.float64) 

660 ww[valid] = w 

661 pkres.psfFitDebugWeight = ww # numpy 

662 pkres.psfFitDebugRampWeight = rw 

663 

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

665 pkres.psfFitR0 = R0 

666 pkres.psfFitR1 = R1 

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

668 pkres.psfFitCenter = (cx, cy) 

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

670 pkres.psfFitBest = (chisq, dof) 

671 pkres.psfFitParams = Xpsf 

672 pkres.psfFitFlux = Xpsf[I_psf] 

673 pkres.psfFitNOthers = len(otherpeaks) 

674 

675 if ispsf: 

676 pkres.setDeblendedAsPsf() 

677 

678 # replace the template image by the PSF + derivatives 

679 # image. 

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

681 

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

683 psfimg = psf.computeImage(cx, cy) 

684 # Scale by fit flux. 

685 psfimg *= Xpsf[I_psf] 

686 psfimg = psfimg.convertF() 

687 

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

689 fpcopy = afwDet.Footprint(fp) 

690 psfbb = psfimg.getBBox() 

691 fpcopy.clipTo(psfbb) 

692 bb = fpcopy.getBBox() 

693 

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

695 psfmod = afwImage.ImageF(bb) 

696 fpcopy.spans.copyImage(psfimg, psfmod) 

697 # Save it as our template. 

698 clipFootprintToNonzeroImpl(fpcopy, psfmod) 

699 pkres.setTemplate(psfmod, fpcopy) 

700 

701 # DEBUG 

702 pkres.setPsfTemplate(psfmod, fpcopy) 

703 

704 return ispsf 

705 

706 

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

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

709 

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

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

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

713 

714 Parameters 

715 ---------- 

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

717 Container for the final deblender results. 

718 log: `log.Log` 

719 LSST logger for logging purposes. 

720 patchEdges: `bool`, optional 

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

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

723 all symmetric templates. 

724 

725 Returns 

726 ------- 

727 modified: `bool` 

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

729 ``modified`` is ``True. Otherwise ``modified`` is ``False``. 

730 """ 

731 modified = False 

732 # Create the Templates for each peak in each filter 

733 for fidx in debResult.filters: 

734 dp = debResult.deblendedParents[fidx] 

735 imbb = dp.img.getBBox() 

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

737 

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

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

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

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

742 if pkres.skip or pkres.deblendedAsPsf: 

743 continue 

744 modified = True 

745 pk = pkres.peak 

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

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

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

749 pkres.setOutOfBounds() 

750 continue 

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

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

753 True, patchEdges) 

754 if timg is None: 

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

756 pkres.setFailedSymmetricTemplate() 

757 continue 

758 

759 if patched: 

760 pkres.setPatched() 

761 

762 # possibly save the original symmetric template 

763 if setOrigTemplate: 

764 pkres.setOrigTemplate(timg, tfoot) 

765 pkres.setTemplate(timg, tfoot) 

766 return modified 

767 

768 

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

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

771 

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

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

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

775 template for the peaks near the edge. 

776 

777 Parameters 

778 ---------- 

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

780 Container for the final deblender results. 

781 log: `log.Log` 

782 LSST logger for logging purposes. 

783 patchEdges: `bool`, optional 

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

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

786 all symmetric templates. 

787 

788 Returns 

789 ------- 

790 modified: `bool` 

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

792 edges, ``modified`` is ``True``. 

793 """ 

794 modified = False 

795 # Loop over all filters 

796 for fidx in debResult.filters: 

797 dp = debResult.deblendedParents[fidx] 

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

799 

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

801 if pkres.skip or pkres.deblendedAsPsf: 

802 continue 

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

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

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

806 try: 

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

808 dp.maskedImage, dp.x0, dp.x1, 

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

810 dp.avgNoise, patchEdges) 

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

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

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

814 pkres.setOutOfBounds() 

815 continue 

816 raise 

817 pkres.setRampedTemplate(timg2, tfoot2) 

818 if patched: 

819 pkres.setPatched() 

820 pkres.setTemplate(timg2, tfoot2) 

821 modified = True 

822 return modified 

823 

824 

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

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

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

828 

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

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

831 ramped pixels. 

832 

833 Parameters 

834 ---------- 

835 log: `log.Log` 

836 LSST logger for logging purposes. 

837 psffwhm: `float` 

838 PSF FWHM in pixels. 

839 t1: `afw.image.ImageF` 

840 The image template that contains the footprint to extend. 

841 tfoot: `afw.detection.Footprint` 

842 Symmetric Footprint to extend. 

843 fp: `afw.detection.Footprint` 

844 Parent Footprint that is being deblended. 

845 maskedImage: `afw.image.MaskedImageF` 

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

847 x0,y0: `init` 

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

849 x1,y1: `int` 

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

851 psf: `afw.detection.Psf` 

852 PSF of the image. 

853 pk: `afw.detection.PeakRecord` 

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

855 sigma1: `float` 

856 Estimated noise level in the image. 

857 patchEdges: `bool` 

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

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

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

861 pixel values are stored. 

862 

863 Results 

864 ------- 

865 t2: `afw.image.ImageF` 

866 Image of the extended footprint. 

867 tfoot2: `afw.detection.Footprint` 

868 Extended Footprint. 

869 patched: `bool` 

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

871 ``True``. Otherwise ``patched`` is ``False``. 

872 """ 

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

874 # Compute the max of: 

875 # -symmetric-template-clipped image * PSF 

876 # -footprint-clipped image 

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

878 # Then find the symmetric template of that image. 

879 

880 # The size we'll grow by 

881 S = psffwhm*1.5 

882 # make it an odd integer 

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

884 

885 tbb = tfoot.getBBox() 

886 tbb.grow(S) 

887 

888 # (footprint+margin)-clipped image; 

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

890 fpcopy = afwDet.Footprint(fp) 

891 fpcopy.dilate(S) 

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

893 fpcopy.removeOrphanPeaks() 

894 padim = maskedImage.Factory(tbb) 

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

896 

897 # find pixels on the edge of the template 

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

899 

900 # instantiate PSF image 

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

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

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

904 pbb = psfim.getBBox() 

905 # shift PSF image to be centered on zero 

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

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

908 pbb = psfim.getBBox() 

909 # clip PSF to S, if necessary 

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

911 if not Sbox.contains(pbb): 

912 # clip PSF image 

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

914 pbb = psfim.getBBox() 

915 px0 = pbb.getMinX() 

916 px1 = pbb.getMaxX() 

917 py0 = pbb.getMinY() 

918 py1 = pbb.getMaxY() 

919 

920 # Compute the ramped-down edge pixels 

921 ramped = t1.Factory(tbb) 

922 Tout = ramped.getArray() 

923 Tin = t1.getArray() 

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

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

926 P = psfim.getArray() 

927 P /= P.max() 

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

929 for span in edgepix.getSpans(): 

930 y = span.getY() 

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

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

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

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

935 

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

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

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

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

940 

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

942 

943 # This template footprint may extend outside the parent 

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

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

946 imbb = maskedImage.getBBox() 

947 tfoot2.clipTo(imbb) 

948 tbb = tfoot2.getBBox() 

949 # clip template image to bbox 

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

951 

952 return t2, tfoot2, patched 

953 

954 

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

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

957 peak in every filter. 

958 

959 Parameters 

960 ---------- 

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

962 Container for the final deblender results. 

963 log: `log.Log` 

964 LSST logger for logging purposes. 

965 medianFilterHalfSize: `int`, optional 

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

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

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

969 image. This parameter is only used when 

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

971 

972 Returns 

973 ------- 

974 modified: `bool` 

975 Whether or not any templates were modified. 

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

977 is not flagged as a PSF. 

978 """ 

979 modified = False 

980 # Loop over all filters 

981 for fidx in debResult.filters: 

982 dp = debResult.deblendedParents[fidx] 

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

984 if pkres.skip or pkres.deblendedAsPsf: 

985 continue 

986 modified = True 

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

988 filtsize = medianFilterHalfsize*2 + 1 

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

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

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

992 # "inimg" for input 

993 inimg = timg.Factory(timg, True) 

994 bUtils.medianFilter(inimg, timg, medianFilterHalfsize) 

995 # possible save this median-filtered template 

996 pkres.setMedianFilteredTemplate(timg, tfoot) 

997 else: 

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

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

1000 pkres.setTemplate(timg, tfoot) 

1001 return modified 

1002 

1003 

1004def makeTemplatesMonotonic(debResult, log): 

1005 """Make the templates monotonic. 

1006 

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

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

1009 

1010 Parameters 

1011 ---------- 

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

1013 Container for the final deblender results. 

1014 log: `log.Log` 

1015 LSST logger for logging purposes. 

1016 

1017 Returns 

1018 ------- 

1019 modified: `bool` 

1020 Whether or not any templates were modified. 

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

1022 is not flagged as a PSF. 

1023 """ 

1024 modified = False 

1025 # Loop over all filters 

1026 for fidx in debResult.filters: 

1027 dp = debResult.deblendedParents[fidx] 

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

1029 if pkres.skip or pkres.deblendedAsPsf: 

1030 continue 

1031 modified = True 

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

1033 pk = pkres.peak 

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

1035 bUtils.makeMonotonic(timg, pk) 

1036 pkres.setTemplate(timg, tfoot) 

1037 return modified 

1038 

1039 

1040def clipFootprintsToNonzero(debResult, log): 

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

1042 

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

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

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

1046 internal zeros). 

1047 

1048 Parameters 

1049 ---------- 

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

1051 Container for the final deblender results. 

1052 log: `log.Log` 

1053 LSST logger for logging purposes. 

1054 

1055 Returns 

1056 ------- 

1057 modified: `bool` 

1058 Whether or not any templates were modified. 

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

1060 is not flagged as a PSF. 

1061 """ 

1062 # Loop over all filters 

1063 for fidx in debResult.filters: 

1064 dp = debResult.deblendedParents[fidx] 

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

1066 if pkres.skip or pkres.deblendedAsPsf: 

1067 continue 

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

1069 clipFootprintToNonzeroImpl(tfoot, timg) 

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

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

1072 pkres.setTemplate(timg, tfoot) 

1073 return False 

1074 

1075 

1076def weightTemplates(debResult, log): 

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

1078 

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

1080 best represents the observed image in that filter. 

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

1082 filters together. 

1083 

1084 Parameters 

1085 ---------- 

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

1087 Container for the final deblender results. 

1088 log: `log.Log` 

1089 LSST logger for logging purposes. 

1090 

1091 Returns 

1092 ------- 

1093 modified: `bool` 

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

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

1096 is always ``False``. 

1097 """ 

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

1099 log.trace('Weighting templates') 

1100 for fidx in debResult.filters: 

1101 _weightTemplates(debResult.deblendedParents[fidx]) 

1102 return False 

1103 

1104 

1105def _weightTemplates(dp): 

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

1107 filter 

1108 

1109 This includes weighting both regular templates and point source templates 

1110 

1111 Parameter 

1112 --------- 

1113 dp: `DeblendedParent` 

1114 The deblended parent to re-weight 

1115 

1116 Returns 

1117 ------- 

1118 None 

1119 """ 

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

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

1122 parentImage = afwImage.ImageF(dp.bb) 

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

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

1125 

1126 index = 0 

1127 for pkres in dp.peaks: 

1128 if pkres.skip: 

1129 continue 

1130 childImage = afwImage.ImageF(dp.bb) 

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

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

1133 index += 1 

1134 

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

1136 del A 

1137 del b 

1138 

1139 index = 0 

1140 for pkres in dp.peaks: 

1141 if pkres.skip: 

1142 continue 

1143 pkres.templateImage *= X1[index] 

1144 pkres.setTemplateWeight(X1[index]) 

1145 index += 1 

1146 

1147 

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

1149 """Remove "degenerate templates" 

1150 

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

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

1153 of shredded galaxies are typically quite similar because they represent 

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

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

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

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

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

1159 

1160 Parameters 

1161 ---------- 

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

1163 Container for the final deblender results. 

1164 log: `log.Log` 

1165 LSST logger for logging purposes. 

1166 maxTempDotProd: `float`, optional 

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

1168 will result in one of the templates removed. 

1169 

1170 Returns 

1171 ------- 

1172 modified: `bool` 

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

1174 """ 

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

1176 

1177 foundReject = False 

1178 for fidx in debResult.filters: 

1179 dp = debResult.deblendedParents[fidx] 

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

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

1182 

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

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

1185 # to compute the dot product. 

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

1187 maxTemplate = [] 

1188 heavies = [] 

1189 for pkres in dp.peaks: 

1190 if pkres.skip: 

1191 continue 

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

1193 afwImage.MaskedImageF(pkres.templateImage))) 

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

1195 

1196 for i in range(nchild): 

1197 for j in range(i + 1): 

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

1199 

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

1201 for i in range(nchild): 

1202 for j in range(i): 

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

1204 if norm <= 0: 

1205 A[i, j] = 0 

1206 else: 

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

1208 

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

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

1211 rejectedIndex = -1 

1212 for i in range(nchild): 

1213 currentMax = 0. 

1214 for j in range(i): 

1215 if A[i, j] > currentMax: 

1216 currentMax = A[i, j] 

1217 if currentMax > maxTempDotProd: 

1218 foundReject = True 

1219 rejectedIndex = j 

1220 

1221 if foundReject: 

1222 break 

1223 

1224 del A 

1225 

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

1227 # with the maximum template value 

1228 if foundReject: 

1229 keep = indexes[i] 

1230 reject = indexes[rejectedIndex] 

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

1232 keep = indexes[rejectedIndex] 

1233 reject = indexes[i] 

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

1235 reject = indexes[rejectedIndex] 

1236 keep = indexes[i] 

1237 else: 

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

1239 keep = indexes[rejectedIndex] 

1240 reject = indexes[i] 

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

1242 reject, currentMax, keep) 

1243 dp.peaks[reject].skip = True 

1244 dp.peaks[reject].degenerate = True 

1245 

1246 return foundReject 

1247 

1248 

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

1250 strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001, 

1251 getTemplateSum=False): 

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

1253 

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

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

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

1257 

1258 Parameters 

1259 ---------- 

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

1261 Container for the final deblender results. 

1262 log: `log.Log` 

1263 LSST logger for logging purposes. 

1264 assignStrayFlux: `bool`, optional 

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

1266 of the template footprints is assigned to templates based on 

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

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

1269 strayFluxAssignment: `string`, optional 

1270 Determines how stray flux is apportioned. 

1271 

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

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

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

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

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

1277 to footprint 

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

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

1280 

1281 strayFluxToPointSources: `string`, optional 

1282 Determines how stray flux is apportioned to point sources 

1283 

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

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

1286 are no extended sources nearby 

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

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

1289 

1290 clipStrayFluxFraction: `float`, optional 

1291 Minimum stray-flux portion. 

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

1293 clipped to zero. 

1294 getTemplateSum: `bool`, optional 

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

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

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

1298 

1299 Returns 

1300 ------- 

1301 modified: `bool` 

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

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

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

1305 """ 

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

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

1308 if strayFluxToPointSources not in validStrayPtSrc: 

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

1310 strayFluxToPointSources) + str(validStrayPtSrc)) 

1311 if strayFluxAssignment not in validStrayAssign: 

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

1313 strayFluxAssignment) + str(validStrayAssign)) 

1314 

1315 for fidx in debResult.filters: 

1316 dp = debResult.deblendedParents[fidx] 

1317 # Prepare inputs to "apportionFlux" call. 

1318 # template maskedImages 

1319 tmimgs = [] 

1320 # template footprints 

1321 tfoots = [] 

1322 # deblended as psf 

1323 dpsf = [] 

1324 # peak x,y 

1325 pkx = [] 

1326 pky = [] 

1327 # indices of valid templates 

1328 ibi = [] 

1329 bb = dp.fp.getBBox() 

1330 

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

1332 if pkres.skip: 

1333 continue 

1334 tmimgs.append(pkres.templateImage) 

1335 tfoots.append(pkres.templateFootprint) 

1336 # for stray flux... 

1337 dpsf.append(pkres.deblendedAsPsf) 

1338 pk = pkres.peak 

1339 pkx.append(pk.getIx()) 

1340 pky.append(pk.getIy()) 

1341 ibi.append(pkres.pki) 

1342 

1343 # Now apportion flux according to the templates 

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

1345 sumimg = afwImage.ImageF(bb) 

1346 # .getDimensions()) 

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

1348 

1349 strayopts = 0 

1350 if strayFluxAssignment == 'trim': 

1351 assignStrayFlux = False 

1352 strayopts |= bUtils.STRAYFLUX_TRIM 

1353 if assignStrayFlux: 

1354 strayopts |= bUtils.ASSIGN_STRAYFLUX 

1355 if strayFluxToPointSources == 'necessary': 

1356 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_WHEN_NECESSARY 

1357 elif strayFluxToPointSources == 'always': 

1358 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_ALWAYS 

1359 

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

1361 # this is the default 

1362 pass 

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

1364 strayopts |= bUtils.STRAYFLUX_R_TO_FOOTPRINT 

1365 elif strayFluxAssignment == 'nearest-footprint': 

1366 strayopts |= bUtils.STRAYFLUX_NEAREST_FOOTPRINT 

1367 

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

1369 pkx, pky, strayopts, clipStrayFluxFraction) 

1370 

1371 # Shrink parent to union of children 

1372 if strayFluxAssignment == 'trim': 

1373 finalSpanSet = afwGeom.SpanSet() 

1374 for foot in tfoots: 

1375 finalSpanSet = finalSpanSet.union(foot.spans) 

1376 dp.fp.setSpans(finalSpanSet) 

1377 

1378 # Store the template sum in the deblender result 

1379 if getTemplateSum: 

1380 debResult.setTemplateSums(sumimg, fidx) 

1381 

1382 # Save the apportioned fluxes 

1383 ii = 0 

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

1385 if pkres.skip: 

1386 continue 

1387 pkres.setFluxPortion(portions[ii]) 

1388 

1389 if assignStrayFlux: 

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

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

1392 stray = strayflux[ii] 

1393 else: 

1394 stray = None 

1395 ii += 1 

1396 

1397 pkres.setStrayFlux(stray) 

1398 

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

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

1401 if pkres.skip: 

1402 continue 

1403 

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

1405 (pkres.strayFlux, False)]: 

1406 if foot is None: 

1407 continue 

1408 pks = foot.getPeaks() 

1409 pks.clear() 

1410 if add: 

1411 pks.append(pk) 

1412 return True