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 # This has to be explicit until we fix camera geometry in DM-20746 

45 wcsFlipX = False 

46 """Control whether the WCS is flipped in the X-direction (`bool`)""" 

47 

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

49 self.filterDefinitions.reset() 

50 self.filterDefinitions.defineFilters() 

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

52 

53 @classmethod 

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

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

56 

57 Parameters 

58 ---------- 

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

60 Raw header metadata, with any fixes (see 

61 `astro_metadata_translator.fix_header`) applied but nothing 

62 stripped. 

63 obsInfo : `astro_metadata_translator.ObservationInfo`, optional 

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

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

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

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

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

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

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

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

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

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

74 

75 Returns 

76 ------- 

77 formatter : `FitsRawFormatterBase` 

78 An instance of ``cls``. 

79 """ 

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

81 self._metadata = metadata 

82 self._observationInfo = obsInfo 

83 return self 

84 

85 @property 

86 @abstractmethod 

87 def translatorClass(self): 

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

89 metadata header to `~astro_metadata_translator.ObservationInfo`. 

90 """ 

91 return None 

92 

93 _observationInfo = None 

94 

95 @property 

96 @abstractmethod 

97 def filterDefinitions(self): 

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

99 instrument. 

100 """ 

101 return None 

102 

103 def readImage(self): 

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

105 

106 Returns 

107 ------- 

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

109 In-memory image component. 

110 """ 

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

112 

113 def readMask(self): 

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

115 

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

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

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

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

120 

121 Returns 

122 ------- 

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

124 In-memory mask component. 

125 """ 

126 return None 

127 

128 def readVariance(self): 

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

130 

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

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

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

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

135 

136 Returns 

137 ------- 

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

139 In-memory variance component. 

140 """ 

141 return None 

142 

143 def isOnSky(self): 

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

145 

146 Returns 

147 ------- 

148 onSky : `bool` 

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

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

151 observation. 

152 

153 Notes 

154 ----- 

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

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

157 Currently the observation type is not checked. 

158 """ 

159 if self.observationInfo.tracking_radec is None: 

160 return False 

161 return True 

162 

163 def stripMetadata(self): 

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

165 """ 

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

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

168 # will strip the VisitInfo keys and more. 

169 self.makeVisitInfo() 

170 self._createSkyWcsFromMetadata() 

171 

172 def makeVisitInfo(self): 

173 """Construct a VisitInfo from metadata. 

174 

175 Returns 

176 ------- 

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

178 Structured metadata about the observation. 

179 """ 

180 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo) 

181 

182 @abstractmethod 

183 def getDetector(self, id): 

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

185 

186 Parameters 

187 ---------- 

188 id : `int` 

189 The identifying number of the detector to get. 

190 

191 Returns 

192 ------- 

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

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

195 """ 

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

197 

198 def makeWcs(self, visitInfo, detector): 

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

200 

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

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

203 the relevant metadata keywords are stripped). 

204 

205 Parameters 

206 ---------- 

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

208 The information about the telescope boresight and camera 

209 orientation angle for this exposure. 

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

211 The detector used to acquire this exposure. 

212 

213 Returns 

214 ------- 

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

216 Reversible mapping from pixel coordinates to sky coordinates. 

217 

218 Raises 

219 ------ 

220 InitialSkyWcsError 

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

222 lower-level exception if available. 

223 """ 

224 if not self.isOnSky(): 

225 # This is not an on-sky observation 

226 return None 

227 

228 skyWcs = self._createSkyWcsFromMetadata() 

229 

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

231 if visitInfo is None: 

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

233 log.warn(msg) 

234 if skyWcs is None: 

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

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

237 return skyWcs 

238 

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

240 visitInfo.getBoresightRotAngle(), 

241 detector) 

242 

243 @classmethod 

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

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

246 

247 Parameters 

248 ---------- 

249 boresight : `lsst.geom.SpherePoint` 

250 The ICRS boresight RA/Dec 

251 orientation : `lsst.geom.Angle` 

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

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

254 Where to get the camera geomtry from. 

255 

256 Returns 

257 ------- 

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

259 Reversible mapping from pixel coordinates to sky coordinates. 

260 """ 

261 return createInitialSkyWcsFromBoresight(boresight, orientation, detector, flipX=cls.wcsFlipX) 

262 

263 def _createSkyWcsFromMetadata(self): 

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

265 

266 Returns 

267 ------- 

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

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

270 creation fails due to invalid metadata. 

271 """ 

272 if not self.isOnSky(): 

273 # This is not an on-sky observation 

274 return None 

275 

276 try: 

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

278 except TypeError as e: 

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

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

281 return None 

282 

283 def makeFilter(self): 

284 """Construct a Filter from metadata. 

285 

286 Returns 

287 ------- 

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

289 Object that identifies the filter for this image. 

290 

291 Raises 

292 ------ 

293 NotFoundError 

294 Raised if the physical filter was not registered via 

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

296 """ 

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

298 

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

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

301 

302 Parameters 

303 ---------- 

304 component : `str`, optional 

305 Component to read from the file. 

306 parameters : `dict`, optional 

307 If specified, a dictionary of slicing parameters that 

308 overrides those in ``fileDescriptor``. 

309 

310 Returns 

311 ------- 

312 obj : component-dependent 

313 In-memory component object. 

314 

315 Raises 

316 ------ 

317 KeyError 

318 Raised if the requested component cannot be handled. 

319 """ 

320 if component == "image": 

321 return self.readImage() 

322 elif component == "mask": 

323 return self.readMask() 

324 elif component == "variance": 

325 return self.readVariance() 

326 elif component == "filter": 

327 return self.makeFilter() 

328 elif component == "visitInfo": 

329 return self.makeVisitInfo() 

330 elif component == "wcs": 

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

332 visitInfo = self.makeVisitInfo() 

333 return self.makeWcs(visitInfo, detector) 

334 return None 

335 

336 def readFull(self, parameters=None): 

337 """Read the full Exposure object. 

338 

339 Parameters 

340 ---------- 

341 parameters : `dict`, optional 

342 If specified, a dictionary of slicing parameters that overrides 

343 those in the `fileDescriptor` attribute. 

344 

345 Returns 

346 ------- 

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

348 Complete in-memory exposure. 

349 """ 

350 from lsst.afw.image import makeExposure, makeMaskedImage 

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

352 mask = self.readMask() 

353 if mask is not None: 

354 full.setMask(mask) 

355 variance = self.readVariance() 

356 if variance is not None: 

357 full.setVariance(variance) 

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

359 info = full.getInfo() 

360 info.setFilter(self.makeFilter()) 

361 info.setVisitInfo(self.makeVisitInfo()) 

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

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

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

365 full.setMetadata(self.metadata) 

366 return full 

367 

368 def readRawHeaderWcs(self, parameters=None): 

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

370 """ 

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

372 

373 def write(self, inMemoryDataset): 

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

375 

376 Parameters 

377 ---------- 

378 inMemoryDataset : `object` 

379 The Python object to store. 

380 

381 Returns 

382 ------- 

383 path : `str` 

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

385 """ 

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

387 

388 @property 

389 def observationInfo(self): 

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

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

392 read-only). 

393 """ 

394 if self._observationInfo is None: 

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

396 return self._observationInfo