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

193 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-17 10:00 +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 optimalInnerRadius: int | None = None 

72 optimalOuterRadius: int | None = None 

73 

74 @classmethod 

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

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

77 interface to construct objects like this. Parameters needed to 

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

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

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

81 each point to lists of values. 

82 

83 Parameters 

84 ---------- 

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

86 Pixel data to pass to the constructor 

87 metadata : `dict` 

88 Dictionary containing the information 

89 needed by the constructor. 

90 idx : `int` 

91 Index into the lists in ``metadata`` 

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

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

94 minValidAnnulusFraction : `float`, optional 

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

96 star. 

97 

98 Returns 

99 ------- 

100 brightstarstamp : `BrightStarStamp` 

101 An instance of this class 

102 """ 

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

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

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

106 position = Point2I(x0, y0) 

107 else: 

108 position = None 

109 return cls( 

110 stamp_im=stamp_im, 

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

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

113 position=position, 

114 archive_element=archive_element, 

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

116 minValidAnnulusFraction=minValidAnnulusFraction, 

117 ) 

118 

119 def measureAndNormalize( 

120 self, 

121 annulus: SpanSet, 

122 statsControl: StatisticsControl = StatisticsControl(), 

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

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

125 ): 

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

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

128 

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

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

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

132 non-saturated pixels. 

133 

134 Parameters 

135 ---------- 

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

137 SpanSet containing the annulus to use for normalization. 

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

139 StatisticsControl to be used when computing flux over all pixels 

140 within the annulus. 

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

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

143 annularFlux. Defaults to a simple MEAN. 

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

145 Collection of mask planes to ignore when computing annularFlux. 

146 """ 

147 stampSize = self.stamp_im.getDimensions() 

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

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

150 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict) 

151 annulusMask = annulusImage.mask 

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

153 annulus.copyMaskedImage(self.stamp_im, annulusImage) 

154 # Set mask planes to be ignored. 

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

156 statsControl.setAndMask(andMask) 

157 

158 annulusStat = makeStatistics(annulusImage, statsFlag, statsControl) 

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

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

161 logger.info( 

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

163 unMasked, 

164 annulus.getArea(), 

165 ) 

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

167 # Compute annularFlux. 

168 self.annularFlux = annulusStat.getValue() 

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

170 else: 

171 raise RuntimeError( 

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

173 ) 

174 if np.isnan(self.annularFlux): 

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

176 if self.annularFlux < 0: 

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

178 # Normalize stamps. 

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

180 return None 

181 

182 

183class BrightStarStamps(Stamps): 

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

185 

186 Parameters 

187 ---------- 

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

189 Sequence of star stamps. Cannot contain both normalized and 

190 unnormalized stamps. 

191 innerRadius : `int`, optional 

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

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

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

195 outerRadius : `int`, optional 

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

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

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

199 nb90Rots : `int`, optional 

200 Number of 90 degree rotations required to compensate for detector 

201 orientation. 

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

203 Metadata associated with the bright stars. 

204 use_mask : `bool` 

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

206 use_variance : `bool` 

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

208 use_archive : `bool` 

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

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

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

212 to the same pixel grid before stacking. 

213 

214 Raises 

215 ------ 

216 ValueError 

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

218 required keys. 

219 AttributeError 

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

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

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

223 

224 Notes 

225 ----- 

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

227 bbox: 

228 

229 >>> starSubregions = butler.get( 

230 "brightStarStamps", 

231 dataId, 

232 parameters={"bbox": bbox} 

233 ) 

