Coverage for python/lsst/meas/algorithms/brightStarStamps.py: 21%

198 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 10:10 +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"""Collection of small images (stamps), each centered on a bright star.""" 

23 

24__all__ = ["BrightStarStamp", "BrightStarStamps"] 

25 

26import logging 

27from collections.abc import Collection 

28from dataclasses import dataclass 

29from functools import reduce 

30from operator import ior 

31 

32import numpy as np 

33from lsst.afw.geom import SpanSet, Stencil 

34from lsst.afw.image import MaskedImageF 

35from lsst.afw.math import Property, StatisticsControl, makeStatistics, stringToStatisticsProperty 

36from lsst.afw.table.io import Persistable 

37from lsst.geom import Point2I 

38 

39from .stamps import AbstractStamp, Stamps, readFitsWithOptions 

40 

41logger = logging.getLogger(__name__) 

42 

43 

44@dataclass 

45class BrightStarStamp(AbstractStamp): 

46 """Single stamp centered on a bright star, normalized by its annularFlux. 

47 

48 Parameters 

49 ---------- 

50 stamp_im : `~lsst.afw.image.MaskedImage` 

51 Pixel data for this postage stamp 

52 gaiaGMag : `float` 

53 Gaia G magnitude for the object in this stamp 

54 gaiaId : `int` 

55 Gaia object identifier 

56 position : `~lsst.geom.Point2I` 

57 Origin of the stamps in its origin exposure (pixels) 

58 archive_element : `~lsst.afw.table.io.Persistable` or None, optional 

59 Archive element (e.g. Transform or WCS) associated with this stamp. 

60 annularFlux : `float` or None, optional 

61 Flux in an annulus around the object 

62 """ 

63 

64 stamp_im: MaskedImageF 

65 gaiaGMag: float 

66 gaiaId: int 

67 position: Point2I 

68 archive_element: Persistable | None = None 

69 annularFlux: float | None = None 

70 minValidAnnulusFraction: float = 0.0 

71 validAnnulusFraction: float | None = None 

72 optimalInnerRadius: int | None = None 

73 optimalOuterRadius: int | None = None 

74 

75 @classmethod 

76 def factory(cls, stamp_im, metadata, idx, archive_element=None, minValidAnnulusFraction=0.0): 

77 """This method is needed to service the FITS reader. We need a standard 

78 interface to construct objects like this. Parameters needed to 

79 construct this object are passed in via a metadata dictionary and then 

80 passed to the constructor of this class. This particular factory 

81 method requires keys: G_MAGS, GAIA_IDS, and ANNULAR_FLUXES. They should 

82 each point to lists of values. 

83 

84 Parameters 

85 ---------- 

86 stamp_im : `~lsst.afw.image.MaskedImage` 

87 Pixel data to pass to the constructor 

88 metadata : `dict` 

89 Dictionary containing the information 

90 needed by the constructor. 

91 idx : `int` 

92 Index into the lists in ``metadata`` 

93 archive_element : `~lsst.afw.table.io.Persistable` or None, optional 

94 Archive element (e.g. Transform or WCS) associated with this stamp. 

95 minValidAnnulusFraction : `float`, optional 

96 The fraction of valid pixels within the normalization annulus of a 

97 star. 

98 

99 Returns 

100 ------- 

101 brightstarstamp : `BrightStarStamp` 

102 An instance of this class 

103 """ 

104 if "X0S" in metadata and "Y0S" in metadata: 

105 x0 = metadata.getArray("X0S")[idx] 

106 y0 = metadata.getArray("Y0S")[idx] 

107 position = Point2I(x0, y0) 

108 else: 

109 position = None 

110 return cls( 

111 stamp_im=stamp_im, 

112 gaiaGMag=metadata.getArray("G_MAGS")[idx], 

113 gaiaId=metadata.getArray("GAIA_IDS")[idx], 

114 position=position, 

115 archive_element=archive_element, 

116 annularFlux=metadata.getArray("ANNULAR_FLUXES")[idx], 

117 minValidAnnulusFraction=minValidAnnulusFraction, 

118 validAnnulusFraction=metadata.getArray("VALID_PIXELS_FRACTION")[idx], 

119 ) 

