Coverage for python/lsst/obs/base/makeRawVisitInfoViaObsInfo.py: 14%
103 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-29 02:39 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-29 02:39 -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__ = ["MakeRawVisitInfoViaObsInfo"]
24import logging
25import warnings
26from typing import ClassVar, Optional
28import astropy.units
29import astropy.utils.exceptions
30from astropy.utils import iers
32# Prefer the standard pyerfa over the Astropy version
33try:
34 import erfa
36 ErfaWarning = erfa.ErfaWarning
37except ImportError:
38 import astropy._erfa as erfa
40 ErfaWarning = None
42from astro_metadata_translator import MetadataTranslator, ObservationInfo
43from lsst.afw.coord import Observatory, Weather
44from lsst.afw.image import RotType, VisitInfo
45from lsst.daf.base import DateTime
46from lsst.geom import SpherePoint, degrees, radians
49class MakeRawVisitInfoViaObsInfo:
50 """Base class functor to make a VisitInfo from the FITS header of a
51 raw image using `~astro_metadata_translator.ObservationInfo` translators.
53 Subclasses can be used if a specific
54 `~astro_metadata_translator.MetadataTranslator` translator should be used.
56 The design philosophy is to make a best effort and log warnings of
57 problems, rather than raising exceptions, in order to extract as much
58 VisitInfo information as possible from a messy FITS header without the
59 user needing to add a lot of error handling.
61 Parameters
62 ----------
63 log : `logging.Logger` or None
64 Logger to use for messages.
65 (None to use
66 ``Log.getLogger("lsst.obs.base.makeRawVisitInfoViaObsInfo")``).
67 doStripHeader : `bool`, optional
68 Strip header keywords from the metadata as they are used?
69 """
71 metadataTranslator: ClassVar[Optional[MetadataTranslator]] = None
72 """Header translator to use to construct VisitInfo, defaulting to
73 automatic determination."""
75 def __init__(self, log=None, doStripHeader=False):
76 if log is None:
77 log = logging.getLogger(__name__)
78 self.log = log
79 self.doStripHeader = doStripHeader
81 def __call__(self, md, exposureId=None):
82 """Construct a VisitInfo and strip associated data from the metadata.
84 Parameters
85 ----------
86 md : `lsst.daf.base.PropertyList` or `lsst.daf.base.PropertySet`
87 Metadata to pull from.
88 May be modified if ``stripHeader`` is ``True``.
89 exposureId : `int`, optional
90 Ignored. Here for compatibility with `MakeRawVisitInfo`.
92 Returns
93 -------
94 visitInfo : `lsst.afw.image.VisitInfo`
95 `~lsst.afw.image.VisitInfo` derived from the header using
96 a `~astro_metadata_translator.MetadataTranslator`.
97 """
99 obsInfo = ObservationInfo(md, translator_class=self.metadataTranslator)
101 if self.doStripHeader:
102 # Strip all the cards out that were used
103 for c in obsInfo.cards_used:
104 del md[c]
106 return self.observationInfo2visitInfo(obsInfo, log=self.log)
108 @staticmethod
109 def observationInfo2visitInfo(obsInfo, log=None):
110 """Construct a `~lsst.afw.image.VisitInfo` from an
111 `~astro_metadata_translator.ObservationInfo`
113 Parameters
114 ----------
115 obsInfo : `astro_metadata_translator.ObservationInfo`
116 Information gathered from the observation metadata.
117 log : `logging.Logger` or `lsst.log.Log`, optional
118 Logger to use for logging informational messages.
119 If `None` logging will be disabled.
121 Returns
122 -------
123 visitInfo : `lsst.afw.image.VisitInfo`
124 `~lsst.afw.image.VisitInfo` derived from the supplied
125 `~astro_metadata_translator.ObservationInfo`.
126 """
127 argDict = dict()
129 # Map the translated information into a form suitable for VisitInfo
130 if obsInfo.exposure_time is not None:
131 argDict["exposureTime"] = obsInfo.exposure_time.to_value("s")
132 if obsInfo.dark_time is not None:
133 argDict["darkTime"] = obsInfo.dark_time.to_value("s")
134 argDict["exposureId"] = obsInfo.detector_exposure_id
135 argDict["id"] = obsInfo.exposure_id
136 argDict["instrumentLabel"] = obsInfo.instrument
137 if obsInfo.focus_z is not None:
138 argDict["focusZ"] = obsInfo.focus_z.to_value("mm")
139 if obsInfo.observation_type is not None:
140 argDict["observationType"] = obsInfo.observation_type
141 if obsInfo.science_program is not None:
142 argDict["scienceProgram"] = obsInfo.science_program
143 if obsInfo.observation_reason is not None:
144 argDict["observationReason"] = obsInfo.observation_reason
145 if obsInfo.object is not None:
146 argDict["object"] = obsInfo.object
147 if obsInfo.has_simulated_content is not None:
148 argDict["hasSimulatedContent"] = obsInfo.has_simulated_content
150 # VisitInfo uses the middle of the observation for the date
151 if obsInfo.datetime_begin is not None and obsInfo.datetime_end is not None:
152 tdelta = obsInfo.datetime_end - obsInfo.datetime_begin
153 middle = obsInfo.datetime_begin + 0.5 * tdelta
155 # DateTime uses nanosecond resolution, regardless of the resolution
156 # of the original date
157 middle.precision = 9
158 # isot is ISO8601 format with "T" separating date and time and no
159 # time zone
160 argDict["date"] = DateTime(middle.tai.isot, DateTime.TAI)
162 # Derive earth rotation angle from UT1 (being out by a second is
163 # not a big deal given the uncertainty over exactly what part of
164 # the observation we are needing it for).
165 # ERFA needs a UT1 time split into two floats
166 # We ignore any problems with DUT1 not being defined for now.
167 try:
168 # Catch any warnings about the time being in the future
169 # since there is nothing we can do about that for simulated
170 # data and it tells us nothing for data from the past.
171 with warnings.catch_warnings():
172 # If we are using the real erfa it is not an AstropyWarning
173 # During transition period filter both
174 warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning)
175 if ErfaWarning is not None:
176 warnings.simplefilter("ignore", category=ErfaWarning)
177 ut1time = middle.ut1
178 except iers.IERSRangeError:
179 ut1time = middle
181 era = erfa.era00(ut1time.jd1, ut1time.jd2)
182 argDict["era"] = era * radians
183 else:
184 argDict["date"] = DateTime()
186 # Coordinates
187 if obsInfo.tracking_radec is not None:
188 icrs = obsInfo.tracking_radec.transform_to("icrs")
189 argDict["boresightRaDec"] = SpherePoint(icrs.ra.degree, icrs.dec.degree, units=degrees)
191 altaz = obsInfo.altaz_begin
192 if altaz is not None:
193 argDict["boresightAzAlt"] = SpherePoint(altaz.az.degree, altaz.alt.degree, units=degrees)
195 argDict["boresightAirmass"] = obsInfo.boresight_airmass
197 if obsInfo.boresight_rotation_angle is not None:
198 argDict["boresightRotAngle"] = obsInfo.boresight_rotation_angle.degree * degrees
200 if obsInfo.boresight_rotation_coord is not None:
201 rotType = RotType.UNKNOWN
202 if obsInfo.boresight_rotation_coord == "sky":
203 rotType = RotType.SKY
204 argDict["rotType"] = rotType
206 # Weather and Observatory Location
207 temperature = float("nan")
208 if obsInfo.temperature is not None:
209 temperature = obsInfo.temperature.to_value("deg_C", astropy.units.temperature())
210 pressure = float("nan")
211 if obsInfo.pressure is not None:
212 pressure = obsInfo.pressure.to_value("Pa")
213 relative_humidity = float("nan")
214 if obsInfo.relative_humidity is not None:
215 relative_humidity = obsInfo.relative_humidity
216 argDict["weather"] = Weather(temperature, pressure, relative_humidity)
218 if obsInfo.location is not None:
219 geolocation = obsInfo.location.to_geodetic()
220 argDict["observatory"] = Observatory(
221 geolocation.lon.degree * degrees,
222 geolocation.lat.degree * degrees,
223 geolocation.height.to_value("m"),
224 )
226 for key in list(argDict.keys()): # use a copy because we may delete items
227 if argDict[key] is None:
228 if log is not None:
229 log.warning("argDict[%s] is None; stripping", key)
230 del argDict[key]
232 return VisitInfo(**argDict)