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.", 

286 version="v22", category=FutureWarning) 

287 def makeFilter(self): 

288 """Construct a Filter from metadata. 

289 

290 Returns 

291 ------- 

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

293 Object that identifies the filter for this image. 

294 

295 Raises 

296 ------ 

297 NotFoundError 

298 Raised if the physical filter was not registered via 

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

300 """ 

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

302 

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

304 def makeFilterLabel(self): 

305 """Construct a FilterLabel from metadata. 

306 

307 Returns 

308 ------- 

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

310 Object that identifies the filter for this image. 

311 """ 

312 physical = self.observationInfo.physical_filter 

313 band = self.filterDefinitions.physical_to_band[physical] 

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

315 

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

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

318 

319 Parameters 

320 ---------- 

321 component : `str`, optional 

322 Component to read from the file. 

323 parameters : `dict`, optional 

324 If specified, a dictionary of slicing parameters that 

325 overrides those in ``fileDescriptor``. 

326 

327 Returns 

328 ------- 

329 obj : component-dependent 

330 In-memory component object. 

331 

332 Raises 

333 ------ 

334 KeyError 

335 Raised if the requested component cannot be handled. 

336 """ 

337 if component == "image": 

338 return self.readImage() 

339 elif component == "mask": 

340 return self.readMask() 

341 elif component == "variance": 

342 return self.readVariance() 

343 elif component == "filter": 

344 return self.makeFilter() 

345 elif component == "filterLabel": 

346 return self.makeFilterLabel() 

347 elif component == "visitInfo": 

348 return self.makeVisitInfo() 

349 elif component == "wcs": 

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

351 visitInfo = self.makeVisitInfo() 

352 return self.makeWcs(visitInfo, detector) 

353 return None 

354 

355 def readFull(self, parameters=None): 

356 """Read the full Exposure object. 

357 

358 Parameters 

359 ---------- 

360 parameters : `dict`, optional 

361 If specified, a dictionary of slicing parameters that overrides 

362 those in the `fileDescriptor` attribute. 

363 

364 Returns 

365 ------- 

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

367 Complete in-memory exposure. 

368 """ 

369 from lsst.afw.image import makeExposure, makeMaskedImage 

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

371 mask = self.readMask() 

372 if mask is not None: 

373 full.setMask(mask) 

374 variance = self.readVariance() 

375 if variance is not None: 

376 full.setVariance(variance) 

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

378 info = full.getInfo() 

379 info.setFilterLabel(self.makeFilterLabel()) 

380 info.setVisitInfo(self.makeVisitInfo()) 

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

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

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

384 full.setMetadata(self.metadata) 

385 return full 

386 

387 def readRawHeaderWcs(self, parameters=None): 

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

389 """ 

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

391 

392 def write(self, inMemoryDataset): 

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

394 

395 Parameters 

396 ---------- 

397 inMemoryDataset : `object` 

398 The Python object to store. 

399 

400 Returns 

401 ------- 

402 path : `str` 

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

404 """ 

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

406 

407 @property 

408 def observationInfo(self): 

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

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

411 read-only). 

412 """ 

413 if self._observationInfo is None: 

414 location = self.fileDescriptor.location 

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

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

417 filename=path) 

418 return self._observationInfo