Coverage for python/lsst/meas/algorithms/sourceSelector.py: 36%

245 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-09 03:51 -0700

1# This file is part of meas_algorithms. 

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__ = ["BaseSourceSelectorConfig", "BaseSourceSelectorTask", "sourceSelectorRegistry", 

23 "ColorLimit", "MagnitudeLimit", "SignalToNoiseLimit", "MagnitudeErrorLimit", 

24 "RequireFlags", "RequireUnresolved", "RequireFiniteRaDec", "RequirePrimary", 

25 "ScienceSourceSelectorConfig", "ScienceSourceSelectorTask", 

26 "ReferenceSourceSelectorConfig", "ReferenceSourceSelectorTask", 

27 "NullSourceSelectorTask" 

28 ] 

29 

30import abc 

31import numpy as np 

32import astropy.units as u 

33import pandas 

34import astropy.table 

35import warnings 

36 

37import lsst.pex.config as pexConfig 

38import lsst.pipe.base as pipeBase 

39 

40 

41class BaseSourceSelectorConfig(pexConfig.Config): 

42 pass 

43 

44 

45class BaseSourceSelectorTask(pipeBase.Task, metaclass=abc.ABCMeta): 

46 """Base class for source selectors 

47 

48 Source selectors are classes that perform a selection on a catalog 

49 object given a set of criteria or cuts. They return the selected catalog 

50 and can optionally set a specified Flag field in the input catalog to 

51 identify if the source was selected. 

52 

53 Register all source selectors with the sourceSelectorRegistry using: 

54 sourceSelectorRegistry.register(name, class) 

55 

56 Attributes 

57 ---------- 

58 usesMatches : `bool` 

59 A boolean variable specify if the inherited source selector uses 

60 matches to an external catalog, and thus requires the ``matches`` 

61 argument to ``run()``. 

62 """ 

63 

64 ConfigClass = BaseSourceSelectorConfig 

65 _DefaultName = "sourceSelector" 

66 usesMatches = False 

67 

68 def __init__(self, **kwargs): 

69 pipeBase.Task.__init__(self, **kwargs) 

70 

71 def run(self, sourceCat, sourceSelectedField=None, matches=None, exposure=None): 

72 """Select sources and return them. 

73 

74 The input catalog must be contiguous in memory. 

75 

76 Parameters 

77 ---------- 

78 sourceCat : Various table formats 

79 Catalog of sources to select from. Can be 

80 `lsst.afw.table.SourceCatalog` or `pandas.DataFrame` or 

81 `astropy.table.Table`, 

82 sourceSelectedField : `str` or None 

83 Name of flag field in sourceCat to set for selected sources. 

84 If set, will modify sourceCat in-place. 

85 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 

86 List of matches to use for source selection. 

87 If usesMatches is set in source selector this field is required. 

88 If not, it is ignored. 

89 exposure : `lsst.afw.image.Exposure` or None 

90 The exposure the catalog was built from; used for debug display. 

91 

92 Returns 

93 ------- 

94 struct : `lsst.pipe.base.Struct` 

95 The struct contains the following data: 

96 

97 ``sourceCat`` 

98 The catalog of sources that were selected. 

99 (may not be memory-contiguous) 

100 (`lsst.afw.table.SourceCatalog` or `pandas.DataFrame` 

101 or `astropy.table.Table`) 

102 ``selected`` 

103 Boolean array of sources that were selected, same length as 

104 sourceCat. 

105 (`numpy.ndarray` of `bool`) 

106 

107 Raises 

108 ------ 

109 RuntimeError 

110 Raised if ``sourceCat`` is not contiguous. 

111 """ 

112 if hasattr(sourceCat, 'isContiguous'): 

113 # Check for continuity on afwTable catalogs 

114 if not sourceCat.isContiguous(): 

115 raise RuntimeError("Input catalogs for source selection must be contiguous.") 

116 

117 result = self.selectSources(sourceCat=sourceCat, 

118 exposure=exposure, 

119 matches=matches) 

120 

121 if sourceSelectedField is not None: 

122 sourceCat[sourceSelectedField] = result.selected 

123 

124 return pipeBase.Struct(sourceCat=sourceCat[result.selected], 

125 selected=result.selected) 

126 

127 @abc.abstractmethod 

128 def selectSources(self, sourceCat, matches=None, exposure=None): 

