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

260 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-06 08:38 +0000

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 "CullFromMaskedRegion", "CentroidErrorLimit", 

26 "ScienceSourceSelectorConfig", "ScienceSourceSelectorTask", 

27 "ReferenceSourceSelectorConfig", "ReferenceSourceSelectorTask", 

28 "NullSourceSelectorTask" 

29 ] 

30 

31import abc 

32import numpy as np 

33import astropy.units as u 

34import pandas 

35import astropy.table 

36import warnings 

37 

38import lsst.pex.config as pexConfig 

39import lsst.pipe.base as pipeBase 

40 

41 

42class BaseSourceSelectorConfig(pexConfig.Config): 

43 pass 

44 

45 

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

47 """Base class for source selectors 

48 

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

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

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

52 identify if the source was selected. 

53 

54 Register all source selectors with the sourceSelectorRegistry using: 

55 sourceSelectorRegistry.register(name, class) 

56 

57 Attributes 

58 ---------- 

59 usesMatches : `bool` 

60 A boolean variable specify if the inherited source selector uses 

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

62 argument to ``run()``. 

63 """ 

64 

65 ConfigClass = BaseSourceSelectorConfig 

66 _DefaultName = "sourceSelector" 

67 usesMatches = False 

68 

69 def __init__(self, **kwargs): 

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

71 

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

73 """Select sources and return them. 

74 

75 The input catalog must be contiguous in memory. 

76 

77 Parameters 

78 ---------- 

79 sourceCat : Various table formats 

80 Catalog of sources to select from. Can be 

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

82 `astropy.table.Table`, 

83 sourceSelectedField : `str` or None 

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

85 If set, will modify sourceCat in-place. 

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

87 List of matches to use for source selection. 

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

89 If not, it is ignored. 

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

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

92 

93 Returns 

94 ------- 

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

96 The struct contains the following data: 

97 

98 ``sourceCat`` 

99 The catalog of sources that were selected. 

100 (may not be memory-contiguous) 

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

102 or `astropy.table.Table`) 

103 ``selected`` 

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

105 sourceCat. 

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

107 

108 Raises 

109 ------ 

110 RuntimeError 

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

112 """ 

113 if hasattr(sourceCat, 'isContiguous'): 

114 # Check for continuity on afwTable catalogs 

115 if not sourceCat.isContiguous(): 

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

117 

118 result = self.selectSources(sourceCat=sourceCat, 

119 exposure=exposure, 

120 matches=matches) 

121 

122 if sourceSelectedField is not None: 

123 sourceCat[sourceSelectedField] = result.selected 

124 

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

126 selected=result.selected) 

127 

128 @abc.abstractmethod 

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

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

131 

132 Parameters 

133 ---------- 

134 sourceCat : Various table formats 

135 Catalog of sources to select from. Supports 

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

137 or `astropy.table.Table` 

138 This catalog must be contiguous in memory. 

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

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

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

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

143 

144 Returns 

145 ------- 

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

147 The struct contains the following data: 

148 

149 ``selected`` 

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

151 sourceCat. 

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

153 """ 

154 raise NotImplementedError("BaseSourceSelectorTask is abstract") 

155 

156 

157sourceSelectorRegistry = pexConfig.makeRegistry( 

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

159 "BaseSourceSelectorTask)", 

160) 

161 

162 

163class BaseLimit(pexConfig.Config): 

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

165 

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

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

168 in the catalog that match the configured limit. 

169 

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

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

172 subclass. 

173 """ 

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

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

176 

177 def apply(self, values): 

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

179 

180 Subclasses should calculate the array of values and then 

181 return the result of calling this method. 

182 

183 Parameters 

184 ---------- 

185 values : `numpy.ndarray` 

186 Array of values to which to apply limits. 

187 

188 Returns 

189 ------- 

190 selected : `numpy.ndarray` 

191 Boolean array indicating for each source whether it is selected 

192 (True means selected). 

193 """ 

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

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

196 if self.minimum is not None: 

197 selected &= values > self.minimum 

198 if self.maximum is not None: 

199 selected &= values < self.maximum 

200 return selected 

201 

202 

203class ColorLimit(BaseLimit): 

204 """Select sources using a color limit 

205 

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

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

208 in the catalog that match the configured limit. 

209 

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

211 two components of the color, which is: 

212 

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

214 """ 

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

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

217 

218 def apply(self, catalog): 

219 """Apply the color limit to a catalog 

220 

221 Parameters 

222 ---------- 

223 catalog : Various table formats 

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

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

226 or `astropy.table.Table` 

227 

228 Returns 

229 ------- 

230 selected : `numpy.ndarray` 

231 Boolean array indicating for each source whether it is selected 

232 (True means selected). 

