Coverage for python/lsst/obs/base/_fitsRawFormatterBase.py: 29%
139 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-11 11:06 +0000
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-11 11:06 +0000
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/>.
22__all__ = ("FitsRawFormatterBase",)
24import logging
25import warnings
26from abc import abstractmethod
28import lsst.afw.fits
29import lsst.afw.geom
30import lsst.afw.image
31from astro_metadata_translator import ObservationInfo, fix_header
32from lsst.daf.butler import FileDescriptor
33from lsst.utils.classes import cached_getter
35from .formatters.fitsExposure import FitsImageFormatterBase, standardizeAmplifierParameters
36from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo
37from .utils import InitialSkyWcsError, createInitialSkyWcsFromBoresight
39log = logging.getLogger(__name__)
42class FitsRawFormatterBase(FitsImageFormatterBase):
43 """Abstract base class for reading and writing raw data to and from
44 FITS files.
45 """
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`)"""
51 def __init__(self, *args, **kwargs):
52 super().__init__(*args, **kwargs)
53 self._metadata = None
54 self._observationInfo = None
56 @classmethod
57 def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None):
58 """Construct a possibly-limited formatter from known metadata.
60 Parameters
61 ----------
62 metadata : `lsst.daf.base.PropertyList`
63 Raw header metadata, with any fixes (see
64 `astro_metadata_translator.fix_header`) applied but nothing
65 stripped.
66 obsInfo : `astro_metadata_translator.ObservationInfo`, optional
67 Structured information already extracted from ``metadata``.
68 If not provided, will be read from ``metadata`` on first use.
69 storageClass : `lsst.daf.butler.StorageClass`, optional
70 StorageClass for this file. If not provided, the formatter will
71 only support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
72 operations that operate purely on metadata and not the actual file.
73 location : `lsst.daf.butler.Location`, optional.
74 Location of the file. If not provided, the formatter will only
75 support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
76 operations that operate purely on metadata and not the actual file.
78 Returns
79 -------
80 formatter : `FitsRawFormatterBase`
81 An instance of ``cls``.
82 """
83 self = cls(FileDescriptor(location, storageClass))
84 self._metadata = metadata
85 self._observationInfo = obsInfo
86 return self
88 @property
89 @abstractmethod
90 def translatorClass(self):
91 """`~astro_metadata_translator.MetadataTranslator` to translate
92 metadata header to `~astro_metadata_translator.ObservationInfo`.
93 """
94 return None
96 @property
97 @abstractmethod
98 def filterDefinitions(self):
99 """`~lsst.obs.base.FilterDefinitions`, defining the filters for this
100 instrument.
101 """
102 return None
104 @property # type: ignore
105 @cached_getter
106 def checked_parameters(self):
107 # Docstring inherited.
108 parameters = super().checked_parameters
109 if "bbox" in parameters:
110 raise TypeError(
111 "Raw formatters do not support reading arbitrary subimages, as some "
112 "implementations may be assembled on-the-fly."
113 )
114 return parameters
116 def readImage(self):
117 """Read just the image component of the Exposure.
119 Returns
120 -------
121 image : `~lsst.afw.image.Image`
122 In-memory image component.
123 """
124 return lsst.afw.image.ImageU(self.fileDescriptor.location.path)
126 def isOnSky(self):
127 """Boolean to determine if the exposure is thought to be on the sky.
129 Returns
130 -------
131 onSky : `bool`
132 Returns `True` if the observation looks like it was taken on the
133 sky. Returns `False` if this observation looks like a calibration
134 observation.
136 Notes
137 -----
138 If there is tracking RA/Dec information associated with the
139 observation it is assumed that the observation is on sky.
140 Currently the observation type is not checked.
141 """
142 if self.observationInfo.tracking_radec is None:
143 return False
144 return True
146 @property
147 def metadata(self):
148 """The metadata read from this file. It will be stripped as
149 components are extracted from it
150 (`lsst.daf.base.PropertyList`).
151 """
152 if self._metadata is None:
153 self._metadata = self.readMetadata()
154 return self._metadata
156 def readMetadata(self):
157 """Read all header metadata directly into a PropertyList.
159 Returns
160 -------
161 metadata : `~lsst.daf.base.PropertyList`
162 Header metadata.
163 """
164 md = lsst.afw.fits.readMetadata(self.fileDescriptor.location.path)
165 fix_header(md)
166 return md
168 def stripMetadata(self):
169 """Remove metadata entries that are parsed into components."""
170 self._createSkyWcsFromMetadata()
172 def makeVisitInfo(self):
173 """Construct a VisitInfo from metadata.
175 Returns
176 -------
177 visitInfo : `~lsst.afw.image.VisitInfo`
178 Structured metadata about the observation.
179 """
180 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo)
182 @abstractmethod
183 def getDetector(self, id):
184 """Return the detector that acquired this raw exposure.
186 Parameters
187 ----------
188 id : `int`
189 The identifying number of the detector to get.
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.")
198 def makeWcs(self, visitInfo, detector):
199 """Create a SkyWcs from information about the exposure.
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).
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.
213 Returns
214 -------
215 skyWcs : `~lsst.afw.geom.SkyWcs`
216 Reversible mapping from pixel coordinates to sky coordinates.
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
228 skyWcs = self._createSkyWcsFromMetadata()
230 if visitInfo is None:
231 msg = "No VisitInfo; cannot access boresight information. Defaulting to metadata-based SkyWcs."
232 log.warning(msg)
233 if skyWcs is None:
234 raise InitialSkyWcsError(
235 "Failed to create both metadata and boresight-based SkyWcs."
236 "See warnings in log messages for details."
237 )
238 return skyWcs
240 return self.makeRawSkyWcsFromBoresight(
241 visitInfo.getBoresightRaDec(), visitInfo.getBoresightRotAngle(), detector
242 )
244 @classmethod
245 def makeRawSkyWcsFromBoresight(cls, boresight, orientation, detector):
246 """Class method to make a raw sky WCS from boresight and detector.
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.
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)
264 def _createSkyWcsFromMetadata(self):
265 """Create a SkyWcs from the FITS header metadata in an Exposure.
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
277 try:
278 return lsst.afw.geom.makeSkyWcs(self.metadata, strip=True)
279 except TypeError as e:
280 log.warning("Cannot create a valid WCS from metadata: %s", e.args[0])
281 return None
283 def makeFilterLabel(self):
284 """Construct a FilterLabel from metadata.
286 Returns
287 -------
288 filter : `~lsst.afw.image.FilterLabel`
289 Object that identifies the filter for this image.
290 """
291 physical = self.observationInfo.physical_filter
292 band = self.filterDefinitions.physical_to_band[physical]
293 return lsst.afw.image.FilterLabel(physical=physical, band=band)
295 def readComponent(self, component):
296 # Docstring inherited.
297 self.checked_parameters # just for checking; no supported parameters.
298 if component == "image":
299 return self.readImage()
300 elif component == "filter":
301 return self.makeFilterLabel()
302 # TODO: remove in DM-27811
303 elif component == "filterLabel":
304 warnings.warn(
305 "Exposure.filterLabel component is deprecated; use .filter instead. "
306 "Will be removed after v24.",
307 FutureWarning,
308 )
309 return self.makeFilterLabel()
310 elif component == "visitInfo":
311 return self.makeVisitInfo()
312 elif component == "detector":
313 return self.getDetector(self.observationInfo.detector_num)
314 elif component == "wcs":
315 detector = self.getDetector(self.observationInfo.detector_num)
316 visitInfo = self.makeVisitInfo()
317 return self.makeWcs(visitInfo, detector)
318 elif component == "metadata":
319 self.stripMetadata()
320 return self.metadata
321 return None
323 def readFull(self):
324 # Docstring inherited.
325 amplifier, detector, _ = standardizeAmplifierParameters(
326 self.checked_parameters,
327 self.getDetector(self.observationInfo.detector_num),
328 )
329 if amplifier is not None:
330 reader = lsst.afw.image.ImageFitsReader(self.fileDescriptor.location.path)
331 amplifier_isolator = lsst.afw.cameraGeom.AmplifierIsolator(
332 amplifier,
333 reader.readBBox(),
334 detector,
335 )
336 subimage = amplifier_isolator.transform_subimage(
337 reader.read(bbox=amplifier_isolator.subimage_bbox)
338 )
339 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(subimage))
340 exposure.setDetector(amplifier_isolator.make_detector())
341 else:
342 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(self.readImage()))
343 exposure.setDetector(detector)
344 self.attachComponentsFromMetadata(exposure)
345 return exposure
347 def write(self, inMemoryDataset):
348 """Write a Python object to a file.
350 Parameters
351 ----------
352 inMemoryDataset : `object`
353 The Python object to store.
355 Returns
356 -------
357 path : `str`
358 The `URI` where the primary file is stored.
359 """
360 raise NotImplementedError("Raw data cannot be `put`.")
362 @property
363 def observationInfo(self):
364 """The `~astro_metadata_translator.ObservationInfo` extracted from
365 this file's metadata (`~astro_metadata_translator.ObservationInfo`,
366 read-only).
367 """
368 if self._observationInfo is None:
369 location = self.fileDescriptor.location
370 path = location.path if location is not None else None
371 self._observationInfo = ObservationInfo(
372 self.metadata, translator_class=self.translatorClass, filename=path
373 )
374 return self._observationInfo
376 def attachComponentsFromMetadata(self, exposure):
377 """Attach all `lsst.afw.image.Exposure` components derived from
378 metadata (including the stripped metadata itself).
380 Parameters
381 ----------
382 exposure : `lsst.afw.image.Exposure`
383 Exposure to attach components to (modified in place). Must already
384 have a detector attached.
385 """
386 info = exposure.getInfo()
387 info.id = self.observationInfo.detector_exposure_id
388 info.setFilter(self.makeFilterLabel())
389 info.setVisitInfo(self.makeVisitInfo())
390 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
391 # We don't need to call stripMetadata() here because it has already
392 # been stripped during creation of the WCS.
393 exposure.setMetadata(self.metadata)