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