25 import astropy.coordinates
34 __all__ = [
"MakeRawVisitInfo"]
37 PascalPerMillibar = 100.0
38 PascalPerMmHg = 133.322387415
39 PascalPerTorr = 101325.0/760.0
40 KelvinMinusCentigrade = 273.15
48 """Base class functor to make a VisitInfo from the FITS header of a raw image.
50 A subclass will be wanted for each camera. Subclasses should override:
52 - `setArgDict`, The override can call the base implementation,
53 which simply sets exposure time and date of observation
56 The design philosophy is to make a best effort and log warnings of problems,
57 rather than raising exceptions, in order to extract as much VisitInfo information as possible
58 from a messy FITS header without the user needing to add a lot of error handling.
60 However, the methods that transform units are less forgiving; they assume
61 the user provides proper data types, since type errors in arguments to those
62 are almost certainly due to coding mistakes.
66 log : `lsst.log.Log` or None
67 Logger to use for messages.
68 (None to use ``Log.getLogger("MakeRawVisitInfo")``).
69 doStripHeader : `bool`, optional
70 Strip header keywords from the metadata as they are used?
73 def __init__(self, log=None, doStripHeader=False):
75 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) if ``doStripHeader``.
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[%s] is None; stripping", key)
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 """Return an item of metadata.
204 The item is removed if ``doStripHeader`` is ``True``.
206 Log a warning if the key is not found.
210 md : `lsst.daf.base.PropertyList` or `PropertySet`
211 Metadata to pull `key` from and (optionally) remove.
213 Metadata key to extract.
215 Value to return if key not found.
220 The value of the specified key, using whatever type md.getScalar(key)
224 if not md.exists(key):
227 val = md.getScalar(key)
231 except Exception
as e:
233 self.
log.
warn(
'Could not read key="{}" in metadata: {}'.
format(key, e))
237 """Pop a float with a default of NaN.
241 md : `lsst.daf.base.PropertyList` or `PropertySet`
242 Metadata to pull `key` from.
249 Value of the requested key as a float; float("nan") if the key is
252 val = self.
popItem(md, key, default=NaN)
255 except Exception
as e:
256 self.
log.
warn(
"Could not interpret {} value {} as a float: {}".
format(key, repr(val), e))
259 def popAngle(self, md, key, units=astropy.units.deg):
260 """Pop an lsst.afw.geom.Angle, whose metadata is in the specified units, with a default of Nan
262 The angle may be specified as a float or sexagesimal string with 1-3 fields.
266 md : `lsst.daf.base.PropertyList` or `PropertySet`
267 Metadata to pull `key` from.
273 `lsst.afw.geom.Angle`
274 Value of the requested key as an angle; Angle(NaN) if the key is
277 angleStr = self.
popItem(md, key, default=
None)
278 if angleStr
is not None:
280 return (astropy.coordinates.Angle(angleStr, unit=units).deg)*degrees
281 except Exception
as e:
282 self.
log.
warn(
"Could not intepret {} value {} as an angle: {}".
format(key, repr(angleStr), e))
286 """Pop a FITS ISO date as an lsst.daf.base.DateTime
290 md : `lsst.daf.base.PropertyList` or `PropertySet`
291 Metadata to pull `key` from.
293 Date key to read from md.
295 Time system as a string (not case sensitive), e.g. "UTC" or None;
296 if None then look for TIMESYS (but do NOT pop it, since it may be
297 used for more than one date) and if not found, use UTC.
301 `lsst.daf.base.DateTime`
302 Value of the requested date; `DateTime()` if the key is not found.
304 isoDateStr = self.
popItem(md=md, key=key)
305 if isoDateStr
is not None:
308 timesys = md.getScalar(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC"
309 if isoDateStr.endswith(
"Z"):
310 isoDateStr = isoDateStr[0:-1]
311 astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format=
"fits")
313 astropyTime.precision = 9
315 return DateTime(astropyTime.tai.isot, DateTime.TAI)
316 except Exception
as e:
317 self.
log.
warn(
"Could not parse {} = {} as an ISO date: {}".
format(key, isoDateStr, e))
321 """Get a FITS MJD date as an ``lsst.daf.base.DateTime``.
325 md : `lsst.daf.base.PropertyList` or `PropertySet`
326 Metadata to pull `key` from.
328 Date key to read from md.
330 Time system as a string (not case sensitive), e.g. "UTC" or None;
331 if None then look for TIMESYS (but do NOT pop it, since it may be
332 used for more than one date) and if not found, use UTC.
336 `lsst.daf.base.DateTime`
337 Value of the requested date; `DateTime()` if the key is not found.
342 timesys = md.getScalar(
"TIMESYS")
if md.exists(
"TIMESYS")
else "UTC"
343 astropyTime = astropy.time.Time(mjdDate, format=
"mjd", scale=timesys.lower())
345 astropyTime.precision = 9
347 return DateTime(astropyTime.tai.isot, DateTime.TAI)
348 except Exception
as e:
349 self.
log.
warn(
"Could not parse {} = {} as an MJD date: {}".
format(key, mjdDate, e))
355 Return an approximate Earth Rotation Angle (afw:Angle) computed from
356 local sidereal time and longitude (both as afw:Angle; Longitude shares
357 the afw:Observatory covention: positive values are E of Greenwich).
359 NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
362 return lst - longitude
366 """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
367 return 90*degrees - zd
371 """Convert temperature from Kelvin to Centigrade"""
372 return tempK - KelvinMinusCentigrade
376 """Convert pressure from millibars to Pascals
378 return mbar*PascalPerMillibar
382 """Convert pressure from mm Hg to Pascals
386 Could use the following, but astropy.units.cds is not fully compatible with Python 2
387 as of astropy 1.2.1 (see https://github.com/astropy/astropy/issues/5350#issuecomment-248612824):
388 astropy.units.cds.mmHg.to(astropy.units.pascal, mmHg)
390 return mmHg*PascalPerMmHg
394 """Convert pressure from torr to Pascals
396 return torr*PascalPerTorr
400 """Return the value if it is not NaN and within min/max, otherwise
406 metadata value returned by popItem, popFloat, or popAngle
407 defaultValue : `float``
408 default value to use if the metadata value is invalid
410 Minimum possible valid value, optional
412 Maximum possible valid value, optional
417 The "validated" value.
420 retVal = defaultValue
422 if minimum
is not None and value < minimum:
423 retVal = defaultValue
424 elif maximum
is not None and value > maximum:
425 retVal = defaultValue