Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

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

26 

27from dataclasses import dataclass 

28from operator import ior 

29from functools import reduce 

30from typing import Optional 

31import numpy as np 

32 

33from lsst.afw.image import MaskedImageF 

34from lsst.afw import geom as afwGeom 

35from lsst.afw import math as afwMath 

36from .stamps import StampsBase, AbstractStamp, readFitsWithOptions 

37 

38 

39@dataclass 

40class BrightStarStamp(AbstractStamp): 

41 """Single stamp centered on a bright star, normalized by its 

42 annularFlux. 

43 

44 Parameters 

45 ---------- 

46 stamp_im : `lsst.afw.image.MaskedImage` 

47 Pixel data for this postage stamp 

48 gaiaGMag : `float` 

49 Gaia G magnitude for the object in this stamp 

50 gaiaId : `int` 

51 Gaia object identifier 

52 annularFlux : `Optional[float]` 

53 Flux in an annulus around the object 

54 """ 

55 stamp_im: MaskedImageF 

56 gaiaGMag: float 

57 gaiaId: int 

58 annularFlux: Optional[float] = None 

59 

60 @classmethod 

61 def factory(cls, stamp_im, metadata, idx): 

62 """This method is needed to service the FITS reader. 

63 We need a standard interface to construct objects like this. 

64 Parameters needed to construct this object are passed in via 

65 a metadata dictionary and then passed to the constructor of 

66 this class. This particular factory method requires keys: 

67 G_MAGS, GAIA_IDS, and ANNULAR_FLUXES. They should each 

68 point to lists of values. 

69 

70 Parameters 

71 ---------- 

72 stamp_im : `lsst.afw.image.MaskedImage` 

73 Pixel data to pass to the constructor 

74 metadata : `dict` 

75 Dictionary containing the information 

76 needed by the constructor. 

77 idx : `int` 

78 Index into the lists in ``metadata`` 

79 

80 Returns 

81 ------- 

82 brightstarstamp : `BrightStarStamp` 

83 An instance of this class 

84 """ 

85 return cls(stamp_im=stamp_im, 

86 gaiaGMag=metadata.getArray('G_MAGS')[idx], 

87 gaiaId=metadata.getArray('GAIA_IDS')[idx], 

88 annularFlux=metadata.getArray('ANNULAR_FLUXES')[idx]) 

89 

90 def measureAndNormalize(self, annulus, statsControl=afwMath.StatisticsControl(), 

91 statsFlag=afwMath.stringToStatisticsProperty("MEAN"), 

92 badMaskPlanes=('BAD', 'SAT', 'NO_DATA')): 

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

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

95 

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

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

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

99 non-saturated pixels. 

100 

101 Parameters 

102 ---------- 

103 annulus : `lsst.afw.geom.spanSet.SpanSet` 

104 SpanSet containing the annulus to use for normalization. 

105 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional 

106 StatisticsControl to be used when computing flux over all pixels 

107 within the annulus. 

108 statsFlag : `lsst.afw.math.statistics.Property`, optional 

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

110 annularFlux. Defaults to a simple MEAN. 

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

112 Collection of mask planes to ignore when computing annularFlux. 

113 """ 

114 stampSize = self.stamp_im.getDimensions() 

115 # create image with the same pixel values within annulus, NO_DATA 

116 # elsewhere 

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

118 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict) 

119 annulusMask = annulusImage.mask 

120 annulusMask.array[:] = 2**maskPlaneDict['NO_DATA'] 

121 annulus.copyMaskedImage(self.stamp_im, annulusImage) 

122 # set mask planes to be ignored 

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

124 statsControl.setAndMask(andMask) 

125 # compute annularFlux 

126 annulusStat = afwMath.makeStatistics(annulusImage, statsFlag, statsControl) 

127 self.annularFlux = annulusStat.getValue() 

128 if np.isnan(self.annularFlux): 

129 raise RuntimeError("Annular flux computation failed, likely because no pixels were valid.") 

130 # normalize stamps 

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

132 return None 

133 

134 

135class BrightStarStamps(StampsBase): 

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

137 

138 Parameters 

139 ---------- 

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

141 Sequence of star stamps. Cannot contain both normalized and 

142 unnormalized stamps. 

143 innerRadius : `int`, optional 

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

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

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

147 outerRadius : `int`, optional 

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

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

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

151 metadata : `lsst.daf.base.PropertyList`, optional 

152 Metadata associated with the bright stars. 

153 use_mask : `bool` 

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

155 use_variance : `bool` 

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

157 

158 Raises 

159 ------ 

160 ValueError 

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

162 required keys. 

163 AttributeError 

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

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

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

167 

168 

169 Notes 

170 ----- 

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

172 bbox: 

173 

174 >>> starSubregions = butler.get("brightStarStamps_sub", dataId, bbox=bbox) 

175 """ 

