lsst.obs.base  13.0-24-gedf0888
 All Classes Namespaces Files Functions Variables
makeRawVisitInfo.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2016 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 from __future__ import absolute_import
23 from __future__ import division
24 from __future__ import print_function
25 import math
26 
27 import astropy.coordinates
28 import astropy.time
29 import astropy.units
30 
31 from lsst.log import Log
32 from lsst.daf.base import DateTime
33 from lsst.afw.geom import degrees
34 from lsst.afw.image import VisitInfo
35 
36 __all__ = ["MakeRawVisitInfo"]
37 
38 
39 PascalPerMillibar = 100.0
40 PascalPerMmHg = 133.322387415 # from Wikipedia; exact
41 PascalPerTorr = 101325.0/760.0 # from Wikipedia; exact
42 KelvinMinusCentigrade = 273.15 # from Wikipedia; exact
43 
44 # have these read at need, to avoid unexpected errors later
45 NaN = float("nan")
46 BadDate = DateTime()
47 
48 
49 class MakeRawVisitInfo(object):
50  """Base class functor to make a VisitInfo from the FITS header of a raw image
51 
52  A subclass will be wanted for each camera. Subclasses should override
53  - setArgDict: the override can call the base implementation,
54  which simply sets exposure time and date of observation
55  - setDateAvg
56 
57  The design philosophy is to make a best effort and log warnings of problems,
58  rather than raising exceptions, in order to extract as much VisitInfo information as possible
59  from a messy FITS header without the user needing to add a lot of error handling.
60 
61  However, the methods that transform units are less forgiving; they assume
62  the user provides proper data types, since type errors in arguments to those
63  are almost certainly due to coding mistakes.
64  """
65 
66  def __init__(self, log=None):
67  """Construct a MakeRawVisitInfo
68  """
69  if log is None:
70  log = Log.getLogger("MakeRawVisitInfo")
71  self.log = log
72 
73  def __call__(self, md, exposureId):
74  """Construct a VisitInfo and strip associated data from the metadata
75 
76  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
77  items that are used are stripped from the metadata
78  (except TIMESYS, because it may apply to more than one other keyword).
79  @param[in] exposureId exposure ID
80 
81  The basic implementation sets date and exposureTime using typical values
82  found in FITS files and logs a warning if neither can be set.
83  """
84  argDict = dict(exposureId=exposureId)
85  self.setArgDict(md, argDict)
86  for key in list(argDict.keys()): # use a copy because we may delete items
87  if argDict[key] is None:
88  self.log.warn("argDict[{}] is None; stripping".format(key, argDict[key]))
89  del argDict[key]
90  return VisitInfo(**argDict)
91 
92  def setArgDict(self, md, argDict):
93  """Fill an argument dict with arguments for VisitInfo and pop associated metadata
94 
95  Subclasses are expected to override this method, though the override
96  may wish to call this default implementation, which:
97  - sets exposureTime from "EXPTIME"
98  - sets date by calling getDateAvg
99 
100  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
101  items that are used are stripped from the metadata
102  (except TIMESYS, because it may apply to more than one other keyword).
103  @param[in,out] argdict a dict of arguments
104 
105  Subclasses should expand this or replace it.
106  """
107  argDict["exposureTime"] = self.popFloat(md, "EXPTIME")
108  argDict["date"] = self.getDateAvg(md=md, exposureTime=argDict["exposureTime"])
109 
110  def getDateAvg(self, md, exposureTime):
111  """Return date at the middle of the exposure
112 
113  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
114  items that are used are stripped from the metadata
115  (except TIMESYS, because it may apply to more than one other keyword).
116  @param[in] exposureTime exposure time (sec)
117 
118  Subclasses must override. Here is a typical implementation:
119  dateObs = self.popIsoDate(md, "DATE-OBS")
120  return self.offsetDate(dateObs, 0.5*exposureTime)
121  """
122  raise NotImplementedError()
123 
124  def offsetDate(self, date, offsetSec):
125  """Return a date offset by a specified number of seconds
126 
127  @param[in] date date (an lsst.daf.base.DateTime)
128  @param[in] offsetSec offset, in seconds (float)
129  @return the offset date (an lsst.daf.base.DateTime)
130  """
131  if not date.isValid():
132  self.log.warn("date is invalid; cannot offset it")
133  return date
134  if math.isnan(offsetSec):
135  self.log.warn("offsetSec is invalid; cannot offset date")
136  return date
137  dateNSec = date.nsecs(DateTime.TAI)
138  return DateTime(dateNSec + int(offsetSec*1.0e9), DateTime.TAI)
139 
140  def popItem(self, md, key, default=None):
141  """Remove an item of metadata and return the value
142 
143  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
144  the popped key is removed
145  @param[in] key metadata key
146  @param[in] default default value to return if key not found; ignored if doRaise true
147  @return the value of the specified key, using whatever type md.get(key) returns
148 
149  Log a warning if the key is not found
150  """
151  try:
152  if not md.exists(key):
153  self.log.warn("Key=\"{}\" not in medata".format(key))
154  return default
155  val = md.get(key)
156  md.remove(key)
157  return val
158  except Exception as e:
159  # this should never happen, but is a last ditch attempt to avoid exceptions
160  self.log.warn("Could not read key=\"{}\" in metadata: {}" % (key, e))
161  return default
162 
163  def popFloat(self, md, key):
164  """Pop a float with a default of Nan
165 
166  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
167  @param[in] key date key to read and remove from md
168  @return the value of the specified key as a float; float("nan") if the key is not found
169  """
170  val = self.popItem(md, key, default=NaN)
171  try:
172  return float(val)
173  except Exception as e:
174  self.log.warn("Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
175  return NaN
176 
177  def popAngle(self, md, key, units=astropy.units.deg):
178  """Pop an lsst.afw.geom.Angle, whose metadata is in the specified units, with a default of Nan
179 
180  The angle may be specified as a float or sexagesimal string with 1-3 fields.
181 
182  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
183  @param[in] key date key to read and remove from md
184  @return angle, as an lsst.afw.geom.Angle; Angle(NaN) if the key is not found
185  """
186  angleStr = self.popItem(md, key, default=None)
187  if angleStr is not None:
188  try:
189  return (astropy.coordinates.Angle(angleStr, unit=units).deg)*degrees
190  except Exception as e:
191  self.log.warn("Could not intepret {} value {} as an angle: {}".format(key, repr(angleStr), e))
192  return NaN*degrees
193 
194  def popIsoDate(self, md, key, timesys=None):
195  """Pop a FITS ISO date as an lsst.daf.base.DateTime
196 
197  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
198  @param[in] key date key to read and remove from md
199  @param[in] timesys time system as a string (not case sensitive), e.g. "UTC" or None;
200  if None then look for TIMESYS (but do NOT pop it, since it may be used
201  for more than one date) and if not found, use UTC
202  @return date as an lsst.daf.base.DateTime; DateTime() if the key is not found
203  """
204  isoDateStr = self.popItem(md=md, key=key)
205  if isoDateStr is not None:
206  try:
207  if timesys is None:
208  timesys = md.get("TIMESYS") if md.exists("TIMESYS") else "UTC"
209  if isoDateStr.endswith("Z"): # illegal in FITS
210  isoDateStr = isoDateStr[0:-1]
211  astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format="fits")
212  # DateTime uses nanosecond resolution, regardless of the resolution of the original date
213  astropyTime.precision = 9
214  # isot is ISO8601 format with "T" separating date and time and no time zone
215  return DateTime(astropyTime.tai.isot, DateTime.TAI)
216  except Exception as e:
217  self.log.warn("Could not parse {} = {} as an ISO date: {}".format(key, isoDateStr, e))
218  return BadDate
219 
220  def popMjdDate(self, md, key, timesys=None):
221  """Get a FITS MJD date as an lsst.daf.base.DateTime
222 
223  @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
224  @param[in] dateKey date key to read and remove from md
225  @param[in] timesys time system as a string, e.g. "UTC" or None;
226  if None then look for TIMESYS (but do NOT pop it, since it may be used
227  for more than one date) and if not found, use UTC
228  @return date as an lsst.daf.base.DateTime; DateTime() if the key is not found
229  """
230  mjdDate = self.popFloat(md, key)
231  try:
232  if timesys is None:
233  timesys = md.get("TIMESYS") if md.exists("TIMESYS") else "UTC"
234  astropyTime = astropy.time.Time(mjdDate, format="mjd", scale=timesys.lower())
235  # DateTime uses nanosecond resolution, regardless of the resolution of the original date
236  astropyTime.precision = 9
237  # isot is ISO8601 format with "T" separating date and time and no time zone
238  return DateTime(astropyTime.tai.isot, DateTime.TAI)
239  except Exception as e:
240  self.log.warn("Could not parse {} = {} as an MJD date: {}".format(key, mjdDate, e))
241  return BadDate
242 
243  @staticmethod
244  def eraFromLstAndLongitude(lst, longitude):
245  """
246  Return an approximate Earth Rotation Angle (afw:Angle) computed from
247  local sidereal time and longitude (both as afw:Angle; Longitude shares
248  the afw:Observatory covention: positive values are E of Greenwich).
249 
250  NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
251  this method.
252  """
253  return lst - longitude
254 
255  @staticmethod
257  """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
258  return 90*degrees - zd
259 
260  @staticmethod
262  """Convert temperature from Kelvin to Centigrade"""
263  return tempK - KelvinMinusCentigrade
264 
265  @staticmethod
266  def pascalFromMBar(mbar):
267  """Convert pressure from millibars to Pascals
268  """
269  return mbar*PascalPerMillibar
270 
271  @staticmethod
272  def pascalFromMmHg(mmHg):
273  """Convert pressure from mm Hg to Pascals
274 
275  @note could use the following, but astropy.units.cds is not fully compatible with Python 2
276  as of astropy 1.2.1 (see https://github.com/astropy/astropy/issues/5350#issuecomment-248612824):
277  astropy.units.cds.mmHg.to(astropy.units.pascal, mmHg)
278  """
279  return mmHg*PascalPerMmHg
280 
281  @staticmethod
282  def pascalFromTorr(torr):
283  """Convert pressure from torr to Pascals
284  """
285  return torr*PascalPerTorr