lsst.obs.base  19.0.0-29-g0c92743
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 # (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/>.
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.reset()
46  self.filterDefinitions.defineFilters()
47  super().__init__(*args, **kwargs)
48 
49  @classmethod
50  def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None):
51  """Construct a possibly-limited formatter from known metadata.
52 
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.
70 
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
80 
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
88 
89  _observationInfo = None
90 
91  @property
92  @abstractmethod
93  def filterDefinitions(self):
94  """`~lsst.obs.base.FilterDefinitions`, defining the filters for this
95  instrument.
96  """
97  return None
98 
99  def readImage(self):
100  """Read just the image component of the Exposure.
101 
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)
108 
109  def readMask(self):
110  """Read just the mask component of the Exposure.
111 
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.
116 
117  Returns
118  -------
119  mask : `~lsst.afw.image.Mask`
120  In-memory mask component.
121  """
122  return None
123 
124  def readVariance(self):
125  """Read just the variance component of the Exposure.
126 
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.
131 
132  Returns
133  -------
134  image : `~lsst.afw.image.Image`
135  In-memory variance component.
136  """
137  return None
138 
139  def isOnSky(self):
140  """Boolean to determine if the exposure is thought to be on the sky.
141 
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.
148 
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
158 
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()
167 
168  def makeVisitInfo(self):
169  """Construct a VisitInfo from metadata.
170 
171  Returns
172  -------
173  visitInfo : `~lsst.afw.image.VisitInfo`
174  Structured metadata about the observation.
175  """
176  return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo)
177 
178  @abstractmethod
179  def getDetector(self, id):
180  """Return the detector that acquired this raw exposure.
181 
182  Parameters
183  ----------
184  id : `int`
185  The identifying number of the detector to get.
186 
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.")
193 
194  def makeWcs(self, visitInfo, detector):
195  """Create a SkyWcs from information about the exposure.
196 
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).
200 
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.
208 
209  Returns
210  -------
211  skyWcs : `~lsst.afw.geom.SkyWcs`
212  Reversible mapping from pixel coordinates to sky coordinates.
213 
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
223 
224  skyWcs = self._createSkyWcsFromMetadata()
225 
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)
235 
236  return skyWcs
237 
238  def _createSkyWcsFromMetadata(self):
239  """Create a SkyWcs from the FITS header metadata in an Exposure.
240 
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
250 
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
257 
258  def makeFilter(self):
259  """Construct a Filter from metadata.
260 
261  Returns
262  -------
263  filter : `~lsst.afw.image.Filter`
264  Object that identifies the filter for this image.
265 
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)
273 
274  def readImageComponent(self, component):
275  """Read the image, mask, or variance component of an Exposure.
276 
277  Parameters
278  ----------
279  component : `str`, optional
280  Component to read from the file. Always one of "image",
281  "variance", or "mask".
282 
283  Returns
284  -------
285  image : `~lsst.afw.image.Image` or `~lsst.afw.image.Mask`
286  In-memory image, variance, or mask component.
287  """
288  if component == "image":
289  return self.readImage()
290  elif component == "mask":
291  return self.readMask()
292  elif component == "variance":
293  return self.readVariance()
294 
295  def readInfoComponent(self, component):
296  """Read a component held by ExposureInfo.
297 
298  The implementation provided by FitsRawFormatter provides only "wcs"
299  and "visitInfo". When adding support for other components, subclasses
300  should delegate to `super()` for those and update `readFull` with
301  similar logic.
302 
303  Parameters
304  ----------
305  component : `str`, optional
306  Component to read from the file.
307 
308  Returns
309  -------
310  obj : component-dependent
311  In-memory component object.
312  """
313  if component == "filter":
314  return self.makeFilter()
315  elif component == "visitInfo":
316  return self.makeVisitInfo()
317  elif component == "wcs":
318  detector = self.getDetector(self.observationInfo.detector_num)
319  visitInfo = self.makeVisitInfo()
320  return self.makeWcs(visitInfo, detector)
321  return None
322 
323  def readFull(self, parameters=None):
324  """Read the full Exposure object.
325 
326  Parameters
327  ----------
328  parameters : `dict`, optional
329  If specified, a dictionary of slicing parameters that overrides
330  those in the `fileDescriptor` attribute.
331 
332  Returns
333  -------
334  exposure : `~lsst.afw.image.Exposure`
335  Complete in-memory exposure.
336  """
337  from lsst.afw.image import makeExposure, makeMaskedImage
338  full = makeExposure(makeMaskedImage(self.readImage()))
339  mask = self.readMask()
340  if mask is not None:
341  full.setMask(mask)
342  variance = self.readVariance()
343  if variance is not None:
344  full.setVariance(variance)
345  full.setDetector(self.getDetector(self.observationInfo.detector_num))
346  info = full.getInfo()
347  info.setFilter(self.makeFilter())
348  info.setVisitInfo(self.makeVisitInfo())
349  info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
350  # We don't need to call stripMetadata() here because it has already
351  # been stripped during creation of the ObservationInfo, WCS, etc.
352  full.setMetadata(self.metadata)
353  return full
354 
355  def readRawHeaderWcs(self, parameters=None):
356  """Read the SkyWcs stored in the un-modified raw FITS WCS header keys.
357  """
358  return lsst.afw.geom.makeSkyWcs(lsst.afw.fits.readMetadata(self.fileDescriptor))
359 
360  def write(self, inMemoryDataset):
361  """Write a Python object to a file.
362 
363  Parameters
364  ----------
365  inMemoryDataset : `object`
366  The Python object to store.
367 
368  Returns
369  -------
370  path : `str`
371  The `URI` where the primary file is stored.
372  """
373  raise NotImplementedError("Raw data cannot be `put`.")
374 
375  @property
376  def observationInfo(self):
377  """The `~astro_metadata_translator.ObservationInfo` extracted from
378  this file's metadata (`~astro_metadata_translator.ObservationInfo`,
379  read-only).
380  """
381  if self._observationInfo is None:
382  self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass)
383  return self._observationInfo
def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None)
def createInitialSkyWcs(visitInfo, detector, flipX=False)
Definition: utils.py:42