129 """Return a selection of sources selected by some criteria. 

130 

131 Parameters 

132 ---------- 

133 sourceCat : Various table formats 

134 Catalog of sources to select from. Supports 

135 `lsst.afw.table.SourceCatalog` or `pandas.DataFrame` 

136 or `astropy.table.Table` 

137 This catalog must be contiguous in memory. 

138 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 

139 A list of lsst.afw.table.ReferenceMatch objects 

140 exposure : `lsst.afw.image.Exposure` or None 

141 The exposure the catalog was built from; used for debug display. 

142 

143 Returns 

144 ------- 

145 struct : `lsst.pipe.base.Struct` 

146 The struct contains the following data: 

147 

148 ``selected`` 

149 Boolean array of sources that were selected, same length as 

150 sourceCat. 

151 (`numpy.ndarray` of `bool`) 

152 """ 

153 raise NotImplementedError("BaseSourceSelectorTask is abstract") 

154 

155 

156sourceSelectorRegistry = pexConfig.makeRegistry( 

157 doc="A registry of source selectors (subclasses of " 

158 "BaseSourceSelectorTask)", 

159) 

160 

161 

162class BaseLimit(pexConfig.Config): 

163 """Base class for selecting sources by applying a limit 

164 

165 This object can be used as a `lsst.pex.config.Config` for configuring 

166 the limit, and then the `apply` method can be used to identify sources 

167 in the catalog that match the configured limit. 

168 

169 This provides the `maximum` and `minimum` fields in the Config, and 

170 a method to apply the limits to an array of values calculated by the 

171 subclass. 

172 """ 

173 minimum = pexConfig.Field(dtype=float, optional=True, doc="Select objects with value greater than this") 

174 maximum = pexConfig.Field(dtype=float, optional=True, doc="Select objects with value less than this") 

175 

176 def apply(self, values): 

177 """Apply the limits to an array of values 

178 

179 Subclasses should calculate the array of values and then 

180 return the result of calling this method. 

181 

182 Parameters 

183 ---------- 

184 values : `numpy.ndarray` 

185 Array of values to which to apply limits. 

186 

187 Returns 

188 ------- 

189 selected : `numpy.ndarray` 

190 Boolean array indicating for each source whether it is selected 

191 (True means selected). 

192 """ 

193 selected = np.ones(len(values), dtype=bool) 

194 with np.errstate(invalid="ignore"): # suppress NAN warnings 

195 if self.minimum is not None: 

196 selected &= values > self.minimum 

197 if self.maximum is not None: 

198 selected &= values < self.maximum 

199 return selected 

200 

201 

202class ColorLimit(BaseLimit): 

203 """Select sources using a color limit 

204 

205 This object can be used as a `lsst.pex.config.Config` for configuring 

206 the limit, and then the `apply` method can be used to identify sources 

207 in the catalog that match the configured limit. 

208 

209 We refer to 'primary' and 'secondary' flux measurements; these are the 

210 two components of the color, which is: 

211 

212 instFluxToMag(cat[primary]) - instFluxToMag(cat[secondary]) 

213 """ 

214 primary = pexConfig.Field(dtype=str, doc="Name of column with primary flux measurement") 

215 secondary = pexConfig.Field(dtype=str, doc="Name of column with secondary flux measurement") 

216 

217 def apply(self, catalog): 

218 """Apply the color limit to a catalog 

219 

220 Parameters 

221 ---------- 

222 catalog : Various table formats 

223 Catalog of sources to which the limit will be applied. 

224 Supports `lsst.afw.table.SourceCatalog` or `pandas.DataFrame` 

225 or `astropy.table.Table` 

226 

227 Returns 

228 ------- 

229 selected : `numpy.ndarray` 

230 Boolean array indicating for each source whether it is selected 

231 (True means selected). 

232 """ 

233 primary = _getFieldFromCatalog(catalog, self.primary) 

234 secondary = _getFieldFromCatalog(catalog, self.secondary) 

235 

236 primary = (primary*u.nJy).to_value(u.ABmag) 

237 secondary = (secondary*u.nJy).to_value(u.ABmag) 

238 color = primary - secondary 

239 return BaseLimit.apply(self, color) 

240 

241 

242class FluxLimit(BaseLimit): 

243 """Select sources using a flux limit 

244 

245 This object can be used as a `lsst.pex.config.Config` for configuring 

246 the limit, and then the `apply` method can be used to identify sources 

247 in the catalog that match the configured limit. 

248 """ 