233 """ 

234 primary = _getFieldFromCatalog(catalog, self.primary) 

235 secondary = _getFieldFromCatalog(catalog, self.secondary) 

236 

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

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

239 color = primary - secondary 

240 return BaseLimit.apply(self, color) 

241 

242 

243class FluxLimit(BaseLimit): 

244 """Select sources using a flux limit 

245 

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

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

248 in the catalog that match the configured limit. 

249 """ 

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

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

252 

253 def apply(self, catalog): 

254 """Apply the flux limits to a catalog 

255 

256 Parameters 

257 ---------- 

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

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

260 

261 Returns 

262 ------- 

263 selected : `numpy.ndarray` 

264 Boolean array indicating for each source whether it is selected 

265 (True means selected). 

266 """ 

267 flagField = self.fluxField + "_flag" 

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

269 flux = _getFieldFromCatalog(catalog, self.fluxField) 

270 

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

272 return selected 

273 

274 

275class MagnitudeLimit(BaseLimit): 

276 """Select sources using a magnitude limit 

277 

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

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

280 intended for reference catalogs rather than catalogs extracted from 

281 science images. 

282 

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

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

285 in the catalog that match the configured limit. 

286 """ 

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

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

289 

290 def apply(self, catalog): 

291 """Apply the magnitude limits to a catalog 

292 

293 Parameters 

294 ---------- 

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

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

297 

298 Returns 

299 ------- 

300 selected : `numpy.ndarray` 

301 Boolean array indicating for each source whether it is selected 

302 (True means selected). 

303 """ 

304 flagField = self.fluxField + "_flag" 

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

306 flux = _getFieldFromCatalog(catalog, self.fluxField) 

307 

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

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

310 return selected 

311 

312 

313class SignalToNoiseLimit(BaseLimit): 

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

315 

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

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

318 in the catalog that match the configured limit. 

319 """ 

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

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

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

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

324 

325 def apply(self, catalog): 

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

327 

328 Parameters 

329 ---------- 

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

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

332 

333 Returns 

334 ------- 

335 selected : `numpy.ndarray` 

336 Boolean array indicating for each source whether it is selected 

337 (True means selected). 

338 """ 

339 flagField = self.fluxField + "_flag" 

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

341 flux = _getFieldFromCatalog(catalog, self.fluxField) 

342 err = _getFieldFromCatalog(catalog, self.errField) 

343 

344 with warnings.catch_warnings(): 

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

346 warnings.simplefilter("ignore") 

347 signalToNoise = flux/err 

348 

349 selected &= ~np.isnan(signalToNoise) 

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

351 return selected 

352 

353 

354class MagnitudeErrorLimit(BaseLimit): 

355 """Select sources using a magnitude error limit 

356 

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

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

359 you only have a magnitude. 

360 

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

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

363 in the catalog that match the configured limit. 

364 """ 

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

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

367 

368 def apply(self, catalog): 

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

370 

371 Parameters 

372 ---------- 

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

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

375 

376 Returns 

377 ------- 

378 selected : `numpy.ndarray` 

379 Boolean array indicating for each source whether it is selected 

380 (True means selected). 

381 """ 

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

383 

384 

385class RequireFlags(pexConfig.Config): 

386 """Select sources using flags 

387 

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

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

390 in the catalog that match the configured limit. 

391 """ 

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

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

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

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

396 

397 def apply(self, catalog): 

398 """Apply the flag requirements to a catalog 

399 

400 Returns whether the source is selected. 

401 

402 Parameters 

403 ---------- 

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

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

406 

407 Returns 

408 ------- 

409 selected : `numpy.ndarray` 

410 Boolean array indicating for each source whether it is selected 

411 (True means selected). 

412 """ 

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

414 for flag in self.good: 

415 selected &= catalog[flag] 

416 for flag in self.bad: 

417 selected &= ~catalog[flag] 

418 return selected 

419 

420 

421class RequireUnresolved(BaseLimit): 

422 """Select sources using star/galaxy separation 

423 

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

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

426 in the catalog that match the configured limit. 

427 """ 

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

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

430 

431 def setDefaults(self): 

432 """Set default 

433 

434 Values below the threshold are unresolved. 

435 """ 

436 self.maximum = 0.1 

437 

438 def apply(self, catalog): 

439 """Apply the flag requirements to a catalog 

440 

441 Returns whether the source is selected. 

442 

443 Parameters 

444 ---------- 

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

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

447 

448 Returns 

449 ------- 

450 selected : `numpy.ndarray` 

451 Boolean array indicating for each source whether it is selected 

452 (True means selected). 

453 """ 

454 value = catalog[self.name] 

455 return BaseLimit.apply(self, value) 

456 

457 

458class RequireIsolated(pexConfig.Config): 

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

460 

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

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

463 

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

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