176 

177 def __init__(self, starStamps, innerRadius=None, outerRadius=None, 

178 metadata=None, use_mask=True, use_variance=False): 

179 super().__init__(starStamps, metadata, use_mask, use_variance) 

180 # Ensure stamps contain a flux measurement if and only if they are 

181 # already expected to be normalized 

182 self._checkNormalization(False, innerRadius, outerRadius) 

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

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

185 self.normalized = True 

186 else: 

187 self.normalized = False 

188 

189 @classmethod 

190 def initAndNormalize(cls, starStamps, innerRadius, outerRadius, 

191 metadata=None, use_mask=True, use_variance=False, 

192 imCenter=None, discardNanFluxObjects=True, 

193 statsControl=afwMath.StatisticsControl(), 

194 statsFlag=afwMath.stringToStatisticsProperty("MEAN"), 

195 badMaskPlanes=('BAD', 'SAT', 'NO_DATA')): 

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

197 BrightStarStamps instance. 

198 

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

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

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

202 non-saturated pixels. 

203 

204 Parameters 

205 ---------- 

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

207 Sequence of star stamps. Cannot contain both normalized and 

208 unnormalized stamps. 

209 innerRadius : `int` 

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

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

212 ``starStamp``. 

213 outerRadius : `int` 

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

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

216 ``starStamp``. 

217 metadata : `lsst.daf.base.PropertyList`, optional 

218 Metadata associated with the bright stars. 

219 use_mask : `bool` 

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

221 use_variance : `bool` 

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

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

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

225 first stamp's pixel grid will be used. 

226 discardNanFluxObjects : `bool` 

227 Whether objects with NaN annular flux should be discarded. 

228 If False, these objects will not be normalized. 

229 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional 

230 StatisticsControl to be used when computing flux over all pixels 

231 within the annulus. 

232 statsFlag : `lsst.afw.math.statistics.Property`, optional 

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

234 annularFlux. Defaults to a simple MEAN. 

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

236 Collection of mask planes to ignore when computing annularFlux. 

237 

238 Raises 

239 ------ 

240 ValueError 

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

242 required keys. 

243 AttributeError 

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

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

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

247 """ 

248 if imCenter is None: 

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

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

251 # Create SpanSet of annulus 

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

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

254 annulus = outerCircle.intersectNot(innerCircle) 

255 # Initialize (unnormalized) brightStarStamps instance 

256 bss = cls(starStamps, innerRadius=None, outerRadius=None, 

257 metadata=metadata, use_mask=use_mask, 

258 use_variance=use_variance) 

259 # Ensure no stamps had already been normalized 

260 bss._checkNormalization(True, innerRadius, outerRadius) 

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

262 # Apply normalization 

263 for j, stamp in enumerate(bss._stamps): 

264 try: 

265 stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag, 

266 badMaskPlanes=badMaskPlanes) 

267 except ValueError: 

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

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

270 # steps needed before bright stars can be subtracted. 

271 if discardNanFluxObjects: 

272 bss._stamps.pop(j) 

273 else: 

274 stamp.annularFlux = np.nan 

275 bss.normalized = True 

276 return bss 

277 

278 def _refresh_metadata(self): 

279 """Refresh the metadata. Should be called before writing this object 

280 out. 

281 """ 

282 # add full list of Gaia magnitudes, IDs and annularFlxes to shared 

283 # metadata 

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

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

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

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

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

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

290 return None 

291 

292 @classmethod 

293 def readFits(cls, filename): 

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

295 

296 Parameters 

297 ---------- 

298 filename : `str` 

299 Name of the file to read 

300 """ 

301 return cls.readFitsWithOptions(filename, None) 

302 

303 @classmethod 

304 def readFitsWithOptions(cls, filename, options): 

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

306 

307 Parameters 

308 ---------- 

309 filename : `str` 

310 Name of the file to read 

311 options : `PropertyList` 

312 Collection of metadata parameters 

313 """ 

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

315 if metadata["NORMALIZED"]: 

316 return cls(stamps, 

317 innerRadius=metadata["INNER_RADIUS"], outerRadius=metadata["OUTER_RADIUS"], 

318 metadata=metadata, use_mask=metadata['HAS_MASK'], 

319 use_variance=metadata['HAS_VARIANCE']) 

320 else: 

321 return cls(stamps, metadata=metadata, use_mask=metadata['HAS_MASK'], 

322 use_variance=metadata['HAS_VARIANCE']) 

323 

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

325 """Add an additional bright star stamp. 

