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.", category=FutureWarning)
286 def makeFilter(self):
287 """Construct a Filter from metadata.
289 Returns
290 -------
291 filter : `~lsst.afw.image.Filter`
292 Object that identifies the filter for this image.
294 Raises
295 ------
296 NotFoundError
297 Raised if the physical filter was not registered via
298 `~lsst.afw.image.utils.defineFilter`.
299 """
300 return lsst.afw.image.Filter(self.observationInfo.physical_filter)
302 # TODO: deprecate in DM-27177, remove in DM-27811
303 def makeFilterLabel(self):
304 """Construct a FilterLabel from metadata.
306 Returns
307 -------
308 filter : `~lsst.afw.image.FilterLabel`
309 Object that identifies the filter for this image.
310 """
311 physical = self.observationInfo.physical_filter
312 band = self.filterDefinitions.physical_to_band[physical]
313 return lsst.afw.image.FilterLabel(physical=physical, band=band)
315 def readComponent(self, component, parameters=None):
316 """Read a component held by the Exposure.
318 Parameters
319 ----------
320 component : `str`, optional
321 Component to read from the file.
322 parameters : `dict`, optional
323 If specified, a dictionary of slicing parameters that
324 overrides those in ``fileDescriptor``.
326 Returns
327 -------
328 obj : component-dependent
329 In-memory component object.
331 Raises
332 ------
333 KeyError
334 Raised if the requested component cannot be handled.
335 """
336 if component == "image":
337 return self.readImage()
338 elif component == "mask":
339 return self.readMask()
340 elif component == "variance":
341 return self.readVariance()
342 elif component == "filter":
343 return self.makeFilter()
344 elif component == "filterLabel":
345 return self.makeFilterLabel()
346 elif component == "visitInfo":
347 return self.makeVisitInfo()
348 elif component == "wcs":
349 detector = self.getDetector(self.observationInfo.detector_num)
350 visitInfo = self.makeVisitInfo()
351 return self.makeWcs(visitInfo, detector)
352 return None
354 def readFull(self, parameters=None):
355 """Read the full Exposure object.
357 Parameters
358 ----------
359 parameters : `dict`, optional
360 If specified, a dictionary of slicing parameters that overrides
361 those in the `fileDescriptor` attribute.
363 Returns
364 -------
365 exposure : `~lsst.afw.image.Exposure`
366 Complete in-memory exposure.
367 """
368 from lsst.afw.image import makeExposure, makeMaskedImage
369 full = makeExposure(makeMaskedImage(self.readImage()))
370 mask = self.readMask()
371 if mask is not None:
372 full.setMask(mask)
373 variance = self.readVariance()
374 if variance is not None:
375 full.setVariance(variance)
376 full.setDetector(self.getDetector(self.observationInfo.detector_num))
377 info = full.getInfo()
378 info.setFilterLabel(self.makeFilterLabel())
379 info.setVisitInfo(self.makeVisitInfo())
380 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
381 # We don't need to call stripMetadata() here because it has already
382 # been stripped during creation of the ObservationInfo, WCS, etc.
383 full.setMetadata(self.metadata)
384 return full
386 def readRawHeaderWcs(self, parameters=None):
387 """Read the SkyWcs stored in the un-modified raw FITS WCS header keys.
388 """
389 return lsst.afw.geom.makeSkyWcs(lsst.afw.fits.readMetadata(self.fileDescriptor.location.path))
391 def write(self, inMemoryDataset):
392 """Write a Python object to a file.
394 Parameters
395 ----------
396 inMemoryDataset : `object`
397 The Python object to store.
399 Returns
400 -------
401 path : `str`
402 The `URI` where the primary file is stored.
403 """
404 raise NotImplementedError("Raw data cannot be `put`.")
406 @property
407 def observationInfo(self):
408 """The `~astro_metadata_translator.ObservationInfo` extracted from
409 this file's metadata (`~astro_metadata_translator.ObservationInfo`,
410 read-only).
411 """
412 if self._observationInfo is None:
413 location = self.fileDescriptor.location
414 path = location.path if location is not None else None
415 self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass,
416 filename=path)
417 return self._observationInfo