Coverage for python/lsst/obs/base/_fitsRawFormatterBase.py: 30%
143 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-05 02:41 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-05 02:41 -0700
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",)
24import logging
25from abc import abstractmethod
27import lsst.afw.fits
28import lsst.afw.geom
29import lsst.afw.image
30from astro_metadata_translator import ObservationInfo, fix_header
31from deprecated.sphinx import deprecated
32from lsst.daf.butler import FileDescriptor
33from lsst.utils.classes import cached_getter
35from .formatters.fitsExposure import FitsImageFormatterBase, standardizeAmplifierParameters
36from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo
37from .utils import InitialSkyWcsError, createInitialSkyWcsFromBoresight
39log = logging.getLogger(__name__)
42class FitsRawFormatterBase(FitsImageFormatterBase):
43 """Abstract base class for reading and writing raw data to and from
44 FITS files.
45 """
47 # This has to be explicit until we fix camera geometry in DM-20746
48 wcsFlipX = False
49 """Control whether the WCS is flipped in the X-direction (`bool`)"""
51 def __init__(self, *args, **kwargs):
52 self.filterDefinitions.reset()
53 self.filterDefinitions.defineFilters()
54 super().__init__(*args, **kwargs)
55 self._metadata = None
56 self._observationInfo = None
58 @classmethod
59 def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None):
60 """Construct a possibly-limited formatter from known metadata.
62 Parameters
63 ----------
64 metadata : `lsst.daf.base.PropertyList`
65 Raw header metadata, with any fixes (see
66 `astro_metadata_translator.fix_header`) applied but nothing
67 stripped.
68 obsInfo : `astro_metadata_translator.ObservationInfo`, optional
69 Structured information already extracted from ``metadata``.
70 If not provided, will be read from ``metadata`` on first use.
71 storageClass : `lsst.daf.butler.StorageClass`, optional
72 StorageClass for this file. If not provided, the formatter will
73 only support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
74 operations that operate purely on metadata and not the actual file.
75 location : `lsst.daf.butler.Location`, optional.
76 Location of the file. If not provided, the formatter will only
77 support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
78 operations that operate purely on metadata and not the actual file.
80 Returns
81 -------
82 formatter : `FitsRawFormatterBase`
83 An instance of ``cls``.
84 """
85 self = cls(FileDescriptor(location, storageClass))
86 self._metadata = metadata
87 self._observationInfo = obsInfo
88 return self
90 @property
91 @abstractmethod
92 def translatorClass(self):
93 """`~astro_metadata_translator.MetadataTranslator` to translate
94 metadata header to `~astro_metadata_translator.ObservationInfo`.
95 """
96 return None
98 @property
99 @abstractmethod
100 def filterDefinitions(self):
101 """`~lsst.obs.base.FilterDefinitions`, defining the filters for this
102 instrument.
103 """
104 return None
106 @property # type: ignore
107 @cached_getter
108 def checked_parameters(self):
109 # Docstring inherited.
110 parameters = super().checked_parameters
111 if "bbox" in parameters:
112 raise TypeError(
113 "Raw formatters do not support reading arbitrary subimages, as some "
114 "implementations may be assembled on-the-fly."
115 )
116 return parameters
118 def readImage(self):
119 """Read just the image component of the Exposure.
121 Returns
122 -------
123 image : `~lsst.afw.image.Image`
124 In-memory image component.
125 """
126 return lsst.afw.image.ImageU(self.fileDescriptor.location.path)
128 def isOnSky(self):
129 """Boolean to determine if the exposure is thought to be on the sky.
131 Returns
132 -------
133 onSky : `bool`
134 Returns `True` if the observation looks like it was taken on the
135 sky. Returns `False` if this observation looks like a calibration
136 observation.
138 Notes
139 -----
140 If there is tracking RA/Dec information associated with the
141 observation it is assumed that the observation is on sky.
142 Currently the observation type is not checked.
143 """
144 if self.observationInfo.tracking_radec is None:
145 return False
146 return True
148 @property
149 def metadata(self):
150 """The metadata read from this file. It will be stripped as
151 components are extracted from it
152 (`lsst.daf.base.PropertyList`).
153 """
154 if self._metadata is None:
155 self._metadata = self.readMetadata()
156 return self._metadata
158 def readMetadata(self):
159 """Read all header metadata directly into a PropertyList.
161 Returns
162 -------
163 metadata : `~lsst.daf.base.PropertyList`
164 Header metadata.
165 """
166 md = lsst.afw.fits.readMetadata(self.fileDescriptor.location.path)
167 fix_header(md)
168 return md
170 def stripMetadata(self):
171 """Remove metadata entries that are parsed into components."""
172 self._createSkyWcsFromMetadata()
174 def makeVisitInfo(self):
175 """Construct a VisitInfo from metadata.
177 Returns
178 -------
179 visitInfo : `~lsst.afw.image.VisitInfo`
180 Structured metadata about the observation.
181 """
182 return MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(self.observationInfo)
184 @abstractmethod
185 def getDetector(self, id):
186 """Return the detector that acquired this raw exposure.
188 Parameters
189 ----------
190 id : `int`
191 The identifying number of the detector to get.
193 Returns
194 -------
195 detector : `~lsst.afw.cameraGeom.Detector`
196 The detector associated with that ``id``.
197 """
198 raise NotImplementedError("Must be implemented by subclasses.")
200 def makeWcs(self, visitInfo, detector):
201 """Create a SkyWcs from information about the exposure.
203 If VisitInfo is not None, use it and the detector to create a SkyWcs,
204 otherwise return the metadata-based SkyWcs (always created, so that
205 the relevant metadata keywords are stripped).
207 Parameters
208 ----------
209 visitInfo : `~lsst.afw.image.VisitInfo`
210 The information about the telescope boresight and camera
211 orientation angle for this exposure.
212 detector : `~lsst.afw.cameraGeom.Detector`
213 The detector used to acquire this exposure.
215 Returns
216 -------
217 skyWcs : `~lsst.afw.geom.SkyWcs`
218 Reversible mapping from pixel coordinates to sky coordinates.
220 Raises
221 ------
222 InitialSkyWcsError
223 Raised if there is an error generating the SkyWcs, chained from the
224 lower-level exception if available.
225 """
226 if not self.isOnSky():
227 # This is not an on-sky observation
228 return None
230 skyWcs = self._createSkyWcsFromMetadata()
232 if visitInfo is None:
233 msg = "No VisitInfo; cannot access boresight information. Defaulting to metadata-based SkyWcs."
234 log.warning(msg)
235 if skyWcs is None:
236 raise InitialSkyWcsError(
237 "Failed to create both metadata and boresight-based SkyWcs."
238 "See warnings in log messages for details."
239 )
240 return skyWcs
242 return self.makeRawSkyWcsFromBoresight(
243 visitInfo.getBoresightRaDec(), visitInfo.getBoresightRotAngle(), detector
244 )
246 @classmethod
247 def makeRawSkyWcsFromBoresight(cls, boresight, orientation, detector):
248 """Class method to make a raw sky WCS from boresight and detector.
250 Parameters
251 ----------
252 boresight : `lsst.geom.SpherePoint`
253 The ICRS boresight RA/Dec
254 orientation : `lsst.geom.Angle`
255 The rotation angle of the focal plane on the sky.
256 detector : `lsst.afw.cameraGeom.Detector`
257 Where to get the camera geomtry from.
259 Returns
260 -------
261 skyWcs : `~lsst.afw.geom.SkyWcs`
262 Reversible mapping from pixel coordinates to sky coordinates.
263 """
264 return createInitialSkyWcsFromBoresight(boresight, orientation, detector, flipX=cls.wcsFlipX)
266 def _createSkyWcsFromMetadata(self):
267 """Create a SkyWcs from the FITS header metadata in an Exposure.
269 Returns
270 -------
271 skyWcs: `lsst.afw.geom.SkyWcs`, or None
272 The WCS that was created from ``self.metadata``, or None if that
273 creation fails due to invalid metadata.
274 """
275 if not self.isOnSky():
276 # This is not an on-sky observation
277 return None
279 try:
280 return lsst.afw.geom.makeSkyWcs(self.metadata, strip=True)
281 except TypeError as e:
282 log.warning("Cannot create a valid WCS from metadata: %s", e.args[0])
283 return None
285 # TODO: remove in DM-27177
286 @deprecated(
287 reason="Replaced with makeFilterLabel. Will be removed after v22.",
288 version="v22",
289 category=FutureWarning,
290 )
291 def makeFilter(self):
292 """Construct a Filter from metadata.
294 Returns
295 -------
296 filter : `~lsst.afw.image.Filter`
297 Object that identifies the filter for this image.
299 Raises
300 ------
301 NotFoundError
302 Raised if the physical filter was not registered via
303 `~lsst.afw.image.utils.defineFilter`.
304 """
305 return lsst.afw.image.Filter(self.observationInfo.physical_filter)
307 # TODO: deprecate in DM-27177, remove in DM-27811
308 def makeFilterLabel(self):
309 """Construct a FilterLabel from metadata.
311 Returns
312 -------
313 filter : `~lsst.afw.image.FilterLabel`
314 Object that identifies the filter for this image.
315 """
316 physical = self.observationInfo.physical_filter
317 band = self.filterDefinitions.physical_to_band[physical]
318 return lsst.afw.image.FilterLabel(physical=physical, band=band)
320 def readComponent(self, component):
321 # Docstring inherited.
322 self.checked_parameters # just for checking; no supported parameters.
323 if component == "image":
324 return self.readImage()
325 elif component == "filter":
326 return self.makeFilter()
327 elif component == "filterLabel":
328 return self.makeFilterLabel()
329 elif component == "visitInfo":
330 return self.makeVisitInfo()
331 elif component == "detector":
332 return self.getDetector(self.observationInfo.detector_num)
333 elif component == "wcs":
334 detector = self.getDetector(self.observationInfo.detector_num)
335 visitInfo = self.makeVisitInfo()
336 return self.makeWcs(visitInfo, detector)
337 elif component == "metadata":
338 self.stripMetadata()
339 return self.metadata
340 return None
342 def readFull(self):
343 # Docstring inherited.
344 amplifier, detector, _ = standardizeAmplifierParameters(
345 self.checked_parameters,
346 self.getDetector(self.observationInfo.detector_num),
347 )
348 if amplifier is not None:
349 reader = lsst.afw.image.ImageFitsReader(self.fileDescriptor.location.path)
350 amplifier_isolator = lsst.afw.cameraGeom.AmplifierIsolator(
351 amplifier,
352 reader.readBBox(),
353 detector,
354 )
355 subimage = amplifier_isolator.transform_subimage(
356 reader.read(bbox=amplifier_isolator.subimage_bbox)
357 )
358 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(subimage))
359 exposure.setDetector(amplifier_isolator.make_detector())
360 else:
361 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(self.readImage()))
362 exposure.setDetector(detector)
363 self.attachComponentsFromMetadata(exposure)
364 return exposure
366 def write(self, inMemoryDataset):
367 """Write a Python object to a file.
369 Parameters
370 ----------
371 inMemoryDataset : `object`
372 The Python object to store.
374 Returns
375 -------
376 path : `str`
377 The `URI` where the primary file is stored.
378 """
379 raise NotImplementedError("Raw data cannot be `put`.")
381 @property
382 def observationInfo(self):
383 """The `~astro_metadata_translator.ObservationInfo` extracted from
384 this file's metadata (`~astro_metadata_translator.ObservationInfo`,
385 read-only).
386 """
387 if self._observationInfo is None:
388 location = self.fileDescriptor.location
389 path = location.path if location is not None else None
390 self._observationInfo = ObservationInfo(
391 self.metadata, translator_class=self.translatorClass, filename=path
392 )
393 return self._observationInfo
395 def attachComponentsFromMetadata(self, exposure):
396 """Attach all `lsst.afw.image.Exposure` components derived from
397 metadata (including the stripped metadata itself).
399 Parameters
400 ----------
401 exposure : `lsst.afw.image.Exposure`
402 Exposure to attach components to (modified in place). Must already
403 have a detector attached.
404 """
405 info = exposure.getInfo()
406 info.id = self.observationInfo.detector_exposure_id
407 info.setFilterLabel(self.makeFilterLabel())
408 info.setVisitInfo(self.makeVisitInfo())
409 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
410 # We don't need to call stripMetadata() here because it has already
411 # been stripped during creation of the WCS.
412 exposure.setMetadata(self.metadata)