LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
plugins.py
Go to the documentation of this file.
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 
26 import numpy as np
27 
29 import lsst.afw.image as afwImage
30 import lsst.afw.detection as afwDet
31 import lsst.afw.geom as afwGeom
32 import lsst.geom as geom
33 
34 # Import C++ routines
35 from .baselineUtils import BaselineUtilsF as bUtils
36 
37 
38 def 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 
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.funcfunc = func
102  self.kwargskwargs = kwargs
103  self.onResetonReset = onReset
104  self.maxIterationsmaxIterations = maxIterations
105  self.kwargskwargs = kwargs
106  self.iterationsiterations = 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.funcfunc.__name__)
115  reset = self.funcfunc(debResult, log, **self.kwargskwargs)
116  if reset:
117  self.iterationsiterations += 1
118  if self.iterationsiterations < self.maxIterationsmaxIterations:
119  return self.onResetonReset
120  return None
121 
122  def __str__(self):
123  return ("<Deblender Plugin: func={0}, kwargs={1}".format(self.funcfunc.__name__, self.kwargskwargs))
124 
125  def __repr__(self):
126  return self.__str____str__()
127 
128 
129 def _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 
164 def 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 
239 def _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 
708 def 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 
770 def 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 
826 def _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 
956 def 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 
1005 def 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 
1041 def 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 
1077 def 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 
1106 def _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 
1149 def 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 
1250 def 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
int min
int max
Class to describe the properties of a detected object from an image.
Definition: Footprint.h:63
A range of pixels within one row of an Image.
Definition: Span.h:47
A compact representation of a collection of pixels.
Definition: SpanSet.h:78
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:77
An integer coordinate rectangle.
Definition: Box.h:55
def __init__(self, func, onReset=None, maxIterations=50, **kwargs)
Definition: plugins.py:76
Provides consistent interface for LSST exceptions.
Definition: Exception.h:107
Reports invalid arguments.
Definition: Runtime.h:66
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=nullptr)
Create a HeavyFootprint with footprint defined by the given Footprint and pixel values from the given...
def dot(symb, c, r, frame=None, size=2, ctype=None, origin=afwImage.PARENT, *args, **kwargs)
Definition: ds9.py:100
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
def makeTemplatesMonotonic(debResult, log)
Definition: plugins.py:1005
def fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2)
Definition: plugins.py:164
def weightTemplates(debResult, log)
Definition: plugins.py:1077
def buildSymmetricTemplates(debResult, log, patchEdges=False, setOrigTemplate=True)
Definition: plugins.py:708
def reconstructTemplates(debResult, log, maxTempDotProd=0.5)
Definition: plugins.py:1149
def medianSmoothTemplates(debResult, log, medianFilterHalfsize=2)
Definition: plugins.py:956
def clipFootprintToNonzeroImpl(foot, image)
Definition: plugins.py:38
def apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak', strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001, getTemplateSum=False)
Definition: plugins.py:1252
def clipFootprintsToNonzero(debResult, log)
Definition: plugins.py:1041
def rampFluxAtEdge(debResult, log, patchEdges=False)
Definition: plugins.py:770
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
Angle abs(Angle const &a)
Definition: Angle.h:106