22 from __future__
import absolute_import
23 from __future__
import division
24 from __future__
import print_function
27 import astropy.coordinates
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
36 __all__ = [
"MakeRawVisitInfo"]
39 PascalPerMillibar = 100.0
40 PascalPerMmHg = 133.322387415
41 PascalPerTorr = 101325.0/760.0
42 KelvinMinusCentigrade = 273.15
50 """Base class functor to make a VisitInfo from the FITS header of a raw image
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
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.
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.
67 """Construct a MakeRawVisitInfo
70 log = Log.getLogger(
"MakeRawVisitInfo")
74 """Construct a VisitInfo and strip associated data from the metadata
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
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.
84 argDict = dict(exposureId=exposureId)
86 for key
in list(argDict.keys()):
87 if argDict[key]
is None:
88 self.log.warn(
"argDict[{}] is None; stripping".format(key, argDict[key]))
90 return VisitInfo(**argDict)
93 """Fill an argument dict with arguments for VisitInfo and pop associated metadata
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
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
105 Subclasses should expand this or replace it.
107 argDict[
"exposureTime"] = self.
popFloat(md,
"EXPTIME")
108 argDict[
"date"] = self.
getDateAvg(md=md, exposureTime=argDict[
"exposureTime"])
111 """Return date at the middle of the exposure
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)
118 Subclasses must override. Here is a typical implementation:
119 dateObs = self.popIsoDate(md, "DATE-OBS")
120 return self.offsetDate(dateObs, 0.5*exposureTime)
122 raise NotImplementedError()
125 """Return a date offset by a specified number of seconds
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)
131 if not date.isValid():
132 self.log.warn(
"date is invalid; cannot offset it")
134 if math.isnan(offsetSec):
135 self.log.warn(
"offsetSec is invalid; cannot offset date")
137 dateNSec = date.nsecs(DateTime.TAI)
138 return DateTime(dateNSec + int(offsetSec*1.0e9), DateTime.TAI)
141 """Remove an item of metadata and return the value
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
149 Log a warning if the key is not found
152 if not md.exists(key):
153 self.log.warn(
"Key=\"{}\" not in metadata".format(key))
158 except Exception
as e:
160 self.log.warn(
"Could not read key=\"{}\" in metadata: {}" % (key, e))
164 """Pop a float with a default of Nan
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
170 val = self.
popItem(md, key, default=NaN)
173 except Exception
as e:
174 self.log.warn(
"Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
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
180 The angle may be specified as a float or sexagesimal string with 1-3 fields.
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
186 angleStr = self.
popItem(md, key, default=
None)
187 if angleStr
is not None:
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))
195 """Pop a FITS ISO date as an lsst.daf.base.DateTime
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
204 isoDateStr = self.
popItem(md=md, key=key)
205 if isoDateStr
is not None:
208 timesys = md.get(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC"
209 if isoDateStr.endswith(
"Z"):
210 isoDateStr = isoDateStr[0:-1]
211 astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format=
"fits")
213 astropyTime.precision = 9
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))
221 """Get a FITS MJD date as an lsst.daf.base.DateTime
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
233 timesys = md.get(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC"
234 astropyTime = astropy.time.Time(mjdDate, format=
"mjd", scale=timesys.lower())
236 astropyTime.precision = 9
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))
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).
250 NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
253 return lst - longitude
257 """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
258 return 90*degrees - zd
262 """Convert temperature from Kelvin to Centigrade"""
263 return tempK - KelvinMinusCentigrade
267 """Convert pressure from millibars to Pascals
269 return mbar*PascalPerMillibar
273 """Convert pressure from mm Hg to Pascals
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)
279 return mmHg*PascalPerMmHg
283 """Convert pressure from torr to Pascals
285 return torr*PascalPerTorr
def altitudeFromZenithDistance
def eraFromLstAndLongitude