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 popIsoDate(self, md, key, timesys=None)
def setArgDict(self, md, argDict)
def eraFromLstAndLongitude(lst, longitude)
def popFloat(self, md, key)
def popAngle(self, md, key, units=astropy.units.deg)
def popMjdDate(self, md, key, timesys=None)
def getDateAvg(self, md, exposureTime)
def altitudeFromZenithDistance(zd)
def popItem(self, md, key, default=None)
def __init__(self, log=None)
def getDarkTime(self, argDict)
def __call__(self, md, exposureId)
def centigradeFromKelvin(tempK)
def offsetDate(self, date, offsetSec)