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 

25from deprecated.sphinx import deprecated 

26 

27from astro_metadata_translator import ObservationInfo 

28 

29import lsst.afw.fits 

30import lsst.afw.geom 

31import lsst.afw.image 

32from lsst.daf.butler import FileDescriptor 

33import lsst.log 

34 

35from .formatters.fitsExposure import FitsImageFormatterBase 

36from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo 

37from .utils import createInitialSkyWcsFromBoresight, InitialSkyWcsError 

38 

39 

40class FitsRawFormatterBase(FitsImageFormatterBase, metaclass=ABCMeta): 

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

42 FITS files. 

43 """ 

44 

45 # This has to be explicit until we fix camera geometry in DM-20746 

46 wcsFlipX = False 

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

48 

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

50 self.filterDefinitions.reset() 

51 self.filterDefinitions.defineFilters() 

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

53 

54 @classmethod 

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

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

57 

58 Parameters 

59 ---------- 

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

61 Raw header metadata, with any fixes (see 

62 `astro_metadata_translator.fix_header`) applied but nothing 

63 stripped. 

64 obsInfo : `astro_metadata_translator.ObservationInfo`, optional 

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

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

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

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

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

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

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

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

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

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

75 

76 Returns 

77 ------- 

78 formatter : `FitsRawFormatterBase` 

79 An instance of ``cls``. 

80 """ 

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

82 self._metadata = metadata 

83 self._observationInfo = obsInfo 

84 return self 

85 

86 @property 

87 @abstractmethod 

88 def translatorClass(self): 

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

90 metadata header to `~astro_metadata_translator.ObservationInfo`. 

91 """ 

92 return None 

93 

94 _observationInfo = None 

95 

96 @property 

97 @abstractmethod 

98 def filterDefinitions(self): 

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

100 instrument. 

101 """ 

102 return None 

103 

104 def readImage(self): 

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

106 

107 Returns 

108 ------- 

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

110 In-memory image component. 

111 """ 

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

113 

114 def readMask(self): 

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

116 

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

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

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

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

121 

122 Returns 

123 ------- 

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

125 In-memory mask component. 

126 """ 

127 return None 

128 

129 def readVariance(self): 

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

131 

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

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

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

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

136 

137 Returns 

138 ------- 

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

140 In-memory variance component. 

141 """ 

142 return None 

143 

144 def isOnSky(self): 

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

146 

147 Returns 

148 ------- 

149 onSky : `bool` 

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

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

152 observation. 

153 

154 Notes 

155 ----- 

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

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

158 Currently the observation type is not checked. 

159 """ 

160 if self.observationInfo.tracking_radec is None: 

161 return False 

162 return True 

163 

164 def stripMetadata(self): 

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

166 """ 

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

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

169 # will strip the VisitInfo keys and more. 

170 self.makeVisitInfo() 

171 self._createSkyWcsFromMetadata() 

172 

173 def makeVisitInfo(self): 

174 """Construct a VisitInfo from metadata. 

175 

176 Returns 

177 ------- 

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

179 Structured metadata about the observation. 

180 """ 

181 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo) 

182 

183 @abstractmethod 

184 def getDetector(self, id): 

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

186 

187 Parameters 

188 ---------- 

189 id : `int` 

190 The identifying number of the detector to get. 

191 

192 Returns 

193 ------- 

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

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

196 """ 

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

198 

199 def makeWcs(self, visitInfo, detector): 

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

201 

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

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

204 the relevant metadata keywords are stripped). 

205 

206 Parameters 

207 ---------- 

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

209 The information about the telescope boresight and camera 

210 orientation angle for this exposure. 

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

212 The detector used to acquire this exposure. 

213 

214 Returns 

215 ------- 

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

217 Reversible mapping from pixel coordinates to sky coordinates. 

218 

219 Raises 

220 ------ 

221 InitialSkyWcsError 

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

223 lower-level exception if available. 

224 """ 

225 if not self.isOnSky(): 

226 # This is not an on-sky observation 

227 return None 

228 

229 skyWcs = self._createSkyWcsFromMetadata() 

230 

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

232 if visitInfo is None: 

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

234 log.warn(msg) 

235 if skyWcs is None: 

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

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

238 return skyWcs 

239 

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

241 visitInfo.getBoresightRotAngle(), 

242 detector) 

243 

244 @classmethod 

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

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

247 

248 Parameters 

249 ---------- 

250 boresight : `lsst.geom.SpherePoint` 

251 The ICRS boresight RA/Dec 

252 orientation : `lsst.geom.Angle` 

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

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

255 Where to get the camera geomtry from. 

256 

257 Returns 

258 ------- 

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

260 Reversible mapping from pixel coordinates to sky coordinates. 

261 """ 

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