120 

121 def measureAndNormalize( 

122 self, 

123 annulus: SpanSet, 

124 statsControl: StatisticsControl = StatisticsControl(), 

125 statsFlag: Property = stringToStatisticsProperty("MEAN"), 

126 badMaskPlanes: Collection[str] = ("BAD", "SAT", "NO_DATA"), 

127 ): 

128 """Compute "annularFlux", the integrated flux within an annulus 

129 around an object's center, and normalize it. 

130 

131 Since the center of bright stars are saturated and/or heavily affected 

132 by ghosts, we measure their flux in an annulus with a large enough 

133 inner radius to avoid the most severe ghosts and contain enough 

134 non-saturated pixels. 

135 

136 Parameters 

137 ---------- 

138 annulus : `~lsst.afw.geom.spanSet.SpanSet` 

139 SpanSet containing the annulus to use for normalization. 

140 statsControl : `~lsst.afw.math.statistics.StatisticsControl`, optional 

141 StatisticsControl to be used when computing flux over all pixels 

142 within the annulus. 

143 statsFlag : `~lsst.afw.math.statistics.Property`, optional 

144 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute 

145 annularFlux. Defaults to a simple MEAN. 

146 badMaskPlanes : `collections.abc.Collection` [`str`] 

147 Collection of mask planes to ignore when computing annularFlux. 

148 """ 

149 stampSize = self.stamp_im.getDimensions() 

150 # Create image: science pixel values within annulus, NO_DATA elsewhere 

151 maskPlaneDict = self.stamp_im.mask.getMaskPlaneDict() 

152 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict) 

153 annulusMask = annulusImage.mask 

154 annulusMask.array[:] = 2 ** maskPlaneDict["NO_DATA"] 

155 annulus.copyMaskedImage(self.stamp_im, annulusImage) 

156 # Set mask planes to be ignored. 

157 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm) for bm in badMaskPlanes)) 

158 statsControl.setAndMask(andMask) 

159 

160 annulusStat = makeStatistics(annulusImage, statsFlag, statsControl) 

161 # Determine the number of valid (unmasked) pixels within the annulus. 

162 unMasked = annulusMask.array.size - np.count_nonzero(annulusMask.array) 

163 self.validAnnulusFraction = unMasked / annulus.getArea() 

164 logger.info( 

165 "The Star's annulus contains %s valid pixels and the annulus itself contains %s pixels.", 

166 unMasked, 

167 annulus.getArea(), 

168 ) 

169 if unMasked > (annulus.getArea() * self.minValidAnnulusFraction): 

170 # Compute annularFlux. 

171 self.annularFlux = annulusStat.getValue() 

172 logger.info("Annular flux is: %s", self.annularFlux) 

173 else: 

174 raise RuntimeError( 

175 f"Less than {self.minValidAnnulusFraction * 100}% of pixels within the annulus are valid." 

176 ) 

177 if np.isnan(self.annularFlux): 

178 raise RuntimeError("Annular flux computation failed, likely because there are no valid pixels.") 

179 if self.annularFlux < 0: 

180 raise RuntimeError("The annular flux is negative. The stamp can not be normalized!") 

181 # Normalize stamps. 

182 self.stamp_im.image.array /= self.annularFlux 

183 return None 

184 

185 

186class BrightStarStamps(Stamps): 

