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

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
25from deprecated.sphinx import deprecated
27from astro_metadata_translator import ObservationInfo
29import lsst.afw.fits
30import lsst.afw.geom
31import lsst.afw.image
32from lsst.daf.butler import FileDescriptor
33import lsst.log
35from .formatters.fitsExposure import FitsImageFormatterBase
36from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo
37from .utils import createInitialSkyWcsFromBoresight, InitialSkyWcsError
40class FitsRawFormatterBase(FitsImageFormatterBase, metaclass=ABCMeta):
41 """Abstract base class for reading and writing raw data to and from
42 FITS files.
43 """
45 # This has to be explicit until we fix camera geometry in DM-20746
46 wcsFlipX = False
47 """Control whether the WCS is flipped in the X-direction (`bool`)"""
49 def __init__(self, *args, **kwargs):
50 self.filterDefinitions.reset()
51 self.filterDefinitions.defineFilters()
52 super().__init__(*args, **kwargs)
54 @classmethod
55 def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None):
56 """Construct a possibly-limited formatter from known metadata.
58 Parameters
59 ----------
60 metadata : `lsst.daf.base.PropertyList`
61 Raw header metadata, with any fixes (see
62 `astro_metadata_translator.fix_header`) applied but nothing
63 stripped.
64 obsInfo : `astro_metadata_translator.ObservationInfo`, optional
65 Structured information already extracted from ``metadata``.
66 If not provided, will be read from ``metadata`` on first use.
67 storageClass : `lsst.daf.butler.StorageClass`, optional
68 StorageClass for this file. If not provided, the formatter will
69 only support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
70 operations that operate purely on metadata and not the actual file.
71 location : `lsst.daf.butler.Location`, optional.
72 Location of the file. If not provided, the formatter will only
73 support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
74 operations that operate purely on metadata and not the actual file.
76 Returns
77 -------
78 formatter : `FitsRawFormatterBase`
79 An instance of ``cls``.
80 """
81 self = cls(FileDescriptor(location, storageClass))
82 self._metadata = metadata
83 self._observationInfo = obsInfo
84 return self
86 @property
87 @abstractmethod
88 def translatorClass(self):
89 """`~astro_metadata_translator.MetadataTranslator` to translate
90 metadata header to `~astro_metadata_translator.ObservationInfo`.
91 """
92 return None
94 _observationInfo = 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 def readImage(self):
105 """Read just the image component of the Exposure.
107 Returns
108 -------
109 image : `~lsst.afw.image.Image`
110 In-memory image component.
111 """
112 return lsst.afw.image.ImageU(self.fileDescriptor.location.path)
114 def readMask(self):
115 """Read just the mask component of the Exposure.
117 May return None (as the default implementation does) to indicate that
118 there is no mask information to be extracted (at least not trivially)
119 from the raw data. This will prohibit direct reading of just the mask,
120 and set the mask of the full Exposure to zeros.
122 Returns
123 -------
124 mask : `~lsst.afw.image.Mask`
125 In-memory mask component.
126 """
127 return None
129 def readVariance(self):
130 """Read just the variance component of the Exposure.
132 May return None (as the default implementation does) to indicate that
133 there is no variance information to be extracted (at least not
134 trivially) from the raw data. This will prohibit direct reading of
135 just the variance, and set the variance of the full Exposure to zeros.
137 Returns
138 -------
139 image : `~lsst.afw.image.Image`
140 In-memory variance component.
141 """
142 return None
144 def isOnSky(self):
145 """Boolean to determine if the exposure is thought to be on the sky.
147 Returns
148 -------
149 onSky : `bool`
150 Returns `True` if the observation looks like it was taken on the
151 sky. Returns `False` if this observation looks like a calibration
152 observation.
154 Notes
155 -----
156 If there is tracking RA/Dec information associated with the
157 observation it is assumed that the observation is on sky.
158 Currently the observation type is not checked.
159 """
160 if self.observationInfo.tracking_radec is None:
161 return False
162 return True
164 def stripMetadata(self):
165 """Remove metadata entries that are parsed into components.
166 """
167 # NOTE: makeVisitInfo() may not strip any metadata itself, but calling
168 # it ensures that ObservationInfo is created from the metadata, which
169 # will strip the VisitInfo keys and more.
170 self.makeVisitInfo()
171 self._createSkyWcsFromMetadata()
173 def makeVisitInfo(self):
174 """Construct a VisitInfo from metadata.
176 Returns
177 -------
178 visitInfo : `~lsst.afw.image.VisitInfo`
179 Structured metadata about the observation.
180 """
181 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo)
183 @abstractmethod
184 def getDetector(self, id):
185 """Return the detector that acquired this raw exposure.
187 Parameters
188 ----------
189 id : `int`
190 The identifying number of the detector to get.
192 Returns
193 -------
194 detector : `~lsst.afw.cameraGeom.Detector`
195 The detector associated with that ``id``.
196 """
197 raise NotImplementedError("Must be implemented by subclasses.")
199 def makeWcs(self, visitInfo, detector):
200 """Create a SkyWcs from information about the exposure.
202 If VisitInfo is not None, use it and the detector to create a SkyWcs,
203 otherwise return the metadata-based SkyWcs (always created, so that
204 the relevant metadata keywords are stripped).
206 Parameters
207 ----------
208 visitInfo : `~lsst.afw.image.VisitInfo`
209 The information about the telescope boresight and camera
210 orientation angle for this exposure.
211 detector : `~lsst.afw.cameraGeom.Detector`
212 The detector used to acquire this exposure.
214 Returns
215 -------
216 skyWcs : `~lsst.afw.geom.SkyWcs`
217 Reversible mapping from pixel coordinates to sky coordinates.
219 Raises
220 ------
221 InitialSkyWcsError
222 Raised if there is an error generating the SkyWcs, chained from the
223 lower-level exception if available.
224 """
225 if not self.isOnSky():
226 # This is not an on-sky observation
227 return None
229 skyWcs = self._createSkyWcsFromMetadata()
231 log = lsst.log.Log.getLogger("fitsRawFormatter")
232 if visitInfo is None:
233 msg = "No VisitInfo; cannot access boresight information. Defaulting to metadata-based SkyWcs."
234 log.warn(msg)
235 if skyWcs is None:
236 raise InitialSkyWcsError("Failed to create both metadata and boresight-based SkyWcs."
237 "See warnings in log messages for details.")
238 return skyWcs
240 return self.makeRawSkyWcsFromBoresight(visitInfo.getBoresightRaDec(),
241 visitInfo.getBoresightRotAngle(),
242 detector)
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 = lsst.log.Log.getLogger("fitsRawFormatter")
281 log.warn("Cannot create a valid WCS from metadata: %s", e.args[0])
282 return None
284 # TODO: remove in DM-27177
285 @deprecated(reason="Replaced with makeFilterLabel. Will be removed after v22.",
286 version="v22", category=FutureWarning)
287 def makeFilter(self):
288 """Construct a Filter from metadata.
290 Returns
291 -------
292 filter : `~lsst.afw.image.Filter`
293 Object that identifies the filter for this image.
295 Raises
296 ------
297 NotFoundError
298 Raised if the physical filter was not registered via
299 `~lsst.afw.image.utils.defineFilter`.
300 """
301 return lsst.afw.image.Filter(self.observationInfo.physical_filter)
303 # TODO: deprecate in DM-27177, remove in DM-27811
304 def makeFilterLabel(self):
305 """Construct a FilterLabel from metadata.
307 Returns
308 -------
309 filter : `~lsst.afw.image.FilterLabel`
310 Object that identifies the filter for this image.
311 """
312 physical = self.observationInfo.physical_filter
313 band = self.filterDefinitions.physical_to_band[physical]
314 return lsst.afw.image.FilterLabel(physical=physical, band=band)
316 def readComponent(self, component, parameters=None):
317 """Read a component held by the Exposure.
319 Parameters
320 ----------
321 component : `str`, optional
322 Component to read from the file.
323 parameters : `dict`, optional
324 If specified, a dictionary of slicing parameters that
325 overrides those in ``fileDescriptor``.
327 Returns
328 -------
329 obj : component-dependent
330 In-memory component object.
332 Raises
333 ------
334 KeyError
335 Raised if the requested component cannot be handled.
336 """
337 if component == "image":
338 return self.readImage()
339 elif component == "mask":
340 return self.readMask()
341 elif component == "variance":
342 return self.readVariance()
343 elif component == "filter":
344 return self.makeFilter()
345 elif component == "filterLabel":
346 return self.makeFilterLabel()
347 elif component == "visitInfo":
348 return self.makeVisitInfo()
349 elif component == "wcs":
350 detector = self.getDetector(self.observationInfo.detector_num)
351 visitInfo = self.makeVisitInfo()
352 return self.makeWcs(visitInfo, detector)
353 return None
355 def readFull(self, parameters=None):
356 """Read the full Exposure object.
358 Parameters
359 ----------
360 parameters : `dict`, optional
361 If specified, a dictionary of slicing parameters that overrides
362 those in the `fileDescriptor` attribute.
364 Returns
365 -------
366 exposure : `~lsst.afw.image.Exposure`
367 Complete in-memory exposure.
368 """
369 from lsst.afw.image import makeExposure, makeMaskedImage
370 full = makeExposure(makeMaskedImage(self.readImage()))
371 mask = self.readMask()
372 if mask is not None:
373 full.setMask(mask)
374 variance = self.readVariance()
375 if variance is not None:
376 full.setVariance(variance)
377 full.setDetector(self.getDetector(self.observationInfo.detector_num))
378 info = full.getInfo()
379 info.setFilterLabel(self.makeFilterLabel())
380 info.setVisitInfo(self.makeVisitInfo())
381 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
382 # We don't need to call stripMetadata() here because it has already
383 # been stripped during creation of the ObservationInfo, WCS, etc.
384 full.setMetadata(self.metadata)
385 return full
387 def readRawHeaderWcs(self, parameters=None):
388 """Read the SkyWcs stored in the un-modified raw FITS WCS header keys.
389 """
390 return lsst.afw.geom.makeSkyWcs(lsst.afw.fits.readMetadata(self.fileDescriptor.location.path))
392 def write(self, inMemoryDataset):
393 """Write a Python object to a file.
395 Parameters
396 ----------
397 inMemoryDataset : `object`
398 The Python object to store.
400 Returns
401 -------
402 path : `str`
403 The `URI` where the primary file is stored.
404 """
405 raise NotImplementedError("Raw data cannot be `put`.")
407 @property
408 def observationInfo(self):
409 """The `~astro_metadata_translator.ObservationInfo` extracted from
410 this file's metadata (`~astro_metadata_translator.ObservationInfo`,
411 read-only).
412 """
413 if self._observationInfo is None:
414 location = self.fileDescriptor.location
415 path = location.path if location is not None else None
416 self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass,
417 filename=path)
418 return self._observationInfo