249 fluxField = pexConfig.Field(dtype=str, default="slot_CalibFlux_instFlux", 

250 doc="Name of the source flux field to use.") 

251 

252 def apply(self, catalog): 

253 """Apply the flux limits to a catalog 

254 

255 Parameters 

256 ---------- 

257 catalog : `lsst.afw.table.SourceCatalog` 

258 Catalog of sources to which the limit will be applied. 

259 

260 Returns 

261 ------- 

262 selected : `numpy.ndarray` 

263 Boolean array indicating for each source whether it is selected 

264 (True means selected). 

265 """ 

266 flagField = self.fluxField + "_flag" 

267 selected = np.logical_not(_getFieldFromCatalog(catalog, flagField, isFlag=True)) 

268 flux = _getFieldFromCatalog(catalog, self.fluxField) 

269 

270 selected &= BaseLimit.apply(self, flux) 

271 return selected 

272 

273 

274class MagnitudeLimit(BaseLimit): 

275 """Select sources using a magnitude limit 

276 

277 Note that this assumes that a zero-point has already been applied and 

278 the fluxes are in AB fluxes in Jansky. It is therefore principally 

279 intended for reference catalogs rather than catalogs extracted from 

280 science images. 

281 

282 This object can be used as a `lsst.pex.config.Config` for configuring 

283 the limit, and then the `apply` method can be used to identify sources 

284 in the catalog that match the configured limit. 

285 """ 

286 fluxField = pexConfig.Field(dtype=str, default="flux", 

287 doc="Name of the source flux field to use.") 

288 

289 def apply(self, catalog): 

290 """Apply the magnitude limits to a catalog 

291 

292 Parameters 

293 ---------- 

294 catalog : `lsst.afw.table.SourceCatalog` 

295 Catalog of sources to which the limit will be applied. 

296 

297 Returns 

298 ------- 

299 selected : `numpy.ndarray` 

300 Boolean array indicating for each source whether it is selected 

301 (True means selected). 

302 """ 

303 flagField = self.fluxField + "_flag" 

304 selected = np.logical_not(_getFieldFromCatalog(catalog, flagField, isFlag=True)) 

305 flux = _getFieldFromCatalog(catalog, self.fluxField) 

306 

307 magnitude = (flux*u.nJy).to_value(u.ABmag) 

308 selected &= BaseLimit.apply(self, magnitude) 

309 return selected 

310 

311 

312class SignalToNoiseLimit(BaseLimit): 

313 """Select sources using a flux signal-to-noise limit 

314 

315 This object can be used as a `lsst.pex.config.Config` for configuring 

316 the limit, and then the `apply` method can be used to identify sources 

317 in the catalog that match the configured limit. 

318 """ 

319 fluxField = pexConfig.Field(dtype=str, default="flux", 

320 doc="Name of the source flux field to use.") 

321 errField = pexConfig.Field(dtype=str, default="flux_err", 

322 doc="Name of the source flux error field to use.") 

323 

324 def apply(self, catalog): 

325 """Apply the signal-to-noise limits to a catalog 

326 

327 Parameters 

328 ---------- 

329 catalog : `lsst.afw.table.SourceCatalog` 

330 Catalog of sources to which the limit will be applied. 

331 

332 Returns 

333 ------- 

334 selected : `numpy.ndarray` 

335 Boolean array indicating for each source whether it is selected 

336 (True means selected). 

337 """ 

338 flagField = self.fluxField + "_flag" 

339 selected = np.logical_not(_getFieldFromCatalog(catalog, flagField, isFlag=True)) 

340 flux = _getFieldFromCatalog(catalog, self.fluxField) 

341 err = _getFieldFromCatalog(catalog, self.errField) 

342 

343 with warnings.catch_warnings(): 

344 # Suppress NaN warnings; these will be filtered below. 

345 warnings.simplefilter("ignore") 

346 signalToNoise = flux/err 

347 

348 selected &= ~np.isnan(signalToNoise) 

349 selected &= BaseLimit.apply(self, signalToNoise) 

350 return selected 

351 

352 

353class MagnitudeErrorLimit(BaseLimit): 

354 """Select sources using a magnitude error limit 

355 

356 Because the magnitude error is the inverse of the signal-to-noise 

357 ratio, this also works to select sources by signal-to-noise when 

358 you only have a magnitude. 

359 

360 This object can be used as a `lsst.pex.config.Config` for configuring 

361 the limit, and then the `apply` method can be used to identify sources 

362 in the catalog that match the configured limit. 

363 """ 