234 """ 

235 

236 def __init__( 

237 self, 

238 starStamps, 

239 innerRadius=None, 

240 outerRadius=None, 

241 nb90Rots=None, 

242 metadata=None, 

243 use_mask=True, 

244 use_variance=False, 

245 use_archive=False, 

246 ): 

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

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

249 self._checkNormalization(False, innerRadius, outerRadius) 

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

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

252 self.normalized = True 

253 else: 

254 self.normalized = False 

255 self.nb90Rots = nb90Rots 

256 

257 @classmethod 

258 def initAndNormalize( 

259 cls, 

260 starStamps, 

261 innerRadius, 

262 outerRadius, 

263 nb90Rots=None, 

264 metadata=None, 

265 use_mask=True, 

266 use_variance=False, 

267 use_archive=False, 

268 imCenter=None, 

269 discardNanFluxObjects=True, 

270 forceFindFlux=False, 

271 statsControl=StatisticsControl(), 

272 statsFlag=stringToStatisticsProperty("MEAN"), 

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

274 ): 

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

276 BrightStarStamps instance. 

277 

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

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

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

281 non-saturated pixels. 

282 

283 Parameters 

284 ---------- 

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

286 Sequence of star stamps. Cannot contain both normalized and 

287 unnormalized stamps. 

288 innerRadius : `int` 

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

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

291 ``starStamp``. 

292 outerRadius : `int` 

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

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

295 ``starStamp``. 

296 nb90Rots : `int`, optional 

297 Number of 90 degree rotations required to compensate for detector 

298 orientation. 

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

300 Metadata associated with the bright stars. 

301 use_mask : `bool` 

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

303 use_variance : `bool` 

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

305 use_archive : `bool` 

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

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

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

309 to the same pixel grid before stacking. 

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

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

312 first stamp's pixel grid will be used. 

313 discardNanFluxObjects : `bool` 

314 Whether objects with NaN annular flux should be discarded. 

315 If False, these objects will not be normalized. 

316 forceFindFlux : `bool` 

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

318 at a different annulus. 

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

320 StatisticsControl to be used when computing flux over all pixels 

321 within the annulus. 

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

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

324 compute annularFlux. Defaults to a simple MEAN. 

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

326 Collection of mask planes to ignore when computing annularFlux. 

327 

328 Raises 

329 ------ 

330 ValueError 

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

332 required keys. 

333 AttributeError 

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

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

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

337 """ 

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

339 if imCenter is None: 

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

341 

342 # Create SpanSet of annulus. 

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

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

345 annulusWidth = outerRadius - innerRadius 

346 if annulusWidth < 1: 

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

348 annulus = outerCircle.intersectNot(innerCircle) 

349 

350 # Initialize (unnormalized) brightStarStamps instance. 

351 bss = cls( 

352 starStamps, 

353 innerRadius=None, 

354 outerRadius=None, 

355 nb90Rots=nb90Rots, 

356 metadata=metadata, 

357 use_mask=use_mask, 

358 use_variance=use_variance, 

359 use_archive=use_archive, 

360 ) 

361 

362 # Ensure that no stamps have already been normalized. 

363 bss._checkNormalization(True, innerRadius, outerRadius) 

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

365 

366 # Apply normalization. 

367 rejects = [] 

368 badStamps = [] 

369 for stamp in bss._stamps: 

370 try: 

371 stamp.measureAndNormalize( 

372 annulus, statsControl=statsControl, statsFlag=statsFlag, badMaskPlanes=badMaskPlanes 

373 ) 

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

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

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

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

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

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

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

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

382 # that have flux within the normalization annulus and another 

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

384 # annulus. 

385 stamp.optimalOuterRadius = outerRadius 

386 stamp.optimalInnerRadius = innerRadius 

387 except RuntimeError as err: 

388 logger.error(err) 

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

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

391 # steps needed before bright stars can be subtracted. 

392 if discardNanFluxObjects: 

393 rejects.append(stamp) 

394 elif forceFindFlux: 

395 newInnerRadius = innerRadius 

396 newOuterRadius = outerRadius 

397 while True: 

398 newOuterRadius += annulusWidth 

399 newInnerRadius += annulusWidth 

400 if newOuterRadius > min(imCenter): 

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

402 stamp.annularFlux = None 

403 badStamps.append(stamp) 

404 break 

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

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

407 newAnnulus = newOuterCircle.intersectNot(newInnerCircle) 

408 try: 

409 stamp.measureAndNormalize( 

410 newAnnulus, 

411 statsControl=statsControl, 

412 statsFlag=statsFlag, 

413 badMaskPlanes=badMaskPlanes, 

414 ) 

415 

416 except RuntimeError: 

417 stamp.annularFlux = np.nan 

418 logger.error( 

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

420 newInnerRadius, 

421 newOuterRadius, 

422 ) 

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

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

425 logger.info( 

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

427 newInnerRadius, 

428 newOuterRadius, 

429 stamp.annularFlux, 

430 ) 

431 stamp.optimalOuterRadius = newOuterRadius 

432 stamp.optimalInnerRadius = newInnerRadius 

433 break 

434 else: 

435 stamp.annularFlux = np.nan 

436 

437 # Remove rejected stamps. 

438 bss.normalized = True 

439 if discardNanFluxObjects: 

440 for reject in rejects: 

441 bss._stamps.remove(reject) 

442 elif forceFindFlux: 

443 for badStamp in badStamps: 

444 bss._stamps.remove(badStamp) 

445 bss._innerRadius, bss._outerRadius = None, None 

446 return bss, badStamps 

447 return bss 

448 

449 def _refresh_metadata(self): 

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

451 

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

453 annular fluxes to the shared metadata. 

454 """ 

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

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

457 positions = self.getPositions() 

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

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

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

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

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

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

464 if self.nb90Rots is not None: 

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

466 return None 

467 

468 @classmethod 

469 def readFits(cls, filename): 

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

471 

472 Parameters 

473 ---------- 

474 filename : `str` 

475 Name of the file to read. 

476 """ 

