lsst.obs.base  19.0.0-10-gea245dd
makeRawVisitInfoViaObsInfo.py
Go to the documentation of this file.
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/>.
21 
22 import warnings
23 import astropy.units
24 import astropy.utils.exceptions
25 from astropy.utils import iers
26 
27 # This is an unofficial ERFA interface provided by Astropy.
28 # We need to use this to calculate the Earth rotation angle.
29 # If Astropy change their ERFA support we will need to either bring the
30 # calculation into this package or use another python ERFA binding.
31 import astropy._erfa as erfa
32 
33 from astro_metadata_translator import ObservationInfo
34 
35 from lsst.log import Log
36 from lsst.daf.base import DateTime
37 from lsst.geom import degrees, radians
38 from lsst.afw.image import VisitInfo, RotType
39 from lsst.afw.coord import Observatory, Weather
40 from lsst.geom import SpherePoint
41 
42 __all__ = ["MakeRawVisitInfoViaObsInfo"]
43 
44 
46  """Base class functor to make a VisitInfo from the FITS header of a
47  raw image using `~astro_metadata_translator.ObservationInfo` translators.
48 
49  Subclasses can be used if a specific
50  `~astro_metadata_translator.MetadataTranslator` translator should be used.
51 
52  The design philosophy is to make a best effort and log warnings of
53  problems, rather than raising exceptions, in order to extract as much
54  VisitInfo information as possible from a messy FITS header without the
55  user needing to add a lot of error handling.
56 
57  Parameters
58  ----------
59  log : `lsst.log.Log` or None
60  Logger to use for messages.
61  (None to use ``Log.getLogger("MakeRawVisitInfoViaObsInfo")``).
62  """
63 
64  metadataTranslator = None
65  """Header translator to use to construct VisitInfo, defaulting to
66  automatic determination."""
67 
68  def __init__(self, log=None):
69  if log is None:
70  log = Log.getLogger("MakeRawVisitInfoViaObsInfo")
71  self.log = log
72 
73  def __call__(self, md, exposureId=None):
74  """Construct a VisitInfo and strip associated data from the metadata.
75 
76  Parameters
77  ----------
78  md : `lsst.daf.base.PropertyList` or `lsst.daf.base.PropertySet`
79  Metadata to pull from.
80  Items that are used are stripped from the metadata.
81  exposureId : `int`, optional
82  Ignored. Here for compatibility with `MakeRawVisitInfo`.
83 
84  Returns
85  -------
86  visitInfo : `lsst.afw.image.VisitInfo`
87  `~lsst.afw.image.VisitInfo` derived from the header using
88  a `~astro_metadata_translator.MetadataTranslator`.
89  """
90 
91  obsInfo = ObservationInfo(md, translator_class=self.metadataTranslator)
92 
93  # Strip all the cards out that were used
94  for c in obsInfo.cards_used:
95  del md[c]
96 
97  return self.observationInfo2visitInfo(obsInfo, log=self.log)
98 
99  @staticmethod
100  def observationInfo2visitInfo(obsInfo, log=None):
101  """Construct a `~lsst.afw.image.VisitInfo` from an
102  `~astro_metadata_translator.ObservationInfo`
103 
104  Parameters
105  ----------
106  obsInfo : `astro_metadata_translator.ObservationInfo`
107  Information gathered from the observation metadata.
108  log : `logging.Logger` or `lsst.log.Log`, optional
109  Logger to use for logging informational messages.
110  If `None` logging will be disabled.
111 
112  Returns
113  -------
114  visitInfo : `lsst.afw.image.VisitInfo`
115  `~lsst.afw.image.VisitInfo` derived from the supplied
116  `~astro_metadata_translator.ObservationInfo`.
117  """
118  argDict = dict()
119 
120  # Map the translated information into a form suitable for VisitInfo
121  if obsInfo.exposure_time is not None:
122  argDict["exposureTime"] = obsInfo.exposure_time.to_value("s")
123  if obsInfo.dark_time is not None:
124  argDict["darkTime"] = obsInfo.dark_time.to_value("s")
125  argDict["exposureId"] = obsInfo.detector_exposure_id
126 
127  # VisitInfo uses the middle of the observation for the date
128  if obsInfo.datetime_begin is not None and obsInfo.datetime_end is not None:
129  tdelta = obsInfo.datetime_end - obsInfo.datetime_begin
130  middle = obsInfo.datetime_begin + 0.5*tdelta
131 
132  # DateTime uses nanosecond resolution, regardless of the resolution
133  # of the original date
134  middle.precision = 9
135  # isot is ISO8601 format with "T" separating date and time and no
136  # time zone
137  argDict["date"] = DateTime(middle.tai.isot, DateTime.TAI)
138 
139  # Derive earth rotation angle from UT1 (being out by a second is not
140  # a big deal given the uncertainty over exactly what part of the
141  # observation we are needing it for).
142  # ERFA needs a UT1 time split into two floats
143  # We ignore any problems with DUT1 not being defined for now.
144  try:
145  # Catch any warnings about the time being in the future
146  # since there is nothing we can do about that for simulated
147  # data and it tells us nothing for data from the past.
148  with warnings.catch_warnings():
149  warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning)
150  ut1time = middle.ut1
151  except iers.IERSRangeError:
152  ut1time = middle
153 
154  era = erfa.era00(ut1time.jd1, ut1time.jd2)
155  argDict["era"] = era * radians
156  else:
157  argDict["date"] = DateTime()
158 
159  # Coordinates
160  if obsInfo.tracking_radec is not None:
161  icrs = obsInfo.tracking_radec.transform_to("icrs")
162  argDict["boresightRaDec"] = SpherePoint(icrs.ra.degree,
163  icrs.dec.degree, units=degrees)
164 
165  altaz = obsInfo.altaz_begin
166  if altaz is not None:
167  argDict["boresightAzAlt"] = SpherePoint(altaz.az.degree,
168  altaz.alt.degree, units=degrees)
169 
170  argDict["boresightAirmass"] = obsInfo.boresight_airmass
171 
172  if obsInfo.boresight_rotation_angle is not None:
173  argDict["boresightRotAngle"] = obsInfo.boresight_rotation_angle.degree*degrees
174 
175  if obsInfo.boresight_rotation_coord is not None:
176  rotType = RotType.UNKNOWN
177  if obsInfo.boresight_rotation_coord == "sky":
178  rotType = RotType.SKY
179  argDict["rotType"] = rotType
180 
181  # Weather and Observatory Location
182  temperature = float("nan")
183  if obsInfo.temperature is not None:
184  temperature = obsInfo.temperature.to_value("deg_C", astropy.units.temperature())
185  pressure = float("nan")
186  if obsInfo.pressure is not None:
187  pressure = obsInfo.pressure.to_value("Pa")
188  relative_humidity = float("nan")
189  if obsInfo.relative_humidity is not None:
190  relative_humidity = obsInfo.relative_humidity
191  argDict["weather"] = Weather(temperature, pressure, relative_humidity)
192 
193  if obsInfo.location is not None:
194  geolocation = obsInfo.location.to_geodetic()
195  argDict["observatory"] = Observatory(geolocation.lon.degree*degrees,
196  geolocation.lat.degree*degrees,
197  geolocation.height.to_value("m"))
198 
199  for key in list(argDict.keys()): # use a copy because we may delete items
200  if argDict[key] is None:
201  if log is not None:
202  log.warn("argDict[{}] is None; stripping".format(key, argDict[key]))
203  del argDict[key]
204 
205  return VisitInfo(**argDict)