364 magErrField = pexConfig.Field(dtype=str, default="mag_err", 

365 doc="Name of the source flux error field to use.") 

366 

367 def apply(self, catalog): 

368 """Apply the magnitude error limits to a catalog 

369 

370 Parameters 

371 ---------- 

372 catalog : `lsst.afw.table.SourceCatalog` 

373 Catalog of sources to which the limit will be applied. 

374 

375 Returns 

376 ------- 

377 selected : `numpy.ndarray` 

378 Boolean array indicating for each source whether it is selected 

379 (True means selected). 

380 """ 

381 return BaseLimit.apply(self, catalog[self.magErrField]) 

382 

383 

384class RequireFlags(pexConfig.Config): 

385 """Select sources using flags 

386 

387 This object can be used as a `lsst.pex.config.Config` for configuring 

388 the limit, and then the `apply` method can be used to identify sources 

389 in the catalog that match the configured limit. 

390 """ 

391 good = pexConfig.ListField(dtype=str, default=[], 

392 doc="List of source flag fields that must be set for a source to be used.") 

393 bad = pexConfig.ListField(dtype=str, default=[], 

394 doc="List of source flag fields that must NOT be set for a source to be used.") 

395 

396 def apply(self, catalog): 

397 """Apply the flag requirements to a catalog 

398 

399 Returns whether the source is selected. 

400 

401 Parameters 

402 ---------- 

403 catalog : `lsst.afw.table.SourceCatalog` 

404 Catalog of sources to which the requirements will be applied. 

405 

406 Returns 

407 ------- 

408 selected : `numpy.ndarray` 

409 Boolean array indicating for each source whether it is selected 

410 (True means selected). 

411 """ 

412 selected = np.ones(len(catalog), dtype=bool) 

413 for flag in self.good: 

414 selected &= catalog[flag] 

415 for flag in self.bad: 

416 selected &= ~catalog[flag] 

417 return selected 

418 

419 

420class RequireUnresolved(BaseLimit): 

421 """Select sources using star/galaxy separation 

422 

423 This object can be used as a `lsst.pex.config.Config` for configuring 

424 the limit, and then the `apply` method can be used to identify sources 

425 in the catalog that match the configured limit. 

426 """ 

427 name = pexConfig.Field(dtype=str, default="base_ClassificationSizeExtendedness_value", 

428 doc="Name of column for star/galaxy separation") 

429 

430 def setDefaults(self): 

431 """Set default 

432 

433 Values below the threshold are unresolved. 

434 """ 

435 self.maximum = 0.1 

436 

437 def apply(self, catalog): 

438 """Apply the flag requirements to a catalog 

439 

440 Returns whether the source is selected. 

441 

442 Parameters 

443 ---------- 

444 catalog : `lsst.afw.table.SourceCatalog` 

445 Catalog of sources to which the requirements will be applied. 

446 

447 Returns 

448 ------- 

449 selected : `numpy.ndarray` 

450 Boolean array indicating for each source whether it is selected 

451 (True means selected). 

452 """ 

453 value = catalog[self.name] 

454 return BaseLimit.apply(self, value) 

455 

456 

457class RequireIsolated(pexConfig.Config): 

458 """Select sources based on whether they are isolated 

459 

460 This object can be used as a `lsst.pex.config.Config` for configuring 

461 the column names to check for "parent" and "nChild" keys. 

462 

463 Note that this should only be run on a catalog that has had the 

464 deblender already run (or else deblend_nChild does not exist). 

465 """ 

466 parentName = pexConfig.Field(dtype=str, default="parent", 

467 doc="Name of column for parent") 

468 nChildName = pexConfig.Field(dtype=str, default="deblend_nChild", 

469 doc="Name of column for nChild") 

470 

471 def apply(self, catalog): 

472 """Apply the isolation requirements to a catalog 

473 

474 Returns whether the source is selected. 

475 

476 Parameters 

477 ---------- 

478 catalog : `lsst.afw.table.SourceCatalog` 

479 Catalog of sources to which the requirements will be applied. 

480 

481 Returns 

482 ------- 

483 selected : `numpy.ndarray` 

484 Boolean array indicating for each source whether it is selected 

485 (True means selected). 

486 """ 

487 selected = ((catalog[self.parentName] == 0) 

488 & (catalog[self.nChildName] == 0)) 

