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

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/>.
22__all__ = ("FitsRawFormatterBase",)
24from abc import ABCMeta, abstractmethod
26from astro_metadata_translator import ObservationInfo
28import lsst.afw.fits
29import lsst.afw.geom
30import lsst.afw.image
31from lsst.daf.butler import FileDescriptor
32import lsst.log
34from .formatters.fitsExposure import FitsExposureFormatter
35from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo
36from .utils import createInitialSkyWcsFromBoresight, InitialSkyWcsError
39class FitsRawFormatterBase(FitsExposureFormatter, metaclass=ABCMeta):
40 """Abstract base class for reading and writing raw data to and from
41 FITS files.
42 """
44 # This has to be explicit until we fix camera geometry in DM-20746
45 wcsFlipX = False
46 """Control whether the WCS is flipped in the X-direction (`bool`)"""
48 def __init__(self, *args, **kwargs):
49 self.filterDefinitions.reset()
50 self.filterDefinitions.defineFilters()
51 super().__init__(*args, **kwargs)
53 @classmethod
54 def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None):
55 """Construct a possibly-limited formatter from known metadata.
57 Parameters
58 ----------
59 metadata : `lsst.daf.base.PropertyList`
60 Raw header metadata, with any fixes (see
61 `astro_metadata_translator.fix_header`) applied but nothing
62 stripped.
63 obsInfo : `astro_metadata_translator.ObservationInfo`, optional
64 Structured information already extracted from ``metadata``.
65 If not provided, will be read from ``metadata`` on first use.
66 storageClass : `lsst.daf.butler.StorageClass`, optional
67 StorageClass for this file. If not provided, the formatter will
68 only support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
69 operations that operate purely on metadata and not the actual file.
70 location : `lsst.daf.butler.Location`, optional.
71 Location of the file. If not provided, the formatter will only
72 support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
73 operations that operate purely on metadata and not the actual file.
75 Returns
76 -------
77 formatter : `FitsRawFormatterBase`
78 An instance of ``cls``.
79 """
80 self = cls(FileDescriptor(location, storageClass))
81 self._metadata = metadata
82 self._observationInfo = obsInfo
83 return self
85 @property
86 @abstractmethod
87 def translatorClass(self):
88 """`~astro_metadata_translator.MetadataTranslator` to translate
89 metadata header to `~astro_metadata_translator.ObservationInfo`.
90 """
91 return None
93 _observationInfo = None
95 @property
96 @abstractmethod
97 def filterDefinitions(self):
98 """`~lsst.obs.base.FilterDefinitions`, defining the filters for this
99 instrument.
100 """
101 return None
103 def readImage(self):
104 """Read just the image component of the Exposure.
106 Returns
107 -------
108 image : `~lsst.afw.image.Image`
109 In-memory image component.
110 """
111 return lsst.afw.image.ImageU(self.fileDescriptor.location.path)
113 def readMask(self):
114 """Read just the mask component of the Exposure.
116 May return None (as the default implementation does) to indicate that
117 there is no mask information to be extracted (at least not trivially)
118 from the raw data. This will prohibit direct reading of just the mask,
119 and set the mask of the full Exposure to zeros.
121 Returns
122 -------
123 mask : `~lsst.afw.image.Mask`
124 In-memory mask component.
125 """
126 return None
128 def readVariance(self):
129 """Read just the variance component of the Exposure.
131 May return None (as the default implementation does) to indicate that
132 there is no variance information to be extracted (at least not
133 trivially) from the raw data. This will prohibit direct reading of
134 just the variance, and set the variance of the full Exposure to zeros.
136 Returns
137 -------
138 image : `~lsst.afw.image.Image`
139 In-memory variance component.
140 """
141 return None
143 def isOnSky(self):
144 """Boolean to determine if the exposure is thought to be on the sky.
146 Returns
147 -------
148 onSky : `bool`
149 Returns `True` if the observation looks like it was taken on the
150 sky. Returns `False` if this observation looks like a calibration
151 observation.
153 Notes
154 -----
155 If there is tracking RA/Dec information associated with the
156 observation it is assumed that the observation is on sky.
157 Currently the observation type is not checked.
158 """
159 if self.observationInfo.tracking_radec is None:
160 return False
161 return True
163 def stripMetadata(self):
164 """Remove metadata entries that are parsed into components.
165 """
166 # NOTE: makeVisitInfo() may not strip any metadata itself, but calling
167 # it ensures that ObservationInfo is created from the metadata, which
168 # will strip the VisitInfo keys and more.
169 self.makeVisitInfo()
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 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
239 return self.makeRawSkyWcsFromBoresight(visitInfo.getBoresightRaDec(),
240 visitInfo.getBoresightRotAngle(),
241 detector)
243 @classmethod
244 def makeRawSkyWcsFromBoresight(cls, boresight, orientation, detector):
245 """Class method to make a raw sky WCS from boresight and detector.
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.
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)
263 def _createSkyWcsFromMetadata(self):
264 """Create a SkyWcs from the FITS header metadata in an Exposure.
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
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
283 def makeFilter(self):
284 """Construct a Filter from metadata.
286 Returns
287 -------
288 filter : `~lsst.afw.image.Filter`
289 Object that identifies the filter for this image.
291 Raises
292 ------
293 NotFoundError
294 Raised if the physical filter was not registered via
295 `~lsst.afw.image.utils.defineFilter`.
296 """
297 return lsst.afw.image.Filter(self.observationInfo.physical_filter)
299 def readComponent(self, component, parameters=None):
300 """Read a component held by the Exposure.
302 Parameters
303 ----------
304 component : `str`, optional
305 Component to read from the file.
306 parameters : `dict`, optional
307 If specified, a dictionary of slicing parameters that
308 overrides those in ``fileDescriptor``.
310 Returns
311 -------
312 obj : component-dependent
313 In-memory component object.
315 Raises
316 ------
317 KeyError
318 Raised if the requested component cannot be handled.
319 """
320 if component == "image":
321 return self.readImage()
322 elif component == "mask":
323 return self.readMask()
324 elif component == "variance":
325 return self.readVariance()
326 elif component == "filter":
327 return self.makeFilter()
328 elif component == "visitInfo":
329 return self.makeVisitInfo()
330 elif component == "wcs":
331 detector = self.getDetector(self.observationInfo.detector_num)
332 visitInfo = self.makeVisitInfo()
333 return self.makeWcs(visitInfo, detector)
334 return None
336 def readFull(self, parameters=None):
337 """Read the full Exposure object.
339 Parameters
340 ----------
341 parameters : `dict`, optional
342 If specified, a dictionary of slicing parameters that overrides
343 those in the `fileDescriptor` attribute.
345 Returns
346 -------
347 exposure : `~lsst.afw.image.Exposure`
348 Complete in-memory exposure.
349 """
350 from lsst.afw.image import makeExposure, makeMaskedImage
351 full = makeExposure(makeMaskedImage(self.readImage()))
352 mask = self.readMask()
353 if mask is not None:
354 full.setMask(mask)
355 variance = self.readVariance()
356 if variance is not None:
357 full.setVariance(variance)
358 full.setDetector(self.getDetector(self.observationInfo.detector_num))
359 info = full.getInfo()
360 info.setFilter(self.makeFilter())
361 info.setVisitInfo(self.makeVisitInfo())
362 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
363 # We don't need to call stripMetadata() here because it has already
364 # been stripped during creation of the ObservationInfo, WCS, etc.
365 full.setMetadata(self.metadata)
366 return full
368 def readRawHeaderWcs(self, parameters=None):
369 """Read the SkyWcs stored in the un-modified raw FITS WCS header keys.
370 """
371 return lsst.afw.geom.makeSkyWcs(lsst.afw.fits.readMetadata(self.fileDescriptor))
373 def write(self, inMemoryDataset):
374 """Write a Python object to a file.
376 Parameters
377 ----------
378 inMemoryDataset : `object`
379 The Python object to store.
381 Returns
382 -------
383 path : `str`
384 The `URI` where the primary file is stored.
385 """
386 raise NotImplementedError("Raw data cannot be `put`.")
388 @property
389 def observationInfo(self):
390 """The `~astro_metadata_translator.ObservationInfo` extracted from
391 this file's metadata (`~astro_metadata_translator.ObservationInfo`,
392 read-only).
393 """
394 if self._observationInfo is None:
395 self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass)
396 return self._observationInfo