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 obs_base. 

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 <http://www.gnu.org/licenses/>. 

21 

22__all__ = ("FitsRawFormatterBase",) 

23 

24from abc import ABCMeta, abstractmethod 

25 

26from astro_metadata_translator import ObservationInfo 

27 

28import lsst.afw.fits 

29import lsst.afw.geom 

30import lsst.afw.image 

31from lsst.daf.butler import FileDescriptor 

32import lsst.log 

33 

34from .formatters.fitsExposure import FitsExposureFormatter 

35from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo 

36from .utils import createInitialSkyWcs, InitialSkyWcsError 

37 

38 

39class FitsRawFormatterBase(FitsExposureFormatter, metaclass=ABCMeta): 

40 """Abstract base class for reading and writing raw data to and from 

41 FITS files. 

42 """ 

43 

44 def __init__(self, *args, **kwargs): 

45 self.filterDefinitions.reset() 

46 self.filterDefinitions.defineFilters() 

47 super().__init__(*args, **kwargs) 

48 

49 @classmethod 

50 def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None): 

51 """Construct a possibly-limited formatter from known metadata. 

52 

53 Parameters 

54 ---------- 

55 metadata : `lsst.daf.base.PropertyList` 

56 Raw header metadata, with any fixes (see 

57 `astro_metadata_translator.fix_header`) applied but nothing 

58 stripped. 

59 obsInfo : `astro_metadata_translator.ObservationInfo`, optional 

60 Structured information already extracted from ``metadata``. 

61 If not provided, will be read from ``metadata`` on first use. 

62 storageClass : `lsst.daf.butler.StorageClass`, optional 

63 StorageClass for this file. If not provided, the formatter will 

64 only support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other 

65 operations that operate purely on metadata and not the actual file. 

66 location : `lsst.daf.butler.Location`, optional. 

67 Location of the file. If not provided, the formatter will only 

68 support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other 

69 operations that operate purely on metadata and not the actual file. 

70 

71 Returns 

72 ------- 

73 formatter : `FitsRawFormatterBase` 

74 An instance of ``cls``. 

75 """ 

76 self = cls(FileDescriptor(location, storageClass)) 

77 self._metadata = metadata 

78 self._observationInfo = obsInfo 

79 return self 

80 

81 @property 

82 @abstractmethod 

83 def translatorClass(self): 

84 """`~astro_metadata_translator.MetadataTranslator` to translate 

85 metadata header to `~astro_metadata_translator.ObservationInfo`. 

86 """ 

87 return None 

88 

89 _observationInfo = None 

90 

91 @property 

92 @abstractmethod 

93 def filterDefinitions(self): 

94 """`~lsst.obs.base.FilterDefinitions`, defining the filters for this 

95 instrument. 

96 """ 

97 return None 

98 

99 def readImage(self): 

100 """Read just the image component of the Exposure. 

101 

102 Returns 

103 ------- 

104 image : `~lsst.afw.image.Image` 

105 In-memory image component. 

106 """ 

107 return lsst.afw.image.ImageU(self.fileDescriptor.location.path) 

108 

109 def readMask(self): 

110 """Read just the mask component of the Exposure. 

111 

112 May return None (as the default implementation does) to indicate that 

113 there is no mask information to be extracted (at least not trivially) 

114 from the raw data. This will prohibit direct reading of just the mask, 

115 and set the mask of the full Exposure to zeros. 

116 

117 Returns 

118 ------- 

119 mask : `~lsst.afw.image.Mask` 

120 In-memory mask component. 

121 """ 

122 return None 

123 

124 def readVariance(self): 

125 """Read just the variance component of the Exposure. 

126 

127 May return None (as the default implementation does) to indicate that 

128 there is no variance information to be extracted (at least not 

129 trivially) from the raw data. This will prohibit direct reading of 

130 just the variance, and set the variance of the full Exposure to zeros. 

131 

132 Returns 

133 ------- 

134 image : `~lsst.afw.image.Image` 

135 In-memory variance component. 

136 """ 

137 return None 

138 

139 def isOnSky(self): 

