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 abstractmethod 

25from deprecated.sphinx import deprecated 

26 

27from astro_metadata_translator import fix_header, ObservationInfo 

28 

29import lsst.afw.fits 

30import lsst.afw.geom 

31import lsst.afw.image 

32from lsst.daf.butler import FileDescriptor 

33from lsst.daf.butler.core.utils import cached_getter 

34import lsst.log 

35 

36from .formatters.fitsExposure import FitsImageFormatterBase, standardizeAmplifierParameters 

37from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo 

38from .utils import createInitialSkyWcsFromBoresight, InitialSkyWcsError 

39 

40 

41class FitsRawFormatterBase(FitsImageFormatterBase): 

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

43 FITS files. 

44 """ 

45 

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

47 wcsFlipX = False 

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

49 

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

51 self.filterDefinitions.reset() 

52 self.filterDefinitions.defineFilters() 

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

54 self._metadata = None 

55 self._observationInfo = None 

56 

57 @classmethod 

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

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

60 

61 Parameters 

62 ---------- 

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

64 Raw header metadata, with any fixes (see 

65 `astro_metadata_translator.fix_header`) applied but nothing 

66 stripped. 

67 obsInfo : `astro_metadata_translator.ObservationInfo`, optional 

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

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

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

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

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

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

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

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

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

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

78 

79 Returns 

80 ------- 

81 formatter : `FitsRawFormatterBase` 

82 An instance of ``cls``. 

83 """ 

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

85 self._metadata = metadata 

86 self._observationInfo = obsInfo 

87 return self 

88 

89 @property 

90 @abstractmethod 

91 def translatorClass(self): 

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

93 metadata header to `~astro_metadata_translator.ObservationInfo`. 

94 """ 

95 return None 

96 

97 @property 

98 @abstractmethod 

99 def filterDefinitions(self): 

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

101 instrument. 

102 """ 

103 return None 

104 

105 @property 

106 @cached_getter 

107 def checked_parameters(self): 

108 # Docstring inherited. 

109 parameters = super().checked_parameters 

110 if "bbox" in parameters: 

111 raise TypeError("Raw formatters do not support reading arbitrary subimages, as some " 

112 "implementations may be assembled on-the-fly.") 

113 return parameters 

114 

115 def readImage(self): 

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

117 

118 Returns 

119 ------- 

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

121 In-memory image component. 

122 """ 

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

124 

125 def isOnSky(self): 

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

127 

128 Returns 

129 ------- 

130 onSky : `bool` 

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

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

133 observation. 

134 

135 Notes 

136 ----- 

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

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

139 Currently the observation type is not checked. 

140 """ 

141 if self.observationInfo.tracking_radec is None: 

142 return False 

143 return True 

144 

145 @property 

146 def metadata(self): 

147 """The metadata read from this file. It will be stripped as 

148 components are extracted from it 

149 (`lsst.daf.base.PropertyList`). 

150 """ 

151 if self._metadata is None: 

152 self._metadata = self.readMetadata() 

153 return self._metadata 

154 

155 def readMetadata(self): 

156 """Read all header metadata directly into a PropertyList. 

157 

158 Returns 

159 ------- 

160 metadata : `~lsst.daf.base.PropertyList` 

161 Header metadata. 

162 """ 

163 md = lsst.afw.fits.readMetadata(self.fileDescriptor.location.path) 

164 fix_header(md) 

165 return md 

166 

167 def stripMetadata(self): 

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

169 """ 

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 # TODO: remove in DM-27177 

284 @deprecated(reason="Replaced with makeFilterLabel. Will be removed after v22.", 

285 version="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): 

316 # Docstring inherited. 

317 self.checked_parameters # just for checking; no supported parameters. 

318 if component == "image": 

319 return self.readImage() 

320 elif component == "filter": 

321 return self.makeFilter() 

322 elif component == "filterLabel": 

323 return self.makeFilterLabel() 

324 elif component == "visitInfo": 

325 return self.makeVisitInfo() 

326 elif component == "detector": 

327 return self.getDetector(self.observationInfo.detector_num) 

328 elif component == "wcs": 

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

330 visitInfo = self.makeVisitInfo() 

331 return self.makeWcs(visitInfo, detector) 

332 elif component == "metadata": 

333 self.stripMetadata() 

334 return self.metadata 

335 return None 

336 

337 def readFull(self): 

338 # Docstring inherited. 

339 amplifier, detector, _ = standardizeAmplifierParameters( 

340 self.checked_parameters, 

341 self.getDetector(self.observationInfo.detector_num), 

342 ) 

343 if amplifier is not None: 

344 reader = lsst.afw.image.ImageFitsReader(self.fileDescriptor.location.path) 

345 amplifier_isolator = lsst.afw.cameraGeom.AmplifierIsolator( 

346 amplifier, 

347 reader.readBBox(), 

348 detector, 

349 ) 

350 subimage = amplifier_isolator.transform_subimage( 

351 reader.read(bbox=amplifier_isolator.subimage_bbox) 

352 ) 

353 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(subimage)) 

354 exposure.setDetector(amplifier_isolator.make_detector()) 

355 else: 

356 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(self.readImage())) 

357 exposure.setDetector(detector) 

358 self.attachComponentsFromMetadata(exposure) 

359 return exposure 

360 

361 def write(self, inMemoryDataset): 

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

363 

364 Parameters 

365 ---------- 

366 inMemoryDataset : `object` 

367 The Python object to store. 

368 

369 Returns 

370 ------- 

371 path : `str` 

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

373 """ 

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

375 

376 @property 

377 def observationInfo(self): 

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

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

380 read-only). 

381 """ 

382 if self._observationInfo is None: 

383 location = self.fileDescriptor.location 

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

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

386 filename=path) 

387 return self._observationInfo 

388 

389 def attachComponentsFromMetadata(self, exposure): 

390 """Attach all `lsst.afw.image.Exposure` components derived from 

391 metadata (including the stripped metadata itself). 

392 

393 Parameters 

394 ---------- 

395 exposure : `lsst.afw.image.Exposure` 

396 Exposure to attach components to (modified in place). Must already 

397 have a detector attached. 

398 """ 

399 info = exposure.getInfo() 

400 info.setFilterLabel(self.makeFilterLabel()) 

401 info.setVisitInfo(self.makeVisitInfo()) 

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

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

404 # been stripped during creation of the WCS. 

405 exposure.setMetadata(self.metadata)