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 

27import collections.abc 

28from typing import NamedTuple 

29from enum import Enum, auto 

30 

31import lsst.afw.image as afwImage 

32import lsst.afw.fits as afwFits 

33from lsst.geom import Box2I, Point2I, Extent2I 

34from lsst.daf.base import PropertySet 

35 

36 

37class RadiiEnum(Enum): 

38 INNER_RADIUS = auto() 

39 OUTER_RADIUS = auto() 

40 

41 def __str__(self): 

42 return self.name 

43 

44 

45class BrightStarStamp(NamedTuple): 

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

47 annularFlux. 

48 """ 

49 starStamp: afwImage.maskedImage.MaskedImageF 

50 gaiaGMag: float 

51 gaiaId: int 

52 annularFlux: float 

53 

54 

55class BrightStarStamps(collections.abc.Sequence): 

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

57 

58 Parameters 

59 ---------- 

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

61 Sequence of star stamps. 

62 innerRadius : `int`, optional 

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

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

65 ``starStamp``. Must be provided if ``"INNER_RADIUS"`` and 

66 ``"OUTER_RADIUS"`` are not present in ``metadata``. 

67 outerRadius : `int`, optional 

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

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

70 ``starStamp``. Must be provided if ``"INNER_RADIUS"`` and 

71 ``"OUTER_RADIUS"`` are not present in ``metadata``. 

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

73 Metadata associated with the bright stars. 

74 

75 Raises 

76 ------ 

77 ValueError 

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

79 required keys. 

80 AttributeError 

81 Raised if the definition of the annulus used to compute each star's 

82 normalization factor are not provided, that is, if ``"INNER_RADIUS"`` 

83 and ``"OUTER_RADIUS"`` are not present in ``metadata`` _and_ 

84 ``innerRadius`` and ``outerRadius`` are not provided. 

85 

86 Notes 

87 ----- 

88 A (gen2) butler can be used to read only a part of the stamps, 

89 specified by a bbox: 

90 

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

92 """ 

93 

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

95 metadata=None,): 

96 for item in starStamps: 

97 if not isinstance(item, BrightStarStamp): 

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

99 self._starStamps = starStamps 

100 self._metadata = PropertySet() if metadata is None else metadata.deepCopy() 

101 # Add inner and outer radii to metadata 

102 self._checkRadius(innerRadius, RadiiEnum.INNER_RADIUS) 

103 self._innerRadius = innerRadius 

104 self._checkRadius(outerRadius, RadiiEnum.OUTER_RADIUS) 

105 self._outerRadius = outerRadius 

106 

107 def __len__(self): 

108 return len(self._starStamps) 

109 

110 def __getitem__(self, index): 

111 return self._starStamps[index] 

112 

113 def __iter__(self): 

114 return iter(self._starStamps) 

115 

116 def append(self, item, innerRadius, outerRadius): 

117 """Add an additional bright star stamp. 

118 

119 Parameters 

120 ---------- 

121 item : `BrightStarStamp` 

122 Bright star stamp to append. 

123 innerRadius : `int` 

124 Inner radius value, in4 pixels. This and ``outerRadius`` define the 

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

126 ``starStamp``. 

127 outerRadius : `int`, optional 

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

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

130 ``starStamp``. 

131 """ 

132 if not isinstance(item, BrightStarStamp): 

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

134 self._checkRadius(innerRadius, RadiiEnum.INNER_RADIUS) 

135 self._checkRadius(outerRadius, RadiiEnum.OUTER_RADIUS) 

136 self._starStamps.append(item) 

137 return None 

138 

139 def extend(self, bss): 

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

141 instance. 

142 

143 Parameters 

144 ---------- 

145 bss : `BrightStarStamps` 

146 Other instance to concatenate. 

147 """ 

148 self._checkRadius(bss._innerRadius, RadiiEnum.INNER_RADIUS) 

149 self._checkRadius(bss._outerRadius, RadiiEnum.OUTER_RADIUS) 

150 self._starStamps += bss._starStamps 

151 

152 def getMaskedImages(self): 

153 """Retrieve star images. 

154 

155 Returns 

156 ------- 

157 maskedImages : 

158 `list` [`lsst.afw.image.maskedImage.maskedImage.MaskedImageF`] 

159 """ 

160 return [stamp.starStamp for stamp in self._starStamps] 

161 

162 def getMagnitudes(self): 

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

164 

165 Returns 

166 ------- 

167 gaiaGMags : `list` [`float`] 

168 """ 

169 return [stamp.gaiaGMag for stamp in self._starStamps] 

170 

171 def getGaiaIds(self): 

172 """Retrieve Gaia IDs for each star. 

173 

174 Returns 

175 ------- 

176 gaiaIds : `list` [`int`] 

177 """ 

178 return [stamp.gaiaId for stamp in self._starStamps] 

179 

180 def getAnnularFluxes(self): 

