Coverage for python/lsst/obs/base/_fitsRawFormatterBase.py: 30%

143 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 03:54 -0700

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 

24import logging 

25from abc import abstractmethod 

26 

27import lsst.afw.fits 

28import lsst.afw.geom 

29import lsst.afw.image 

30from astro_metadata_translator import ObservationInfo, fix_header 

31from deprecated.sphinx import deprecated 

32from lsst.daf.butler import FileDescriptor 

33from lsst.utils.classes import cached_getter 

34 

35from .formatters.fitsExposure import FitsImageFormatterBase, standardizeAmplifierParameters 

36from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo 

37from .utils import InitialSkyWcsError, createInitialSkyWcsFromBoresight 

38 

39log = logging.getLogger(__name__) 

40 

41 

42class FitsRawFormatterBase(FitsImageFormatterBase): 

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

44 FITS files. 

45 """ 

46 

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

48 wcsFlipX = False 

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

50 

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

52 self.filterDefinitions.reset() 

53 self.filterDefinitions.defineFilters() 

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

55 self._metadata = None 

56 self._observationInfo = None 

57 

58 @classmethod 

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

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

61 

62 Parameters 

63 ---------- 

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

65 Raw header metadata, with any fixes (see 

66 `astro_metadata_translator.fix_header`) applied but nothing 

67 stripped. 

68 obsInfo : `astro_metadata_translator.ObservationInfo`, optional 

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

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

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

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

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

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

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

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

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

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

79 

80 Returns 

81 ------- 

82 formatter : `FitsRawFormatterBase` 

83 An instance of ``cls``. 

84 """ 

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

86 self._metadata = metadata 

87 self._observationInfo = obsInfo 

88 return self 

89 

90 @property 

91 @abstractmethod 

92 def translatorClass(self): 

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

94 metadata header to `~astro_metadata_translator.ObservationInfo`. 

95 """ 

96 return None 

97 

98 @property 

99 @abstractmethod 

100 def filterDefinitions(self): 

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

102 instrument. 

103 """ 

104 return None 

105 

106 @property # type: ignore 

107 @cached_getter 

108 def checked_parameters(self): 

109 # Docstring inherited. 

110 parameters = super().checked_parameters 

111 if "bbox" in parameters: 

112 raise TypeError( 

113 "Raw formatters do not support reading arbitrary subimages, as some " 

114 "implementations may be assembled on-the-fly." 

115 ) 

116 return parameters 

117 

118 def readImage(self): 

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

120 

121 Returns 

122 ------- 

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

124 In-memory image component. 

125 """ 

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

127 

128 def isOnSky(self): 

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

130 

131 Returns 

132 ------- 

133 onSky : `bool` 

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

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

136 observation. 

137 

138 Notes 

139 ----- 

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

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

142 Currently the observation type is not checked. 

143 """ 

144 if self.observationInfo.tracking_radec is None: 

145 return False 

146 return True 

147 

148 @property 

149 def metadata(self): 

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

151 components are extracted from it 

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

153 """ 

154 if self._metadata is None: 

155 self._metadata = self.readMetadata() 

156 return self._metadata 

157 

158 def readMetadata(self): 

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

160 

161 Returns 

162 ------- 

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

164 Header metadata. 

165 """ 

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

167 fix_header(md) 

168 return md 

169 

170 def stripMetadata(self): 

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

172 self._createSkyWcsFromMetadata() 

173 

174 def makeVisitInfo(self): 

175 """Construct a VisitInfo from metadata. 

176 

177 Returns 

178 ------- 

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

180 Structured metadata about the observation. 

181 """ 

182 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo) 

183 

184 @abstractmethod 

185 def getDetector(self, id): 

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

187 

188 Parameters 

189 ---------- 

190 id : `int` 

191 The identifying number of the detector to get. 

192 

193 Returns 

194 ------- 

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

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

197 """ 

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

199 

200 def makeWcs(self, visitInfo, detector): 

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

202 

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

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

205 the relevant metadata keywords are stripped). 

206 

207 Parameters 

208 ---------- 

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

210 The information about the telescope boresight and camera 

211 orientation angle for this exposure. 

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

213 The detector used to acquire this exposure. 

214 

215 Returns 

216 ------- 

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

218 Reversible mapping from pixel coordinates to sky coordinates. 

219 

220 Raises 

221 ------ 

222 InitialSkyWcsError 

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

224 lower-level exception if available. 

225 """ 

226 if not self.isOnSky(): 

227 # This is not an on-sky observation 

228 return None 

229 

230 skyWcs = self._createSkyWcsFromMetadata() 

231 

232 if visitInfo is None: 

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

