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