Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+83433b07ee,g16d25e1f1b+23bc9e47ac,g1ec0fe41b4+3ea9d11450,g1fd858c14a+9be2b0f3b9,g2440f9efcc+8c5ae1fdc5,g35bb328faa+8c5ae1fdc5,g4a4af6cd76+d25431c27e,g4d2262a081+c74e83464e,g53246c7159+8c5ae1fdc5,g55585698de+1e04e59700,g56a49b3a55+92a7603e7a,g60b5630c4e+1e04e59700,g67b6fd64d1+3fc8cb0b9e,g78460c75b0+7e33a9eb6d,g786e29fd12+668abc6043,g8352419a5c+8c5ae1fdc5,g8852436030+60e38ee5ff,g89139ef638+3fc8cb0b9e,g94187f82dc+1e04e59700,g989de1cb63+3fc8cb0b9e,g9d31334357+1e04e59700,g9f33ca652e+0a83e03614,gabe3b4be73+8856018cbb,gabf8522325+977d9fabaf,gb1101e3267+8b4b9c8ed7,gb89ab40317+3fc8cb0b9e,gc0af124501+57ccba3ad1,gcf25f946ba+60e38ee5ff,gd6cbbdb0b4+1cc2750d2e,gd794735e4e+7be992507c,gdb1c4ca869+be65c9c1d7,gde0f65d7ad+c7f52e58fe,ge278dab8ac+6b863515ed,ge410e46f29+3fc8cb0b9e,gf35d7ec915+97dd712d81,gf5e32f922b+8c5ae1fdc5,gf618743f1b+747388abfa,gf67bdafdda+3fc8cb0b9e,w.2025.18
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
psfexStarSelector.py
Go to the documentation of this file.
1# This file is part of meas_extensions_psfex.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22__all__ = ("PsfexStarSelectorConfig", "PsfexStarSelectorTask")
23
24import re
25import sys
26
27import numpy as np
28try:
29 import matplotlib.pyplot as plt
30 fig = None
31except ImportError:
32 plt = None
33
34from lsst.pipe.base import Struct
35import lsst.pex.config as pexConfig
36import lsst.afw.display as afwDisplay
37from lsst.meas.algorithms import BaseSourceSelectorTask, sourceSelectorRegistry
38from . import psfexLib
39from .psfex import compute_fwhmrange
40
41
42class PsfexStarSelectorConfig(BaseSourceSelectorTask.ConfigClass):
43 fluxName = pexConfig.Field(
44 dtype=str,
45 doc="Name of photometric flux key ",
46 default="base_PsfFlux",
47 )
48 fluxErrName = pexConfig.Field(
49 dtype=str,
50 doc="Name of phot. flux err. key",
51 default="",
52 )
53 minFwhm = pexConfig.Field(
54 dtype=float,
55 doc="Maximum allowed FWHM ",
56 default=2,
57 )
58 maxFwhm = pexConfig.Field(
59 dtype=float,
60 doc="Minimum allowed FWHM ",
61 default=10,
62 )
63 maxFwhmVariability = pexConfig.Field(
64 dtype=float,
65 doc="Allowed FWHM variability (1.0 = 100%)",
66 default=0.2,
67 )
68 maxellip = pexConfig.Field(
69 dtype=float,
70 doc="Maximum (A-B)/(A+B) ",
71 default=0.3,
72 check=lambda x: x >= 0.0,
73 )
74 minsn = pexConfig.Field(
75 dtype=float,
76 doc="Minimum S/N for candidates",
77 default=100,
78 check=lambda x: x >= 0.0,
79 )
80
81 def validate(self):
82 pexConfig.Config.validate(self)
83
84 if self.fluxErrName == "":
85 self.fluxErrName = self.fluxName + ".err"
86 elif self.fluxErrName != self.fluxName + ".err":
87 msg = f"fluxErrName ({self.fluxErrName}) doesn't correspond to fluxName ({self.fluxName})"
88 raise pexConfig.FieldValidationError(PsfexStarSelectorConfig.fluxName, self, msg)
89
90 if self.minFwhm > self.maxFwhm:
91 raise pexConfig.FieldValidationError(PsfexStarSelectorConfig.minFwhm, self,
92 f"minFwhm ({self.minFwhm}) > maxFwhm ({self.maxFwhm})")
93
94 def setDefaults(self):
95 super().setDefaults()
96 self.badFlags = [
97 "base_PixelFlags_flag_edge",
98 "base_PixelFlags_flag_nodata",
99 "base_PixelFlags_flag_saturatedCenter",
100 "base_PixelFlags_flag_crCenter",
101 "base_PixelFlags_flag_bad",
102 "base_PixelFlags_flag_suspectCenter",
103 "base_PsfFlux_flag",
104 # "parent", # actually this is a test on deblend_nChild
105 ]
106
107
109 """A class to handle key strokes with matplotlib displays
110 """
111
112 def __init__(self, axes, xs, ys, x, y, frames=[0]):
113 self.axes = axes
114 self.xs = xs
115 self.ys = ys
116 self.x = x
117 self.y = y
118 self.frames = frames
119
120 self.cid = self.axes.figure.canvas.mpl_connect('key_press_event', self)
121
122 def __call__(self, ev):
123 if ev.inaxes != self.axes:
124 return
125
126 if ev.key and ev.key in ("p"):
127 dist = np.hypot(self.xs - ev.xdata, self.ys - ev.ydata)
128 dist[np.where(np.isnan(dist))] = 1e30
129
130 which = np.where(dist == min(dist))
131
132 x = self.x[which][0]
133 y = self.y[which][0]
134 for frame in self.frames:
135 disp = afwDisplay.Display(frame=frame)
136 disp.pan(x, y)
137 disp.flush()
138 else:
139 pass
140
141
142def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
143 clear=True):
144
145 global fig
146 if not fig:
147 fig = plt.figure()
148 newFig = True
149 else:
150 newFig = False
151 if clear:
152 fig.clf()
153
154 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
155
156 xmin = sorted(mag)[int(0.05*len(mag))]
157 xmax = sorted(mag)[int(0.95*len(mag))]
158
159 axes.set_xlim(-17.5, -13)
160 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
161 axes.set_ylim(0, 10)
162
163 colors = ["r", "g", "b", "c", "m", "k", ]
164 for k, mean in enumerate(centers):
165 if k == 0:
166 axes.plot(axes.get_xlim(), (mean, mean,), "k%s" % ltype)
167
168 ll = (clusterId == k)
169 axes.plot(mag[ll], width[ll], marker, markersize=markersize, markeredgewidth=markeredgewidth,
170 color=colors[k%len(colors)])
171
172 ll = (clusterId == -1)
173 axes.plot(mag[ll], width[ll], marker, markersize=markersize, markeredgewidth=markeredgewidth,
174 color='k')
175
176 if newFig:
177 axes.set_xlabel("model")
178 axes.set_ylabel(r"$\sqrt{I_{xx} + I_{yy}}$")
179
180 return fig
181
182
188
189
190@pexConfig.registerConfigurable("psfex", sourceSelectorRegistry)
192 r"""A star selector whose algorithm is not yet documented.
193
194 @anchor PsfexStarSelectorTask_
195
196 @section meas_extensions_psfex_psfexStarSelectorStarSelector_Contents Contents
197
198 - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Purpose
199 - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Initialize
200 - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_IO
201 - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Config
202 - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Debug
203
204 @section meas_extensions_psfex_psfexStarSelectorStarSelector_Purpose Description
205
206 A star selector whose algorithm is not yet documented
207
208 @section meas_extensions_psfex_psfexStarSelectorStarSelector_Initialize Task initialisation
209
210 @copydoc \_\_init\_\_
211
212 @section meas_extensions_psfex_psfexStarSelectorStarSelector_IO Invoking the Task
213
214 Like all star selectors, the main method is `run`.
215
216 @section meas_extensions_psfex_psfexStarSelectorStarSelector_Config Configuration parameters
217
218 See @ref PsfexStarSelectorConfig
219
220 @section meas_extensions_psfex_psfexStarSelectorStarSelector_Debug Debug variables
221
222 PsfexStarSelectorTask has a debug dictionary with the following keys:
223 <dl>
224 <dt>display
225 <dd>bool; if True display debug information
226 <dt>displayExposure
227 <dd>bool; if True display the exposure and spatial cells
228 <dt>plotFwhmHistogram
229 <dd>bool; if True plot histogram of FWHM
230 <dt>plotFlags
231 <dd>bool: if True plot the sources coloured by their flags
232 <dt>plotRejection
233 <dd>bool; if True plot why sources are rejected
234 </dl>
235
236 For example, put something like:
237 @code{.py}
238 import lsstDebug
239 def DebugInfo(name):
240 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
241 if name.endswith("objectSizeStarSelector"):
242 di.display = True
243 di.displayExposure = True
244 di.plotFwhmHistogram = True
245
246 return di
247
248 lsstDebug.Info = DebugInfo
249 @endcode
250 into your `debug.py` file and run your task with the `--debug` flag.
251 """ # noqa: W505
252 ConfigClass = PsfexStarSelectorConfig
253 usesMatches = False # selectStars does not use its matches argument
254
255 def selectSources(self, sourceCat, matches=None, exposure=None):
256 """Return a selection of psf-like objects.
257
258 Parameters
259 ----------
260 sourceCat : `lsst.afw.table.SourceCatalog`
261 Catalog of sources to select from.
262 This catalog must be contiguous in memory.
263 matches : `list` of `lsst.afw.table.ReferenceMatch` or None
264 Ignored by this source selector.
265 exposure : `lsst.afw.image.Exposure` or None
266 The exposure the catalog was built from; used for debug display.
267
268 Return
269 ------
270 struct : `lsst.pipe.base.Struct`
271 The struct contains the following data:
272
273 - selected : `numpy.ndarray` of `bool``
274 Boolean array of sources that were selected, same length as
275 sourceCat.
276 """
277 import lsstDebug
278 display = lsstDebug.Info(__name__).display
279
280 displayExposure = display and \
281 lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
282 plotFwhmHistogram = display and plt and \
283 lsstDebug.Info(__name__).plotFwhmHistogram # Plot histogram of FWHM
284 plotFlags = display and plt and \
285 lsstDebug.Info(__name__).plotFlags # Plot the sources coloured by their flags
286 plotRejection = display and plt and \
287 lsstDebug.Info(__name__).plotRejection # Plot why sources are rejected
288 afwDisplay.setDefaultMaskTransparency(75)
289
290 fluxName = self.config.fluxName
291 fluxErrName = self.config.fluxErrName
292 minFwhm = self.config.minFwhm
293 maxFwhm = self.config.maxFwhm
294 maxFwhmVariability = self.config.maxFwhmVariability
295 maxellip = self.config.maxellip
296 minsn = self.config.minsn
297
298 maxelong = (maxellip + 1.0)/(1.0 - maxellip) if maxellip < 1.0 else 100
299
300 # Unpack the catalogue
301 shape = sourceCat.getShapeDefinition()
302 ixx = sourceCat.get("%s.xx" % shape)
303 iyy = sourceCat.get("%s.yy" % shape)
304
305 fwhm = 2*np.sqrt(2*np.log(2))*np.sqrt(0.5*(ixx + iyy))
306 elong = 0.5*(ixx - iyy)/(ixx + iyy)
307
308 flux = sourceCat.get(fluxName)
309 fluxErr = sourceCat.get(fluxErrName)
310 sn = flux/np.where(fluxErr > 0, fluxErr, 1)
311 sn[fluxErr <= 0] = -psfexLib.BIG
312
313 flags = 0x0
314 for i, f in enumerate(self.config.badFlags):
315 flags = np.bitwise_or(flags, np.where(sourceCat.get(f), 1 << i, 0))
316 #
317 # Estimate the acceptable range of source widths
318 #
319 good = np.logical_and(sn > minsn, np.logical_not(flags))
320 good = np.logical_and(good, elong < maxelong)
321 good = np.logical_and(good, fwhm >= minFwhm)
322 good = np.logical_and(good, fwhm < maxFwhm)
323
324 fwhmMode, fwhmMin, fwhmMax = compute_fwhmrange(fwhm[good], maxFwhmVariability, minFwhm, maxFwhm,
325 plot=dict(fwhmHistogram=plotFwhmHistogram))
326
327 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
328 #
329 # Here's select_candidates
330 #
331 # ---- Apply some selection over flags, fluxes...
332
333 bad = (flags != 0)
334 # set.setBadFlags(int(sum(bad)))
335
336 if plotRejection:
337 selectionVectors = []
338 selectionVectors.append((bad, "flags %d" % sum(bad)))
339
340 dbad = sn < minsn
341 # set.setBadSN(int(sum(dbad)))
342 bad = np.logical_or(bad, dbad)
343 if plotRejection:
344 selectionVectors.append((dbad, "S/N %d" % sum(dbad)))
345
346 dbad = fwhm < fwhmMin
347 # set.setBadFrmin(int(sum(dbad)))
348 bad = np.logical_or(bad, dbad)
349 if plotRejection:
350 selectionVectors.append((dbad, "fwhmMin %d" % sum(dbad)))
351
352 dbad = fwhm > fwhmMax
353 # set.setBadFrmax(int(sum(dbad)))
354 bad = np.logical_or(bad, dbad)
355 if plotRejection:
356 selectionVectors.append((dbad, "fwhmMax %d" % sum(dbad)))
357
358 dbad = elong > maxelong
359 # set.setBadElong(int(sum(dbad)))
360 bad = np.logical_or(bad, dbad)
361 if plotRejection:
362 selectionVectors.append((dbad, "elong %d" % sum(dbad)))
363
364 good = np.logical_not(bad)
365 #
366 # We know enough to plot, if so requested
367 #
368 frame = 0
369 if displayExposure:
370 mi = exposure.getMaskedImage()
371 disp = afwDisplay.Display(frame=frame)
372 disp.mtv(mi, title="PSF candidates")
373
374 with disp.Buffering():
375 for i, source in enumerate(sourceCat):
376 if good[i]:
377 ctype = afwDisplay.GREEN # star candidate
378 else:
379 ctype = afwDisplay.RED # not star
380
381 disp.dot("+", source.getX() - mi.getX0(), source.getY() - mi.getY0(),
382 ctype=ctype)
383
384 if plotFlags or plotRejection:
385 imag = -2.5*np.log10(flux)
386 plt.clf()
387
388 alpha = 0.5
389 if plotFlags:
390 isSet = np.where(flags == 0x0)[0]
391 plt.plot(imag[isSet], fwhm[isSet], 'o', alpha=alpha, label="good")
392
393 for i, f in enumerate(self.config.badFlags):
394 mask = 1 << i
395 isSet = np.where(np.bitwise_and(flags, mask))[0]
396 if isSet.any():
397 if np.isfinite(imag[isSet] + fwhm[isSet]).any():
398 label = re.sub(r"\_flag", "",
399 re.sub(r"^base\_", "",
400 re.sub(r"^.*base\_PixelFlags\_flag\_", "", f)))
401 plt.plot(imag[isSet], fwhm[isSet], 'o', alpha=alpha, label=label)
402 else:
403 for bad, label in selectionVectors:
404 plt.plot(imag[bad], fwhm[bad], 'o', alpha=alpha, label=label)
405
406 plt.plot(imag[good], fwhm[good], 'o', color="black", label="selected")
407 [plt.axhline(_, color='red') for _ in [fwhmMin, fwhmMax]]
408 plt.xlim(np.median(imag[good]) + 5*np.array([-1, 1]))
409 plt.ylim(fwhm[np.where(np.isfinite(fwhm + imag))].min(), 2*fwhmMax)
410 plt.legend(loc=2)
411 plt.xlabel("Instrumental %s Magnitude" % fluxName.split(".")[-1].title())
412 plt.ylabel("fwhm")
413 title = "PSFEX Star Selection"
414 plt.title("%s %d selected" % (title, sum(good)))
415
416 if displayExposure:
417 global eventHandler
418 eventHandler = EventHandler(plt.axes(), imag, fwhm, sourceCat.getX(), sourceCat.getY(),
419 frames=[frame])
420
421 if plotFlags or plotRejection:
422 while True:
423 try:
424 reply = input("continue? [y[es] h(elp) p(db) q(uit)] ").strip()
425 except EOFError:
426 reply = "y"
427
428 if not reply:
429 reply = "y"
430
431 if reply[0] == "h":
432 print("""\
433At this prompt, you can continue with almost any key; 'p' enters pdb,
434 'q' returns to the shell, and
435 'h' prints this text
436""", end=' ')
437
438 if displayExposure:
439 print("""
440If you put the cursor on a point in the matplotlib scatter plot and hit 'p' you'll see it in ds9.""")
441 elif reply[0] == "p":
442 import pdb
443 pdb.set_trace()
444 elif reply[0] == 'q':
445 sys.exit(1)
446 else:
447 break
448
449 return Struct(selected=good)
plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-', clear=True)