234 log.warning(msg) 

235 if skyWcs is None: 

236 raise InitialSkyWcsError( 

237 "Failed to create both metadata and boresight-based SkyWcs." 

238 "See warnings in log messages for details." 

239 ) 

240 return skyWcs 

241 

242 return self.makeRawSkyWcsFromBoresight( 

243 visitInfo.getBoresightRaDec(), visitInfo.getBoresightRotAngle(), detector 

244 ) 

245 

246 @classmethod 

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

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

249 

250 Parameters 

251 ---------- 

252 boresight : `lsst.geom.SpherePoint` 

253 The ICRS boresight RA/Dec 

254 orientation : `lsst.geom.Angle` 

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

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

257 Where to get the camera geomtry from. 

258 

259 Returns 

260 ------- 

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

262 Reversible mapping from pixel coordinates to sky coordinates. 

263 """ 

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

265 

266 def _createSkyWcsFromMetadata(self): 

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

268 

269 Returns 

270 ------- 

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

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

273 creation fails due to invalid metadata. 

274 """ 

275 if not self.isOnSky(): 

276 # This is not an on-sky observation 

277 return None 

278 

279 try: 

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

281 except TypeError as e: 

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

283 return None 

284 

285 # TODO: remove in DM-27177 

286 @deprecated( 

287 reason="Replaced with makeFilterLabel. Will be removed after v22.", 

288 version="v22", 

289 category=FutureWarning, 

290 ) 

291 def makeFilter(self): 

292 """Construct a Filter from metadata. 

293 

294 Returns 

295 ------- 

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

297 Object that identifies the filter for this image. 

298 

299 Raises 

300 ------ 

301 NotFoundError 

302 Raised if the physical filter was not registered via 

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

304 """ 

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

306 

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

308 def makeFilterLabel(self): 

309 """Construct a FilterLabel from metadata. 

310 

311 Returns 

312 ------- 

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

314 Object that identifies the filter for this image. 

315 """ 

316 physical = self.observationInfo.physical_filter 

317 band = self.filterDefinitions.physical_to_band[physical] 

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

319 

320 def readComponent(self, component): 

321 # Docstring inherited. 

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

323 if component == "image": 

324 return self.readImage() 

325 elif component == "filter": 

326 return self.makeFilter() 

327 elif component == "filterLabel": 

328 return self.makeFilterLabel() 

329 elif component == "visitInfo": 

330 return self.makeVisitInfo() 

331 elif component == "detector": 

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

333 elif component == "wcs": 

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

335 visitInfo = self.makeVisitInfo() 

336 return self.makeWcs(visitInfo, detector) 

337 elif component == "metadata": 

338 self.stripMetadata() 

339 return self.metadata 

340 return None 

341 

342 def readFull(self): 

343 # Docstring inherited. 

344 amplifier, detector, _ = standardizeAmplifierParameters( 

345 self.checked_parameters, 

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

347 ) 

348 if amplifier is not None: 

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

350 amplifier_isolator = lsst.afw.cameraGeom.AmplifierIsolator( 

351 amplifier, 

352 reader.readBBox(), 

353 detector, 

354 ) 

355 subimage = amplifier_isolator.transform_subimage( 

356 reader.read(bbox=amplifier_isolator.subimage_bbox) 

357 ) 

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

359 exposure.setDetector(amplifier_isolator.make_detector()) 

360 else: 

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

362 exposure.setDetector(detector) 

363 self.attachComponentsFromMetadata(exposure) 

364 return exposure 

365 

366 def write(self, inMemoryDataset): 

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

368 

369 Parameters 

370 ---------- 

371 inMemoryDataset : `object` 

372 The Python object to store. 

373 

374 Returns 

375 ------- 

376 path : `str` 

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

378 """ 

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

380 

381 @property 

382 def observationInfo(self): 

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

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

385 read-only). 

386 """ 

387 if self._observationInfo is None: 

388 location = self.fileDescriptor.location 

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

390 self._observationInfo = ObservationInfo( 

391 self.metadata, translator_class=self.translatorClass, filename=path 

392 ) 

393 return self._observationInfo 

394 

395 def attachComponentsFromMetadata(self, exposure): 

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

397 metadata (including the stripped metadata itself). 

398 

399 Parameters 

400 ---------- 

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

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

403 have a detector attached. 

404 """ 

405 info = exposure.getInfo() 

406 info.id = self.observationInfo.detector_exposure_id 

407 info.setFilterLabel(self.makeFilterLabel()) 

408 info.setVisitInfo(self.makeVisitInfo()) 

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

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

411 # been stripped during creation of the WCS. 

412 exposure.setMetadata(self.metadata)