489 return selected 

490 

491 

492class RequireFiniteRaDec(pexConfig.Config): 

493 """Select sources that have finite RA and Dec sky coordinate values 

494 

495 This object can be used as a `lsst.pex.config.Config` for configuring 

496 the column names to check for "coord_ra" and "coord_dec" keys. 

497 

498 This will select against objects for which either the RA or Dec coordinate 

499 entries are not numpy.isfinite(). 

500 """ 

501 raColName = pexConfig.Field(dtype=str, default="coord_ra", doc="Name of column for RA coordinate") 

502 decColName = pexConfig.Field(dtype=str, default="coord_dec", doc="Name of column for Dec coordinate") 

503 

504 def apply(self, catalog): 

505 """Apply the sky coordinate requirements to a catalog 

506 

507 Returns whether the sources were selected. 

508 

509 Parameters 

510 ---------- 

511 catalog : `lsst.afw.table.SourceCatalog` or `pandas.DataFrame` 

512 or `astropy.table.Table` 

513 Catalog of sources to which the requirements will be applied. 

514 

515 Returns 

516 ------- 

517 selected : `numpy.ndarray` 

518 Boolean array indicating for each source whether it is selected 

519 (True means selected). 

520 """ 

521 selected = (np.isfinite(_getFieldFromCatalog(catalog, self.raColName)) 

522 & np.isfinite(_getFieldFromCatalog(catalog, self.decColName))) 

523 return selected 

524 

525 

526class RequirePrimary(pexConfig.Config): 

527 """Select sources that have the detect_isPrimary flag set. 

528 

529 This object can be used as a `lsst.pex.config.Config` for configuring 

530 the column names to check for "detect_isPrimary". For single frame 

531 catalogs this will be True when the source is not a sky object, and is 

532 either an isolated parent that is un-modeled or deblended from a parent 

533 with multiple children. For meas_deblender, this is equivalent to 

534 deblend_nChild=0. For coadd catalogs there is an additional constraint 

535 that the source is located on the interior of a patch and tract. 

536 """ 

537 primaryColName = pexConfig.Field( 

538 dtype=str, 

539 default="detect_isPrimary", 

540 doc="Name of primary flag column", 

541 ) 

542 

543 def apply(self, catalog): 

544 """Apply the primary requirements to a catalog. 

545 

546 Returns whether the sources were selected. 

547 

548 Parameters 

549 ---------- 

550 catalog : lsst.afw.table.SourceCatalog` or `pandas.DataFrame` 

551 or `astropy.table.Table` 

552 Catalog of sources to which the requirement will be applied. 

553 

554 Returns 

555 ------- 

556 selected : `numpy.ndarray` 

557 Boolean array indicating for each source whether it is selected 

558 (True means selected). 

559 """ 

560 selected = (_getFieldFromCatalog(catalog, self.primaryColName)).astype(bool) 

561 

562 return selected 

563 

564 

565class CullFromMaskedRegion(pexConfig.Config): 

566 """Deselect sources that lie in a "bad" mask plane. 

567 

568 This will select against objects whose image coordinates lie in a region 

569 with any of the mask bits in the `badMaskNames` list set. Namely used for 

570 a reference catalog for which the flag columns we would get from the 

571 measurement plugins do not exist. 

572 

573 NOTE: In the context of reference objects, it is recommended NOT to include 

574 EDGE in the `badMaskNames` list as that will remove all the reference objects 

575 outside the detector but within the pixelMargin (thus nulling the pixelMargin 

576 padding all together!) 

577 """ 

578 badMaskNames = pexConfig.ListField( 

579 dtype=str, 

580 default=["NO_DATA", "NOT_DEBLENDED"], 

581 doc="List of mask planes for which sources should be removed if a bit is set.", 

582 ) 

583 xColName = pexConfig.Field( 

584 dtype=str, 

585 default="centroid_x", 

586 doc="Name of column for image x coordinate." 

587 ) 

588 yColName = pexConfig.Field( 

589 dtype=str, 

590 default="centroid_y", 

591 doc="Name of column for image y coordinate." 

592 ) 

593 

594 def apply(self, catalog, exposure): 