140 """Boolean to determine if the exposure is thought to be on the sky. 

141 

142 Returns 

143 ------- 

144 onSky : `bool` 

145 Returns `True` if the observation looks like it was taken on the 

146 sky. Returns `False` if this observation looks like a calibration 

147 observation. 

148 

149 Notes 

150 ----- 

151 If there is tracking RA/Dec information associated with the 

152 observation it is assumed that the observation is on sky. 

153 Currently the observation type is not checked. 

154 """ 

155 if self.observationInfo.tracking_radec is None: 

156 return False 

157 return True 

158 

159 def stripMetadata(self): 

160 """Remove metadata entries that are parsed into components. 

161 """ 

162 # NOTE: makeVisitInfo() may not strip any metadata itself, but calling 

163 # it ensures that ObservationInfo is created from the metadata, which 

164 # will strip the VisitInfo keys and more. 

165 self.makeVisitInfo() 

166 self._createSkyWcsFromMetadata() 

167 

168 def makeVisitInfo(self): 

169 """Construct a VisitInfo from metadata. 

170 

171 Returns 

172 ------- 

173 visitInfo : `~lsst.afw.image.VisitInfo` 

174 Structured metadata about the observation. 

175 """ 

176 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo) 

177 

178 @abstractmethod 

179 def getDetector(self, id): 

180 """Return the detector that acquired this raw exposure. 

181 

182 Parameters 

183 ---------- 

184 id : `int` 

185 The identifying number of the detector to get. 

186 

187 Returns 

188 ------- 

189 detector : `~lsst.afw.cameraGeom.Detector` 

190 The detector associated with that ``id``. 

191 """ 

192 raise NotImplementedError("Must be implemented by subclasses.") 

193 

194 def makeWcs(self, visitInfo, detector): 

195 """Create a SkyWcs from information about the exposure. 

196 

197 If VisitInfo is not None, use it and the detector to create a SkyWcs, 

198 otherwise return the metadata-based SkyWcs (always created, so that 

199 the relevant metadata keywords are stripped). 

200 

201 Parameters 

202 ---------- 

203 visitInfo : `~lsst.afw.image.VisitInfo` 

204 The information about the telescope boresight and camera 

205 orientation angle for this exposure. 

206 detector : `~lsst.afw.cameraGeom.Detector` 

207 The detector used to acquire this exposure. 

208 

209 Returns 

210 ------- 

211 skyWcs : `~lsst.afw.geom.SkyWcs` 

212 Reversible mapping from pixel coordinates to sky coordinates. 

213 

214 Raises 

215 ------ 

216 InitialSkyWcsError 

217 Raised if there is an error generating the SkyWcs, chained from the 

218 lower-level exception if available. 

219 """ 

220 if not self.isOnSky(): 

221 # This is not an on-sky observation 

222 return None 

223 

224 skyWcs = self._createSkyWcsFromMetadata() 

225 

226 log = lsst.log.Log.getLogger("fitsRawFormatter") 

227 if visitInfo is None: 

228 msg = "No VisitInfo; cannot access boresight information. Defaulting to metadata-based SkyWcs." 

229 log.warn(msg) 

230 if skyWcs is None: 

231 raise InitialSkyWcsError("Failed to create both metadata and boresight-based SkyWcs." 

232 "See warnings in log messages for details.") 

233 return skyWcs 

234 skyWcs = createInitialSkyWcs(visitInfo, detector) 

235 

236 return skyWcs 

237 

238 def _createSkyWcsFromMetadata(self): 

239 """Create a SkyWcs from the FITS header metadata in an Exposure. 

240 

241 Returns 

242 ------- 

243 skyWcs: `lsst.afw.geom.SkyWcs`, or None 

244 The WCS that was created from ``self.metadata``, or None if that 

245 creation fails due to invalid metadata. 

246 """ 

247 if not self.isOnSky(): 

248 # This is not an on-sky observation 

249 return None 

250 

251 try: 

252 return lsst.afw.geom.makeSkyWcs(self.metadata, strip=True) 

