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 createInitialSkyWcsFromBoresight, 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 

235 return self.makeRawSkyWcsFromBoresight(visitInfo.getBoresightRaDec(), 

236 visitInfo.getBoresightRotAngle(), 

237 detector) 

238 

239 @classmethod 

240 def makeRawSkyWcsFromBoresight(cls, boresight, orientation, detector): 

241 """Class method to make a raw sky WCS from boresight and detector. 

242 

243 Parameters 

244 ---------- 

245 boresight : `lsst.geom.SpherePoint` 

246 The ICRS boresight RA/Dec 

247 orientation : `lsst.geom.Angle` 

248 The rotation angle of the focal plane on the sky. 

249 detector : `lsst.afw.cameraGeom.Detector` 

250 Where to get the camera geomtry from. 

251 

252 Returns 

253 ------- 

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

255 Reversible mapping from pixel coordinates to sky coordinates. 

256 """ 

257 return createInitialSkyWcsFromBoresight(boresight, orientation, detector) 

258 

259 def _createSkyWcsFromMetadata(self): 

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

261 

262 Returns 

263 ------- 

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

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

266 creation fails due to invalid metadata. 

267 """ 

268 if not self.isOnSky(): 

269 # This is not an on-sky observation 

270 return None 

271 

272 try: 

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

274 except TypeError as e: 

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

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

277 return None 

278 

279 def makeFilter(self): 

280 """Construct a Filter from metadata. 

281 

282 Returns 

283 ------- 

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

285 Object that identifies the filter for this image. 

286 

287 Raises 

288 ------ 

289 NotFoundError 

290 Raised if the physical filter was not registered via 

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

292 """ 

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

294 

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

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

297 

298 Parameters 

299 ---------- 

300 component : `str`, optional 

301 Component to read from the file. 

302 parameters : `dict`, optional 

303 If specified, a dictionary of slicing parameters that 

304 overrides those in ``fileDescriptor``. 

305 

306 Returns 

307 ------- 

308 obj : component-dependent 

309 In-memory component object. 

310 

311 Raises 

312 ------ 

313 KeyError 

314 Raised if the requested component cannot be handled. 

315 """ 

316 if component == "image": 

317 return self.readImage() 

318 elif component == "mask": 

319 return self.readMask() 

320 elif component == "variance": 

321 return self.readVariance() 

322 elif component == "filter": 

323 return self.makeFilter() 

324 elif component == "visitInfo": 

325 return self.makeVisitInfo() 

326 elif component == "wcs": 

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

328 visitInfo = self.makeVisitInfo() 

329 return self.makeWcs(visitInfo, detector) 

330 return None 

331 

332 def readFull(self, parameters=None): 

333 """Read the full Exposure object. 

334 

335 Parameters 

336 ---------- 

337 parameters : `dict`, optional 

338 If specified, a dictionary of slicing parameters that overrides 

339 those in the `fileDescriptor` attribute. 

340 

341 Returns 

342 ------- 

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

344 Complete in-memory exposure. 

345 """ 

346 from lsst.afw.image import makeExposure, makeMaskedImage 

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

348 mask = self.readMask() 

349 if mask is not None: 

350 full.setMask(mask) 

351 variance = self.readVariance() 

352 if variance is not None: 

353 full.setVariance(variance) 

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

355 info = full.getInfo() 

356 info.setFilter(self.makeFilter()) 

357 info.setVisitInfo(self.makeVisitInfo()) 

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

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

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

361 full.setMetadata(self.metadata) 

362 return full 

363 

364 def readRawHeaderWcs(self, parameters=None): 

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

366 """ 

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

368 

369 def write(self, inMemoryDataset): 

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

371 

372 Parameters 

373 ---------- 

374 inMemoryDataset : `object` 

375 The Python object to store. 

376 

377 Returns 

378 ------- 

379 path : `str` 

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

381 """ 

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

383 

384 @property 

385 def observationInfo(self): 

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

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

388 read-only). 

389 """ 

390 if self._observationInfo is None: 

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

392 return self._observationInfo