595 """Apply the mask plane requirements to a catalog. 

596 

597 Returns whether the sources were selected. 

598 

599 Parameters 

600 ---------- 

601 catalog : `lsst.afw.table.SourceCatalog` or `pandas.DataFrame` 

602 or `astropy.table.Table` 

603 Catalog of sources to which the requirements will be applied. 

604 exposure : `lsst.afw.image.Exposure` or None 

605 The exposure whose mask plane is to be respected. 

606 

607 

608 Returns 

609 ------- 

610 selected : `numpy.ndarray` 

611 Boolean array indicating for each source whether it is selected 

612 (True means selected). 

613 

614 Raises 

615 ------ 

616 RuntimeError 

617 Raised if exposure passed is `None`. 

618 """ 

619 if exposure is None: 

620 raise RuntimeError("Must provide an exposure to CullFromMaskedRegion selection.") 

621 xRefList = catalog[self.xColName] 

622 yRefList = catalog[self.yColName] 

623 # Convert x, y coords to integers to map to indices in mask plane. 

624 # If reference object nominally lies outside the exposure, consider 

625 # it to be at the edge (and thus obeys those mask planes). 

626 x0, y0 = exposure.getXY0() 

627 xMax, yMax = exposure.getDimensions() 

628 xRefList = [int(min(max(0, xRef - x0), xMax - 1)) for xRef in xRefList] 

629 yRefList = [int(min(max(0, yRef - y0), yMax - 1)) for yRef in yRefList] 

630 badMaskNames = [] 

631 maskPlaneDict = exposure.getMask().getMaskPlaneDict() 

632 for badName in self.badMaskNames: 

633 if badName in maskPlaneDict: 

634 badMaskNames.append(badName) 

635 bitmask = exposure.mask.getPlaneBitMask(badMaskNames) 

636 toKeep = ((exposure.mask.array & bitmask) == 0) 

637 selected = toKeep[yRefList, xRefList] # x & y flipped for numpy arrays 

638 

639 return selected 

640 

641 

642class ScienceSourceSelectorConfig(pexConfig.Config): 

643 """Configuration for selecting science sources""" 

644 doFluxLimit = pexConfig.Field(dtype=bool, default=False, doc="Apply flux limit?") 

645 doFlags = pexConfig.Field(dtype=bool, default=False, doc="Apply flag limitation?") 

646 doUnresolved = pexConfig.Field(dtype=bool, default=False, doc="Apply unresolved limitation?") 

647 doSignalToNoise = pexConfig.Field(dtype=bool, default=False, doc="Apply signal-to-noise limit?") 

648 doIsolated = pexConfig.Field(dtype=bool, default=False, doc="Apply isolated limitation?") 

649 doRequireFiniteRaDec = pexConfig.Field(dtype=bool, default=False, 

650 doc="Apply finite sky coordinate check?") 

651 doRequirePrimary = pexConfig.Field(dtype=bool, default=False, 

652 doc="Apply source is primary check?") 

653 doSkySources = pexConfig.Field(dtype=bool, default=False, 

654 doc="Include sky sources, unioned with all other criteria?") 

655 fluxLimit = pexConfig.ConfigField(dtype=FluxLimit, doc="Flux limit to apply") 

656 flags = pexConfig.ConfigField(dtype=RequireFlags, doc="Flags to require") 

657 unresolved = pexConfig.ConfigField(dtype=RequireUnresolved, doc="Star/galaxy separation to apply") 

658 signalToNoise = pexConfig.ConfigField(dtype=SignalToNoiseLimit, doc="Signal-to-noise limit to apply") 

659 isolated = pexConfig.ConfigField(dtype=RequireIsolated, doc="Isolated criteria to apply") 

660 requireFiniteRaDec = pexConfig.ConfigField(dtype=RequireFiniteRaDec, 

661 doc="Finite sky coordinate criteria to apply") 

662 requirePrimary = pexConfig.ConfigField(dtype=RequirePrimary, 

663 doc="Primary source criteria to apply") 

664 skyFlag = pexConfig.ConfigField(dtype=RequireFlags, doc="Sky source flag to include") 

665 

666 def setDefaults(self): 

667 pexConfig.Config.setDefaults(self) 

668 self.flags.bad = [ 

669 "base_PixelFlags_flag_edge", 

670 "base_PixelFlags_flag_nodata", 

671 "base_PixelFlags_flag_saturated", 

672 "base_PsfFlux_flag" 

673 ] 

674 self.signalToNoise.fluxField = "base_PsfFlux_instFlux" 

675 self.signalToNoise.errField = "base_PsfFlux_instFluxErr" 