253 except TypeError as e: 

254 log = lsst.log.Log.getLogger("fitsRawFormatter") 

255 log.warn("Cannot create a valid WCS from metadata: %s", e.args[0]) 

256 return None 

257 

258 def makeFilter(self): 

259 """Construct a Filter from metadata. 

260 

261 Returns 

262 ------- 

263 filter : `~lsst.afw.image.Filter` 

264 Object that identifies the filter for this image. 

265 

266 Raises 

267 ------ 

268 NotFoundError 

269 Raised if the physical filter was not registered via 

270 `~lsst.afw.image.utils.defineFilter`. 

271 """ 

272 return lsst.afw.image.Filter(self.observationInfo.physical_filter) 

273 

274 def readComponent(self, component, parameters=None): 

275 """Read a component held by the Exposure. 

276 

277 Parameters 

278 ---------- 

279 component : `str`, optional 

280 Component to read from the file. 

281 parameters : `dict`, optional 

282 If specified, a dictionary of slicing parameters that 

283 overrides those in ``fileDescriptor``. 

284 

285 Returns 

286 ------- 

287 obj : component-dependent 

288 In-memory component object. 

289 

290 Raises 

291 ------ 

292 KeyError 

293 Raised if the requested component cannot be handled. 

294 """ 

295 if component == "image": 

296 return self.readImage() 

297 elif component == "mask": 

298 return self.readMask() 

299 elif component == "variance": 

300 return self.readVariance() 

301 elif component == "filter": 

302 return self.makeFilter() 

303 elif component == "visitInfo": 

304 return self.makeVisitInfo() 

305 elif component == "wcs": 

306 detector = self.getDetector(self.observationInfo.detector_num) 

307 visitInfo = self.makeVisitInfo() 

308 return self.makeWcs(visitInfo, detector) 

309 return None 

310 

311 def readFull(self, parameters=None): 

312 """Read the full Exposure object. 

313 

314 Parameters 

315 ---------- 

316 parameters : `dict`, optional 

317 If specified, a dictionary of slicing parameters that overrides 

318 those in the `fileDescriptor` attribute. 

319 

320 Returns 

321 ------- 

322 exposure : `~lsst.afw.image.Exposure` 

323 Complete in-memory exposure. 

324 """ 

325 from lsst.afw.image import makeExposure, makeMaskedImage 

326 full = makeExposure(makeMaskedImage(self.readImage())) 

327 mask = self.readMask() 

328 if mask is not None: 

329 full.setMask(mask) 

330 variance = self.readVariance() 

331 if variance is not None: 

332 full.setVariance(variance) 

333 full.setDetector(self.getDetector(self.observationInfo.detector_num)) 

334 info = full.getInfo() 

335 info.setFilter(self.makeFilter()) 

336 info.setVisitInfo(self.makeVisitInfo()) 

337 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector())) 

338 # We don't need to call stripMetadata() here because it has already 

339 # been stripped during creation of the ObservationInfo, WCS, etc. 

340 full.setMetadata(self.metadata) 

341 return full 

342 

343 def readRawHeaderWcs(self, parameters=None): 

344 """Read the SkyWcs stored in the un-modified raw FITS WCS header keys. 

345 """ 

346 return lsst.afw.geom.makeSkyWcs(lsst.afw.fits.readMetadata(self.fileDescriptor)) 

347 

348 def write(self, inMemoryDataset): 

349 """Write a Python object to a file. 

350 

351 Parameters 

352 ---------- 

353 inMemoryDataset : `object` 

354 The Python object to store. 

355 

356 Returns 

357 ------- 

358 path : `str` 

359 The `URI` where the primary file is stored. 

360 """ 

361 raise NotImplementedError("Raw data cannot be `put`.") 

362 

363 @property 

364 def observationInfo(self): 

365 """The `~astro_metadata_translator.ObservationInfo` extracted from 

366 this file's metadata (`~astro_metadata_translator.ObservationInfo`, 

367 read-only). 

368 """ 

369 if self._observationInfo is None: 

370 self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass) 

371 return self._observationInfo