181 """Retrieve normalization factors for each star. 

182 

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

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

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

186 recovered from the metadata. 

187 

188 Returns 

189 ------- 

190 annularFluxes : list[`float`] 

191 """ 

192 return [stamp.annularFlux for stamp in self._starStamps] 

193 

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

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

196 magnitude cuts (in Gaia G). 

197 

198 Parameters 

199 ---------- 

200 magMin : `float`, optional 

201 Keep only stars fainter than this value. 

202 magMax : `float`, optional 

203 Keep only stars brighter than this value. 

204 """ 

205 subset = [stamp for stamp in self._starStamps 

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

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

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

209 # it is already guaranteed to be the correct type 

210 instance = BrightStarStamps((), metadata=self._metadata) 

211 instance._starStamps = subset 

212 return instance 

213 

214 @property 

215 def metadata(self): 

216 return self._metadata.deepCopy() 

217 

218 def _checkRadius(self, radiusValue, metadataEnum): 

219 """Ensure provided annulus radius is consistent with that present 

220 in metadata. If metadata does not contain annulus radius, add it. 

221 """ 

222 # if a radius value is already present in metadata, ensure it matches 

223 # the one given 

224 metadataName = str(metadataEnum) 

225 if self._metadata.exists(metadataName): 

226 if radiusValue is not None: 

227 if self._metadata[metadataName] != radiusValue: 

228 raise AttributeError("BrightStarStamps instance already contains different annulus radii " 

229 + f"values ({metadataName}).") 

230 # if not already in metadata, a value must be provided 

231 elif radiusValue is None: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true

232 raise AttributeError("No radius value provided for the AnnularFlux measurement " 

233 + f"({metadataName}), and none present in metadata.") 

234 else: 

235 self._metadata[metadataName] = radiusValue 

236 return None 

237 

238 def writeFits(self, filename): 

239 """Write a single FITS file containing all bright star stamps. 

240 """ 

241 # ensure metadata contains current number of objects 

242 self._metadata["N_STARS"] = len(self) 

243 

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

245 # metadata 

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

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

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

249 

250 # create primary HDU with global metadata 

251 fitsPrimary = afwFits.Fits(filename, "w") 

252 fitsPrimary.createEmpty() 

253 fitsPrimary.writeMetadata(self._metadata) 

254 fitsPrimary.closeFile() 

255 

256 # add all stamps and mask planes 

257 for stamp in self.getMaskedImages(): 

258 stamp.getImage().writeFits(filename, mode='a') 

259 stamp.getMask().writeFits(filename, mode='a') 

260 return None 

261 

262 @classmethod 

263 def readFits(cls, filename): 

264 """Read bright star stamps from FITS file. 

265 

266 Returns 

267 ------- 

268 bss : `BrightStarStamps` 

269 Collection of bright star stamps. 

270 """ 

271 bss = cls.readFitsWithOptions(filename, None) 

272 return bss 

273 

274 @classmethod 

275 def readFitsWithOptions(cls, filename, options): 

276 """Read bright star stamps from FITS file, allowing for only a 

277 subregion of the stamps to be read. 

278 

279 Returns 

280 ------- 

281 bss : `BrightStarStamps` 

282 Collection of bright star stamps. 

283 """ 

284 # extract necessary info from metadata 

285 visitMetadata = afwFits.readMetadata(filename, hdu=0) 

286 nbStarStamps = visitMetadata["N_STARS"] 

287 gaiaGMags = visitMetadata.getArray("G_MAGS") 

288 gaiaIds = visitMetadata.getArray("GAIA_IDS") 

289 annularFluxes = visitMetadata.getArray("ANNULAR_FLUXES") 

290 # check if a bbox was provided 

291 kwargs = {} 

292 if options and options.exists("llcX"): 292 ↛ 293line 292 didn't jump to line 293, because the condition on line 292 was never true

293 llcX = options["llcX"] 

294 llcY = options["llcY"] 

295 width = options["width"] 

296 height = options["height"] 

297 bbox = Box2I(Point2I(llcX, llcY), Extent2I(width, height)) 

298 kwargs["bbox"] = bbox 

299 # read stamps themselves 

300 starStamps = [] 

301 for bStarIdx in range(nbStarStamps): 

302 imReader = afwImage.ImageFitsReader(filename, hdu=2*bStarIdx + 1) 

303 maskReader = afwImage.MaskFitsReader(filename, hdu=2*(bStarIdx + 1)) 

304 maskedImage = afwImage.MaskedImageF(image=imReader.read(**kwargs), 

305 mask=maskReader.read(**kwargs)) 

306 starStamps.append(BrightStarStamp(starStamp=maskedImage, 

307 gaiaGMag=gaiaGMags[bStarIdx], 

308 gaiaId=gaiaIds[bStarIdx], 

309 annularFlux=annularFluxes[bStarIdx])) 

310 bss = cls(starStamps, metadata=visitMetadata) 

311 return bss