676 self.skyFlag.good = ["sky_source"] 

677 

678 

679@pexConfig.registerConfigurable("science", sourceSelectorRegistry) 

680class ScienceSourceSelectorTask(BaseSourceSelectorTask): 

681 """Science source selector 

682 

683 By "science" sources, we mean sources that are on images that we 

684 are processing, as opposed to sources from reference catalogs. 

685 

686 This selects (science) sources by (optionally) applying each of a 

687 magnitude limit, flag requirements and star/galaxy separation. 

688 """ 

689 ConfigClass = ScienceSourceSelectorConfig 

690 

691 def selectSources(self, sourceCat, matches=None, exposure=None): 

692 """Return a selection of sources selected by specified criteria. 

693 

694 Parameters 

695 ---------- 

696 sourceCat : `lsst.afw.table.SourceCatalog` 

697 Catalog of sources to select from. 

698 This catalog must be contiguous in memory. 

699 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 

700 Ignored in this SourceSelector. 

701 exposure : `lsst.afw.image.Exposure` or None 

702 The exposure the catalog was built from; used for debug display. 

703 

704 Returns 

705 ------- 

706 struct : `lsst.pipe.base.Struct` 

707 The struct contains the following data: 

708 

709 ``selected`` 

710 Boolean array of sources that were selected, same length as 

711 sourceCat. 

712 (`numpy.ndarray` of `bool`) 

713 """ 

714 selected = np.ones(len(sourceCat), dtype=bool) 

715 if self.config.doFluxLimit: 

716 selected &= self.config.fluxLimit.apply(sourceCat) 

717 if self.config.doFlags: 

718 selected &= self.config.flags.apply(sourceCat) 

719 if self.config.doUnresolved: 

720 selected &= self.config.unresolved.apply(sourceCat) 

721 if self.config.doSignalToNoise: 

722 selected &= self.config.signalToNoise.apply(sourceCat) 

723 if self.config.doIsolated: 

724 selected &= self.config.isolated.apply(sourceCat) 

725 if self.config.doRequireFiniteRaDec: 

726 selected &= self.config.requireFiniteRaDec.apply(sourceCat) 

727 if self.config.doRequirePrimary: 

728 selected &= self.config.requirePrimary.apply(sourceCat) 

729 if self.config.doSkySources: 

730 selected |= self.config.skyFlag.apply(sourceCat) 

731 

732 self.log.info("Selected %d/%d sources", selected.sum(), len(sourceCat)) 

733 

734 return pipeBase.Struct(selected=selected) 

735 

736 

737class ReferenceSourceSelectorConfig(pexConfig.Config): 

738 doMagLimit = pexConfig.Field(dtype=bool, default=False, doc="Apply magnitude limit?") 

739 doFlags = pexConfig.Field(dtype=bool, default=False, doc="Apply flag limitation?") 

740 doUnresolved = pexConfig.Field(dtype=bool, default=False, doc="Apply unresolved limitation?") 

741 doSignalToNoise = pexConfig.Field(dtype=bool, default=False, doc="Apply signal-to-noise limit?") 

742 doMagError = pexConfig.Field(dtype=bool, default=False, doc="Apply magnitude error limit?") 

743 doRequireFiniteRaDec = pexConfig.Field(dtype=bool, default=True, 

744 doc="Apply finite sky coordinate check?") 

745 doCullFromMaskedRegion = pexConfig.Field(dtype=bool, default=False, 

746 doc="Apply image masked region culling?") 

747 magLimit = pexConfig.ConfigField(dtype=MagnitudeLimit, doc="Magnitude limit to apply") 

748 flags = pexConfig.ConfigField(dtype=RequireFlags, doc="Flags to require") 

749 unresolved = pexConfig.ConfigField(dtype=RequireUnresolved, doc="Star/galaxy separation to apply") 

750 requireFiniteRaDec = pexConfig.ConfigField(dtype=RequireFiniteRaDec, 

751 doc="Finite sky coordinate criteria to apply") 

752 signalToNoise = pexConfig.ConfigField(dtype=SignalToNoiseLimit, doc="Signal-to-noise limit to apply") 

753 magError = pexConfig.ConfigField(dtype=MagnitudeErrorLimit, doc="Magnitude error limit to apply") 

754 colorLimits = pexConfig.ConfigDictField(keytype=str, itemtype=ColorLimit, default={}, 

755 doc="Color limits to apply; key is used as a label only") 

