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: 55 - `setArgDict`, The override can call the base implementation, 56 which simply sets exposure time and date of observation 59 The design philosophy is to make a best effort and log warnings of problems, 60 rather than raising exceptions, in order to extract as much VisitInfo information as possible 61 from a messy FITS header without the user needing to add a lot of error handling. 63 However, the methods that transform units are less forgiving; they assume 64 the user provides proper data types, since type errors in arguments to those 65 are almost certainly due to coding mistakes. 69 log : `lsst.log.Log` or None 70 Logger to use for messages. 71 (None to use ``Log.getLogger("MakeRawVisitInfo")``). 76 log = Log.getLogger(
"MakeRawVisitInfo")
80 """Construct a VisitInfo and strip associated data from the metadata. 84 md : `lsst.daf.base.PropertyList` or `lsst.daf.base.PropertySet` 85 Metadata to pull from. 86 Items that are used are stripped from the metadata (except TIMESYS, 87 because it may apply to other keywords). 93 The basic implementation sets `date` and `exposureTime` using typical values 94 found in FITS files and logs a warning if neither can be set. 96 argDict = dict(exposureId=exposureId)
98 for key
in list(argDict.keys()):
99 if argDict[key]
is None:
100 self.
log.warn(
"argDict[{}] is None; stripping".format(key, argDict[key]))
102 return VisitInfo(**argDict)
105 """Fill an argument dict with arguments for VisitInfo and pop associated metadata 107 Subclasses are expected to override this method, though the override 108 may wish to call this default implementation, which: 110 - sets exposureTime from "EXPTIME" 111 - sets date by calling getDateAvg 115 md : `lsst.daf.base.PropertyList` or `PropertySet` 116 Metadata to pull from. 117 Items that are used are stripped from the metadata (except TIMESYS, 118 because it may apply to other keywords). 124 Subclasses should expand this or replace it. 126 argDict[
"exposureTime"] = self.
popFloat(md,
"EXPTIME")
127 argDict[
"date"] = self.
getDateAvg(md=md, exposureTime=argDict[
"exposureTime"])
130 """Return date at the middle of the exposure. 134 md : `lsst.daf.base.PropertyList` or `PropertySet` 135 Metadata to pull from. 136 Items that are used are stripped from the metadata (except TIMESYS, 137 because it may apply to other keywords). 138 exposureTime : `float` 143 Subclasses must override. Here is a typical implementation:: 145 dateObs = self.popIsoDate(md, "DATE-OBS") 146 return self.offsetDate(dateObs, 0.5*exposureTime) 148 raise NotImplementedError()
151 """Get the darkTime from the DARKTIME keyword, else expTime, else NaN, 153 If dark time is available then subclasses should call this method by 154 putting the following in their `__init__` method:: 156 argDict['darkTime'] = self.getDarkTime(argDict) 166 Dark time, as inferred from the metadata. 168 darkTime = argDict.get(
"darkTime", NaN)
169 if np.isfinite(darkTime):
172 self.
log.info(
"darkTime is NaN/Inf; using exposureTime")
173 exposureTime = argDict.get(
"exposureTime", NaN)
174 if not np.isfinite(exposureTime):
175 raise RuntimeError(
"Tried to substitute exposureTime for darkTime but it is not available")
180 """Return a date offset by a specified number of seconds. 182 date : `lsst.daf.base.DateTime` 183 Date baseline to offset from. 189 `lsst.daf.base.DateTime` 192 if not date.isValid():
193 self.
log.warn(
"date is invalid; cannot offset it")
195 if math.isnan(offsetSec):
196 self.
log.warn(
"offsetSec is invalid; cannot offset date")
198 dateNSec = date.nsecs(DateTime.TAI)
199 return DateTime(dateNSec + int(offsetSec*1.0e9), DateTime.TAI)
202 """Remove an item of metadata and return the value. 204 Log a warning if the key is not found. 208 md : `lsst.daf.base.PropertyList` or `PropertySet` 209 Metadata to pull `key` from and remove. 211 Metadata key to extract. 213 Value to return if key not found. 218 The value of the specified key, using whatever type md.get(key) 222 if not md.exists(key):
223 self.
log.warn(
"Key=\"{}\" not in metadata".format(key))
228 except Exception
as e:
230 self.
log.warn(
"Could not read key=\"{}\" in metadata: {}" % (key, e))
234 """Pop a float with a default of NaN. 238 md : `lsst.daf.base.PropertyList` or `PropertySet` 239 Metadata to pull `key` from and remove. 241 Key to read and remove from md. 246 Value of the requested key as a float; float("nan") if the key is 249 val = self.
popItem(md, key, default=NaN)
252 except Exception
as e:
253 self.
log.warn(
"Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
256 def popAngle(self, md, key, units=astropy.units.deg):
257 """Pop an lsst.afw.geom.Angle, whose metadata is in the specified units, with a default of Nan 259 The angle may be specified as a float or sexagesimal string with 1-3 fields. 263 md : `lsst.daf.base.PropertyList` or `PropertySet` 264 Metadata to pull `key` from and remove. 266 Key to read and remove from md. 270 `lsst.afw.geom.Angle` 271 Value of the requested key as an angle; Angle(NaN) if the key is 274 angleStr = self.
popItem(md, key, default=
None)
275 if angleStr
is not None:
277 return (astropy.coordinates.Angle(angleStr, unit=units).deg)*degrees
278 except Exception
as e:
279 self.
log.warn(
"Could not intepret {} value {} as an angle: {}".format(key, repr(angleStr), e))
283 """Pop a FITS ISO date as an lsst.daf.base.DateTime 287 md : `lsst.daf.base.PropertyList` or `PropertySet` 288 Metadata to pull `key` from and remove. 290 Date key to read and remove from md. 292 Time system as a string (not case sensitive), e.g. "UTC" or None; 293 if None then look for TIMESYS (but do NOT pop it, since it may be 294 used for more than one date) and if not found, use UTC. 298 `lsst.daf.base.DateTime` 299 Value of the requested date; `DateTime()` if the key is not found. 301 isoDateStr = self.
popItem(md=md, key=key)
302 if isoDateStr
is not None:
305 timesys = md.get(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC" 306 if isoDateStr.endswith(
"Z"):
307 isoDateStr = isoDateStr[0:-1]
308 astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format=
"fits")
310 astropyTime.precision = 9
312 return DateTime(astropyTime.tai.isot, DateTime.TAI)
313 except Exception
as e:
314 self.
log.warn(
"Could not parse {} = {} as an ISO date: {}".format(key, isoDateStr, e))
318 """Get a FITS MJD date as an ``lsst.daf.base.DateTime``. 322 md : `lsst.daf.base.PropertyList` or `PropertySet` 323 Metadata to pull `key` from and remove. 325 Date key to read and remove from md. 327 Time system as a string (not case sensitive), e.g. "UTC" or None; 328 if None then look for TIMESYS (but do NOT pop it, since it may be 329 used for more than one date) and if not found, use UTC. 333 `lsst.daf.base.DateTime` 334 Value of the requested date; `DateTime()` if the key is not found. 339 timesys = md.get(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC" 340 astropyTime = astropy.time.Time(mjdDate, format=
"mjd", scale=timesys.lower())
342 astropyTime.precision = 9
344 return DateTime(astropyTime.tai.isot, DateTime.TAI)
345 except Exception
as e:
346 self.
log.warn(
"Could not parse {} = {} as an MJD date: {}".format(key, mjdDate, e))
352 Return an approximate Earth Rotation Angle (afw:Angle) computed from 353 local sidereal time and longitude (both as afw:Angle; Longitude shares 354 the afw:Observatory covention: positive values are E of Greenwich). 356 NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove 359 return lst - longitude
363 """Convert zenith distance to altitude (lsst.afw.geom.Angle)""" 364 return 90*degrees - zd
368 """Convert temperature from Kelvin to Centigrade""" 369 return tempK - KelvinMinusCentigrade
373 """Convert pressure from millibars to Pascals 375 return mbar*PascalPerMillibar
379 """Convert pressure from mm Hg to Pascals 383 Could use the following, but astropy.units.cds is not fully compatible with Python 2 384 as of astropy 1.2.1 (see https://github.com/astropy/astropy/issues/5350#issuecomment-248612824): 385 astropy.units.cds.mmHg.to(astropy.units.pascal, mmHg) 387 return mmHg*PascalPerMmHg
391 """Convert pressure from torr to Pascals 393 return torr*PascalPerTorr
397 """Return the value if it is not NaN and within min/max, otherwise 403 metadata value returned by popItem, popFloat, or popAngle 404 defaultValue : `float`` 405 default value to use if the metadata value is invalid 407 Minimum possible valid value, optional 409 Maximum possible valid value, optional 414 The "validated" value. 417 retVal = defaultValue
419 if minimum
is not None and value < minimum:
420 retVal = defaultValue
421 elif maximum
is not None and value > maximum:
422 retVal = defaultValue
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 defaultMetadata(value, defaultValue, minimum=None, maximum=None)
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)