187 """Collection of bright star stamps and associated metadata. 

188 

189 Parameters 

190 ---------- 

191 starStamps : `collections.abc.Sequence` [`BrightStarStamp`] 

192 Sequence of star stamps. Cannot contain both normalized and 

193 unnormalized stamps. 

194 innerRadius : `int`, optional 

195 Inner radius value, in pixels. This and ``outerRadius`` define the 

196 annulus used to compute the ``"annularFlux"`` values within each 

197 ``starStamp``. Must be provided if ``normalize`` is True. 

198 outerRadius : `int`, optional 

199 Outer radius value, in pixels. This and ``innerRadius`` define the 

200 annulus used to compute the ``"annularFlux"`` values within each 

201 ``starStamp``. Must be provided if ``normalize`` is True. 

202 nb90Rots : `int`, optional 

203 Number of 90 degree rotations required to compensate for detector 

204 orientation. 

205 metadata : `~lsst.daf.base.PropertyList`, optional 

206 Metadata associated with the bright stars. 

207 use_mask : `bool` 

208 If `True` read and write mask data. Default `True`. 

209 use_variance : `bool` 

210 If ``True`` read and write variance data. Default ``False``. 

211 use_archive : `bool` 

212 If ``True`` read and write an Archive that contains a Persistable 

213 associated with each stamp. In the case of bright stars, this is 

214 usually a ``TransformPoint2ToPoint2``, used to warp each stamp 

215 to the same pixel grid before stacking. 

216 

217 Raises 

218 ------ 

219 ValueError 

220 Raised if one of the star stamps provided does not contain the 

221 required keys. 

222 AttributeError 

223 Raised if there is a mix-and-match of normalized and unnormalized 

224 stamps, stamps normalized with different annulus definitions, or if 

225 stamps are to be normalized but annular radii were not provided. 

226 

227 Notes 

228 ----- 

229 A butler can be used to read only a part of the stamps, specified by a 

230 bbox: 

231 

232 >>> starSubregions = butler.get( 

233 "brightStarStamps", 

234 dataId, 

235 parameters={"bbox": bbox} 

236 ) 

237 """ 

238 

239 def __init__( 

240 self, 

241 starStamps, 

242 innerRadius=None, 

243 outerRadius=None, 

244 nb90Rots=None, 

245 metadata=None, 

246 use_mask=True, 

247 use_variance=False, 

248 use_archive=False, 

249 ): 

250 super().__init__(starStamps, metadata, use_mask, use_variance, use_archive) 

251 # Ensure stamps contain a flux measure if expected to be normalized. 

252 self._checkNormalization(False, innerRadius, outerRadius) 

253 self._innerRadius, self._outerRadius = innerRadius, outerRadius 

254 if innerRadius is not None and outerRadius is not None: 

255 self.normalized = True 

256 else: 

257 self.normalized = False 

258 self.nb90Rots = nb90Rots 

259 

260 @classmethod 

261 def initAndNormalize( 

262 cls, 

263 starStamps, 

264 innerRadius, 

265 outerRadius, 

266 nb90Rots=None, 

267 metadata=None, 

268 use_mask=True, 

269 use_variance=False, 

270 use_archive=False, 

271 imCenter=None, 

272 discardNanFluxObjects=True, 

273 forceFindFlux=False, 

274 statsControl=StatisticsControl(), 

275 statsFlag=stringToStatisticsProperty("MEAN"), 

276 badMaskPlanes=("BAD", "SAT", "NO_DATA"), 

277 ): 

