LSST Applications g0265f82a02+0e5473021a,g02d81e74bb+f5613e8b4f,g1470d8bcf6+190ad2ba91,g14a832a312+311607e4ab,g2079a07aa2+86d27d4dc4,g2305ad1205+a8e3196225,g295015adf3+b67ee847e5,g2bbee38e9b+0e5473021a,g337abbeb29+0e5473021a,g3ddfee87b4+a761f810f3,g487adcacf7+17c8fdbcbd,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+65b5bd823e,g5a732f18d5+53520f316c,g64a986408d+f5613e8b4f,g6c1bc301e9+51106c2951,g858d7b2824+f5613e8b4f,g8a8a8dda67+585e252eca,g99cad8db69+6729933424,g9ddcbc5298+9a081db1e4,ga1e77700b3+15fc3df1f7,ga8c6da7877+ef4e3a5875,gb0e22166c9+60f28cb32d,gb6a65358fc+0e5473021a,gba4ed39666+c2a2e4ac27,gbb8dafda3b+e9bba80f27,gc120e1dc64+eee469a5e5,gc28159a63d+0e5473021a,gcf0d15dbbd+a761f810f3,gdaeeff99f8+f9a426f77a,ge6526c86ff+d4c1d4bfef,ge79ae78c31+0e5473021a,gee10cc3b42+585e252eca,gf1cff7945b+f5613e8b4f,w.2024.16
LSST Data Management Base Package
Loading...
Searching...
No Matches
utils.py
Go to the documentation of this file.
1# This file is part of ip_diffim.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22"""Support utilities for Measuring sources"""
23
24# Export DipoleTestImage to expose fake image generating funcs
25__all__ = ["DipoleTestImage", "evaluateMeanPsfFwhm", "getPsfFwhm"]
26
27import itertools
28import numpy as np
29import lsst.geom as geom
30import lsst.afw.detection as afwDet
31import lsst.afw.display as afwDisplay
32import lsst.afw.detection as afwDetection
33import lsst.afw.geom as afwGeom
34import lsst.afw.image as afwImage
35import lsst.afw.math as afwMath
36import lsst.afw.table as afwTable
37import lsst.meas.algorithms as measAlg
38import lsst.meas.base as measBase
39from lsst.meas.algorithms.testUtils import plantSources
40from lsst.pex.exceptions import InvalidParameterError
41from lsst.utils.logging import getLogger
42from .dipoleFitTask import DipoleFitAlgorithm
43from . import diffimLib
44
45afwDisplay.setDefaultMaskTransparency(75)
46keptPlots = False # Have we arranged to keep spatial plots open?
47
48_LOG = getLogger(__name__)
49
50
51def showSourceSet(sSet, xy0=(0, 0), frame=0, ctype=afwDisplay.GREEN, symb="+", size=2):
52 """Draw the (XAstrom, YAstrom) positions of a set of Sources.
53
54 Image has the given XY0.
55 """
56 disp = afwDisplay.afwDisplay(frame=frame)
57 with disp.Buffering():
58 for s in sSet:
59 xc, yc = s.getXAstrom() - xy0[0], s.getYAstrom() - xy0[1]
60
61 if symb == "id":
62 disp.dot(str(s.getId()), xc, yc, ctype=ctype, size=size)
63 else:
64 disp.dot(symb, xc, yc, ctype=ctype, size=size)
65
66
67# Kernel display utilities
68#
69
70
71def showKernelSpatialCells(maskedIm, kernelCellSet, showChi2=False, symb="o",
72 ctype=None, ctypeUnused=None, ctypeBad=None, size=3,
73 frame=None, title="Spatial Cells"):
74 """Show the SpatialCells.
75
76 If symb is something that display.dot understands (e.g. "o"), the top
77 nMaxPerCell candidates will be indicated with that symbol, using ctype
78 and size.
79 """
80 disp = afwDisplay.Display(frame=frame)
81 disp.mtv(maskedIm, title=title)
82 with disp.Buffering():
83 origin = [-maskedIm.getX0(), -maskedIm.getY0()]
84 for cell in kernelCellSet.getCellList():
85 afwDisplay.utils.drawBBox(cell.getBBox(), origin=origin, display=disp)
86
87 goodies = ctypeBad is None
88 for cand in cell.begin(goodies):
89 xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1]
90 if cand.getStatus() == afwMath.SpatialCellCandidate.BAD:
91 color = ctypeBad
92 elif cand.getStatus() == afwMath.SpatialCellCandidate.GOOD:
93 color = ctype
94 elif cand.getStatus() == afwMath.SpatialCellCandidate.UNKNOWN:
95 color = ctypeUnused
96 else:
97 continue
98
99 if color:
100 disp.dot(symb, xc, yc, ctype=color, size=size)
101
102 if showChi2:
103 rchi2 = cand.getChi2()
104 if rchi2 > 1e100:
105 rchi2 = np.nan
106 disp.dot("%d %.1f" % (cand.getId(), rchi2),
107 xc - size, yc - size - 4, ctype=color, size=size)
108
109
110def showDiaSources(sources, exposure, isFlagged, isDipole, frame=None):
111 """Display Dia Sources.
112 """
113 #
114 # Show us the ccandidates
115 #
116 # Too many mask planes in diffims
117 disp = afwDisplay.Display(frame=frame)
118 for plane in ("BAD", "CR", "EDGE", "INTERPOlATED", "INTRP", "SAT", "SATURATED"):
119 disp.setMaskPlaneColor(plane, color="ignore")
120
121 mos = afwDisplay.utils.Mosaic()
122 for i in range(len(sources)):
123 source = sources[i]
124 badFlag = isFlagged[i]
125 dipoleFlag = isDipole[i]
126 bbox = source.getFootprint().getBBox()
127 stamp = exposure.Factory(exposure, bbox, True)
128 im = afwDisplay.utils.Mosaic(gutter=1, background=0, mode="x")
129 im.append(stamp.getMaskedImage())
130 lab = "%.1f,%.1f:" % (source.getX(), source.getY())
131 if badFlag:
132 ctype = afwDisplay.RED
133 lab += "BAD"
134 if dipoleFlag:
135 ctype = afwDisplay.YELLOW
136 lab += "DIPOLE"
137 if not badFlag and not dipoleFlag:
138 ctype = afwDisplay.GREEN
139 lab += "OK"
140 mos.append(im.makeMosaic(), lab, ctype)
141 title = "Dia Sources"
142 mosaicImage = mos.makeMosaic(display=disp, title=title)
143 return mosaicImage
144
145
146def showKernelCandidates(kernelCellSet, kernel, background, frame=None, showBadCandidates=True,
147 resids=False, kernels=False):
148 """Display the Kernel candidates.
149
150 If kernel is provided include spatial model and residuals;
151 If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi.
152 """
153 #
154 # Show us the ccandidates
155 #
156 if kernels:
157 mos = afwDisplay.utils.Mosaic(gutter=5, background=0)
158 else:
159 mos = afwDisplay.utils.Mosaic(gutter=5, background=-1)
160 #
161 candidateCenters = []
162 candidateCentersBad = []
163 candidateIndex = 0
164 for cell in kernelCellSet.getCellList():
165 for cand in cell.begin(False): # include bad candidates
166 # Original difference image; if does not exist, skip candidate
167 try:
168 resid = cand.getDifferenceImage(diffimLib.KernelCandidateF.ORIG)
169 except Exception:
170 continue
171
172 rchi2 = cand.getChi2()
173 if rchi2 > 1e100:
174 rchi2 = np.nan
175
176 if not showBadCandidates and cand.isBad():
177 continue
178
179 im_resid = afwDisplay.utils.Mosaic(gutter=1, background=-0.5, mode="x")
180
181 try:
182 im = cand.getScienceMaskedImage()
183 im = im.Factory(im, True)
184 im.setXY0(cand.getScienceMaskedImage().getXY0())
185 except Exception:
186 continue
187 if (not resids and not kernels):
188 im_resid.append(im.Factory(im, True))
189 try:
190 im = cand.getTemplateMaskedImage()
191 im = im.Factory(im, True)
192 im.setXY0(cand.getTemplateMaskedImage().getXY0())
193 except Exception:
194 continue
195 if (not resids and not kernels):
196 im_resid.append(im.Factory(im, True))
197
198 # Difference image with original basis
199 if resids:
200 var = resid.variance
201 var = var.Factory(var, True)
202 np.sqrt(var.array, var.array) # inplace sqrt
203 resid = resid.image
204 resid /= var
205 bbox = kernel.shrinkBBox(resid.getBBox())
206 resid = resid.Factory(resid, bbox, deep=True)
207 elif kernels:
208 kim = cand.getKernelImage(diffimLib.KernelCandidateF.ORIG).convertF()
209 resid = kim.Factory(kim, True)
210 im_resid.append(resid)
211
212 # residuals using spatial model
213 ski = afwImage.ImageD(kernel.getDimensions())
214 kernel.computeImage(ski, False, int(cand.getXCenter()), int(cand.getYCenter()))
215 sk = afwMath.FixedKernel(ski)
216 sbg = 0.0
217 if background:
218 sbg = background(int(cand.getXCenter()), int(cand.getYCenter()))
219 sresid = cand.getDifferenceImage(sk, sbg)
220 resid = sresid
221 if resids:
222 resid = sresid.image
223 resid /= var
224 bbox = kernel.shrinkBBox(resid.getBBox())
225 resid = resid.Factory(resid, bbox, deep=True)
226 elif kernels:
227 kim = ski.convertF()
228 resid = kim.Factory(kim, True)
229 im_resid.append(resid)
230
231 im = im_resid.makeMosaic()
232
233 lab = "%d chi^2 %.1f" % (cand.getId(), rchi2)
234 ctype = afwDisplay.RED if cand.isBad() else afwDisplay.GREEN
235
236 mos.append(im, lab, ctype)
237
238 if False and np.isnan(rchi2):
239 disp = afwDisplay.Display(frame=1)
240 disp.mtv(cand.getScienceMaskedImage.image, title="candidate")
241 print("rating", cand.getCandidateRating())
242
243 im = cand.getScienceMaskedImage()
244 center = (candidateIndex, cand.getXCenter() - im.getX0(), cand.getYCenter() - im.getY0())
245 candidateIndex += 1
246 if cand.isBad():
247 candidateCentersBad.append(center)
248 else:
249 candidateCenters.append(center)
250
251 if resids:
252 title = "chi Diffim"
253 elif kernels:
254 title = "Kernels"
255 else:
256 title = "Candidates & residuals"
257
258 disp = afwDisplay.Display(frame=frame)
259 mosaicImage = mos.makeMosaic(display=disp, title=title)
260
261 return mosaicImage
262
263
264def showKernelBasis(kernel, frame=None):
265 """Display a Kernel's basis images.
266 """
267 mos = afwDisplay.utils.Mosaic()
268
269 for k in kernel.getKernelList():
270 im = afwImage.ImageD(k.getDimensions())
271 k.computeImage(im, False)
272 mos.append(im)
273
274 disp = afwDisplay.Display(frame=frame)
275 mos.makeMosaic(display=disp, title="Kernel Basis Images")
276
277 return mos
278
279
280
281
282def plotKernelSpatialModel(kernel, kernelCellSet, showBadCandidates=True,
283 numSample=128, keepPlots=True, maxCoeff=10):
284 """Plot the Kernel spatial model.
285 """
286 try:
287 import matplotlib.pyplot as plt
288 import matplotlib.colors
289 except ImportError as e:
290 print("Unable to import numpy and matplotlib: %s" % e)
291 return
292
293 x0 = kernelCellSet.getBBox().getBeginX()
294 y0 = kernelCellSet.getBBox().getBeginY()
295
296 candPos = list()
297 candFits = list()
298 badPos = list()
299 badFits = list()
300 candAmps = list()
301 badAmps = list()
302 for cell in kernelCellSet.getCellList():
303 for cand in cell.begin(False):
304 if not showBadCandidates and cand.isBad():
305 continue
306 candCenter = geom.PointD(cand.getXCenter(), cand.getYCenter())
307 try:
308 im = cand.getTemplateMaskedImage()
309 except Exception:
310 continue
311
312 targetFits = badFits if cand.isBad() else candFits
313 targetPos = badPos if cand.isBad() else candPos
314 targetAmps = badAmps if cand.isBad() else candAmps
315
316 # compare original and spatial kernel coefficients
317 kp0 = np.array(cand.getKernel(diffimLib.KernelCandidateF.ORIG).getKernelParameters())
318 amp = cand.getCandidateRating()
319
320 targetFits = badFits if cand.isBad() else candFits
321 targetPos = badPos if cand.isBad() else candPos
322 targetAmps = badAmps if cand.isBad() else candAmps
323
324 targetFits.append(kp0)
325 targetPos.append(candCenter)
326 targetAmps.append(amp)
327
328 xGood = np.array([pos.getX() for pos in candPos]) - x0
329 yGood = np.array([pos.getY() for pos in candPos]) - y0
330 zGood = np.array(candFits)
331
332 xBad = np.array([pos.getX() for pos in badPos]) - x0
333 yBad = np.array([pos.getY() for pos in badPos]) - y0
334 zBad = np.array(badFits)
335 numBad = len(badPos)
336
337 xRange = np.linspace(0, kernelCellSet.getBBox().getWidth(), num=numSample)
338 yRange = np.linspace(0, kernelCellSet.getBBox().getHeight(), num=numSample)
339
340 if maxCoeff:
341 maxCoeff = min(maxCoeff, kernel.getNKernelParameters())
342 else:
343 maxCoeff = kernel.getNKernelParameters()
344
345 for k in range(maxCoeff):
346 func = kernel.getSpatialFunction(k)
347 dfGood = zGood[:, k] - np.array([func(pos.getX(), pos.getY()) for pos in candPos])
348 yMin = dfGood.min()
349 yMax = dfGood.max()
350 if numBad > 0:
351 dfBad = zBad[:, k] - np.array([func(pos.getX(), pos.getY()) for pos in badPos])
352 # Can really screw up the range...
353 yMin = min([yMin, dfBad.min()])
354 yMax = max([yMax, dfBad.max()])
355 yMin -= 0.05*(yMax - yMin)
356 yMax += 0.05*(yMax - yMin)
357
358 fRange = np.ndarray((len(xRange), len(yRange)))
359 for j, yVal in enumerate(yRange):
360 for i, xVal in enumerate(xRange):
361 fRange[j][i] = func(xVal, yVal)
362
363 fig = plt.figure(k)
364
365 fig.clf()
366 try:
367 fig.canvas._tkcanvas._root().lift() # == Tk's raise, but raise is a python reserved word
368 except Exception: # protect against API changes
369 pass
370
371 fig.suptitle('Kernel component %d' % k)
372
373 # LL
374 ax = fig.add_axes((0.1, 0.05, 0.35, 0.35))
375 vmin = fRange.min() # - 0.05*np.fabs(fRange.min())
376 vmax = fRange.max() # + 0.05*np.fabs(fRange.max())
377 norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
378 im = ax.imshow(fRange, aspect='auto', norm=norm,
379 extent=[0, kernelCellSet.getBBox().getWidth() - 1,
380 0, kernelCellSet.getBBox().getHeight() - 1])
381 ax.set_title('Spatial polynomial')
382 plt.colorbar(im, orientation='horizontal', ticks=[vmin, vmax])
383
384 # UL
385 ax = fig.add_axes((0.1, 0.55, 0.35, 0.35))
386 ax.plot(-2.5*np.log10(candAmps), zGood[:, k], 'b+')
387 if numBad > 0:
388 ax.plot(-2.5*np.log10(badAmps), zBad[:, k], 'r+')
389 ax.set_title("Basis Coefficients")
390 ax.set_xlabel("Instr mag")
391 ax.set_ylabel("Coeff")
392
393 # LR
394 ax = fig.add_axes((0.55, 0.05, 0.35, 0.35))
395 ax.set_autoscale_on(False)
396 ax.set_xbound(lower=0, upper=kernelCellSet.getBBox().getHeight())
397 ax.set_ybound(lower=yMin, upper=yMax)
398 ax.plot(yGood, dfGood, 'b+')
399 if numBad > 0:
400 ax.plot(yBad, dfBad, 'r+')
401 ax.axhline(0.0)
402 ax.set_title('dCoeff (indiv-spatial) vs. y')
403
404 # UR
405 ax = fig.add_axes((0.55, 0.55, 0.35, 0.35))
406 ax.set_autoscale_on(False)
407 ax.set_xbound(lower=0, upper=kernelCellSet.getBBox().getWidth())
408 ax.set_ybound(lower=yMin, upper=yMax)
409 ax.plot(xGood, dfGood, 'b+')
410 if numBad > 0:
411 ax.plot(xBad, dfBad, 'r+')
412 ax.axhline(0.0)
413 ax.set_title('dCoeff (indiv-spatial) vs. x')
414
415 fig.show()
416
417 global keptPlots
418 if keepPlots and not keptPlots:
419 # Keep plots open when done
420 def show():
421 print("%s: Please close plots when done." % __name__)
422 try:
423 plt.show()
424 except Exception:
425 pass
426 print("Plots closed, exiting...")
427 import atexit
428 atexit.register(show)
429 keptPlots = True
430
431
432def plotKernelCoefficients(spatialKernel, kernelCellSet, showBadCandidates=False, keepPlots=True):
433 """Plot the individual kernel candidate and the spatial kernel solution coefficients.
434
435 Parameters
436 ----------
437
438 spatialKernel : `lsst.afw.math.LinearCombinationKernel`
439 The spatial spatialKernel solution model which is a spatially varying linear combination
440 of the spatialKernel basis functions.
441 Typically returned by `lsst.ip.diffim.SpatialKernelSolution.getSolutionPair()`.
442
443 kernelCellSet : `lsst.afw.math.SpatialCellSet`
444 The spatial cells that was used for solution for the spatialKernel. They contain the
445 local solutions of the AL kernel for the selected sources.
446
447 showBadCandidates : `bool`, optional
448 If True, plot the coefficient values for kernel candidates where the solution was marked
449 bad by the numerical algorithm. Defaults to False.
450
451 keepPlots: `bool`, optional
452 If True, sets ``plt.show()`` to be called before the task terminates, so that the plots
453 can be explored interactively. Defaults to True.
454
455 Notes
456 -----
457 This function produces 3 figures per image subtraction operation.
458 * A grid plot of the local solutions. Each grid cell corresponds to a proportional area in
459 the image. In each cell, local kernel solution coefficients are plotted of kernel candidates (color)
460 that fall into this area as a function of the kernel basis function number.
461 * A grid plot of the spatial solution. Each grid cell corresponds to a proportional area in
462 the image. In each cell, the spatial solution coefficients are evaluated for the center of the cell.
463 * Histogram of the local solution coefficients. Red line marks the spatial solution value at
464 center of the image.
465
466 This function is called if ``lsst.ip.diffim.psfMatch.plotKernelCoefficients==True`` in lsstDebug. This
467 function was implemented as part of DM-17825.
468 """
469 try:
470 import matplotlib.pyplot as plt
471 except ImportError as e:
472 print("Unable to import matplotlib: %s" % e)
473 return
474
475 # Image dimensions
476 imgBBox = kernelCellSet.getBBox()
477 x0 = imgBBox.getBeginX()
478 y0 = imgBBox.getBeginY()
479 wImage = imgBBox.getWidth()
480 hImage = imgBBox.getHeight()
481 imgCenterX = imgBBox.getCenterX()
482 imgCenterY = imgBBox.getCenterY()
483
484 # Plot the local solutions
485 # ----
486
487 # Grid size
488 nX = 8
489 nY = 8
490 wCell = wImage / nX
491 hCell = hImage / nY
492
493 fig = plt.figure()
494 fig.suptitle("Kernel candidate parameters on an image grid")
495 arrAx = fig.subplots(nrows=nY, ncols=nX, sharex=True, sharey=True, gridspec_kw=dict(
496 wspace=0, hspace=0))
497
498 # Bottom left panel is for bottom left part of the image
499 arrAx = arrAx[::-1, :]
500
501 allParams = []
502 for cell in kernelCellSet.getCellList():
503 cellBBox = geom.Box2D(cell.getBBox())
504 # Determine which panel this spatial cell belongs to
505 iX = int((cellBBox.getCenterX() - x0)//wCell)
506 iY = int((cellBBox.getCenterY() - y0)//hCell)
507
508 for cand in cell.begin(False):
509 try:
510 kernel = cand.getKernel(cand.ORIG)
511 except Exception:
512 continue
513
514 if not showBadCandidates and cand.isBad():
515 continue
516
517 nKernelParams = kernel.getNKernelParameters()
518 kernelParams = np.array(kernel.getKernelParameters())
519 allParams.append(kernelParams)
520
521 if cand.isBad():
522 color = 'red'
523 else:
524 color = None
525 arrAx[iY, iX].plot(np.arange(nKernelParams), kernelParams, '.-',
526 color=color, drawstyle='steps-mid', linewidth=0.1)
527 for ax in arrAx.ravel():
528 ax.grid(True, axis='y')
529
530 # Plot histogram of the local parameters and the global solution at the image center
531 # ----
532
533 spatialFuncs = spatialKernel.getSpatialFunctionList()
534 nKernelParams = spatialKernel.getNKernelParameters()
535 nX = 8
536 fig = plt.figure()
537 fig.suptitle("Hist. of parameters marked with spatial solution at img center")
538 arrAx = fig.subplots(nrows=int(nKernelParams//nX)+1, ncols=nX)
539 arrAx = arrAx[::-1, :]
540 allParams = np.array(allParams)
541 for k in range(nKernelParams):
542 ax = arrAx.ravel()[k]
543 ax.hist(allParams[:, k], bins=20, edgecolor='black')
544 ax.set_xlabel('P{}'.format(k))
545 valueParam = spatialFuncs[k](imgCenterX, imgCenterY)
546 ax.axvline(x=valueParam, color='red')
547 ax.text(0.1, 0.9, '{:.1f}'.format(valueParam),
548 transform=ax.transAxes, backgroundcolor='lightsteelblue')
549
550 # Plot grid of the spatial solution
551 # ----
552
553 nX = 8
554 nY = 8
555 wCell = wImage / nX
556 hCell = hImage / nY
557 x0 += wCell / 2
558 y0 += hCell / 2
559
560 fig = plt.figure()
561 fig.suptitle("Spatial solution of kernel parameters on an image grid")
562 arrAx = fig.subplots(nrows=nY, ncols=nX, sharex=True, sharey=True, gridspec_kw=dict(
563 wspace=0, hspace=0))
564 arrAx = arrAx[::-1, :]
565 kernelParams = np.zeros(nKernelParams, dtype=float)
566
567 for iX in range(nX):
568 for iY in range(nY):
569 x = x0 + iX * wCell
570 y = y0 + iY * hCell
571 # Evaluate the spatial solution functions for this x,y location
572 kernelParams = [f(x, y) for f in spatialFuncs]
573 arrAx[iY, iX].plot(np.arange(nKernelParams), kernelParams, '.-', drawstyle='steps-mid')
574 arrAx[iY, iX].grid(True, axis='y')
575
576 global keptPlots
577 if keepPlots and not keptPlots:
578 # Keep plots open when done
579 def show():
580 print("%s: Please close plots when done." % __name__)
581 try:
582 plt.show()
583 except Exception:
584 pass
585 print("Plots closed, exiting...")
586 import atexit
587 atexit.register(show)
588 keptPlots = True
589
590
591def showKernelMosaic(bbox, kernel, nx=7, ny=None, frame=None, title=None,
592 showCenter=True, showEllipticity=True):
593 """Show a mosaic of Kernel images.
594 """
595 mos = afwDisplay.utils.Mosaic()
596
597 x0 = bbox.getBeginX()
598 y0 = bbox.getBeginY()
599 width = bbox.getWidth()
600 height = bbox.getHeight()
601
602 if not ny:
603 ny = int(nx*float(height)/width + 0.5)
604 if not ny:
605 ny = 1
606
607 schema = afwTable.SourceTable.makeMinimalSchema()
608 centroidName = "base_SdssCentroid"
609 shapeName = "base_SdssShape"
610 control = measBase.SdssCentroidControl()
611 schema.getAliasMap().set("slot_Centroid", centroidName)
612 schema.getAliasMap().set("slot_Centroid_flag", centroidName + "_flag")
613 centroider = measBase.SdssCentroidAlgorithm(control, centroidName, schema)
614 sdssShape = measBase.SdssShapeControl()
615 shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema)
616 table = afwTable.SourceTable.make(schema)
617 table.defineCentroid(centroidName)
618 table.defineShape(shapeName)
619
620 centers = []
621 shapes = []
622 for iy in range(ny):
623 for ix in range(nx):
624 x = int(ix*(width - 1)/(nx - 1)) + x0
625 y = int(iy*(height - 1)/(ny - 1)) + y0
626
627 im = afwImage.ImageD(kernel.getDimensions())
628 ksum = kernel.computeImage(im, False, x, y)
629 lab = "Kernel(%d,%d)=%.2f" % (x, y, ksum) if False else ""
630 mos.append(im, lab)
631
632 # SdssCentroidAlgorithm.measure requires an exposure of floats
634
635 w, h = im.getWidth(), im.getHeight()
636 centerX = im.getX0() + w//2
637 centerY = im.getY0() + h//2
638 src = table.makeRecord()
639 spans = afwGeom.SpanSet(exp.getBBox())
640 foot = afwDet.Footprint(spans)
641 foot.addPeak(centerX, centerY, 1)
642 src.setFootprint(foot)
643
644 try: # The centroider requires a psf, so this will fail if none is attached to exp
645 centroider.measure(src, exp)
646 centers.append((src.getX(), src.getY()))
647
648 shaper.measure(src, exp)
649 shapes.append((src.getIxx(), src.getIxy(), src.getIyy()))
650 except Exception:
651 pass
652
653 disp = afwDisplay.Display(frame=frame)
654 mos.makeMosaic(display=disp, title=title if title else "Model Kernel", mode=nx)
655
656 if centers and frame is not None:
657 disp = afwDisplay.Display(frame=frame)
658 i = 0
659 with disp.Buffering():
660 for cen, shape in zip(centers, shapes):
661 bbox = mos.getBBox(i)
662 i += 1
663 xc, yc = cen[0] + bbox.getMinX(), cen[1] + bbox.getMinY()
664 if showCenter:
665 disp.dot("+", xc, yc, ctype=afwDisplay.BLUE)
666
667 if showEllipticity:
668 ixx, ixy, iyy = shape
669 disp.dot("@:%g,%g,%g" % (ixx, ixy, iyy), xc, yc, ctype=afwDisplay.RED)
670
671 return mos
672
673
674def plotWhisker(results, newWcs):
675 """Plot whisker diagram of astromeric offsets between results.matches.
676 """
677 refCoordKey = results.matches[0].first.getTable().getCoordKey()
678 inCentroidKey = results.matches[0].second.getTable().getCentroidSlot().getMeasKey()
679 positions = [m.first.get(refCoordKey) for m in results.matches]
680 residuals = [m.first.get(refCoordKey).getOffsetFrom(
681 newWcs.pixelToSky(m.second.get(inCentroidKey))) for
682 m in results.matches]
683 import matplotlib.pyplot as plt
684 fig = plt.figure()
685 sp = fig.add_subplot(1, 1, 0)
686 xpos = [x[0].asDegrees() for x in positions]
687 ypos = [x[1].asDegrees() for x in positions]
688 xpos.append(0.02*(max(xpos) - min(xpos)) + min(xpos))
689 ypos.append(0.98*(max(ypos) - min(ypos)) + min(ypos))
690 xidxs = np.isfinite(xpos)
691 yidxs = np.isfinite(ypos)
692 X = np.asarray(xpos)[xidxs]
693 Y = np.asarray(ypos)[yidxs]
694 distance = [x[1].asArcseconds() for x in residuals]
695 distance.append(0.2)
696 distance = np.asarray(distance)[xidxs]
697 # NOTE: This assumes that the bearing is measured positive from +RA through North.
698 # From the documentation this is not clear.
699 bearing = [x[0].asRadians() for x in residuals]
700 bearing.append(0)
701 bearing = np.asarray(bearing)[xidxs]
702 U = (distance*np.cos(bearing))
703 V = (distance*np.sin(bearing))
704 sp.quiver(X, Y, U, V)
705 sp.set_title("WCS Residual")
706 plt.show()
707
708
710 """Utility class for dipole measurement testing.
711
712 Generate an image with simulated dipoles and noise; store the original
713 "pre-subtraction" images and catalogs as well.
714 Used to generate test data for DMTN-007 (http://dmtn-007.lsst.io).
715 """
716
717 def __init__(self, w=101, h=101, xcenPos=[27.], ycenPos=[25.], xcenNeg=[23.], ycenNeg=[25.],
718 psfSigma=2., flux=[30000.], fluxNeg=None, noise=10., gradientParams=None):
719 self.w = w
720 self.h = h
721 self.xcenPos = xcenPos
722 self.ycenPos = ycenPos
723 self.xcenNeg = xcenNeg
724 self.ycenNeg = ycenNeg
725 self.psfSigma = psfSigma
726 self.flux = flux
727 self.fluxNeg = fluxNeg
728 if fluxNeg is None:
729 self.fluxNeg = self.flux
730 self.noise = noise
731 self.gradientParams = gradientParams
732 self._makeDipoleImage()
733
735 """Generate an exposure and catalog with the given dipole source(s).
736 """
737 # Must seed the pos/neg images with different values to ensure they get different noise realizations
738 posImage, posCatalog = self._makeStarImage(
739 xc=self.xcenPos, yc=self.ycenPos, flux=self.flux, randomSeed=111)
740
741 negImage, negCatalog = self._makeStarImage(
742 xc=self.xcenNeg, yc=self.ycenNeg, flux=self.fluxNeg, randomSeed=222)
743
744 dipole = posImage.clone()
745 di = dipole.getMaskedImage()
746 di -= negImage.getMaskedImage()
747
748 self.diffim, self.posImage, self.posCatalog, self.negImage, self.negCatalog \
749 = dipole, posImage, posCatalog, negImage, negCatalog
750
751 def _makeStarImage(self, xc=[15.3], yc=[18.6], flux=[2500], schema=None, randomSeed=None):
752 """Generate an exposure and catalog with the given stellar source(s).
753 """
754 from lsst.meas.base.tests import TestDataset
755 bbox = geom.Box2I(geom.Point2I(0, 0), geom.Point2I(self.w - 1, self.h - 1))
756 dataset = TestDataset(bbox, psfSigma=self.psfSigma, threshold=1.)
757
758 for i in range(len(xc)):
759 dataset.addSource(instFlux=flux[i], centroid=geom.Point2D(xc[i], yc[i]))
760
761 if schema is None:
762 schema = TestDataset.makeMinimalSchema()
763 exposure, catalog = dataset.realize(noise=self.noise, schema=schema, randomSeed=randomSeed)
764
765 if self.gradientParams is not None:
766 y, x = np.mgrid[:self.w, :self.h]
767 gp = self.gradientParams
768 gradient = gp[0] + gp[1]*x + gp[2]*y
769 if len(self.gradientParams) > 3: # it includes a set of 2nd-order polynomial params
770 gradient += gp[3]*x*y + gp[4]*x*x + gp[5]*y*y
771 imgArr = exposure.image.array
772 imgArr += gradient
773
774 return exposure, catalog
775
776 def fitDipoleSource(self, source, **kwds):
777 alg = DipoleFitAlgorithm(self.diffim, self.posImage, self.negImage)
778 fitResult = alg.fitDipole(source, **kwds)
779 return fitResult
780
781 def detectDipoleSources(self, doMerge=True, diffim=None, detectSigma=5.5, grow=3, minBinSize=32):
782 """Utility function for detecting dipoles.
783
784 Detect pos/neg sources in the diffim, then merge them. A
785 bigger "grow" parameter leads to a larger footprint which
786 helps with dipole measurement for faint dipoles.
787
788 Parameters
789 ----------
790 doMerge : `bool`
791 Whether to merge the positive and negagive detections into a single
792 source table.
793 diffim : `lsst.afw.image.exposure.exposure.ExposureF`
794 Difference image on which to perform detection.
795 detectSigma : `float`
796 Threshold for object detection.
797 grow : `int`
798 Number of pixels to grow the footprints before merging.
799 minBinSize : `int`
800 Minimum bin size for the background (re)estimation (only applies if
801 the default leads to min(nBinX, nBinY) < fit order so the default
802 config parameter needs to be decreased, but not to a value smaller
803 than ``minBinSize``, in which case the fitting algorithm will take
804 over and decrease the fit order appropriately.)
805
806 Returns
807 -------
808 sources : `lsst.afw.table.SourceCatalog`
809 If doMerge=True, the merged source catalog is returned OR
810 detectTask : `lsst.meas.algorithms.SourceDetectionTask`
811 schema : `lsst.afw.table.Schema`
812 If doMerge=False, the source detection task and its schema are
813 returned.
814 """
815 if diffim is None:
816 diffim = self.diffim
817
818 # Start with a minimal schema - only the fields all SourceCatalogs need
819 schema = afwTable.SourceTable.makeMinimalSchema()
820
821 # Customize the detection task a bit (optional)
822 detectConfig = measAlg.SourceDetectionConfig()
823 detectConfig.returnOriginalFootprints = False # should be the default
824
825 # code from imageDifference.py:
826 detectConfig.thresholdPolarity = "both"
827 detectConfig.thresholdValue = detectSigma
828 # detectConfig.nSigmaToGrow = psfSigma
829 detectConfig.reEstimateBackground = True # if False, will fail often for faint sources on gradients?
830 detectConfig.thresholdType = "pixel_stdev"
831 detectConfig.excludeMaskPlanes = ["EDGE"]
832 # Test images are often quite small, so may need to adjust background binSize
833 while ((min(diffim.getWidth(), diffim.getHeight()))//detectConfig.background.binSize
834 < detectConfig.background.approxOrderX and detectConfig.background.binSize > minBinSize):
835 detectConfig.background.binSize = max(minBinSize, detectConfig.background.binSize//2)
836
837 # Create the detection task. We pass the schema so the task can declare a few flag fields
838 detectTask = measAlg.SourceDetectionTask(schema, config=detectConfig)
839
840 table = afwTable.SourceTable.make(schema)
841 catalog = detectTask.run(table, diffim)
842
843 # Now do the merge.
844 if doMerge:
845 fpSet = catalog.positive
846 fpSet.merge(catalog.negative, grow, grow, False)
847 sources = afwTable.SourceCatalog(table)
848 fpSet.makeSources(sources)
849
850 return sources
851
852 else:
853 return detectTask, schema
854
855
856def getPsfFwhm(psf, average=True, position=None):
857 """Directly calculate the horizontal and vertical widths
858 of a PSF at half its maximum value.
859
860 Parameters
861 ----------
862 psf : `~lsst.afw.detection.Psf`
863 Point spread function (PSF) to evaluate.
864 average : `bool`, optional
865 Set to return the average width over Y and X axes.
866 position : `~lsst.geom.Point2D`, optional
867 The position at which to evaluate the PSF. If `None`, then the
868 average position is used.
869
870 Returns
871 -------
872 psfSize : `float` | `tuple` [`float`]
873 The FWHM of the PSF computed at its average position.
874 Returns the widths along the Y and X axes,
875 or the average of the two if `average` is set.
876
877 See Also
878 --------
879 evaluateMeanPsfFwhm
880 """
881 if position is None:
882 position = psf.getAveragePosition()
883 shape = psf.computeShape(position)
884 sigmaToFwhm = 2*np.log(2*np.sqrt(2))
885
886 if average:
887 return sigmaToFwhm*shape.getTraceRadius()
888 else:
889 return [sigmaToFwhm*np.sqrt(shape.getIxx()), sigmaToFwhm*np.sqrt(shape.getIyy())]
890
891
892def evaluateMeanPsfFwhm(exposure: afwImage.Exposure,
893 fwhmExposureBuffer: float, fwhmExposureGrid: int) -> float:
894 """Get the mean PSF FWHM by evaluating it on a grid within an exposure.
895
896 Parameters
897 ----------
898 exposure : `~lsst.afw.image.Exposure`
899 The exposure for which the mean FWHM of the PSF is to be computed.
900 The exposure must contain a `psf` attribute.
901 fwhmExposureBuffer : `float`
902 Fractional buffer margin to be left out of all sides of the image
903 during the construction of the grid to compute mean PSF FWHM in an
904 exposure.
905 fwhmExposureGrid : `int`
906 Grid size to compute the mean FWHM in an exposure.
907
908 Returns
909 -------
910 meanFwhm : `float`
911 The mean PSF FWHM on the exposure.
912
913 Raises
914 ------
915 ValueError
916 Raised if the PSF cannot be computed at any of the grid points.
917
918 See Also
919 --------
920 `getPsfFwhm`
921 `computeAveragePsf`
922 """
923
924 psf = exposure.psf
925
926 bbox = exposure.getBBox()
927 xmax, ymax = bbox.getMax()
928 xmin, ymin = bbox.getMin()
929
930 xbuffer = fwhmExposureBuffer*(xmax-xmin)
931 ybuffer = fwhmExposureBuffer*(ymax-ymin)
932
933 width = []
934 for (x, y) in itertools.product(np.linspace(xmin+xbuffer, xmax-xbuffer, fwhmExposureGrid),
935 np.linspace(ymin+ybuffer, ymax-ybuffer, fwhmExposureGrid)
936 ):
937 pos = geom.Point2D(x, y)
938 try:
939 fwhm = getPsfFwhm(psf, average=True, position=pos)
940 except InvalidParameterError:
941 _LOG.debug("Unable to compute PSF FWHM at position (%f, %f).", x, y)
942 continue
943
944 width.append(fwhm)
945
946 if not width:
947 raise ValueError("Unable to compute PSF FWHM at any position on the exposure.")
948
949 return np.nanmean(width)
950
951
952def computeAveragePsf(exposure: afwImage.Exposure,
953 psfExposureBuffer: float, psfExposureGrid: int) -> afwImage.ImageD:
954 """Get the average PSF by evaluating it on a grid within an exposure.
955
956 Parameters
957 ----------
958 exposure : `~lsst.afw.image.Exposure`
959 The exposure for which the average PSF is to be computed.
960 The exposure must contain a `psf` attribute.
961 psfExposureBuffer : `float`
962 Fractional buffer margin to be left out of all sides of the image
963 during the construction of the grid to compute average PSF in an
964 exposure.
965 psfExposureGrid : `int`
966 Grid size to compute the average PSF in an exposure.
967
968 Returns
969 -------
970 psfImage : `~lsst.afw.image.Image`
971 The average PSF across the exposure.
972
973 Raises
974 ------
975 ValueError
976 Raised if the PSF cannot be computed at any of the grid points.
977
978 See Also
979 --------
980 `evaluateMeanPsfFwhm`
981 """
982
983 psf = exposure.psf
984
985 bbox = exposure.getBBox()
986 xmax, ymax = bbox.getMax()
987 xmin, ymin = bbox.getMin()
988
989 xbuffer = psfExposureBuffer*(xmax-xmin)
990 ybuffer = psfExposureBuffer*(ymax-ymin)
991
992 nImg = 0
993 psfArray = None
994 for (x, y) in itertools.product(np.linspace(xmin+xbuffer, xmax-xbuffer, psfExposureGrid),
995 np.linspace(ymin+ybuffer, ymax-ybuffer, psfExposureGrid)
996 ):
997 pos = geom.Point2D(x, y)
998 try:
999 singleImage = psf.computeKernelImage(pos)
1000 except InvalidParameterError:
1001 _LOG.debug("Unable to compute PSF image at position (%f, %f).", x, y)
1002 continue
1003
1004 if psfArray is None:
1005 psfArray = singleImage.array
1006 else:
1007 psfArray += singleImage.array
1008 nImg += 1
1009
1010 if psfArray is None:
1011 raise ValueError("Unable to compute PSF image at any position on the exposure.")
1012
1013 psfImage = afwImage.ImageD(psfArray/nImg)
1014 return psfImage
1015
1016
1017def detectTestSources(exposure, addMaskPlanes=None):
1018 """Minimal source detection wrapper suitable for unit tests.
1019
1020 Parameters
1021 ----------
1022 exposure : `lsst.afw.image.Exposure`
1023 Exposure on which to run detection/measurement
1024 The exposure is modified in place to set the 'DETECTED' mask plane.
1025 addMaskPlanes : `list` of `str`, optional
1026 Additional mask planes to add to the maskedImage of the exposure.
1027
1028 Returns
1029 -------
1030 selectSources
1031 Source catalog containing candidates
1032 """
1033 if addMaskPlanes is None:
1034 # add empty streak mask plane in lieu of maskStreaksTask
1035 # And add empty INJECTED and INJECTED_TEMPLATE mask planes
1036 addMaskPlanes = ["STREAK", "INJECTED", "INJECTED_TEMPLATE"]
1037
1038 schema = afwTable.SourceTable.makeMinimalSchema()
1039 selectDetection = measAlg.SourceDetectionTask(schema=schema)
1040 selectMeasurement = measBase.SingleFrameMeasurementTask(schema=schema)
1041 table = afwTable.SourceTable.make(schema)
1042
1043 detRet = selectDetection.run(
1044 table=table,
1045 exposure=exposure,
1046 sigma=None, # The appropriate sigma is calculated from the PSF
1047 doSmooth=True
1048 )
1049 for mp in addMaskPlanes:
1050 exposure.mask.addMaskPlane(mp)
1051
1052 selectSources = detRet.sources
1053 selectMeasurement.run(measCat=selectSources, exposure=exposure)
1054
1055 return selectSources
1056
1057
1059 """Make a fake, affine Wcs.
1060 """
1061 crpix = geom.Point2D(123.45, 678.9)
1062 crval = geom.SpherePoint(0.1, 0.1, geom.degrees)
1063 cdMatrix = np.array([[5.19513851e-05, -2.81124812e-07],
1064 [-3.25186974e-07, -5.19112119e-05]])
1065 return afwGeom.makeSkyWcs(crpix, crval, cdMatrix)
1066
1067
1068def makeTestImage(seed=5, nSrc=20, psfSize=2., noiseLevel=5.,
1069 noiseSeed=6, fluxLevel=500., fluxRange=2.,
1070 kernelSize=32, templateBorderSize=0,
1071 background=None,
1072 xSize=256,
1073 ySize=256,
1074 x0=12345,
1075 y0=67890,
1076 calibration=1.,
1077 doApplyCalibration=False,
1078 xLoc=None,
1079 yLoc=None,
1080 flux=None,
1081 clearEdgeMask=False,
1082 addMaskPlanes=None,
1083 ):
1084 """Make a reproduceable PSF-convolved exposure for testing.
1085
1086 Parameters
1087 ----------
1088 seed : `int`, optional
1089 Seed value to initialize the random number generator for sources.
1090 nSrc : `int`, optional
1091 Number of sources to simulate.
1092 psfSize : `float`, optional
1093 Width of the PSF of the simulated sources, in pixels.
1094 noiseLevel : `float`, optional
1095 Standard deviation of the noise to add to each pixel.
1096 noiseSeed : `int`, optional
1097 Seed value to initialize the random number generator for noise.
1098 fluxLevel : `float`, optional
1099 Reference flux of the simulated sources.
1100 fluxRange : `float`, optional
1101 Range in flux amplitude of the simulated sources.
1102 kernelSize : `int`, optional
1103 Size in pixels of the kernel for simulating sources.
1104 templateBorderSize : `int`, optional
1105 Size in pixels of the image border used to pad the image.
1106 background : `lsst.afw.math.Chebyshev1Function2D`, optional
1107 Optional background to add to the output image.
1108 xSize, ySize : `int`, optional
1109 Size in pixels of the simulated image.
1110 x0, y0 : `int`, optional
1111 Origin of the image.
1112 calibration : `float`, optional
1113 Conversion factor between instFlux and nJy.
1114 doApplyCalibration : `bool`, optional
1115 Apply the photometric calibration and return the image in nJy?
1116 xLoc, yLoc : `list` of `float`, optional
1117 User-specified coordinates of the simulated sources.
1118 If specified, must have length equal to ``nSrc``
1119 flux : `list` of `float`, optional
1120 User-specified fluxes of the simulated sources.
1121 If specified, must have length equal to ``nSrc``
1122 clearEdgeMask : `bool`, optional
1123 Clear the "EDGE" mask plane after source detection.
1124 addMaskPlanes : `list` of `str`, optional
1125 Mask plane names to add to the image.
1126
1127 Returns
1128 -------
1129 modelExposure : `lsst.afw.image.Exposure`
1130 The model image, with the mask and variance planes. The DETECTED
1131 plane is filled in for the injected source footprints.
1132 sourceCat : `lsst.afw.table.SourceCatalog`
1133 Catalog of sources inserted in the model image.
1134
1135 Raises
1136 ------
1137 ValueError
1138 If `xloc`, `yloc`, or `flux` are supplied with inconsistant lengths.
1139 """
1140 # Distance from the inner edge of the bounding box to avoid placing test
1141 # sources in the model images.
1142 bufferSize = kernelSize/2 + templateBorderSize + 1
1143
1144 bbox = geom.Box2I(geom.Point2I(x0, y0), geom.Extent2I(xSize, ySize))
1145 if templateBorderSize > 0:
1146 bbox.grow(templateBorderSize)
1147
1148 rng = np.random.RandomState(seed)
1149 rngNoise = np.random.RandomState(noiseSeed)
1150 x0, y0 = bbox.getBegin()
1151 xSize, ySize = bbox.getDimensions()
1152 if xLoc is None:
1153 xLoc = rng.rand(nSrc)*(xSize - 2*bufferSize) + bufferSize + x0
1154 else:
1155 if len(xLoc) != nSrc:
1156 raise ValueError("xLoc must have length equal to nSrc. %f supplied vs %f", len(xLoc), nSrc)
1157 if yLoc is None:
1158 yLoc = rng.rand(nSrc)*(ySize - 2*bufferSize) + bufferSize + y0
1159 else:
1160 if len(yLoc) != nSrc:
1161 raise ValueError("yLoc must have length equal to nSrc. %f supplied vs %f", len(yLoc), nSrc)
1162
1163 if flux is None:
1164 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*fluxLevel
1165 else:
1166 if len(flux) != nSrc:
1167 raise ValueError("flux must have length equal to nSrc. %f supplied vs %f", len(flux), nSrc)
1168 sigmas = [psfSize for src in range(nSrc)]
1169 injectList = list(zip(xLoc, yLoc, flux, sigmas))
1170 skyLevel = 0
1171 # Don't use the built in poisson noise: it modifies the global state of numpy random
1172 modelExposure = plantSources(bbox, kernelSize, skyLevel, injectList, addPoissonNoise=False)
1173 modelExposure.setWcs(makeFakeWcs())
1174 noise = rngNoise.randn(ySize, xSize)*noiseLevel
1175 noise -= np.mean(noise)
1176 modelExposure.variance.array = np.sqrt(np.abs(modelExposure.image.array)) + noiseLevel**2
1177 modelExposure.image.array += noise
1178
1179 # Run source detection to set up the mask plane
1180 detectTestSources(modelExposure, addMaskPlanes=addMaskPlanes)
1181 if clearEdgeMask:
1182 modelExposure.mask &= ~modelExposure.mask.getPlaneBitMask("EDGE")
1183 modelExposure.setPhotoCalib(afwImage.PhotoCalib(calibration, 0., bbox))
1184 if background is not None:
1185 modelExposure.image += background
1186 modelExposure.maskedImage /= calibration
1187 modelExposure.info.setId(seed)
1188 if doApplyCalibration:
1189 modelExposure.maskedImage = modelExposure.photoCalib.calibrateImage(modelExposure.maskedImage)
1190
1191 truth = _fillTruthCatalog(injectList)
1192
1193 return modelExposure, truth
1194
1195
1197 """Make a schema for the truth catalog produced by `makeTestImage`.
1198
1199 Returns
1200 -------
1201 keys : `dict` [`str`]
1202 Fields added to the catalog, to make it easier to set them.
1203 schema : `lsst.afw.table.Schema`
1204 Schema to use to make a "truth" SourceCatalog.
1205 Calib, Ap, and Psf flux slots all are set to ``truth_instFlux``.
1206 """
1207 schema = afwTable.SourceTable.makeMinimalSchema()
1208 keys = {}
1209 # Don't use a FluxResultKey so we can manage the flux and err separately.
1210 keys["instFlux"] = schema.addField("truth_instFlux", type=np.float64,
1211 doc="true instFlux", units="count")
1212 keys["instFluxErr"] = schema.addField("truth_instFluxErr", type=np.float64,
1213 doc="true instFluxErr", units="count")
1214 keys["centroid"] = afwTable.Point2DKey.addFields(schema, "truth", "true simulated centroid", "pixel")
1215 schema.addField("truth_flag", "Flag", "truth flux failure flag.")
1216 # Add the flag fields a source selector would need.
1217 schema.addField("sky_source", "Flag", "testing flag.")
1218 schema.addField("base_PixelFlags_flag_interpolated", "Flag", "testing flag.")
1219 schema.addField("base_PixelFlags_flag_saturated", "Flag", "testing flag.")
1220 schema.addField("base_PixelFlags_flag_bad", "Flag", "testing flag.")
1221 schema.getAliasMap().set("slot_Centroid", "truth")
1222 schema.getAliasMap().set("slot_CalibFlux", "truth")
1223 schema.getAliasMap().set("slot_ApFlux", "truth")
1224 schema.getAliasMap().set("slot_PsfFlux", "truth")
1225 return keys, schema
1226
1227
1228def _fillTruthCatalog(injectList):
1229 """Add injected sources to the truth catalog.
1230
1231 Parameters
1232 ----------
1233 injectList : `list` [`float`]
1234 Sources that were injected; tuples of (x, y, flux, size).
1235
1236 Returns
1237 -------
1238 catalog : `lsst.afw.table.SourceCatalog`
1239 Catalog with centroids and instFlux/instFluxErr values filled in and
1240 appropriate slots set.
1241 """
1242 keys, schema = _makeTruthSchema()
1243 catalog = afwTable.SourceCatalog(schema)
1244 catalog.reserve(len(injectList))
1245 for x, y, flux, size in injectList:
1246 record = catalog.addNew()
1247 keys["centroid"].set(record, geom.PointD(x, y))
1248 keys["instFlux"].set(record, flux)
1249 # Approximate injected errors
1250 keys["instFluxErr"].set(record, 20)
1251 # 5-sigma effective source width
1252 circle = afwGeom.Ellipse(afwGeom.ellipses.Axes(5*size, 5*size, 0), geom.Point2D(x, y))
1253 footprint = afwDetection.Footprint(afwGeom.SpanSet.fromShape(circle))
1254 footprint.addPeak(x, y, flux)
1255 record.setFootprint(footprint)
1256
1257 return catalog
1258
1259
1260def makeStats(badMaskPlanes=None):
1261 """Create a statistics control for configuring calculations on images.
1262
1263 Parameters
1264 ----------
1265 badMaskPlanes : `list` of `str`, optional
1266 List of mask planes to exclude from calculations.
1267
1268 Returns
1269 -------
1270 statsControl : ` lsst.afw.math.StatisticsControl`
1271 Statistics control object for configuring calculations on images.
1272 """
1273 if badMaskPlanes is None:
1274 badMaskPlanes = ("INTRP", "EDGE", "DETECTED", "SAT", "CR",
1275 "BAD", "NO_DATA", "DETECTED_NEGATIVE")
1276 statsControl = afwMath.StatisticsControl()
1277 statsControl.setNumSigmaClip(3.)
1278 statsControl.setNumIter(3)
1279 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(badMaskPlanes))
1280 return statsControl
1281
1282
1283def computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP):
1284 """Calculate a robust mean of the variance plane of an exposure.
1285
1286 Parameters
1287 ----------
1288 image : `lsst.afw.image.Image`
1289 Image or variance plane of an exposure to evaluate.
1290 mask : `lsst.afw.image.Mask`
1291 Mask plane to use for excluding pixels.
1292 statsCtrl : `lsst.afw.math.StatisticsControl`
1293 Statistics control object for configuring the calculation.
1294 statistic : `lsst.afw.math.Property`, optional
1295 The type of statistic to compute. Typical values are
1296 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``.
1297
1298 Returns
1299 -------
1300 value : `float`
1301 The result of the statistic calculated from the unflagged pixels.
1302 """
1303 statObj = afwMath.makeStatistics(image, mask, statistic, statsCtrl)
1304 return statObj.getValue(statistic)
1305
1306
1308 """Compute the noise equivalent area for an image psf
1309
1310 Parameters
1311 ----------
1312 psf : `lsst.afw.detection.Psf`
1313
1314 Returns
1315 -------
1316 nea : `float`
1317 """
1318 psfImg = psf.computeImage(psf.getAveragePosition())
1319 nea = 1./np.sum(psfImg.array**2)
1320 return nea
1321
1322
1323def angleMean(angles):
1324 """Calculate the mean of an array of angles.
1325
1326 Parameters
1327 ----------
1328 angles : `ndarray`
1329 An array of angles, in degrees
1330
1331 Returns
1332 -------
1333 `lsst.geom.Angle`
1334 The mean angle
1335 """
1336 complexArray = [complex(np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))) for angle in angles]
1337 return (geom.Angle(np.angle(np.mean(complexArray))))
1338
1339
1340def evaluateMaskFraction(mask, maskPlane):
1341 """Evaluate the fraction of masked pixels in a mask plane.
1342
1343 Parameters
1344 ----------
1345 mask : `lsst.afw.image.Mask`
1346 The mask to evaluate the fraction on
1347 maskPlane : `str`
1348 The particular mask plane to evaluate the fraction
1349
1350 Returns
1351 -------
1352 value : `float`
1353 The calculated fraction of masked pixels
1354 """
1355 nMaskSet = np.count_nonzero((mask.array & mask.getPlaneBitMask(maskPlane)))
1356 return nMaskSet/mask.array.size
int min
int max
Class to describe the properties of a detected object from an image.
Definition Footprint.h:63
A compact representation of a collection of pixels.
Definition SpanSet.h:78
An ellipse defined by an arbitrary BaseCore and a center point.
Definition Ellipse.h:51
The photometric calibration of an exposure.
Definition PhotoCalib.h:114
A kernel created from an Image.
Definition Kernel.h:471
Pass parameters to a Statistics object.
Definition Statistics.h:83
A class representing an angle.
Definition Angle.h:128
A floating-point coordinate rectangle geometry.
Definition Box.h:413
An integer coordinate rectangle.
Definition Box.h:55
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
detectDipoleSources(self, doMerge=True, diffim=None, detectSigma=5.5, grow=3, minBinSize=32)
Definition utils.py:781
_makeStarImage(self, xc=[15.3], yc=[18.6], flux=[2500], schema=None, randomSeed=None)
Definition utils.py:751
__init__(self, w=101, h=101, xcenPos=[27.], ycenPos=[25.], xcenNeg=[23.], ycenNeg=[25.], psfSigma=2., flux=[30000.], fluxNeg=None, noise=10., gradientParams=None)
Definition utils.py:718
fitDipoleSource(self, source, **kwds)
Definition utils.py:776
daf::base::PropertySet * set
Definition fits.cc:931
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Definition SkyWcs.cc:521
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT > > image, typename std::shared_ptr< Mask< MaskPixelT > > mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT > > variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Definition Exposure.h:484
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
afwImage.ImageD computeAveragePsf(afwImage.Exposure exposure, float psfExposureBuffer, int psfExposureGrid)
Definition utils.py:953
makeTestImage(seed=5, nSrc=20, psfSize=2., noiseLevel=5., noiseSeed=6, fluxLevel=500., fluxRange=2., kernelSize=32, templateBorderSize=0, background=None, xSize=256, ySize=256, x0=12345, y0=67890, calibration=1., doApplyCalibration=False, xLoc=None, yLoc=None, flux=None, clearEdgeMask=False, addMaskPlanes=None)
Definition utils.py:1083
plotKernelSpatialModel(kernel, kernelCellSet, showBadCandidates=True, numSample=128, keepPlots=True, maxCoeff=10)
Definition utils.py:283
detectTestSources(exposure, addMaskPlanes=None)
Definition utils.py:1017
makeStats(badMaskPlanes=None)
Definition utils.py:1260
showKernelMosaic(bbox, kernel, nx=7, ny=None, frame=None, title=None, showCenter=True, showEllipticity=True)
Definition utils.py:592
showSourceSet(sSet, xy0=(0, 0), frame=0, ctype=afwDisplay.GREEN, symb="+", size=2)
Definition utils.py:51
showKernelSpatialCells(maskedIm, kernelCellSet, showChi2=False, symb="o", ctype=None, ctypeUnused=None, ctypeBad=None, size=3, frame=None, title="Spatial Cells")
Definition utils.py:73
plotWhisker(results, newWcs)
Definition utils.py:674
showKernelBasis(kernel, frame=None)
Definition utils.py:264
_fillTruthCatalog(injectList)
Definition utils.py:1228
showDiaSources(sources, exposure, isFlagged, isDipole, frame=None)
Definition utils.py:110
computePSFNoiseEquivalentArea(psf)
Definition utils.py:1307
showKernelCandidates(kernelCellSet, kernel, background, frame=None, showBadCandidates=True, resids=False, kernels=False)
Definition utils.py:147
evaluateMaskFraction(mask, maskPlane)
Definition utils.py:1340
plotKernelCoefficients(spatialKernel, kernelCellSet, showBadCandidates=False, keepPlots=True)
Definition utils.py:432
getPsfFwhm(psf, average=True, position=None)
Definition utils.py:856
float evaluateMeanPsfFwhm(afwImage.Exposure exposure, float fwhmExposureBuffer, int fwhmExposureGrid)
Definition utils.py:893
computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP)
Definition utils.py:1283