263 

264 def _createSkyWcsFromMetadata(self): 

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

266 

267 Returns 

268 ------- 

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

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

271 creation fails due to invalid metadata. 

272 """ 

273 if not self.isOnSky(): 

274 # This is not an on-sky observation 

275 return None 

276 

277 try: 

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

279 except TypeError as e: 

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

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

282 return None 

283 

284 # TODO: remove in DM-27177 

285 @deprecated(reason="Replaced with makeFilterLabel. Will be removed after v22.", category=FutureWarning) 

286 def makeFilter(self): 

287 """Construct a Filter from metadata. 

288 

289 Returns 

290 ------- 

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

292 Object that identifies the filter for this image. 

293 

294 Raises 

295 ------ 

296 NotFoundError 

297 Raised if the physical filter was not registered via 

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

299 """ 

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

301 

302 # TODO: deprecate in DM-27177, remove in DM-27811 

303 def makeFilterLabel(self): 

304 """Construct a FilterLabel from metadata. 

305 

306 Returns 

307 ------- 

308 filter : `~lsst.afw.image.FilterLabel` 

309 Object that identifies the filter for this image. 

310 """ 

311 physical = self.observationInfo.physical_filter 

312 band = self.filterDefinitions.physical_to_band[physical] 

313 return lsst.afw.image.FilterLabel(physical=physical, band=band) 

314 

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

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

317 

318 Parameters 

319 ---------- 

320 component : `str`, optional 

321 Component to read from the file. 

322 parameters : `dict`, optional 

323 If specified, a dictionary of slicing parameters that 

324 overrides those in ``fileDescriptor``. 

325 

326 Returns 

327 ------- 

328 obj : component-dependent 

329 In-memory component object. 

330 

331 Raises 

332 ------ 

333 KeyError 

334 Raised if the requested component cannot be handled. 

335 """ 

336 if component == "image": 

337 return self.readImage() 

338 elif component == "mask": 

339 return self.readMask() 

340 elif component == "variance": 

341 return self.readVariance() 

342 elif component == "filter": 

343 return self.makeFilter() 

344 elif component == "filterLabel": 

345 return self.makeFilterLabel() 

346 elif component == "visitInfo": 

347 return self.makeVisitInfo() 

348 elif component == "wcs": 

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

350 visitInfo = self.makeVisitInfo() 

351 return self.makeWcs(visitInfo, detector) 

352 return None 

353 

354 def readFull(self, parameters=None): 

355 """Read the full Exposure object. 

356 

357 Parameters 

358 ---------- 

359 parameters : `dict`, optional 

360 If specified, a dictionary of slicing parameters that overrides 

361 those in the `fileDescriptor` attribute. 

362 

363 Returns 

364 ------- 

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

366 Complete in-memory exposure. 

367 """ 

368 from lsst.afw.image import makeExposure, makeMaskedImage 

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

370 mask = self.readMask() 

371 if mask is not None: 

372 full.setMask(mask) 

373 variance = self.readVariance() 

374 if variance is not None: 

375 full.setVariance(variance) 

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

377 info = full.getInfo() 

378 info.setFilterLabel(self.makeFilterLabel()) 

379 info.setVisitInfo(self.makeVisitInfo()) 

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

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

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

383 full.setMetadata(self.metadata) 

384 return full 

385 

386 def readRawHeaderWcs(self, parameters=None): 

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

388 """ 

389 return lsst.afw.geom.makeSkyWcs(lsst.afw.fits.readMetadata(self.fileDescriptor.location.path)) 

390 

391 def write(self, inMemoryDataset): 

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

393 

394 Parameters 

395 ---------- 

396 inMemoryDataset : `object` 

397 The Python object to store. 

398 

399 Returns 

400 ------- 

401 path : `str` 

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

403 """ 

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

405 

406 @property 

407 def observationInfo(self): 

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

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

410 read-only). 

411 """ 

412 if self._observationInfo is None: 

413 location = self.fileDescriptor.location 

414 path = location.path if location is not None else None 

415 self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass, 

416 filename=path) 

417 return self._observationInfo