278 """Normalize a set of bright star stamps and initialize a 

279 BrightStarStamps instance. 

280 

281 Since the center of bright stars are saturated and/or heavily affected 

282 by ghosts, we measure their flux in an annulus with a large enough 

283 inner radius to avoid the most severe ghosts and contain enough 

284 non-saturated pixels. 

285 

286 Parameters 

287 ---------- 

288 starStamps : `collections.abc.Sequence` [`BrightStarStamp`] 

289 Sequence of star stamps. Cannot contain both normalized and 

290 unnormalized stamps. 

291 innerRadius : `int` 

292 Inner radius value, in pixels. This and ``outerRadius`` define the 

293 annulus used to compute the ``"annularFlux"`` values within each 

294 ``starStamp``. 

295 outerRadius : `int` 

296 Outer radius value, in pixels. This and ``innerRadius`` define the 

297 annulus used to compute the ``"annularFlux"`` values within each 

298 ``starStamp``. 

299 nb90Rots : `int`, optional 

300 Number of 90 degree rotations required to compensate for detector 

301 orientation. 

302 metadata : `~lsst.daf.base.PropertyList`, optional 

303 Metadata associated with the bright stars. 

304 use_mask : `bool` 

305 If `True` read and write mask data. Default `True`. 

306 use_variance : `bool` 

307 If ``True`` read and write variance data. Default ``False``. 

308 use_archive : `bool` 

309 If ``True`` read and write an Archive that contains a Persistable 

310 associated with each stamp. In the case of bright stars, this is 

311 usually a ``TransformPoint2ToPoint2``, used to warp each stamp 

312 to the same pixel grid before stacking. 

313 imCenter : `collections.abc.Sequence`, optional 

314 Center of the object, in pixels. If not provided, the center of the 

315 first stamp's pixel grid will be used. 

316 discardNanFluxObjects : `bool` 

317 Whether objects with NaN annular flux should be discarded. 

318 If False, these objects will not be normalized. 

319 forceFindFlux : `bool` 

320 Whether to try to find the flux of objects with NaN annular flux 

321 at a different annulus. 

322 statsControl : `~lsst.afw.math.statistics.StatisticsControl`, optional 

323 StatisticsControl to be used when computing flux over all pixels 

324 within the annulus. 

325 statsFlag : `~lsst.afw.math.statistics.Property`, optional 

326 statsFlag to be passed on to ``~lsst.afw.math.makeStatistics`` to 

327 compute annularFlux. Defaults to a simple MEAN. 

328 badMaskPlanes : `collections.abc.Collection` [`str`] 

329 Collection of mask planes to ignore when computing annularFlux. 

330 

331 Raises 

332 ------ 

333 ValueError 

334 Raised if one of the star stamps provided does not contain the 

335 required keys. 

336 AttributeError 

337 Raised if there is a mix-and-match of normalized and unnormalized 

338 stamps, stamps normalized with different annulus definitions, or if 

339 stamps are to be normalized but annular radii were not provided. 

340 """ 

341 stampSize = starStamps[0].stamp_im.getDimensions() 

342 if imCenter is None: 

343 imCenter = stampSize[0] // 2, stampSize[1] // 2 

344 

345 # Create SpanSet of annulus. 

346 outerCircle = SpanSet.fromShape(outerRadius, Stencil.CIRCLE, offset=imCenter) 

347 innerCircle = SpanSet.fromShape(innerRadius, Stencil.CIRCLE, offset=imCenter) 

348 annulusWidth = outerRadius - innerRadius 

349 if annulusWidth < 1: 

350 raise ValueError("The annulus width must be greater than 1 pixel.") 

351 annulus = outerCircle.intersectNot(innerCircle) 

352 

353 # Initialize (unnormalized) brightStarStamps instance. 

354 bss = cls( 

355 starStamps, 

356 innerRadius=None, 

357 outerRadius=None, 

358 nb90Rots=nb90Rots, 

359 metadata=metadata, 

360 use_mask=use_mask, 

361 use_variance=use_variance, 

362 use_archive=use_archive, 

363 ) 

364 

365 # Ensure that no stamps have already been normalized. 

366 bss._checkNormalization(True, innerRadius, outerRadius) 

367 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius 

368 

369 # Apply normalization. 

370 rejects = [] 

371 badStamps = [] 

372 for stamp in bss._stamps: 

373 try: 

374 stamp.measureAndNormalize( 

375 annulus, statsControl=statsControl, statsFlag=statsFlag, badMaskPlanes=badMaskPlanes 

376 ) 

377 # Stars that are missing from input bright star stamps may 

378 # still have a flux within the normalization annulus. The 

379 # following two lines make sure that these stars are included 

380 # in the subtraction process. Failing to assign the optimal 

381 # radii values may result in an error in the `createAnnulus` 

382 # method of the `SubtractBrightStarsTask` class. An alternative 

383 # to handle this is to create two types of stamps that are 

384 # missing from the input brightStarStamps object. One for those 

385 # that have flux within the normalization annulus and another 

386 # for those that do not have a flux within the normalization 

387 # annulus. 

388 stamp.optimalOuterRadius = outerRadius 

389 stamp.optimalInnerRadius = innerRadius 

390 except RuntimeError as err: 