477 return cls.readFitsWithOptions(filename, None) 

478 

479 @classmethod 

480 def readFitsWithOptions(cls, filename, options): 

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

482 

483 Parameters 

484 ---------- 

485 filename : `str` 

486 Name of the file to read. 

487 options : `PropertyList` 

488 Collection of metadata parameters. 

489 """ 

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

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

492 if metadata["NORMALIZED"]: 

493 return cls( 

494 stamps, 

495 innerRadius=metadata["INNER_RADIUS"], 

496 outerRadius=metadata["OUTER_RADIUS"], 

497 nb90Rots=nb90Rots, 

498 metadata=metadata, 

499 use_mask=metadata["HAS_MASK"], 

500 use_variance=metadata["HAS_VARIANCE"], 

501 use_archive=metadata["HAS_ARCHIVE"], 

502 ) 

503 else: 

504 return cls( 

505 stamps, 

506 nb90Rots=nb90Rots, 

507 metadata=metadata, 

508 use_mask=metadata["HAS_MASK"], 

509 use_variance=metadata["HAS_VARIANCE"], 

510 use_archive=metadata["HAS_ARCHIVE"], 

511 ) 

512 

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

514 """Add an additional bright star stamp. 

515 

516 Parameters 

517 ---------- 

518 item : `BrightStarStamp` 

519 Bright star stamp to append. 

520 innerRadius : `int`, optional 

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

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

523 ``BrightStarStamp``. 

524 outerRadius : `int`, optional 

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

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

527 ``BrightStarStamp``. 

528 """ 

529 if not isinstance(item, BrightStarStamp): 

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

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

532 raise AttributeError( 

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

534 "instance, or vice-versa." 

535 ) 

536 else: 

537 self._checkRadius(innerRadius, outerRadius) 

538 self._stamps.append(item) 

539 return None 

540 

541 def extend(self, bss): 

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

543 instance. 

544 

545 Parameters 

546 ---------- 

547 bss : `BrightStarStamps` 

548 Other instance to concatenate. 

549 """ 

550 if not isinstance(bss, BrightStarStamps): 

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

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

553 self._stamps += bss._stamps 

554 

555 def getMagnitudes(self): 

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

557 

558 Returns 

559 ------- 

560 gaiaGMags : `list` [`float`] 

561 Gaia G-band magnitudes for each star. 

562 """ 

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

564 

565 def getGaiaIds(self): 

566 """Retrieve Gaia IDs for each star. 

567 

568 Returns 

569 ------- 

570 gaiaIds : `list` [`int`] 

571 Gaia IDs for each star. 

572 """ 

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

574 

575 def getAnnularFluxes(self): 

576 """Retrieve normalization factor for each star. 

577 

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

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

580 saturation. 

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

582 the metadata. 

583 

584 Returns 

585 ------- 

586 annularFluxes : `list` [`float`] 

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

588 """ 

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

590 

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

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

593 magnitude cuts (in Gaia G). 

594 

595 Parameters 

596 ---------- 

597 magMin : `float`, optional 

598 Keep only stars fainter than this value. 

599 magMax : `float`, optional 

600 Keep only stars brighter than this value. 

601 """ 

602 subset = [ 

603 stamp 

604 for stamp in self._stamps 

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

606 ] 

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

608 instance = BrightStarStamps( 

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

610 ) 

611 instance._stamps = subset 

612 return instance 

613 

614 def _checkRadius(self, innerRadius, outerRadius): 

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

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

617 """ 

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

619 raise AttributeError( 

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

621 "of BrightStarStamp instance\n" 

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

623 ) 

624 

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

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

627 that, if requested, normalization can be performed. 

628 """ 

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

630 nStamps = len(self) 

631 nFluxVals = nStamps - noneFluxCount 

632 if noneFluxCount and noneFluxCount < nStamps: 

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

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

635 raise AttributeError( 

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

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

638 "contain an annularFlux value." 

639 ) 

640 elif normalize: 

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

642 # and they have no annularFlux. 

643 if innerRadius is None or outerRadius is None: 

644 raise AttributeError( 

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

646 "for both innerRadius and outerRadius." 

647 ) 

648 elif noneFluxCount < nStamps: 

649 raise AttributeError( 

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

651 "all their annularFlux must be None." 

652 ) 

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

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

655 # contain annularFluxes. 

656 if noneFluxCount: 

657 raise AttributeError( 

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

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

660 ) 

661 else: 

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

663 # already been normalized. 

664 if nFluxVals: 

665 raise AttributeError( 

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

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

668 ) 

669 return None