326 

327 Parameters 

328 ---------- 

329 item : `BrightStarStamp` 

330 Bright star stamp to append. 

331 innerRadius : `int`, optional 

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

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

334 ``BrightStarStamp``. 

335 outerRadius : `int`, optional 

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

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

338 ``BrightStarStamp``. 

339 """ 

340 if not isinstance(item, BrightStarStamp): 

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

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

343 raise AttributeError("Trying to append an unnormalized stamp to a normalized BrightStarStamps " 

344 "instance, or vice-versa.") 

345 else: 

346 self._checkRadius(innerRadius, outerRadius) 

347 self._stamps.append(item) 

348 return None 

349 

350 def extend(self, bss): 

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

352 instance. 

353 

354 Parameters 

355 ---------- 

356 bss : `BrightStarStamps` 

357 Other instance to concatenate. 

358 """ 

359 if not isinstance(bss, BrightStarStamps): 

360 raise ValueError('Can only extend with a BrightStarStamps object. ' 

361 f'Got {type(bss)}.') 

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

363 self._stamps += bss._stamps 

364 

365 def getMagnitudes(self): 

366 """Retrieve Gaia G magnitudes for each star. 

367 

368 Returns 

369 ------- 

370 gaiaGMags : `list` [`float`] 

371 """ 

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

373 

374 def getGaiaIds(self): 

375 """Retrieve Gaia IDs for each star. 

376 

377 Returns 

378 ------- 

379 gaiaIds : `list` [`int`] 

380 """ 

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

382 

383 def getAnnularFluxes(self): 

384 """Retrieve normalization factors for each star. 

385 

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

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

388 saturation. The inner and outer radii that define the annulus can be 

389 recovered from the metadata. 

390 

391 Returns 

392 ------- 

393 annularFluxes : `list` [`float`] 

394 """ 

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

396 

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

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

399 magnitude cuts (in Gaia G). 

400 

401 Parameters 

402 ---------- 

403 magMin : `float`, optional 

404 Keep only stars fainter than this value. 

405 magMax : `float`, optional 

406 Keep only stars brighter than this value. 

407 """ 

408 subset = [stamp for stamp in self._stamps 

409 if (magMin is None or stamp.gaiaGMag > magMin) 

410 and (magMax is None or stamp.gaiaGMag < magMax)] 

411 # This is an optimization to save looping over the init argument when 

412 # it is already guaranteed to be the correct type 

413 instance = BrightStarStamps((), 

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

415 metadata=self._metadata) 

416 instance._stamps = subset 

417 return instance 

418 

419 def _checkRadius(self, innerRadius, outerRadius): 

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

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

422 """ 

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

424 raise AttributeError("Trying to mix stamps normalized with annulus radii " 

425 f"{innerRadius, outerRadius} with those of BrightStarStamp instance\n" 

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

427 

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

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

430 that, if requested, normalization can be performed. 

431 """ 

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

433 nStamps = len(self) 

434 nFluxVals = nStamps - noneFluxCount 

435 if noneFluxCount and noneFluxCount < nStamps: 

436 # at least one stamp contains an annularFlux value (i.e. has been 

437 # normalized), but not all of them do 

438 raise AttributeError(f"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a " 

439 "BrightStarStamps instance must either be normalized with the same annulus " 

440 "definition, or none of them can contain an annularFlux value.") 

441 elif normalize: 

442 # stamps are to be normalized; ensure annular radii are specified 

443 # and they have no annularFlux 

444 if innerRadius is None or outerRadius is None: 

445 raise AttributeError("For stamps to be normalized (normalize=True), please provide a valid " 

446 "value (in pixels) for both innerRadius and outerRadius.") 

447 elif noneFluxCount < nStamps: 

448 raise AttributeError(f"{nFluxVals} stamps already contain an annularFlux value. For stamps to" 

449 " be normalized, all their annularFlux must be None.") 

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

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

452 # already contain annularFluxes 

453 if noneFluxCount: 

454 raise AttributeError(f"{noneFluxCount} stamps contain no annularFlux, but annular radius " 

455 "values were provided and normalize=False.\nTo normalize stamps, set " 

456 "normalize to True.") 

457 else: 

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

459 # already been normalized 

460 if nFluxVals: 

461 raise AttributeError(f"{nFluxVals} stamps contain an annularFlux value. If stamps have " 

462 "been normalized, the innerRadius and outerRadius values used must " 

463 "be provided.") 

464 return None