391 logger.error(err) 

392 # Optionally keep NaN flux objects, for bookkeeping purposes, 

393 # and to avoid having to re-find and redo the preprocessing 

394 # steps needed before bright stars can be subtracted. 

395 if discardNanFluxObjects: 

396 rejects.append(stamp) 

397 elif forceFindFlux: 

398 newInnerRadius = innerRadius 

399 newOuterRadius = outerRadius 

400 while True: 

401 newOuterRadius += annulusWidth 

402 newInnerRadius += annulusWidth 

403 if newOuterRadius > min(imCenter): 

404 logger.info("No flux found for the star with Gaia ID of %s", stamp.gaiaId) 

405 stamp.annularFlux = None 

406 badStamps.append(stamp) 

407 break 

408 newOuterCircle = SpanSet.fromShape(newOuterRadius, Stencil.CIRCLE, offset=imCenter) 

409 newInnerCircle = SpanSet.fromShape(newInnerRadius, Stencil.CIRCLE, offset=imCenter) 

410 newAnnulus = newOuterCircle.intersectNot(newInnerCircle) 

411 try: 

412 stamp.measureAndNormalize( 

413 newAnnulus, 

414 statsControl=statsControl, 

415 statsFlag=statsFlag, 

416 badMaskPlanes=badMaskPlanes, 

417 ) 

418 

419 except RuntimeError: 

420 stamp.annularFlux = np.nan 

421 logger.error( 

422 "The annular flux was not found for radii %d and %d", 

423 newInnerRadius, 

424 newOuterRadius, 

425 ) 

426 if stamp.annularFlux and stamp.annularFlux > 0: 

427 logger.info("The flux is found within an optimized annulus.") 

428 logger.info( 

429 "The optimized annulus radii are %d and %d and the flux is %f", 

430 newInnerRadius, 

431 newOuterRadius, 

432 stamp.annularFlux, 

433 ) 

434 stamp.optimalOuterRadius = newOuterRadius 

435 stamp.optimalInnerRadius = newInnerRadius 

436 break 

437 else: 

438 stamp.annularFlux = np.nan 

439 

440 # Remove rejected stamps. 

441 bss.normalized = True 

442 if discardNanFluxObjects: 

443 for reject in rejects: 

444 bss._stamps.remove(reject) 

445 elif forceFindFlux: 

446 for badStamp in badStamps: 

447 bss._stamps.remove(badStamp) 

448 bss._innerRadius, bss._outerRadius = None, None 

449 return bss, badStamps 

450 return bss 

451 

452 def _refresh_metadata(self): 

453 """Refresh metadata. Should be called before writing the object out. 

454 

455 This method adds full lists of positions, Gaia magnitudes, IDs and 

456 annular fluxes to the shared metadata. 

457 """ 

458 self._metadata["G_MAGS"] = self.getMagnitudes() 

459 self._metadata["GAIA_IDS"] = self.getGaiaIds() 

460 positions = self.getPositions() 

461 self._metadata["X0S"] = [xy0[0] for xy0 in positions] 

462 self._metadata["Y0S"] = [xy0[1] for xy0 in positions] 

463 self._metadata["ANNULAR_FLUXES"] = self.getAnnularFluxes() 

464 self._metadata["VALID_PIXELS_FRACTION"] = self.getValidPixelsFraction() 

465 self._metadata["NORMALIZED"] = self.normalized 

466 self._metadata["INNER_RADIUS"] = self._innerRadius 

467 self._metadata["OUTER_RADIUS"] = self._outerRadius 

468 if self.nb90Rots is not None: 

469 self._metadata["NB_90_ROTS"] = self.nb90Rots 

470 return None 

471 

472 @classmethod 

473 def readFits(cls, filename): 

474 """Build an instance of this class from a file. 

475 

476 Parameters 

477 ---------- 

478 filename : `str` 

479 Name of the file to read. 

480 """ 

481 return cls.readFitsWithOptions(filename, None) 

482 

483 @classmethod 

484 def readFitsWithOptions(cls, filename, options): 

