Coverage for python/lsst/obs/base/makeRawVisitInfo.py : 22%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of obs_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import math
23import numpy as np
25import astropy.coordinates
26import astropy.time
27import astropy.units
29from lsst.log import Log
30from lsst.daf.base import DateTime
31from lsst.geom import degrees
32from lsst.afw.image import VisitInfo
34__all__ = ["MakeRawVisitInfo"]
37PascalPerMillibar = 100.0
38PascalPerMmHg = 133.322387415 # from Wikipedia; exact
39PascalPerTorr = 101325.0/760.0 # from Wikipedia; exact
40KelvinMinusCentigrade = 273.15 # from Wikipedia; exact
42# have these read at need, to avoid unexpected errors later
43NaN = float("nan")
44BadDate = DateTime()
47class MakeRawVisitInfo(object):
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
54 - `getDateAvg`
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.
64 Parameters
65 ----------
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?
71 """
73 def __init__(self, log=None, doStripHeader=False):
74 if log is None:
75 log = Log.getLogger("MakeRawVisitInfo")
76 self.log = log
77 self.doStripHeader = doStripHeader
79 def __call__(self, md, exposureId):
80 """Construct a VisitInfo and strip associated data from the metadata.
82 Parameters
83 ----------
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``.
88 exposureId : `int`
89 exposure ID
91 Notes
92 -----
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.
95 """
96 argDict = dict(exposureId=exposureId)
97 self.setArgDict(md, argDict)
98 for key in list(argDict.keys()): # use a copy because we may delete items
99 if argDict[key] is None:
100 self.log.warn("argDict[%s] is None; stripping", key)
101 del argDict[key]
102 return VisitInfo(**argDict)
104 def setArgDict(self, md, 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
113 Parameters
114 ----------
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).
119 argdict : `dict`
120 dict of arguments
122 Notes
123 -----
124 Subclasses should expand this or replace it.
125 """
126 argDict["exposureTime"] = self.popFloat(md, "EXPTIME")
127 argDict["date"] = self.getDateAvg(md=md, exposureTime=argDict["exposureTime"])
129 def getDateAvg(self, md, exposureTime):
130 """Return date at the middle of the exposure.
132 Parameters
133 ----------
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`
139 Exposure time (sec)
141 Notes
142 -----
143 Subclasses must override. Here is a typical implementation::
145 dateObs = self.popIsoDate(md, "DATE-OBS")
146 return self.offsetDate(dateObs, 0.5*exposureTime)
147 """
148 raise NotImplementedError()
150 def getDarkTime(self, argDict):
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)
158 Parameters
159 ----------
160 argdict : `dict`
161 Dict of arguments.
163 Returns
164 -------
165 `float`
166 Dark time, as inferred from the metadata.
167 """
168 darkTime = argDict.get("darkTime", NaN)
169 if np.isfinite(darkTime):
170 return 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")
177 return exposureTime
179 def offsetDate(self, date, offsetSec):
180 """Return a date offset by a specified number of seconds.
182 date : `lsst.daf.base.DateTime`
183 Date baseline to offset from.
184 offsetSec : `float`
185 Offset, in seconds.
187 Returns
188 -------
189 `lsst.daf.base.DateTime`
190 The offset date.
191 """
192 if not date.isValid():
193 self.log.warn("date is invalid; cannot offset it")
194 return date
195 if math.isnan(offsetSec):
196 self.log.warn("offsetSec is invalid; cannot offset date")
197 return date
198 dateNSec = date.nsecs(DateTime.TAI)
199 return DateTime(dateNSec + int(offsetSec*1.0e9), DateTime.TAI)
201 def popItem(self, md, key, default=None):
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.
208 Parameters
209 ----------
210 md : `lsst.daf.base.PropertyList` or `PropertySet`
211 Metadata to pull `key` from and (optionally) remove.
212 key : `str`
213 Metadata key to extract.
214 default : `object`
215 Value to return if key not found.
217 Returns
218 -------
219 `object`
220 The value of the specified key, using whatever type md.getScalar(key)
221 returns.
222 """
223 try:
224 if not md.exists(key):
225 self.log.warn("Key=\"{}\" not in metadata".format(key))
226 return default
227 val = md.getScalar(key)
228 if self.doStripHeader:
229 md.remove(key)
230 return val
231 except Exception as e:
232 # this should never happen, but is a last ditch attempt to avoid exceptions
233 self.log.warn('Could not read key="{}" in metadata: {}'.format(key, e))
234 return default
236 def popFloat(self, md, key):
237 """Pop a float with a default of NaN.
239 Parameters
240 ----------
241 md : `lsst.daf.base.PropertyList` or `PropertySet`
242 Metadata to pull `key` from.
243 key : `str`
244 Key to read.
246 Returns
247 -------
248 `float`
249 Value of the requested key as a float; float("nan") if the key is
250 not found.
251 """
252 val = self.popItem(md, key, default=NaN)
253 try:
254 return float(val)
255 except Exception as e:
256 self.log.warn("Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
257 return NaN
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.
264 Parameters
265 ----------
266 md : `lsst.daf.base.PropertyList` or `PropertySet`
267 Metadata to pull `key` from.
268 key : `str`
269 Key to read from md.
271 Returns
272 -------
273 `lsst.afw.geom.Angle`
274 Value of the requested key as an angle; Angle(NaN) if the key is
275 not found.
276 """
277 angleStr = self.popItem(md, key, default=None)
278 if angleStr is not None:
279 try:
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))
283 return NaN*degrees
285 def popIsoDate(self, md, key, timesys=None):
286 """Pop a FITS ISO date as an lsst.daf.base.DateTime
288 Parameters
289 ----------
290 md : `lsst.daf.base.PropertyList` or `PropertySet`
291 Metadata to pull `key` from.
292 key : `str`
293 Date key to read from md.
294 timesys : `str`
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.
299 Returns
300 -------
301 `lsst.daf.base.DateTime`
302 Value of the requested date; `DateTime()` if the key is not found.
303 """
304 isoDateStr = self.popItem(md=md, key=key)
305 if isoDateStr is not None:
306 try:
307 if timesys is None:
308 timesys = md.getScalar("TIMESYS") if md.exists("TIMESYS") else "UTC"
309 if isoDateStr.endswith("Z"): # illegal in FITS
310 isoDateStr = isoDateStr[0:-1]
311 astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format="fits")
312 # DateTime uses nanosecond resolution, regardless of the resolution of the original date
313 astropyTime.precision = 9
314 # isot is ISO8601 format with "T" separating date and time and no time zone
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))
318 return BadDate
320 def popMjdDate(self, md, key, timesys=None):
321 """Get a FITS MJD date as an ``lsst.daf.base.DateTime``.
323 Parameters
324 ----------
325 md : `lsst.daf.base.PropertyList` or `PropertySet`
326 Metadata to pull `key` from.
327 key : `str`
328 Date key to read from md.
329 timesys : `str`
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.
334 Returns
335 -------
336 `lsst.daf.base.DateTime`
337 Value of the requested date; `DateTime()` if the key is not found.
338 """
339 mjdDate = self.popFloat(md, key)
340 try:
341 if timesys is None:
342 timesys = md.getScalar("TIMESYS") if md.exists("TIMESYS") else "UTC"
343 astropyTime = astropy.time.Time(mjdDate, format="mjd", scale=timesys.lower())
344 # DateTime uses nanosecond resolution, regardless of the resolution of the original date
345 astropyTime.precision = 9
346 # isot is ISO8601 format with "T" separating date and time and no time zone
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))
350 return BadDate
352 @staticmethod
353 def eraFromLstAndLongitude(lst, longitude):
354 """
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
360 this method.
361 """
362 return lst - longitude
364 @staticmethod
365 def altitudeFromZenithDistance(zd):
366 """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
367 return 90*degrees - zd
369 @staticmethod
370 def centigradeFromKelvin(tempK):
371 """Convert temperature from Kelvin to Centigrade"""
372 return tempK - KelvinMinusCentigrade
374 @staticmethod
375 def pascalFromMBar(mbar):
376 """Convert pressure from millibars to Pascals
377 """
378 return mbar*PascalPerMillibar
380 @staticmethod
381 def pascalFromMmHg(mmHg):
382 """Convert pressure from mm Hg to Pascals
384 Notes
385 -----
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)
389 """
390 return mmHg*PascalPerMmHg
392 @staticmethod
393 def pascalFromTorr(torr):
394 """Convert pressure from torr to Pascals
395 """
396 return torr*PascalPerTorr
398 @staticmethod
399 def defaultMetadata(value, defaultValue, minimum=None, maximum=None):
400 """Return the value if it is not NaN and within min/max, otherwise
401 return defaultValue.
403 Parameters
404 ----------
405 value : `float`
406 metadata value returned by popItem, popFloat, or popAngle
407 defaultValue : `float``
408 default value to use if the metadata value is invalid
409 minimum : `float`
410 Minimum possible valid value, optional
411 maximum : `float`
412 Maximum possible valid value, optional
414 Returns
415 -------
416 `float`
417 The "validated" value.
418 """
419 if np.isnan(value):
420 retVal = defaultValue
421 else:
422 if minimum is not None and value < minimum:
423 retVal = defaultValue
424 elif maximum is not None and value > maximum:
425 retVal = defaultValue
426 else:
427 retVal = value
428 return retVal