466 """ 

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

468 doc="Name of column for parent") 

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

470 doc="Name of column for nChild") 

471 

472 def apply(self, catalog): 

473 """Apply the isolation requirements to a catalog 

474 

475 Returns whether the source is selected. 

476 

477 Parameters 

478 ---------- 

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

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

481 

482 Returns 

483 ------- 

484 selected : `numpy.ndarray` 

485 Boolean array indicating for each source whether it is selected 

486 (True means selected). 

487 """ 

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

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

490 return selected 

491 

492 

493class RequireFiniteRaDec(pexConfig.Config): 

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

495 

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

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

498 

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

500 entries are not numpy.isfinite(). 

501 """ 

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

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

504 

505 def apply(self, catalog): 

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

507 

508 Returns whether the sources were selected. 

509 

510 Parameters 

511 ---------- 

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

513 or `astropy.table.Table` 

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

515 

516 Returns 

517 ------- 

518 selected : `numpy.ndarray` 

519 Boolean array indicating for each source whether it is selected 

520 (True means selected). 

521 """ 

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

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

524 return selected 

525 

526 

527class RequirePrimary(pexConfig.Config): 

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

529 

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

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

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

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

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

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

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

537 """ 

538 primaryColName = pexConfig.Field( 

539 dtype=str, 

540 default="detect_isPrimary", 

541 doc="Name of primary flag column", 

542 ) 

543 

544 def apply(self, catalog): 

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

546 

547 Returns whether the sources were selected. 

548 

549 Parameters 

550 ---------- 

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

552 or `astropy.table.Table` 

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

554 

555 Returns 

556 ------- 

557 selected : `numpy.ndarray` 

558 Boolean array indicating for each source whether it is selected 

559 (True means selected). 

560 """ 

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

562 

563 return selected 

564 

565 

566class CullFromMaskedRegion(pexConfig.Config): 

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

568 

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

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

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

572 measurement plugins do not exist. 

573 

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

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

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

577 padding all together!) 

578 """ 

579 badMaskNames = pexConfig.ListField( 

580 dtype=str, 

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

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

583 ) 

584 xColName = pexConfig.Field( 

585 dtype=str, 

586 default="centroid_x", 

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

588 ) 

589 yColName = pexConfig.Field( 

590 dtype=str, 

591 default="centroid_y", 

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

593 ) 

594 

595 def apply(self, catalog, exposure): 

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

597 

598 Returns whether the sources were selected. 

599 

600 Parameters 

601 ---------- 

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

603 or `astropy.table.Table` 

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

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

606 The exposure whose mask plane is to be respected. 

607 

608 

609 Returns 

610 ------- 

611 selected : `numpy.ndarray` 

612 Boolean array indicating for each source whether it is selected 

613 (True means selected). 

614 

615 Raises 

616 ------ 

617 RuntimeError 

618 Raised if exposure passed is `None`. 

619 """ 

620 if exposure is None: 

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

622 xRefList = catalog[self.xColName] 

623 yRefList = catalog[self.yColName] 

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

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

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

627 x0, y0 = exposure.getXY0() 

628 xMax, yMax = exposure.getDimensions() 

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

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

631 badMaskNames = [] 

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

633 for badName in self.badMaskNames: 

634 if badName in maskPlaneDict: 

635 badMaskNames.append(badName) 

636 bitmask = exposure.mask.getPlaneBitMask(badMaskNames) 

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

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

639 

640 return selected 

641 

642 

643class CentroidErrorLimit(BaseLimit): 

644 """Select sources using a limit on the centroid errors. 

645 

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

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

648 in the catalog that match the configured limit. 

649 """ 

650 centroidField = pexConfig.Field(dtype=str, default="slot_Centroid", 

651 doc="Name of the source centroid field to use.") 

652 

653 def setDefaults(self): 

654 self.maximum = 3.0 

655 

656 def apply(self, catalog): 

657 """Apply the limit on source centroid errors to a catalog. 

658 

659 Parameters 

660 ---------- 

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

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

663 

664 Returns 

665 ------- 

666 selected : `numpy.ndarray` 

667 Boolean array indicating for each source whether it is selected 

668 (True means selected). 