485 """Build an instance of this class with options. 

486 

487 Parameters 

488 ---------- 

489 filename : `str` 

490 Name of the file to read. 

491 options : `PropertyList` 

492 Collection of metadata parameters. 

493 """ 

494 stamps, metadata = readFitsWithOptions(filename, BrightStarStamp.factory, options) 

495 nb90Rots = metadata["NB_90_ROTS"] if "NB_90_ROTS" in metadata else None 

496 if metadata["NORMALIZED"]: 

497 return cls( 

498 stamps, 

499 innerRadius=metadata["INNER_RADIUS"], 

500 outerRadius=metadata["OUTER_RADIUS"], 

501 nb90Rots=nb90Rots, 

502 metadata=metadata, 

503 use_mask=metadata["HAS_MASK"], 

504 use_variance=metadata["HAS_VARIANCE"], 

505 use_archive=metadata["HAS_ARCHIVE"], 

506 ) 

507 else: 

508 return cls( 

509 stamps, 

510 nb90Rots=nb90Rots, 

511 metadata=metadata, 

512 use_mask=metadata["HAS_MASK"], 

513 use_variance=metadata["HAS_VARIANCE"], 

514 use_archive=metadata["HAS_ARCHIVE"], 

515 ) 

516 

517 def append(self, item, innerRadius=None, outerRadius=None): 

518 """Add an additional bright star stamp. 

519 

520 Parameters 

521 ---------- 

522 item : `BrightStarStamp` 

523 Bright star stamp to append. 

524 innerRadius : `int`, optional 

525 Inner radius value, in pixels. This and ``outerRadius`` define the 

526 annulus used to compute the ``"annularFlux"`` values within each 

527 ``BrightStarStamp``. 

528 outerRadius : `int`, optional 

529 Outer radius value, in pixels. This and ``innerRadius`` define the 

530 annulus used to compute the ``"annularFlux"`` values within each 

531 ``BrightStarStamp``. 

532 """ 

533 if not isinstance(item, BrightStarStamp): 

534 raise ValueError(f"Can only add instances of BrightStarStamp, got {type(item)}.") 

535 if (item.annularFlux is None) == self.normalized: 

536 raise AttributeError( 

537 "Trying to append an unnormalized stamp to a normalized BrightStarStamps " 

538 "instance, or vice-versa." 

539 ) 

540 else: 

541 self._checkRadius(innerRadius, outerRadius) 

542 self._stamps.append(item) 

543 return None 

544 

545 def extend(self, bss): 

546 """Extend BrightStarStamps instance by appending elements from another 

547 instance. 

548 

549 Parameters 

550 ---------- 

551 bss : `BrightStarStamps` 

552 Other instance to concatenate. 

553 """ 

554 if not isinstance(bss, BrightStarStamps): 

555 raise ValueError(f"Can only extend with a BrightStarStamps object. Got {type(bss)}.") 

556 self._checkRadius(bss._innerRadius, bss._outerRadius) 

557 self._stamps += bss._stamps 

558 

559 def getMagnitudes(self): 

560 """Retrieve Gaia G-band magnitudes for each star. 

561 

562 Returns 

563 ------- 

564 gaiaGMags : `list` [`float`] 

565 Gaia G-band magnitudes for each star. 

566 """ 

567 return [stamp.gaiaGMag for stamp in self._stamps] 

568 

569 def getGaiaIds(self): 

570 """Retrieve Gaia IDs for each star. 

571 

572 Returns 

573 ------- 

574 gaiaIds : `list` [`int`] 

575 Gaia IDs for each star. 

576 """ 

577 return [stamp.gaiaId for stamp in self._stamps] 

578 

579 def getAnnularFluxes(self): 

580 """Retrieve normalization factor for each star. 

581 

582 These are computed by integrating the flux in annulus centered on the 

583 bright star, far enough from center to be beyond most severe ghosts and 

584 saturation. 

585 The inner and outer radii that define the annulus can be recovered from 

586 the metadata. 

587 

588 Returns 

589 ------- 

590 annularFluxes : `list` [`float`] 

591 Annular fluxes which give the normalization factor for each star. 

592 """ 

