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