669 """ 

670 xErrField = self.centroidField + "_xErr" 

671 yErrField = self.centroidField + "_yErr" 

672 selected = ((np.isfinite(catalog[xErrField])) & (np.isfinite(catalog[yErrField]))) 

673 selected &= BaseLimit.apply(self, catalog[xErrField]) 

674 selected &= BaseLimit.apply(self, catalog[yErrField]) 

675 return selected 

676 

677 

678class ScienceSourceSelectorConfig(pexConfig.Config): 

679 """Configuration for selecting science sources""" 

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

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

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

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

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

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

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

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

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

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

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

691 doCentroidErrorLimit = pexConfig.Field(dtype=bool, default=False, doc="Apply limit on centroid errors?") 

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

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

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

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

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

697 requireFiniteRaDec = pexConfig.ConfigField(dtype=RequireFiniteRaDec, 

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

699 requirePrimary = pexConfig.ConfigField(dtype=RequirePrimary, 

700 doc="Primary source criteria to apply") 

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

702 centroidErrorLimit = pexConfig.ConfigField(dtype=CentroidErrorLimit, 

703 doc="Limit to place on centroid errors.") 

704 

705 def setDefaults(self): 

706 pexConfig.Config.setDefaults(self) 

707 self.flags.bad = [ 

708 "base_PixelFlags_flag_edge", 

709 "base_PixelFlags_flag_nodata", 

710 "base_PixelFlags_flag_saturated", 

711 "base_PsfFlux_flag" 

712 ] 

713 self.signalToNoise.fluxField = "base_PsfFlux_instFlux" 

714 self.signalToNoise.errField = "base_PsfFlux_instFluxErr" 

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

716 

717 

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

719class ScienceSourceSelectorTask(BaseSourceSelectorTask): 

720 """Science source selector 

721 

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

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

724 

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

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

727 """ 

728 ConfigClass = ScienceSourceSelectorConfig 

729 

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

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

732 

733 Parameters 

734 ---------- 

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

736 Catalog of sources to select from. 

737 This catalog must be contiguous in memory. 

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

739 Ignored in this SourceSelector. 

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

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

742 

743 Returns 

744 ------- 

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

746 The struct contains the following data: 

747 

748 ``selected`` 

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

750 sourceCat. 

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

752 """ 

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

754 if self.config.doFluxLimit: 

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

756 if self.config.doFlags: 

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

758 if self.config.doUnresolved: 

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

760 if self.config.doSignalToNoise: 

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

762 if self.config.doIsolated: 

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

764 if self.config.doRequireFiniteRaDec: 

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

766 if self.config.doRequirePrimary: 

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

768 if self.config.doSkySources: 

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

770 if self.config.doCentroidErrorLimit: 

771 selected &= self.config.centroidErrorLimit.apply(sourceCat) 

772 

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

774 

775 return pipeBase.Struct(selected=selected) 

776 

777 

778class ReferenceSourceSelectorConfig(pexConfig.Config): 

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

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

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

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

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

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

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

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

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

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

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

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

791 requireFiniteRaDec = pexConfig.ConfigField(dtype=RequireFiniteRaDec, 

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

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

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

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

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

797 cullFromMaskedRegion = pexConfig.ConfigField(dtype=CullFromMaskedRegion, 

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

799 

800 

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

802class ReferenceSourceSelectorTask(BaseSourceSelectorTask): 

803 """Reference source selector 

804 

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

806 magnitude limit, flag requirements and color limits. 

807 """ 

808 ConfigClass = ReferenceSourceSelectorConfig 

809 

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

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

812 

813 Parameters 

814 ---------- 

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

816 Catalog of sources to select from. 

817 This catalog must be contiguous in memory. 

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

819 Ignored in this SourceSelector. 

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

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

822 

823 Returns 

824 ------- 

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

826 The struct contains the following data: 

827 

828 ``selected`` 

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

830 sourceCat. 

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

832 """ 

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

834 if self.config.doMagLimit: 

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

836 if self.config.doFlags: 

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

838 if self.config.doUnresolved: 

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

840 if self.config.doSignalToNoise: 

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

842 if self.config.doMagError: 

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

844 if self.config.doRequireFiniteRaDec: 

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

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

847 selected &= limit.apply(sourceCat) 

848 if self.config.doCullFromMaskedRegion: 

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

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

851 

852 return pipeBase.Struct(selected=selected) 

853 

854 

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

856class NullSourceSelectorTask(BaseSourceSelectorTask): 

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

858 

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

860 """ 

861 ConfigClass = BaseSourceSelectorConfig 

862 

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

864 # docstring inherited 

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

866 

867 

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

869 """ 

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

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

872 

873 Parameters 

874 ---------- 

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

876 or `astropy.table.Table` 

877 Catalog of sources to extract field array 

878 field : `str` 

879 Name of field 

880 isFlag : `bool`, optional 

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

882 of False. 

883 

884 Returns 

885 ------- 

886 array : `np.ndarray` 

887 Array of field values from the catalog. 

888 """ 

889 found = False 

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

891 if field in catalog.columns: 

892 found = True 

893 # Sequences must be converted to numpy arrays 

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

895 else: 

896 if field in catalog.schema: 

897 found = True 

898 arr = catalog[field] 

899 

900 if isFlag and not found: 

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

902 elif not found: 

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

904 

905 return arr