593 return [stamp.annularFlux for stamp in self._stamps] 

594 

595 def getValidPixelsFraction(self): 

596 """Retrieve the fraction of valid pixels within the normalization 

597 annulus for each star. 

598 

599 Returns 

600 ------- 

601 validPixelsFractions : `list` [`float`] 

602 Fractions of valid pixels within the normalization annulus for each 

603 star. 

604 """ 

605 return [stamp.validAnnulusFraction for stamp in self._stamps] 

606 

607 def selectByMag(self, magMin=None, magMax=None): 

608 """Return the subset of bright star stamps for objects with specified 

609 magnitude cuts (in Gaia G). 

610 

611 Parameters 

612 ---------- 

613 magMin : `float`, optional 

614 Keep only stars fainter than this value. 

615 magMax : `float`, optional 

616 Keep only stars brighter than this value. 

617 """ 

618 subset = [ 

619 stamp 

620 for stamp in self._stamps 

621 if (magMin is None or stamp.gaiaGMag > magMin) and (magMax is None or stamp.gaiaGMag < magMax) 

622 ] 

623 # This saves looping over init when guaranteed to be the correct type. 

624 instance = BrightStarStamps( 

625 (), innerRadius=self._innerRadius, outerRadius=self._outerRadius, metadata=self._metadata 

626 ) 

627 instance._stamps = subset 

628 return instance 

629 

630 def _checkRadius(self, innerRadius, outerRadius): 

631 """Ensure provided annulus radius is consistent with that already 

632 present in the instance, or with arguments passed on at initialization. 

633 """ 

634 if innerRadius != self._innerRadius or outerRadius != self._outerRadius: 

635 raise AttributeError( 

636 f"Trying to mix stamps normalized with annulus radii {innerRadius, outerRadius} with those " 

637 "of BrightStarStamp instance\n" 

638 f"(computed with annular radii {self._innerRadius, self._outerRadius})." 

639 ) 

640 

641 def _checkNormalization(self, normalize, innerRadius, outerRadius): 

642 """Ensure there is no mixing of normalized and unnormalized stars, and 

643 that, if requested, normalization can be performed. 

644 """ 

645 noneFluxCount = self.getAnnularFluxes().count(None) 

646 nStamps = len(self) 

647 nFluxVals = nStamps - noneFluxCount 

648 if noneFluxCount and noneFluxCount < nStamps: 

649 # At least one stamp contains an annularFlux value (i.e. has been 

650 # normalized), but not all of them do. 

651 raise AttributeError( 

652 f"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a BrightStarStamps " 

653 "instance must either be normalized with the same annulus definition, or none of them can " 

654 "contain an annularFlux value." 

655 ) 

656 elif normalize: 

657 # Stamps are to be normalized; ensure annular radii are specified 

658 # and they have no annularFlux. 

659 if innerRadius is None or outerRadius is None: 

660 raise AttributeError( 

661 "For stamps to be normalized (normalize=True), please provide a valid value (in pixels) " 

662 "for both innerRadius and outerRadius." 

663 ) 

664 elif noneFluxCount < nStamps: 

665 raise AttributeError( 

666 f"{nFluxVals} stamps already contain an annularFlux value. For stamps to be normalized, " 

667 "all their annularFlux must be None." 

668 ) 

669 elif innerRadius is not None and outerRadius is not None: 

670 # Radii provided, but normalize=False; check that stamps already 

671 # contain annularFluxes. 

672 if noneFluxCount: 

673 raise AttributeError( 

674 f"{noneFluxCount} stamps contain no annularFlux, but annular radius values were provided " 

675 "and normalize=False.\nTo normalize stamps, set normalize to True." 

676 ) 

677 else: 

678 # At least one radius value is missing; ensure no stamps have 

679 # already been normalized. 

680 if nFluxVals: 

681 raise AttributeError( 

682 f"{nFluxVals} stamps contain an annularFlux value. If stamps have been normalized, the " 

683 "innerRadius and outerRadius values used must be provided." 

684 ) 

685 return None