lsst.ip.diffim g004a122ce6+82f5638230
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.geom as afwGeom
33import lsst.afw.image as afwImage
34import lsst.afw.math as afwMath
35import lsst.afw.table as afwTable
36import lsst.meas.algorithms as measAlg
37import lsst.meas.base as measBase
38from lsst.meas.algorithms.testUtils import plantSources
39from lsst.pex.exceptions import InvalidParameterError
40from lsst.utils.logging import getLogger
41from .dipoleFitTask import DipoleFitAlgorithm
42from . import diffimLib
43from . import diffimTools
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
439 The spatial spatialKernel solution model which is a spatially varying linear combination
440 of the spatialKernel basis functions.
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 plotPixelResiduals(exposure, warpedTemplateExposure, diffExposure, kernelCellSet,
675 kernel, background, testSources, config,
676 origVariance=False, nptsFull=1e6, keepPlots=True, titleFs=14):
677 """Plot diffim residuals for LOCAL and SPATIAL models.
678 """
679 candidateResids = []
680 spatialResids = []
681 nonfitResids = []
682
683 for cell in kernelCellSet.getCellList():
684 for cand in cell.begin(True): # only look at good ones
685 # Be sure
686 if not (cand.getStatus() == afwMath.SpatialCellCandidate.GOOD):
687 continue
688
689 diffim = cand.getDifferenceImage(diffimLib.KernelCandidateF.ORIG)
690 orig = cand.getScienceMaskedImage()
691
692 ski = afwImage.ImageD(kernel.getDimensions())
693 kernel.computeImage(ski, False, int(cand.getXCenter()), int(cand.getYCenter()))
694 sk = afwMath.FixedKernel(ski)
695 sbg = background(int(cand.getXCenter()), int(cand.getYCenter()))
696 sdiffim = cand.getDifferenceImage(sk, sbg)
697
698 # trim edgs due to convolution
699 bbox = kernel.shrinkBBox(diffim.getBBox())
700 tdiffim = diffim.Factory(diffim, bbox)
701 torig = orig.Factory(orig, bbox)
702 tsdiffim = sdiffim.Factory(sdiffim, bbox)
703
704 if origVariance:
705 candidateResids.append(np.ravel(tdiffim.image.array
706 / np.sqrt(torig.variance.array)))
707 spatialResids.append(np.ravel(tsdiffim.image.array
708 / np.sqrt(torig.variance.array)))
709 else:
710 candidateResids.append(np.ravel(tdiffim.image.array
711 / np.sqrt(tdiffim.variance.array)))
712 spatialResids.append(np.ravel(tsdiffim.image.array
713 / np.sqrt(tsdiffim.variance.array)))
714
715 fullIm = diffExposure.image.array
716 fullMask = diffExposure.mask.array
717 if origVariance:
718 fullVar = exposure.variance.array
719 else:
720 fullVar = diffExposure.variance.array
721
722 bitmaskBad = 0
723 bitmaskBad |= afwImage.Mask.getPlaneBitMask('NO_DATA')
724 bitmaskBad |= afwImage.Mask.getPlaneBitMask('SAT')
725 idx = np.where((fullMask & bitmaskBad) == 0)
726 stride = int(len(idx[0])//nptsFull)
727 sidx = idx[0][::stride], idx[1][::stride]
728 allResids = fullIm[sidx]/np.sqrt(fullVar[sidx])
729
730 testFootprints = diffimTools.sourceToFootprintList(testSources, warpedTemplateExposure,
731 exposure, config,
732 _LOG.getChild("plotPixelResiduals"))
733 for fp in testFootprints:
734 subexp = diffExposure.Factory(diffExposure, fp["footprint"].getBBox())
735 subim = subexp.image
736 if origVariance:
737 subvar = afwImage.ExposureF(exposure, fp["footprint"].getBBox()).variance
738 else:
739 subvar = subexp.variance
740 nonfitResids.append(np.ravel(subim.array/np.sqrt(subvar.array)))
741
742 candidateResids = np.ravel(np.array(candidateResids))
743 spatialResids = np.ravel(np.array(spatialResids))
744 nonfitResids = np.ravel(np.array(nonfitResids))
745
746 try:
747 import pylab
748 from matplotlib.font_manager import FontProperties
749 except ImportError as e:
750 print("Unable to import pylab: %s" % e)
751 return
752
753 fig = pylab.figure()
754 fig.clf()
755 try:
756 fig.canvas._tkcanvas._root().lift() # == Tk's raise, but raise is a python reserved word
757 except Exception: # protect against API changes
758 pass
759 if origVariance:
760 fig.suptitle("Diffim residuals: Normalized by sqrt(input variance)", fontsize=titleFs)
761 else:
762 fig.suptitle("Diffim residuals: Normalized by sqrt(diffim variance)", fontsize=titleFs)
763
764 sp1 = pylab.subplot(221)
765 sp2 = pylab.subplot(222, sharex=sp1, sharey=sp1)
766 sp3 = pylab.subplot(223, sharex=sp1, sharey=sp1)
767 sp4 = pylab.subplot(224, sharex=sp1, sharey=sp1)
768 xs = np.arange(-5, 5.05, 0.1)
769 ys = 1./np.sqrt(2*np.pi)*np.exp(-0.5*xs**2)
770
771 sp1.hist(candidateResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)"
772 % (np.mean(candidateResids), np.var(candidateResids)))
773 sp1.plot(xs, ys, "r-", lw=2, label="N(0,1)")
774 sp1.set_title("Candidates: basis fit", fontsize=titleFs - 2)
775 sp1.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6))
776
777 sp2.hist(spatialResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)"
778 % (np.mean(spatialResids), np.var(spatialResids)))
779 sp2.plot(xs, ys, "r-", lw=2, label="N(0,1)")
780 sp2.set_title("Candidates: spatial fit", fontsize=titleFs - 2)
781 sp2.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6))
782
783 sp3.hist(nonfitResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)"
784 % (np.mean(nonfitResids), np.var(nonfitResids)))
785 sp3.plot(xs, ys, "r-", lw=2, label="N(0,1)")
786 sp3.set_title("Control sample: spatial fit", fontsize=titleFs - 2)
787 sp3.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6))
788
789 sp4.hist(allResids, bins=xs, normed=True, alpha=0.5, label="N(%.2f, %.2f)"
790 % (np.mean(allResids), np.var(allResids)))
791 sp4.plot(xs, ys, "r-", lw=2, label="N(0,1)")
792 sp4.set_title("Full image (subsampled)", fontsize=titleFs - 2)
793 sp4.legend(loc=1, fancybox=True, shadow=True, prop=FontProperties(size=titleFs - 6))
794
795 pylab.setp(sp1.get_xticklabels() + sp1.get_yticklabels(), fontsize=titleFs - 4)
796 pylab.setp(sp2.get_xticklabels() + sp2.get_yticklabels(), fontsize=titleFs - 4)
797 pylab.setp(sp3.get_xticklabels() + sp3.get_yticklabels(), fontsize=titleFs - 4)
798 pylab.setp(sp4.get_xticklabels() + sp4.get_yticklabels(), fontsize=titleFs - 4)
799
800 sp1.set_xlim(-5, 5)
801 sp1.set_ylim(0, 0.5)
802 fig.show()
803
804 global keptPlots
805 if keepPlots and not keptPlots:
806 # Keep plots open when done
807 def show():
808 print("%s: Please close plots when done." % __name__)
809 try:
810 pylab.show()
811 except Exception:
812 pass
813 print("Plots closed, exiting...")
814 import atexit
815 atexit.register(show)
816 keptPlots = True
817
818
819def calcCentroid(arr):
820 """Calculate first moment of a (kernel) image.
821 """
822 y, x = arr.shape
823 sarr = arr*arr
824 xarr = np.asarray([[el for el in range(x)] for el2 in range(y)])
825 yarr = np.asarray([[el2 for el in range(x)] for el2 in range(y)])
826 narr = xarr*sarr
827 sarrSum = sarr.sum()
828 centx = narr.sum()/sarrSum
829 narr = yarr*sarr
830 centy = narr.sum()/sarrSum
831 return centx, centy
832
833
834def calcWidth(arr, centx, centy):
835 """Calculate second moment of a (kernel) image.
836 """
837 y, x = arr.shape
838 # Square the flux so we don't have to deal with negatives
839 sarr = arr*arr
840 xarr = np.asarray([[el for el in range(x)] for el2 in range(y)])
841 yarr = np.asarray([[el2 for el in range(x)] for el2 in range(y)])
842 narr = sarr*np.power((xarr - centx), 2.)
843 sarrSum = sarr.sum()
844 xstd = np.sqrt(narr.sum()/sarrSum)
845 narr = sarr*np.power((yarr - centy), 2.)
846 ystd = np.sqrt(narr.sum()/sarrSum)
847 return xstd, ystd
848
849
850def printSkyDiffs(sources, wcs):
851 """Print differences in sky coordinates.
852
853 The difference is that between the source Position and its Centroid mapped
854 through Wcs.
855 """
856 for s in sources:
857 sCentroid = s.getCentroid()
858 sPosition = s.getCoord().getPosition(geom.degrees)
859 dra = 3600*(sPosition.getX() - wcs.pixelToSky(sCentroid).getPosition(geom.degrees).getX())/0.2
860 ddec = 3600*(sPosition.getY() - wcs.pixelToSky(sCentroid).getPosition(geom.degrees).getY())/0.2
861 if np.isfinite(dra) and np.isfinite(ddec):
862 print(dra, ddec)
863
864
865def makeRegions(sources, outfilename, wcs=None):
866 """Create regions file for display from input source list.
867 """
868 fh = open(outfilename, "w")
869 fh.write("global color=red font=\"helvetica 10 normal\" "
870 "select=1 highlite=1 edit=1 move=1 delete=1 include=1 fixed=0 source\nfk5\n")
871 for s in sources:
872 if wcs:
873 (ra, dec) = wcs.pixelToSky(s.getCentroid()).getPosition(geom.degrees)
874 else:
875 (ra, dec) = s.getCoord().getPosition(geom.degrees)
876 if np.isfinite(ra) and np.isfinite(dec):
877 fh.write("circle(%f,%f,2\")\n"%(ra, dec))
878 fh.flush()
879 fh.close()
880
881
882def showSourceSetSky(sSet, wcs, xy0, frame=0, ctype=afwDisplay.GREEN, symb="+", size=2):
883 """Draw the (RA, Dec) positions of a set of Sources. Image has the XY0.
884 """
885 disp = afwDisplay.Display(frame=frame)
886 with disp.Buffering():
887 for s in sSet:
888 (xc, yc) = wcs.skyToPixel(s.getCoord().getRa(), s.getCoord().getDec())
889 xc -= xy0[0]
890 yc -= xy0[1]
891 disp.dot(symb, xc, yc, ctype=ctype, size=size)
892
893
894def plotWhisker(results, newWcs):
895 """Plot whisker diagram of astromeric offsets between results.matches.
896 """
897 refCoordKey = results.matches[0].first.getTable().getCoordKey()
898 inCentroidKey = results.matches[0].second.getTable().getCentroidSlot().getMeasKey()
899 positions = [m.first.get(refCoordKey) for m in results.matches]
900 residuals = [m.first.get(refCoordKey).getOffsetFrom(
901 newWcs.pixelToSky(m.second.get(inCentroidKey))) for
902 m in results.matches]
903 import matplotlib.pyplot as plt
904 fig = plt.figure()
905 sp = fig.add_subplot(1, 1, 0)
906 xpos = [x[0].asDegrees() for x in positions]
907 ypos = [x[1].asDegrees() for x in positions]
908 xpos.append(0.02*(max(xpos) - min(xpos)) + min(xpos))
909 ypos.append(0.98*(max(ypos) - min(ypos)) + min(ypos))
910 xidxs = np.isfinite(xpos)
911 yidxs = np.isfinite(ypos)
912 X = np.asarray(xpos)[xidxs]
913 Y = np.asarray(ypos)[yidxs]
914 distance = [x[1].asArcseconds() for x in residuals]
915 distance.append(0.2)
916 distance = np.asarray(distance)[xidxs]
917 # NOTE: This assumes that the bearing is measured positive from +RA through North.
918 # From the documentation this is not clear.
919 bearing = [x[0].asRadians() for x in residuals]
920 bearing.append(0)
921 bearing = np.asarray(bearing)[xidxs]
922 U = (distance*np.cos(bearing))
923 V = (distance*np.sin(bearing))
924 sp.quiver(X, Y, U, V)
925 sp.set_title("WCS Residual")
926 plt.show()
927
928
929class DipoleTestImage(object):
930 """Utility class for dipole measurement testing.
931
932 Generate an image with simulated dipoles and noise; store the original
933 "pre-subtraction" images and catalogs as well.
934 Used to generate test data for DMTN-007 (http://dmtn-007.lsst.io).
935 """
936
937 def __init__(self, w=101, h=101, xcenPos=[27.], ycenPos=[25.], xcenNeg=[23.], ycenNeg=[25.],
938 psfSigma=2., flux=[30000.], fluxNeg=None, noise=10., gradientParams=None):
939 self.w = w
940 self.h = h
941 self.xcenPos = xcenPos
942 self.ycenPos = ycenPos
943 self.xcenNeg = xcenNeg
944 self.ycenNeg = ycenNeg
945 self.psfSigma = psfSigma
946 self.flux = flux
947 self.fluxNeg = fluxNeg
948 if fluxNeg is None:
949 self.fluxNeg = self.flux
950 self.noise = noise
951 self.gradientParams = gradientParams
952 self._makeDipoleImage()
953
954 def _makeDipoleImage(self):
955 """Generate an exposure and catalog with the given dipole source(s).
956 """
957 # Must seed the pos/neg images with different values to ensure they get different noise realizations
958 posImage, posCatalog = self._makeStarImage(
959 xc=self.xcenPos, yc=self.ycenPos, flux=self.flux, randomSeed=111)
960
961 negImage, negCatalog = self._makeStarImage(
962 xc=self.xcenNeg, yc=self.ycenNeg, flux=self.fluxNeg, randomSeed=222)
963
964 dipole = posImage.clone()
965 di = dipole.getMaskedImage()
966 di -= negImage.getMaskedImage()
967
968 self.diffim, self.posImage, self.posCatalog, self.negImage, self.negCatalog \
969 = dipole, posImage, posCatalog, negImage, negCatalog
970
971 def _makeStarImage(self, xc=[15.3], yc=[18.6], flux=[2500], schema=None, randomSeed=None):
972 """Generate an exposure and catalog with the given stellar source(s).
973 """
974 from lsst.meas.base.tests import TestDataset
975 bbox = geom.Box2I(geom.Point2I(0, 0), geom.Point2I(self.w - 1, self.h - 1))
976 dataset = TestDataset(bbox, psfSigma=self.psfSigma, threshold=1.)
977
978 for i in range(len(xc)):
979 dataset.addSource(instFlux=flux[i], centroid=geom.Point2D(xc[i], yc[i]))
980
981 if schema is None:
982 schema = TestDataset.makeMinimalSchema()
983 exposure, catalog = dataset.realize(noise=self.noise, schema=schema, randomSeed=randomSeed)
984
985 if self.gradientParams is not None:
986 y, x = np.mgrid[:self.w, :self.h]
987 gp = self.gradientParams
988 gradient = gp[0] + gp[1]*x + gp[2]*y
989 if len(self.gradientParams) > 3: # it includes a set of 2nd-order polynomial params
990 gradient += gp[3]*x*y + gp[4]*x*x + gp[5]*y*y
991 imgArr = exposure.image.array
992 imgArr += gradient
993
994 return exposure, catalog
995
996 def fitDipoleSource(self, source, **kwds):
997 alg = DipoleFitAlgorithm(self.diffim, self.posImage, self.negImage)
998 fitResult = alg.fitDipole(source, **kwds)
999 return fitResult
1000
1001 def detectDipoleSources(self, doMerge=True, diffim=None, detectSigma=5.5, grow=3, minBinSize=32):
1002 """Utility function for detecting dipoles.
1003
1004 Detect pos/neg sources in the diffim, then merge them. A
1005 bigger "grow" parameter leads to a larger footprint which
1006 helps with dipole measurement for faint dipoles.
1007
1008 Parameters
1009 ----------
1010 doMerge : `bool`
1011 Whether to merge the positive and negagive detections into a single
1012 source table.
1013 diffim : `lsst.afw.image.exposure.exposure.ExposureF`
1014 Difference image on which to perform detection.
1015 detectSigma : `float`
1016 Threshold for object detection.
1017 grow : `int`
1018 Number of pixels to grow the footprints before merging.
1019 minBinSize : `int`
1020 Minimum bin size for the background (re)estimation (only applies if
1021 the default leads to min(nBinX, nBinY) < fit order so the default
1022 config parameter needs to be decreased, but not to a value smaller
1023 than ``minBinSize``, in which case the fitting algorithm will take
1024 over and decrease the fit order appropriately.)
1025
1026 Returns
1027 -------
1029 If doMerge=True, the merged source catalog is returned OR
1030 detectTask : `lsst.meas.algorithms.SourceDetectionTask`
1031 schema : `lsst.afw.table.Schema`
1032 If doMerge=False, the source detection task and its schema are
1033 returned.
1034 """
1035 if diffim is None:
1036 diffim = self.diffim
1037
1038 # Start with a minimal schema - only the fields all SourceCatalogs need
1039 schema = afwTable.SourceTable.makeMinimalSchema()
1040
1041 # Customize the detection task a bit (optional)
1042 detectConfig = measAlg.SourceDetectionConfig()
1043 detectConfig.returnOriginalFootprints = False # should be the default
1044
1045 # code from imageDifference.py:
1046 detectConfig.thresholdPolarity = "both"
1047 detectConfig.thresholdValue = detectSigma
1048 # detectConfig.nSigmaToGrow = psfSigma
1049 detectConfig.reEstimateBackground = True # if False, will fail often for faint sources on gradients?
1050 detectConfig.thresholdType = "pixel_stdev"
1051 # Test images are often quite small, so may need to adjust background binSize
1052 while ((min(diffim.getWidth(), diffim.getHeight()))//detectConfig.background.binSize
1053 < detectConfig.background.approxOrderX and detectConfig.background.binSize > minBinSize):
1054 detectConfig.background.binSize = max(minBinSize, detectConfig.background.binSize//2)
1055
1056 # Create the detection task. We pass the schema so the task can declare a few flag fields
1057 detectTask = measAlg.SourceDetectionTask(schema, config=detectConfig)
1058
1059 table = afwTable.SourceTable.make(schema)
1060 catalog = detectTask.run(table, diffim)
1061
1062 # Now do the merge.
1063 if doMerge:
1064 fpSet = catalog.positive
1065 fpSet.merge(catalog.negative, grow, grow, False)
1066 sources = afwTable.SourceCatalog(table)
1067 fpSet.makeSources(sources)
1068
1069 return sources
1070
1071 else:
1072 return detectTask, schema
1073
1074
1075def _sliceWidth(image, threshold, peaks, axis):
1076 vec = image.take(peaks[1 - axis], axis=axis)
1077 low = np.interp(threshold, vec[:peaks[axis] + 1], np.arange(peaks[axis] + 1))
1078 high = np.interp(threshold, vec[:peaks[axis] - 1:-1], np.arange(len(vec) - 1, peaks[axis] - 1, -1))
1079 return high - low
1080
1081
1082def getPsfFwhm(psf, average=True, position=None):
1083 """Directly calculate the horizontal and vertical widths
1084 of a PSF at half its maximum value.
1085
1086 Parameters
1087 ----------
1088 psf : `~lsst.afw.detection.Psf`
1089 Point spread function (PSF) to evaluate.
1090 average : `bool`, optional
1091 Set to return the average width over Y and X axes.
1092 position : `~lsst.geom.Point2D`, optional
1093 The position at which to evaluate the PSF. If `None`, then the
1094 average position is used.
1095
1096 Returns
1097 -------
1098 psfSize : `float` | `tuple` [`float`]
1099 The FWHM of the PSF computed at its average position.
1100 Returns the widths along the Y and X axes,
1101 or the average of the two if `average` is set.
1102
1103 See Also
1104 --------
1105 evaluateMeanPsfFwhm
1106 """
1107 if position is None:
1108 position = psf.getAveragePosition()
1109 image = psf.computeKernelImage(position).array
1110 peak = psf.computePeak(position)
1111 peakLocs = np.unravel_index(np.argmax(image), image.shape)
1112 width = _sliceWidth(image, peak/2., peakLocs, axis=0), _sliceWidth(image, peak/2., peakLocs, axis=1)
1113 return np.nanmean(width) if average else width
1114
1115
1116def evaluateMeanPsfFwhm(exposure: afwImage.Exposure,
1117 fwhmExposureBuffer: float, fwhmExposureGrid: int) -> float:
1118 """Get the median PSF FWHM by evaluating it on a grid within an exposure.
1119
1120 Parameters
1121 ----------
1122 exposure : `~lsst.afw.image.Exposure`
1123 The exposure for which the mean FWHM of the PSF is to be computed.
1124 The exposure must contain a `psf` attribute.
1125 fwhmExposureBuffer : `float`
1126 Fractional buffer margin to be left out of all sides of the image
1127 during the construction of the grid to compute mean PSF FWHM in an
1128 exposure.
1129 fwhmExposureGrid : `int`
1130 Grid size to compute the mean FWHM in an exposure.
1131
1132 Returns
1133 -------
1134 meanFwhm : `float`
1135 The mean PSF FWHM on the exposure.
1136
1137 Raises
1138 ------
1139 ValueError
1140 Raised if the PSF cannot be computed at any of the grid points.
1141
1142 See Also
1143 --------
1144 getPsfFwhm
1145 """
1146
1147 psf = exposure.psf
1148
1149 bbox = exposure.getBBox()
1150 xmax, ymax = bbox.getMax()
1151 xmin, ymin = bbox.getMin()
1152
1153 xbuffer = fwhmExposureBuffer*(xmax-xmin)
1154 ybuffer = fwhmExposureBuffer*(ymax-ymin)
1155
1156 width = []
1157 for (x, y) in itertools.product(np.linspace(xmin+xbuffer, xmax-xbuffer, fwhmExposureGrid),
1158 np.linspace(ymin+ybuffer, ymax-ybuffer, fwhmExposureGrid)
1159 ):
1160 pos = geom.Point2D(x, y)
1161 try:
1162 fwhm = getPsfFwhm(psf, average=True, position=pos)
1163 except InvalidParameterError:
1164 _LOG.debug("Unable to compute PSF FWHM at position (%f, %f).", x, y)
1165 continue
1166
1167 width.append(fwhm)
1168
1169 if not width:
1170 raise ValueError("Unable to compute PSF FWHM at any position on the exposure.")
1171
1172 return np.nanmean(width)
1173
1174
1175def detectTestSources(exposure):
1176 """Minimal source detection wrapper suitable for unit tests.
1177
1178 Parameters
1179 ----------
1180 exposure : `lsst.afw.image.Exposure`
1181 Exposure on which to run detection/measurement
1182 The exposure is modified in place to set the 'DETECTED' mask plane.
1183
1184 Returns
1185 -------
1186 selectSources :
1187 Source catalog containing candidates
1188 """
1189
1190 schema = afwTable.SourceTable.makeMinimalSchema()
1191 selectDetection = measAlg.SourceDetectionTask(schema=schema)
1192 selectMeasurement = measBase.SingleFrameMeasurementTask(schema=schema)
1193 table = afwTable.SourceTable.make(schema)
1194
1195 detRet = selectDetection.run(
1196 table=table,
1197 exposure=exposure,
1198 sigma=None, # The appropriate sigma is calculated from the PSF
1199 doSmooth=True
1200 )
1201 selectSources = detRet.sources
1202 selectMeasurement.run(measCat=selectSources, exposure=exposure)
1203
1204 return selectSources
1205
1206
1208 """Make a fake, affine Wcs.
1209 """
1210 crpix = geom.Point2D(123.45, 678.9)
1211 crval = geom.SpherePoint(0.1, 0.1, geom.degrees)
1212 cdMatrix = np.array([[5.19513851e-05, -2.81124812e-07],
1213 [-3.25186974e-07, -5.19112119e-05]])
1214 return afwGeom.makeSkyWcs(crpix, crval, cdMatrix)
1215
1216
1217def makeTestImage(seed=5, nSrc=20, psfSize=2., noiseLevel=5.,
1218 noiseSeed=6, fluxLevel=500., fluxRange=2.,
1219 kernelSize=32, templateBorderSize=0,
1220 background=None,
1221 xSize=256,
1222 ySize=256,
1223 x0=12345,
1224 y0=67890,
1225 calibration=1.,
1226 doApplyCalibration=False,
1227 xLoc=None,
1228 yLoc=None,
1229 flux=None,
1230 clearEdgeMask=False,
1231 ):
1232 """Make a reproduceable PSF-convolved exposure for testing.
1233
1234 Parameters
1235 ----------
1236 seed : `int`, optional
1237 Seed value to initialize the random number generator for sources.
1238 nSrc : `int`, optional
1239 Number of sources to simulate.
1240 psfSize : `float`, optional
1241 Width of the PSF of the simulated sources, in pixels.
1242 noiseLevel : `float`, optional
1243 Standard deviation of the noise to add to each pixel.
1244 noiseSeed : `int`, optional
1245 Seed value to initialize the random number generator for noise.
1246 fluxLevel : `float`, optional
1247 Reference flux of the simulated sources.
1248 fluxRange : `float`, optional
1249 Range in flux amplitude of the simulated sources.
1250 kernelSize : `int`, optional
1251 Size in pixels of the kernel for simulating sources.
1252 templateBorderSize : `int`, optional
1253 Size in pixels of the image border used to pad the image.
1254 background : `lsst.afw.math.Chebyshev1Function2D`, optional
1255 Optional background to add to the output image.
1256 xSize, ySize : `int`, optional
1257 Size in pixels of the simulated image.
1258 x0, y0 : `int`, optional
1259 Origin of the image.
1260 calibration : `float`, optional
1261 Conversion factor between instFlux and nJy.
1262 doApplyCalibration : `bool`, optional
1263 Apply the photometric calibration and return the image in nJy?
1264 xLoc, yLoc : `list` of `float`, optional
1265 User-specified coordinates of the simulated sources.
1266 If specified, must have length equal to ``nSrc``
1267 flux : `list` of `float`, optional
1268 User-specified fluxes of the simulated sources.
1269 If specified, must have length equal to ``nSrc``
1270 clearEdgeMask : `bool`, optional
1271 Clear the "EDGE" mask plane after source detection.
1272
1273 Returns
1274 -------
1275 modelExposure : `lsst.afw.image.Exposure`
1276 The model image, with the mask and variance planes.
1277 sourceCat : `lsst.afw.table.SourceCatalog`
1278 Catalog of sources detected on the model image.
1279
1280 Raises
1281 ------
1282 ValueError
1283 If `xloc`, `yloc`, or `flux` are supplied with inconsistant lengths.
1284 """
1285 # Distance from the inner edge of the bounding box to avoid placing test
1286 # sources in the model images.
1287 bufferSize = kernelSize/2 + templateBorderSize + 1
1288
1289 bbox = geom.Box2I(geom.Point2I(x0, y0), geom.Extent2I(xSize, ySize))
1290 if templateBorderSize > 0:
1291 bbox.grow(templateBorderSize)
1292
1293 rng = np.random.RandomState(seed)
1294 rngNoise = np.random.RandomState(noiseSeed)
1295 x0, y0 = bbox.getBegin()
1296 xSize, ySize = bbox.getDimensions()
1297 if xLoc is None:
1298 xLoc = rng.rand(nSrc)*(xSize - 2*bufferSize) + bufferSize + x0
1299 else:
1300 if len(xLoc) != nSrc:
1301 raise ValueError("xLoc must have length equal to nSrc. %f supplied vs %f", len(xLoc), nSrc)
1302 if yLoc is None:
1303 yLoc = rng.rand(nSrc)*(ySize - 2*bufferSize) + bufferSize + y0
1304 else:
1305 if len(yLoc) != nSrc:
1306 raise ValueError("yLoc must have length equal to nSrc. %f supplied vs %f", len(yLoc), nSrc)
1307
1308 if flux is None:
1309 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*fluxLevel
1310 else:
1311 if len(flux) != nSrc:
1312 raise ValueError("flux must have length equal to nSrc. %f supplied vs %f", len(flux), nSrc)
1313 sigmas = [psfSize for src in range(nSrc)]
1314 coordList = list(zip(xLoc, yLoc, flux, sigmas))
1315 skyLevel = 0
1316 # Don't use the built in poisson noise: it modifies the global state of numpy random
1317 modelExposure = plantSources(bbox, kernelSize, skyLevel, coordList, addPoissonNoise=False)
1318 modelExposure.setWcs(makeFakeWcs())
1319 noise = rngNoise.randn(ySize, xSize)*noiseLevel
1320 noise -= np.mean(noise)
1321 modelExposure.variance.array = np.sqrt(np.abs(modelExposure.image.array)) + noiseLevel**2
1322 modelExposure.image.array += noise
1323
1324 # Run source detection to set up the mask plane
1325 sourceCat = detectTestSources(modelExposure)
1326 if clearEdgeMask:
1327 modelExposure.mask &= ~modelExposure.mask.getPlaneBitMask("EDGE")
1328 modelExposure.setPhotoCalib(afwImage.PhotoCalib(calibration, 0., bbox))
1329 if background is not None:
1330 modelExposure.image += background
1331 modelExposure.maskedImage /= calibration
1332 modelExposure.info.setId(seed)
1333 if doApplyCalibration:
1334 modelExposure.maskedImage = modelExposure.photoCalib.calibrateImage(modelExposure.maskedImage)
1335
1336 return modelExposure, sourceCat
1337
1338
1339def makeStats(badMaskPlanes=None):
1340 """Create a statistics control for configuring calculations on images.
1341
1342 Parameters
1343 ----------
1344 badMaskPlanes : `list` of `str`, optional
1345 List of mask planes to exclude from calculations.
1346
1347 Returns
1348 -------
1349 statsControl : ` lsst.afw.math.StatisticsControl`
1350 Statistics control object for configuring calculations on images.
1351 """
1352 if badMaskPlanes is None:
1353 badMaskPlanes = ("INTRP", "EDGE", "DETECTED", "SAT", "CR",
1354 "BAD", "NO_DATA", "DETECTED_NEGATIVE")
1355 statsControl = afwMath.StatisticsControl()
1356 statsControl.setNumSigmaClip(3.)
1357 statsControl.setNumIter(3)
1358 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(badMaskPlanes))
1359 return statsControl
1360
1361
1362def computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP):
1363 """Calculate a robust mean of the variance plane of an exposure.
1364
1365 Parameters
1366 ----------
1367 image : `lsst.afw.image.Image`
1368 Image or variance plane of an exposure to evaluate.
1369 mask : `lsst.afw.image.Mask`
1370 Mask plane to use for excluding pixels.
1372 Statistics control object for configuring the calculation.
1373 statistic : `lsst.afw.math.Property`, optional
1374 The type of statistic to compute. Typical values are
1375 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``.
1376
1377 Returns
1378 -------
1379 value : `float`
1380 The result of the statistic calculated from the unflagged pixels.
1381 """
1382 statObj = afwMath.makeStatistics(image, mask, statistic, statsCtrl)
1383 return statObj.getValue(statistic)
1384
1385
1387 """Compute the noise equivalent area for an image psf
1388
1389 Parameters
1390 ----------
1392
1393 Returns
1394 -------
1395 nea : `float`
1396 """
1397 psfImg = psf.computeImage(psf.getAveragePosition())
1398 nea = 1./np.sum(psfImg.array**2)
1399 return nea
std::pair< std::shared_ptr< lsst::afw::math::LinearCombinationKernel >, lsst::afw::math::Kernel::SpatialFunctionPtr > getSolutionPair()
def __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:938
def fitDipoleSource(self, source, **kwds)
Definition: utils.py:996
def detectDipoleSources(self, doMerge=True, diffim=None, detectSigma=5.5, grow=3, minBinSize=32)
Definition: utils.py:1001
def _makeStarImage(self, xc=[15.3], yc=[18.6], flux=[2500], schema=None, randomSeed=None)
Definition: utils.py:971
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
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 >())
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 >())
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
def showSourceSet(sSet, xy0=(0, 0), frame=0, ctype=afwDisplay.GREEN, symb="+", size=2)
Definition: utils.py:51
def 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)
Definition: utils.py:1231
def plotWhisker(results, newWcs)
Definition: utils.py:894
def plotPixelResiduals(exposure, warpedTemplateExposure, diffExposure, kernelCellSet, kernel, background, testSources, config, origVariance=False, nptsFull=1e6, keepPlots=True, titleFs=14)
Definition: utils.py:676
def showKernelBasis(kernel, frame=None)
Definition: utils.py:264
def showSourceSetSky(sSet, wcs, xy0, frame=0, ctype=afwDisplay.GREEN, symb="+", size=2)
Definition: utils.py:882
def showKernelCandidates(kernelCellSet, kernel, background, frame=None, showBadCandidates=True, resids=False, kernels=False)
Definition: utils.py:147
def printSkyDiffs(sources, wcs)
Definition: utils.py:850
def computePSFNoiseEquivalentArea(psf)
Definition: utils.py:1386
def makeStats(badMaskPlanes=None)
Definition: utils.py:1339
def showKernelSpatialCells(maskedIm, kernelCellSet, showChi2=False, symb="o", ctype=None, ctypeUnused=None, ctypeBad=None, size=3, frame=None, title="Spatial Cells")
Definition: utils.py:73
def detectTestSources(exposure)
Definition: utils.py:1175
def makeRegions(sources, outfilename, wcs=None)
Definition: utils.py:865
def showKernelMosaic(bbox, kernel, nx=7, ny=None, frame=None, title=None, showCenter=True, showEllipticity=True)
Definition: utils.py:592
def showDiaSources(sources, exposure, isFlagged, isDipole, frame=None)
Definition: utils.py:110
def plotKernelCoefficients(spatialKernel, kernelCellSet, showBadCandidates=False, keepPlots=True)
Definition: utils.py:432
def plotKernelSpatialModel(kernel, kernelCellSet, showBadCandidates=True, numSample=128, keepPlots=True, maxCoeff=10)
Definition: utils.py:283
def computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP)
Definition: utils.py:1362