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