22 from __future__
import absolute_import
23 from __future__
import division
24 from __future__
import print_function
28 import astropy.coordinates
32 from lsst.log
import Log
33 from lsst.daf.base
import DateTime
34 from lsst.afw.geom
import degrees
35 from lsst.afw.image
import VisitInfo
37 __all__ = [
"MakeRawVisitInfo"]
40 PascalPerMillibar = 100.0
41 PascalPerMmHg = 133.322387415
42 PascalPerTorr = 101325.0/760.0
43 KelvinMinusCentigrade = 273.15
51 """Base class functor to make a VisitInfo from the FITS header of a raw image
53 A subclass will be wanted for each camera. Subclasses should override
54 - setArgDict: the override can call the base implementation,
55 which simply sets exposure time and date of observation
58 The design philosophy is to make a best effort and log warnings of problems,
59 rather than raising exceptions, in order to extract as much VisitInfo information as possible
60 from a messy FITS header without the user needing to add a lot of error handling.
62 However, the methods that transform units are less forgiving; they assume
63 the user provides proper data types, since type errors in arguments to those
64 are almost certainly due to coding mistakes.
68 """Construct a MakeRawVisitInfo
71 log = Log.getLogger(
"MakeRawVisitInfo")
75 """Construct a VisitInfo and strip associated data from the metadata
77 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
78 items that are used are stripped from the metadata
79 (except TIMESYS, because it may apply to more than one other keyword).
80 @param[in] exposureId exposure ID
82 The basic implementation sets date and exposureTime using typical values
83 found in FITS files and logs a warning if neither can be set.
85 argDict = dict(exposureId=exposureId)
87 for key
in list(argDict.keys()):
88 if argDict[key]
is None:
89 self.log.warn(
"argDict[{}] is None; stripping".format(key, argDict[key]))
91 return VisitInfo(**argDict)
94 """Fill an argument dict with arguments for VisitInfo and pop associated metadata
96 Subclasses are expected to override this method, though the override
97 may wish to call this default implementation, which:
98 - sets exposureTime from "EXPTIME"
99 - sets date by calling getDateAvg
101 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
102 items that are used are stripped from the metadata
103 (except TIMESYS, because it may apply to more than one other keyword).
104 @param[in,out] argdict a dict of arguments
106 Subclasses should expand this or replace it.
108 argDict[
"exposureTime"] = self.
popFloat(md,
"EXPTIME")
109 argDict[
"date"] = self.
getDateAvg(md=md, exposureTime=argDict[
"exposureTime"])
112 """Return date at the middle of the exposure
114 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
115 items that are used are stripped from the metadata
116 (except TIMESYS, because it may apply to more than one other keyword).
117 @param[in] exposureTime exposure time (sec)
119 Subclasses must override. Here is a typical implementation:
120 dateObs = self.popIsoDate(md, "DATE-OBS")
121 return self.offsetDate(dateObs, 0.5*exposureTime)
123 raise NotImplementedError()
126 """Get the darkTime from the DARKTIME keyword, else expTime, else NaN
128 Subclasses should call this function if desired, by putting:
129 argDict['darkTime'] = self.getDarkTime(argDict)
130 in their __init__() method of the derived class.
132 @param[in] argDict argDict
133 @return darkTime darkTime, as inferred from the metadata
136 darkTime = argDict.get(
"darkTime", NaN)
137 if np.isfinite(darkTime):
140 self.log.info(
"darkTime is NaN/Inf; using exposureTime")
141 exposureTime = argDict.get(
"exposureTime", NaN)
142 if not np.isfinite(exposureTime):
143 raise RuntimeError(
"Tried to substitute exposureTime for darkTime but it is not available")
148 """Return a date offset by a specified number of seconds
150 @param[in] date date (an lsst.daf.base.DateTime)
151 @param[in] offsetSec offset, in seconds (float)
152 @return the offset date (an lsst.daf.base.DateTime)
154 if not date.isValid():
155 self.log.warn(
"date is invalid; cannot offset it")
157 if math.isnan(offsetSec):
158 self.log.warn(
"offsetSec is invalid; cannot offset date")
160 dateNSec = date.nsecs(DateTime.TAI)
161 return DateTime(dateNSec + int(offsetSec*1.0e9), DateTime.TAI)
164 """Remove an item of metadata and return the value
166 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet;
167 the popped key is removed
168 @param[in] key metadata key
169 @param[in] default default value to return if key not found; ignored if doRaise true
170 @return the value of the specified key, using whatever type md.get(key) returns
172 Log a warning if the key is not found
175 if not md.exists(key):
176 self.log.warn(
"Key=\"{}\" not in metadata".format(key))
181 except Exception
as e:
183 self.log.warn(
"Could not read key=\"{}\" in metadata: {}" % (key, e))
187 """Pop a float with a default of Nan
189 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
190 @param[in] key date key to read and remove from md
191 @return the value of the specified key as a float; float("nan") if the key is not found
193 val = self.
popItem(md, key, default=NaN)
196 except Exception
as e:
197 self.log.warn(
"Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
200 def popAngle(self, md, key, units=astropy.units.deg):
201 """Pop an lsst.afw.geom.Angle, whose metadata is in the specified units, with a default of Nan
203 The angle may be specified as a float or sexagesimal string with 1-3 fields.
205 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
206 @param[in] key date key to read and remove from md
207 @return angle, as an lsst.afw.geom.Angle; Angle(NaN) if the key is not found
209 angleStr = self.
popItem(md, key, default=
None)
210 if angleStr
is not None:
212 return (astropy.coordinates.Angle(angleStr, unit=units).deg)*degrees
213 except Exception
as e:
214 self.log.warn(
"Could not intepret {} value {} as an angle: {}".format(key, repr(angleStr), e))
218 """Pop a FITS ISO date as an lsst.daf.base.DateTime
220 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
221 @param[in] key date key to read and remove from md
222 @param[in] timesys time system as a string (not case sensitive), e.g. "UTC" or None;
223 if None then look for TIMESYS (but do NOT pop it, since it may be used
224 for more than one date) and if not found, use UTC
225 @return date as an lsst.daf.base.DateTime; DateTime() if the key is not found
227 isoDateStr = self.
popItem(md=md, key=key)
228 if isoDateStr
is not None:
231 timesys = md.get(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC"
232 if isoDateStr.endswith(
"Z"):
233 isoDateStr = isoDateStr[0:-1]
234 astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format=
"fits")
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 ISO date: {}".format(key, isoDateStr, e))
244 """Get a FITS MJD date as an lsst.daf.base.DateTime
246 @param[in,out] md metadata, as an lsst.daf.base.PropertyList or PropertySet
247 @param[in] dateKey date key to read and remove from md
248 @param[in] timesys time system as a string, e.g. "UTC" or None;
249 if None then look for TIMESYS (but do NOT pop it, since it may be used
250 for more than one date) and if not found, use UTC
251 @return date as an lsst.daf.base.DateTime; DateTime() if the key is not found
256 timesys = md.get(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC"
257 astropyTime = astropy.time.Time(mjdDate, format=
"mjd", scale=timesys.lower())
259 astropyTime.precision = 9
261 return DateTime(astropyTime.tai.isot, DateTime.TAI)
262 except Exception
as e:
263 self.log.warn(
"Could not parse {} = {} as an MJD date: {}".format(key, mjdDate, e))
269 Return an approximate Earth Rotation Angle (afw:Angle) computed from
270 local sidereal time and longitude (both as afw:Angle; Longitude shares
271 the afw:Observatory covention: positive values are E of Greenwich).
273 NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
276 return lst - longitude
280 """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
281 return 90*degrees - zd
285 """Convert temperature from Kelvin to Centigrade"""
286 return tempK - KelvinMinusCentigrade
290 """Convert pressure from millibars to Pascals
292 return mbar*PascalPerMillibar
296 """Convert pressure from mm Hg to Pascals
298 @note could use the following, but astropy.units.cds is not fully compatible with Python 2
299 as of astropy 1.2.1 (see https://github.com/astropy/astropy/issues/5350#issuecomment-248612824):
300 astropy.units.cds.mmHg.to(astropy.units.pascal, mmHg)
302 return mmHg*PascalPerMmHg
306 """Convert pressure from torr to Pascals
308 return torr*PascalPerTorr
def altitudeFromZenithDistance
def eraFromLstAndLongitude