756 cullFromMaskedRegion = pexConfig.ConfigField(dtype=CullFromMaskedRegion, 

757 doc="Image mask plane criteria to apply") 

758 

759 

760@pexConfig.registerConfigurable("references", sourceSelectorRegistry) 

761class ReferenceSourceSelectorTask(BaseSourceSelectorTask): 

762 """Reference source selector 

763 

764 This selects reference sources by (optionally) applying each of a 

765 magnitude limit, flag requirements and color limits. 

766 """ 

767 ConfigClass = ReferenceSourceSelectorConfig 

768 

769 def selectSources(self, sourceCat, matches=None, exposure=None): 

770 """Return a selection of reference sources selected by some criteria. 

771 

772 Parameters 

773 ---------- 

774 sourceCat : `lsst.afw.table.SourceCatalog` 

775 Catalog of sources to select from. 

776 This catalog must be contiguous in memory. 

777 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 

778 Ignored in this SourceSelector. 

779 exposure : `lsst.afw.image.Exposure` or None 

780 The exposure the catalog was built from; used for debug display. 

781 

782 Returns 

783 ------- 

784 struct : `lsst.pipe.base.Struct` 

785 The struct contains the following data: 

786 

787 ``selected`` 

788 Boolean array of sources that were selected, same length as 

789 sourceCat. 

790 (`numpy.ndarray` of `bool`) 

791 """ 

792 selected = np.ones(len(sourceCat), dtype=bool) 

793 if self.config.doMagLimit: 

794 selected &= self.config.magLimit.apply(sourceCat) 

795 if self.config.doFlags: 

796 selected &= self.config.flags.apply(sourceCat) 

797 if self.config.doUnresolved: 

798 selected &= self.config.unresolved.apply(sourceCat) 

799 if self.config.doSignalToNoise: 

800 selected &= self.config.signalToNoise.apply(sourceCat) 

801 if self.config.doMagError: 

802 selected &= self.config.magError.apply(sourceCat) 

803 if self.config.doRequireFiniteRaDec: 

804 selected &= self.config.requireFiniteRaDec.apply(sourceCat) 

805 for limit in self.config.colorLimits.values(): 

806 selected &= limit.apply(sourceCat) 

807 if self.config.doCullFromMaskedRegion: 

808 selected &= self.config.cullFromMaskedRegion.apply(sourceCat, exposure) 

809 self.log.info("Selected %d/%d references", selected.sum(), len(sourceCat)) 

810 

811 return pipeBase.Struct(selected=selected) 

812 

813 

814@pexConfig.registerConfigurable("null", sourceSelectorRegistry) 

815class NullSourceSelectorTask(BaseSourceSelectorTask): 

816 """Source selector that returns true for all sources. 

817 

818 Use this when you do not want any sub-selection on your inputs. 

819 """ 

820 ConfigClass = BaseSourceSelectorConfig 

821 

822 def selectSources(self, sourceCat, **kwargs): 

823 # docstring inherited 

824 return pipeBase.Struct(selected=np.ones(len(sourceCat), dtype=bool)) 

825 

826 

827def _getFieldFromCatalog(catalog, field, isFlag=False): 

828 """ 

829 Get a field from a catalog, for `lsst.afw.table` catalogs or 

830 `pandas.DataFrame` or `astropy.table.Table` catalogs. 

831 

832 Parameters 

833 ---------- 

834 catalog : `lsst.afw.table.SourceCatalog` or `pandas.DataFrame` 

835 or `astropy.table.Table` 

836 Catalog of sources to extract field array 

837 field : `str` 

838 Name of field 

839 isFlag : `bool`, optional 

840 Is this a flag column? If it does not exist, return array 

841 of False. 

842 

843 Returns 

844 ------- 

845 array : `np.ndarray` 

846 Array of field values from the catalog. 

847 """ 

848 found = False 

849 if isinstance(catalog, (pandas.DataFrame, astropy.table.Table)): 

850 if field in catalog.columns: 

851 found = True 

852 # Sequences must be converted to numpy arrays 

853 arr = np.array(catalog[field]) 

854 else: 

855 if field in catalog.schema: 

856 found = True 

857 arr = catalog[field] 

858 

859 if isFlag and not found: 

860 arr = np.zeros(len(catalog), dtype=bool) 

861 elif not found: 

862 raise KeyError(f"Could not find field {field} in catalog.") 

863 

864 return arr