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