Coverage for python/lsst/obs/base/_fitsRawFormatterBase.py: 29%
Shortcuts 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
Shortcuts 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 abstractmethod
25from deprecated.sphinx import deprecated
26import logging
28from astro_metadata_translator import fix_header, ObservationInfo
30import lsst.afw.fits
31import lsst.afw.geom
32import lsst.afw.image
33from lsst.utils.classes import cached_getter
34from lsst.daf.butler import FileDescriptor
36from .formatters.fitsExposure import FitsImageFormatterBase, standardizeAmplifierParameters
37from .makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo
38from .utils import createInitialSkyWcsFromBoresight, InitialSkyWcsError
40log = logging.getLogger("fitsRawFormatter")
43class FitsRawFormatterBase(FitsImageFormatterBase):
44 """Abstract base class for reading and writing raw data to and from
45 FITS files.
46 """
48 # This has to be explicit until we fix camera geometry in DM-20746
49 wcsFlipX = False
50 """Control whether the WCS is flipped in the X-direction (`bool`)"""
52 def __init__(self, *args, **kwargs):
53 self.filterDefinitions.reset()
54 self.filterDefinitions.defineFilters()
55 super().__init__(*args, **kwargs)
56 self._metadata = None
57 self._observationInfo = None
59 @classmethod
60 def fromMetadata(cls, metadata, obsInfo=None, storageClass=None, location=None):
61 """Construct a possibly-limited formatter from known metadata.
63 Parameters
64 ----------
65 metadata : `lsst.daf.base.PropertyList`
66 Raw header metadata, with any fixes (see
67 `astro_metadata_translator.fix_header`) applied but nothing
68 stripped.
69 obsInfo : `astro_metadata_translator.ObservationInfo`, optional
70 Structured information already extracted from ``metadata``.
71 If not provided, will be read from ``metadata`` on first use.
72 storageClass : `lsst.daf.butler.StorageClass`, optional
73 StorageClass for this file. If not provided, the formatter will
74 only support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
75 operations that operate purely on metadata and not the actual file.
76 location : `lsst.daf.butler.Location`, optional.
77 Location of the file. If not provided, the formatter will only
78 support `makeWcs`, `makeVisitInfo`, `makeFilter`, and other
79 operations that operate purely on metadata and not the actual file.
81 Returns
82 -------
83 formatter : `FitsRawFormatterBase`
84 An instance of ``cls``.
85 """
86 self = cls(FileDescriptor(location, storageClass))
87 self._metadata = metadata
88 self._observationInfo = obsInfo
89 return self
91 @property
92 @abstractmethod
93 def translatorClass(self):
94 """`~astro_metadata_translator.MetadataTranslator` to translate
95 metadata header to `~astro_metadata_translator.ObservationInfo`.
96 """
97 return None
99 @property
100 @abstractmethod
101 def filterDefinitions(self):
102 """`~lsst.obs.base.FilterDefinitions`, defining the filters for this
103 instrument.
104 """
105 return None
107 @property
108 @cached_getter
109 def checked_parameters(self):
110 # Docstring inherited.
111 parameters = super().checked_parameters
112 if "bbox" in parameters:
113 raise TypeError("Raw formatters do not support reading arbitrary subimages, as some "
114 "implementations may be assembled on-the-fly.")
115 return parameters
117 def readImage(self):
118 """Read just the image component of the Exposure.
120 Returns
121 -------
122 image : `~lsst.afw.image.Image`
123 In-memory image component.
124 """
125 return lsst.afw.image.ImageU(self.fileDescriptor.location.path)
127 def isOnSky(self):
128 """Boolean to determine if the exposure is thought to be on the sky.
130 Returns
131 -------
132 onSky : `bool`
133 Returns `True` if the observation looks like it was taken on the
134 sky. Returns `False` if this observation looks like a calibration
135 observation.
137 Notes
138 -----
139 If there is tracking RA/Dec information associated with the
140 observation it is assumed that the observation is on sky.
141 Currently the observation type is not checked.
142 """
143 if self.observationInfo.tracking_radec is None:
144 return False
145 return True
147 @property
148 def metadata(self):
149 """The metadata read from this file. It will be stripped as
150 components are extracted from it
151 (`lsst.daf.base.PropertyList`).
152 """
153 if self._metadata is None:
154 self._metadata = self.readMetadata()
155 return self._metadata
157 def readMetadata(self):
158 """Read all header metadata directly into a PropertyList.
160 Returns
161 -------
162 metadata : `~lsst.daf.base.PropertyList`
163 Header metadata.
164 """
165 md = lsst.afw.fits.readMetadata(self.fileDescriptor.location.path)
166 fix_header(md)
167 return md
169 def stripMetadata(self):
170 """Remove metadata entries that are parsed into components.
171 """
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("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.warning("Cannot create a valid WCS from metadata: %s", e.args[0])
281 return None
283 # TODO: remove in DM-27177
284 @deprecated(reason="Replaced with makeFilterLabel. Will be removed after v22.",
285 version="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):
316 # Docstring inherited.
317 self.checked_parameters # just for checking; no supported parameters.
318 if component == "image":
319 return self.readImage()
320 elif component == "filter":
321 return self.makeFilter()
322 elif component == "filterLabel":
323 return self.makeFilterLabel()
324 elif component == "visitInfo":
325 return self.makeVisitInfo()
326 elif component == "detector":
327 return self.getDetector(self.observationInfo.detector_num)
328 elif component == "wcs":
329 detector = self.getDetector(self.observationInfo.detector_num)
330 visitInfo = self.makeVisitInfo()
331 return self.makeWcs(visitInfo, detector)
332 elif component == "metadata":
333 self.stripMetadata()
334 return self.metadata
335 return None
337 def readFull(self):
338 # Docstring inherited.
339 amplifier, detector, _ = standardizeAmplifierParameters(
340 self.checked_parameters,
341 self.getDetector(self.observationInfo.detector_num),
342 )
343 if amplifier is not None:
344 reader = lsst.afw.image.ImageFitsReader(self.fileDescriptor.location.path)
345 amplifier_isolator = lsst.afw.cameraGeom.AmplifierIsolator(
346 amplifier,
347 reader.readBBox(),
348 detector,
349 )
350 subimage = amplifier_isolator.transform_subimage(
351 reader.read(bbox=amplifier_isolator.subimage_bbox)
352 )
353 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(subimage))
354 exposure.setDetector(amplifier_isolator.make_detector())
355 else:
356 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(self.readImage()))
357 exposure.setDetector(detector)
358 self.attachComponentsFromMetadata(exposure)
359 return exposure
361 def write(self, inMemoryDataset):
362 """Write a Python object to a file.
364 Parameters
365 ----------
366 inMemoryDataset : `object`
367 The Python object to store.
369 Returns
370 -------
371 path : `str`
372 The `URI` where the primary file is stored.
373 """
374 raise NotImplementedError("Raw data cannot be `put`.")
376 @property
377 def observationInfo(self):
378 """The `~astro_metadata_translator.ObservationInfo` extracted from
379 this file's metadata (`~astro_metadata_translator.ObservationInfo`,
380 read-only).
381 """
382 if self._observationInfo is None:
383 location = self.fileDescriptor.location
384 path = location.path if location is not None else None
385 self._observationInfo = ObservationInfo(self.metadata, translator_class=self.translatorClass,
386 filename=path)
387 return self._observationInfo
389 def attachComponentsFromMetadata(self, exposure):
390 """Attach all `lsst.afw.image.Exposure` components derived from
391 metadata (including the stripped metadata itself).
393 Parameters
394 ----------
395 exposure : `lsst.afw.image.Exposure`
396 Exposure to attach components to (modified in place). Must already
397 have a detector attached.
398 """
399 info = exposure.getInfo()
400 info.id = self.observationInfo.detector_exposure_id
401 info.setFilterLabel(self.makeFilterLabel())
402 info.setVisitInfo(self.makeVisitInfo())
403 info.setWcs(self.makeWcs(info.getVisitInfo(), info.getDetector()))
404 # We don't need to call stripMetadata() here because it has already
405 # been stripped during creation of the WCS.
406 exposure.setMetadata(self.metadata)