Coverage for python / lsst / obs / base / makeRawVisitInfoViaObsInfo.py: 16%
109 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:20 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:20 +0000
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/>.
22from __future__ import annotations
24__all__ = ["MakeRawVisitInfoViaObsInfo"]
26import logging
27import warnings
28from typing import TYPE_CHECKING, Any, ClassVar
30import astropy.units
31import astropy.utils.exceptions
32from astropy.utils import iers
34# Prefer the standard pyerfa over the Astropy version
35try:
36 import erfa
38 ErfaWarning = erfa.ErfaWarning
39except ImportError:
40 import astropy._erfa as erfa
42 ErfaWarning = None
44from astro_metadata_translator import MetadataTranslator, ObservationInfo, VisitInfoTranslator
46from lsst.afw.coord import Observatory, Weather
47from lsst.afw.image import RotType, VisitInfo, setVisitInfoMetadata
48from lsst.daf.base import DateTime, PropertyList
49from lsst.geom import SpherePoint, degrees, radians
51if TYPE_CHECKING:
52 import lsst.afw.image
53 import lsst.daf.base
56class MakeRawVisitInfoViaObsInfo:
57 """Base class functor to make a VisitInfo from the FITS header of a
58 raw image using `~astro_metadata_translator.ObservationInfo` translators.
60 Subclasses can be used if a specific
61 `~astro_metadata_translator.MetadataTranslator` translator should be used.
63 The design philosophy is to make a best effort and log warnings of
64 problems, rather than raising exceptions, in order to extract as much
65 VisitInfo information as possible from a messy FITS header without the
66 user needing to add a lot of error handling.
68 Parameters
69 ----------
70 log : `logging.Logger` or None
71 Logger to use for messages. If `None` uses a logger named
72 "lsst.obs.base.makeRawVisitInfoViaObsInfo".
73 doStripHeader : `bool`, optional
74 Strip header keywords from the metadata as they are used?
75 """
77 metadataTranslator: ClassVar[type[MetadataTranslator] | None] = None
78 """Header translator to use to construct VisitInfo, defaulting to
79 automatic determination."""
81 def __init__(self, log: logging.Logger | None = None, doStripHeader: bool = False):
82 if log is None:
83 log = logging.getLogger(__name__)
84 self.log = log
85 self.doStripHeader = doStripHeader
87 def __call__(self, md: lsst.daf.base.PropertySet) -> lsst.afw.image.VisitInfo:
88 """Construct a VisitInfo and strip associated data from the metadata.
90 Parameters
91 ----------
92 md : `lsst.daf.base.PropertyList` or `lsst.daf.base.PropertySet`
93 Metadata to pull from.
94 May be modified if ``stripHeader`` is ``True``.
96 Returns
97 -------
98 visitInfo : `lsst.afw.image.VisitInfo`
99 `~lsst.afw.image.VisitInfo` derived from the header using
100 a `~astro_metadata_translator.MetadataTranslator`.
101 """
102 obsInfo = ObservationInfo(md, translator_class=self.metadataTranslator)
104 if self.doStripHeader:
105 # Strip all the cards out that were used
106 for c in obsInfo.cards_used:
107 del md[c]
109 return self.observationInfo2visitInfo(obsInfo, log=self.log)
111 @staticmethod
112 def observationInfo2visitInfo(
113 obsInfo: ObservationInfo, log: logging.Logger | None = None
114 ) -> lsst.afw.image.VisitInfo:
115 """Construct a `~lsst.afw.image.VisitInfo` from an
116 `~astro_metadata_translator.ObservationInfo`.
118 Parameters
119 ----------
120 obsInfo : `astro_metadata_translator.ObservationInfo`
121 Information gathered from the observation metadata.
122 log : `logging.Logger` or `lsst.log.Log`, optional
123 Logger to use for logging informational messages.
124 If `None` logging will be disabled.
126 Returns
127 -------
128 visitInfo : `lsst.afw.image.VisitInfo`
129 `~lsst.afw.image.VisitInfo` derived from the supplied
130 `~astro_metadata_translator.ObservationInfo`.
131 """
132 argDict: dict[str, Any] = {}
134 # Map the translated information into a form suitable for VisitInfo
135 if obsInfo.exposure_time is not None:
136 argDict["exposureTime"] = float(obsInfo.exposure_time.to_value("s"))
137 if obsInfo.dark_time is not None:
138 argDict["darkTime"] = float(obsInfo.dark_time.to_value("s"))
139 argDict["id"] = obsInfo.exposure_id
140 argDict["instrumentLabel"] = obsInfo.instrument
141 if obsInfo.focus_z is not None:
142 argDict["focusZ"] = float(obsInfo.focus_z.to_value("mm"))
143 if obsInfo.observation_type is not None:
144 argDict["observationType"] = obsInfo.observation_type
145 if obsInfo.science_program is not None:
146 argDict["scienceProgram"] = obsInfo.science_program
147 if obsInfo.observation_reason is not None:
148 argDict["observationReason"] = obsInfo.observation_reason
149 if obsInfo.object is not None:
150 argDict["object"] = obsInfo.object
151 if obsInfo.has_simulated_content is not None:
152 argDict["hasSimulatedContent"] = obsInfo.has_simulated_content
154 # VisitInfo uses the middle of the observation for the date
155 if obsInfo.datetime_begin is not None and obsInfo.datetime_end is not None:
156 tdelta = obsInfo.datetime_end - obsInfo.datetime_begin
157 middle = obsInfo.datetime_begin + 0.5 * tdelta
159 # DateTime uses nanosecond resolution, regardless of the resolution
160 # of the original date
161 middle.precision = 9
162 # isot is ISO8601 format with "T" separating date and time and no
163 # time zone
164 argDict["date"] = DateTime(middle.tai.isot, DateTime.TAI)
166 # Derive earth rotation angle from UT1 (being out by a second is
167 # not a big deal given the uncertainty over exactly what part of
168 # the observation we are needing it for).
169 # ERFA needs a UT1 time split into two floats
170 # We ignore any problems with DUT1 not being defined for now.
171 try:
172 # Catch any warnings about the time being in the future
173 # since there is nothing we can do about that for simulated
174 # data and it tells us nothing for data from the past.
175 with warnings.catch_warnings():
176 # If we are using the real erfa it is not an AstropyWarning
177 # During transition period filter both
178 warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning)
179 if ErfaWarning is not None:
180 warnings.simplefilter("ignore", category=ErfaWarning)
181 ut1time = middle.ut1
182 except iers.IERSRangeError:
183 ut1time = middle
185 era = erfa.era00(ut1time.jd1, ut1time.jd2)
186 argDict["era"] = era * radians
187 else:
188 argDict["date"] = DateTime()
190 # Coordinates
191 if obsInfo.tracking_radec is not None:
192 icrs = obsInfo.tracking_radec.transform_to("icrs")
193 argDict["boresightRaDec"] = SpherePoint(icrs.ra.degree, icrs.dec.degree, units=degrees)
195 altaz = obsInfo.altaz_begin
196 if altaz is not None:
197 argDict["boresightAzAlt"] = SpherePoint(altaz.az.degree, altaz.alt.degree, units=degrees)
199 argDict["boresightAirmass"] = obsInfo.boresight_airmass
201 if obsInfo.boresight_rotation_angle is not None:
202 argDict["boresightRotAngle"] = obsInfo.boresight_rotation_angle.degree * degrees
204 if obsInfo.boresight_rotation_coord is not None:
205 rotType = RotType.UNKNOWN
206 if obsInfo.boresight_rotation_coord == "sky":
207 rotType = RotType.SKY
208 argDict["rotType"] = rotType
210 # Weather and Observatory Location
211 temperature = float("nan")
212 if obsInfo.temperature is not None:
213 temperature = float(obsInfo.temperature.to_value("deg_C", astropy.units.temperature()))
214 pressure = float("nan")
215 if obsInfo.pressure is not None:
216 pressure = float(obsInfo.pressure.to_value("Pa"))
217 relative_humidity = float("nan")
218 if obsInfo.relative_humidity is not None:
219 relative_humidity = obsInfo.relative_humidity
220 argDict["weather"] = Weather(temperature, pressure, relative_humidity)
222 if obsInfo.location is not None:
223 geolocation = obsInfo.location.to_geodetic()
224 argDict["observatory"] = Observatory(
225 geolocation.lon.degree * degrees,
226 geolocation.lat.degree * degrees,
227 float(geolocation.height.to_value("m")),
228 )
230 for key in list(argDict.keys()): # use a copy because we may delete items
231 if argDict[key] is None:
232 if log is not None:
233 log.warning("argDict[%s] is None; stripping", key)
234 del argDict[key]
236 return VisitInfo(**argDict)
238 @staticmethod
239 def visitInfo2observationInfo(visitInfo: lsst.afw.image.VisitInfo) -> ObservationInfo:
240 """Construct a `~astro_metadata_translator.ObservationInfo` from a
241 `~lsst.afw.image.VisitInfo`.
243 Parameters
244 ----------
245 visitInfo : `lsst.afw.image.VisitInfo`
246 The object to convert.
248 Returns
249 -------
250 observationInfo : `~astro_metadata_translator.ObservationInfo`
251 New `~astro_metadata_translator.ObservationInfo` derived from
252 the supplied `~lsst.afw.image.VisitInfo`.
254 Notes
255 -----
256 Since a `~lsst.afw.image.VisitInfo` has less information than a
257 `~astro_metadata_translator.ObservationInfo`, many fields in the
258 returned object will be `None`.
259 """
260 pl = PropertyList()
261 setVisitInfoMetadata(pl, visitInfo)
263 # Try the given header looking for VisitInfo hints.
264 # We get lots of warnings if nothing can be found. Currently
265 # no way to disable those without capturing them.
266 obs_info = ObservationInfo.from_header(pl, translator_class=VisitInfoTranslator, quiet=True)
267 return obs_info