Coverage for python/lsst/obs/base/fitsRawFormatterBase.py : 25%

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
32from lsst.daf.butler.formatters.fitsExposureFormatter import FitsExposureFormatter
33import lsst.log
35from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo
36from .utils import createInitialSkyWcs, InitialSkyWcsError
39class FitsRawFormatterBase(FitsExposureFormatter, metaclass=ABCMeta):
40 """Abstract base class for reading and writing raw data to and from
41 FITS files.
42 """
44 def __init__(self, *args, **kwargs):
45 self.filterDefinitions.reset()
46 self.filterDefinitions.defineFilters()
47 super().__init__(*args, **kwargs)
49 @classmethod
50 def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None):
51 """Construct a possibly-limited formatter from known metadata.
53 Parameters
54 ----------
55 metadata : `lsst.daf.base.PropertyList`
56 Raw header metadata, with any fixes (see
57 `astro_metadata_translator.fix_header`) applied but nothing
58 stripped.
59 obsInfo : `astro_metadata_translator.ObservationInfo`, optional
60 Structured information already extracted from ``metadata``.
61 If not provided, will be read from ``metadata`` on first use.
62 storageClass : `lsst.daf.butler.StorageClass`, optional
63 StorageClass for this file. If not provided, the formatter will
64 only support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
65 operations that operate purely on metadata and not the actual file.
66 location : `lsst.daf.butler.Location`, optional.
67 Location of the file. If not provided, the formatter will only
68 support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
69 operations that operate purely on metadata and not the actual file.
71 Returns
72 -------
73 formatter : `FitsRawFormatterBase`
74 An instance of ``cls``.
75 """
76 self = cls(FileDescriptor(location, storageClass))
77 self._metadata = metadata
78 self._observationInfo = obsInfo
79 return self
81 @property
82 @abstractmethod
83 def translatorClass(self):
84 """`~astro_metadata_translator.MetadataTranslator` to translate
85 metadata header to `~astro_metadata_translator.ObservationInfo`.
86 """
87 return None
89 _observationInfo = None
91 @property
92 @abstractmethod
93 def filterDefinitions(self):
94 """`~lsst.obs.base.FilterDefinitions`, defining the filters for this
95 instrument.
96 """
97 return None
99 def readImage(self):
100 """Read just the image component of the Exposure.
102 Returns
103 -------
104 image : `~lsst.afw.image.Image`
105 In-memory image component.
106 """
107 return lsst.afw.image.ImageU(self.fileDescriptor.location.path)
109 def readMask(self):
110 """Read just the mask component of the Exposure.
112 May return None (as the default implementation does) to indicate that
113 there is no mask information to be extracted (at least not trivially)
114 from the raw data. This will prohibit direct reading of just the mask,
115 and set the mask of the full Exposure to zeros.
117 Returns
118 -------
119 mask : `~lsst.afw.image.Mask`
120 In-memory mask component.
121 """
122 return None
124 def readVariance(self):
125 """Read just the variance component of the Exposure.
127 May return None (as the default implementation does) to indicate that
128 there is no variance information to be extracted (at least not
129 trivially) from the raw data. This will prohibit direct reading of
130 just the variance, and set the variance of the full Exposure to zeros.
132 Returns
133 -------
134 image : `~lsst.afw.image.Image`
135 In-memory variance component.
136 """
137 return None
139 def isOnSky(self):
140 """Boolean to determine if the exposure is thought to be on the sky.
142 Returns
143 -------
144 onSky : `bool`
145 Returns `True` if the observation looks like it was taken on the
146 sky. Returns `False` if this observation looks like a calibration
147 observation.
149 Notes
150 -----
151 If there is tracking RA/Dec information associated with the
152 observation it is assumed that the observation is on sky.
153 Currently the observation type is not checked.
154 """
155 if self.observationInfo.tracking_radec is None:
156 return False
157 return True
159 def stripMetadata(self):
160 """Remove metadata entries that are parsed into components.
161 """
162 # NOTE: makeVisitInfo() may not strip any metadata itself, but calling
163 # it ensures that ObservationInfo is created from the metadata, which
164 # will strip the VisitInfo keys and more.
165 self.makeVisitInfo()
166 self._createSkyWcsFromMetadata()
168 def makeVisitInfo(self):
169 """Construct a VisitInfo from metadata.
171 Returns
172 -------
173 visitInfo : `~lsst.afw.image.VisitInfo`
174 Structured metadata about the observation.
175 """
176 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo)
178 @abstractmethod
179 def getDetector(self, id):
180 """Return the detector that acquired this raw exposure.
182 Parameters
183 ----------
184 id : `int`
185 The identifying number of the detector to get.
187 Returns
188 -------
189 detector : `~lsst.afw.cameraGeom.Detector`
190 The detector associated with that ``id``.
191 """
192 raise NotImplementedError("Must be implemented by subclasses.")
194 def makeWcs(self, visitInfo, detector):
195 """Create a SkyWcs from information about the exposure.
197 If VisitInfo is not None, use it and the detector to create a SkyWcs,
198 otherwise return the metadata-based SkyWcs (always created, so that
199 the relevant metadata keywords are stripped).
201 Parameters
202 ----------
203 visitInfo : `~lsst.afw.image.VisitInfo`
204 The information about the telescope boresight and camera
205 orientation angle for this exposure.
206 detector : `~lsst.afw.cameraGeom.Detector`
207 The detector used to acquire this exposure.
209 Returns
210 -------
211 skyWcs : `~lsst.afw.geom.SkyWcs`
212 Reversible mapping from pixel coordinates to sky coordinates.
214 Raises
215 ------
216 InitialSkyWcsError
217 Raised if there is an error generating the SkyWcs, chained from the
218 lower-level exception if available.
219 """
220 if not self.isOnSky():
221 # This is not an on-sky observation
222 return None
224 skyWcs = self._createSkyWcsFromMetadata()
226 log = lsst.log.Log.getLogger("fitsRawFormatter")
227 if visitInfo is None:
228 msg = "No VisitInfo; cannot access boresight information. Defaulting to metadata-based SkyWcs."
229 log.warn(msg)
230 if skyWcs is None:
231 raise InitialSkyWcsError("Failed to create both metadata and boresight-based SkyWcs."
232 "See warnings in log messages for details.")
233 return skyWcs
234 skyWcs = createInitialSkyWcs(visitInfo, detector)
236 return skyWcs
238 def _createSkyWcsFromMetadata(self):
239 """Create a SkyWcs from the FITS header metadata in an Exposure.
241 Returns
242 -------
243 skyWcs: `lsst.afw.geom.SkyWcs`, or None
244 The WCS that was created from ``self.metadata``, or None if that
245 creation fails due to invalid metadata.
246 """
247 if not self.isOnSky():
248 # This is not an on-sky observation
249 return None
251 try:
252 return lsst.afw.geom.makeSkyWcs(self.metadata, strip=True)
253 except TypeError as e:
254 log = lsst.log.Log.getLogger("fitsRawFormatter")
255 log.warn("Cannot create a valid WCS from metadata: %s", e.args[0])
256 return None
258 def makeFilter(self):
259 """Construct a Filter from metadata.
261 Returns
262 -------
263 filter : `~lsst.afw.image.Filter`
264 Object that identifies the filter for this image.
266 Raises
267 ------
268 NotFoundError
269 Raised if the physical filter was not registered via
270 `~lsst.afw.image.utils.defineFilter`.
271 """
272 return lsst.afw.image.Filter(self.observationInfo.physical_filter)
274 def readComponent(self, component, parameters=None):
275 """Read a component held by the Exposure.
277 Parameters
278 ----------
279 component : `str`, optional
280 Component to read from the file.
281 parameters : `dict`, optional
282 If specified, a dictionary of slicing parameters that
283 overrides those in ``fileDescriptor``.
285 Returns
286 -------
287 obj : component-dependent
288 In-memory component object.
290 Raises
291 ------
292 KeyError
293 Raised if the requested component cannot be handled.
294 """
295 if component == "image":
296 return self.readImage()
297 elif component == "mask":
298 return self.readMask()
299 elif component == "variance":
300 return self.readVariance()
301 elif component == "filter":
302 return self.makeFilter()
303 elif component == "visitInfo":
304 return self.makeVisitInfo()
305 elif component == "wcs":
306 detector = self.getDetector(self.observationInfo.detector_num)
307 visitInfo = self.makeVisitInfo()
308 return self.makeWcs(visitInfo, detector)
309 return None
311 def readFull(self, parameters=None):
312 """Read the full Exposure object.
314 Parameters
315 ----------
316 parameters : `dict`, optional
317 If specified, a dictionary of slicing parameters that overrides
318 those in the `fileDescriptor` attribute.
320 Returns
321 -------
322 exposure : `~lsst.afw.image.Exposure`
323 Complete in-memory exposure.
324 """
325 from lsst.afw.image import makeExposure, makeMaskedImage
326 full = makeExposure(makeMaskedImage(self.readImage()))
327 mask = self.readMask()
328 if mask is not None:
329 full.setMask(mask)
330 variance = self.readVariance()
331 if variance is not None:
332 full.setVariance(variance)
333 full.setDetector(self.getDetector(self.observationInfo.detector_num))
334 info = full.getInfo()
335 info.setFilter(self.makeFilter())
336 info.setVisitInfo(self.makeVisitInfo())
337 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
338 # We don't need to call stripMetadata() here because it has already
339 # been stripped during creation of the ObservationInfo, WCS, etc.
340 full.setMetadata(self.metadata)
341 return full
343 def readRawHeaderWcs(self, parameters=None):
344 """Read the SkyWcs stored in the un-modified raw FITS WCS header keys.
345 """
346 return lsst.afw.geom.makeSkyWcs(lsst.afw.fits.readMetadata(self.fileDescriptor))
348 def write(self, inMemoryDataset):
349 """Write a Python object to a file.
351 Parameters
352 ----------
353 inMemoryDataset : `object`
354 The Python object to store.
356 Returns
357 -------
358 path : `str`
359 The `URI` where the primary file is stored.
360 """
361 raise NotImplementedError("Raw data cannot be `put`.")
363 @property
364 def observationInfo(self):
365 """The `~astro_metadata_translator.ObservationInfo` extracted from
366 this file's metadata (`~astro_metadata_translator.ObservationInfo`,
367 read-only).
368 """
369 if self._observationInfo is None:
370 self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass)
371 return self._observationInfo