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
49 image.
51 A subclass will be wanted for each camera. Subclasses should override:
53 - `setArgDict`, The override can call the base implementation,
54 which simply sets exposure time and date of observation
55 - `getDateAvg`
57 The design philosophy is to make a best effort and log warnings of
58 problems, rather than raising exceptions, in order to extract as much
59 VisitInfo information as possible from a messy FITS header without the
60 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
64 those are almost certainly due to coding mistakes.
66 Parameters
67 ----------
68 log : `lsst.log.Log` or None
69 Logger to use for messages.
70 (None to use ``Log.getLogger("MakeRawVisitInfo")``).
71 doStripHeader : `bool`, optional
72 Strip header keywords from the metadata as they are used?
73 """
75 def __init__(self, log=None, doStripHeader=False):
76 if log is None:
77 log = Log.getLogger("MakeRawVisitInfo")
78 self.log = log
79 self.doStripHeader = doStripHeader
81 def __call__(self, md, exposureId):
82 """Construct a VisitInfo and strip associated data from the metadata.
84 Parameters
85 ----------
86 md : `lsst.daf.base.PropertyList` or `lsst.daf.base.PropertySet`
87 Metadata to pull from.
88 Items that are used are stripped from the metadata (except TIMESYS,
89 because it may apply to other keywords) if ``doStripHeader``.
90 exposureId : `int`
91 exposure ID
93 Notes
94 -----
95 The basic implementation sets `date` and `exposureTime` using typical
96 values found in FITS files and logs a warning if neither can be set.
97 """
98 argDict = dict(exposureId=exposureId)
99 self.setArgDict(md, argDict)
100 for key in list(argDict.keys()): # use a copy because we may delete items
101 if argDict[key] is None:
102 self.log.warn("argDict[%s] is None; stripping", key)
103 del argDict[key]
104 return VisitInfo(**argDict)
106 def setArgDict(self, md, argDict):
107 """Fill an argument dict with arguments for VisitInfo and pop
108 associated metadata
110 Subclasses are expected to override this method, though the override
111 may wish to call this default implementation, which:
113 - sets exposureTime from "EXPTIME"
114 - sets date by calling getDateAvg
116 Parameters
117 ----------
118 md : `lsst.daf.base.PropertyList` or `PropertySet`
119 Metadata to pull from.
120 Items that are used are stripped from the metadata (except TIMESYS,
121 because it may apply to other keywords).
122 argdict : `dict`
123 dict of arguments
125 Notes
126 -----
127 Subclasses should expand this or replace it.
128 """
129 argDict["exposureTime"] = self.popFloat(md, "EXPTIME")
130 argDict["date"] = self.getDateAvg(md=md, exposureTime=argDict["exposureTime"])
132 def getDateAvg(self, md, exposureTime):
133 """Return date at the middle of the exposure.
135 Parameters
136 ----------
137 md : `lsst.daf.base.PropertyList` or `PropertySet`
138 Metadata to pull from.
139 Items that are used are stripped from the metadata (except TIMESYS,
140 because it may apply to other keywords).
141 exposureTime : `float`
142 Exposure time (sec)
144 Notes
145 -----
146 Subclasses must override. Here is a typical implementation::
148 dateObs = self.popIsoDate(md, "DATE-OBS")
149 return self.offsetDate(dateObs, 0.5*exposureTime)
150 """
151 raise NotImplementedError()
153 def getDarkTime(self, argDict):
154 """Get the darkTime from the DARKTIME keyword, else expTime, else NaN,
156 If dark time is available then subclasses should call this method by
157 putting the following in their `__init__` method::
159 argDict['darkTime'] = self.getDarkTime(argDict)
161 Parameters
162 ----------
163 argdict : `dict`
164 Dict of arguments.
166 Returns
167 -------
168 `float`
169 Dark time, as inferred from the metadata.
170 """
171 darkTime = argDict.get("darkTime", NaN)
172 if np.isfinite(darkTime):
173 return darkTime
175 self.log.info("darkTime is NaN/Inf; using exposureTime")
176 exposureTime = argDict.get("exposureTime", NaN)
177 if not np.isfinite(exposureTime):
178 raise RuntimeError("Tried to substitute exposureTime for darkTime but it is not available")
180 return exposureTime
182 def offsetDate(self, date, offsetSec):
183 """Return a date offset by a specified number of seconds.
185 date : `lsst.daf.base.DateTime`
186 Date baseline to offset from.
187 offsetSec : `float`
188 Offset, in seconds.
190 Returns
191 -------
192 `lsst.daf.base.DateTime`
193 The offset date.
194 """
195 if not date.isValid():
196 self.log.warn("date is invalid; cannot offset it")
197 return date
198 if math.isnan(offsetSec):
199 self.log.warn("offsetSec is invalid; cannot offset date")
200 return date
201 dateNSec = date.nsecs(DateTime.TAI)
202 return DateTime(dateNSec + int(offsetSec*1.0e9), DateTime.TAI)
204 def popItem(self, md, key, default=None):
205 """Return an item of metadata.
207 The item is removed if ``doStripHeader`` is ``True``.
209 Log a warning if the key is not found.
211 Parameters
212 ----------
213 md : `lsst.daf.base.PropertyList` or `PropertySet`
214 Metadata to pull `key` from and (optionally) remove.
215 key : `str`
216 Metadata key to extract.
217 default : `object`
218 Value to return if key not found.
220 Returns
221 -------
222 `object`
223 The value of the specified key, using whatever type
224 md.getScalar(key) returns.
225 """
226 try:
227 if not md.exists(key):
228 self.log.warn("Key=\"{}\" not in metadata".format(key))
229 return default
230 val = md.getScalar(key)
231 if self.doStripHeader:
232 md.remove(key)
233 return val
234 except Exception as e:
235 # this should never happen, but is a last ditch attempt to avoid
236 # exceptions
237 self.log.warn('Could not read key="{}" in metadata: {}'.format(key, e))
238 return default
240 def popFloat(self, md, key):
241 """Pop a float with a default of NaN.
243 Parameters
244 ----------
245 md : `lsst.daf.base.PropertyList` or `PropertySet`
246 Metadata to pull `key` from.
247 key : `str`
248 Key to read.
250 Returns
251 -------
252 `float`
253 Value of the requested key as a float; float("nan") if the key is
254 not found.
255 """
256 val = self.popItem(md, key, default=NaN)
257 try:
258 return float(val)
259 except Exception as e:
260 self.log.warn("Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
261 return NaN
263 def popAngle(self, md, key, units=astropy.units.deg):
264 """Pop an lsst.afw.geom.Angle, whose metadata is in the specified
265 units, with a default of Nan
267 The angle may be specified as a float or sexagesimal string with 1-3
268 fields.
270 Parameters
271 ----------
272 md : `lsst.daf.base.PropertyList` or `PropertySet`
273 Metadata to pull `key` from.
274 key : `str`
275 Key to read from md.
277 Returns
278 -------
279 `lsst.afw.geom.Angle`
280 Value of the requested key as an angle; Angle(NaN) if the key is
281 not found.
282 """
283 angleStr = self.popItem(md, key, default=None)
284 if angleStr is not None:
285 try:
286 return (astropy.coordinates.Angle(angleStr, unit=units).deg)*degrees
287 except Exception as e:
288 self.log.warn("Could not intepret {} value {} as an angle: {}".format(key, repr(angleStr), e))
289 return NaN*degrees
291 def popIsoDate(self, md, key, timesys=None):
292 """Pop a FITS ISO date as an lsst.daf.base.DateTime
294 Parameters
295 ----------
296 md : `lsst.daf.base.PropertyList` or `PropertySet`
297 Metadata to pull `key` from.
298 key : `str`
299 Date key to read from md.
300 timesys : `str`
301 Time system as a string (not case sensitive), e.g. "UTC" or None;
302 if None then look for TIMESYS (but do NOT pop it, since it may be
303 used for more than one date) and if not found, use UTC.
305 Returns
306 -------
307 `lsst.daf.base.DateTime`
308 Value of the requested date; `DateTime()` if the key is not found.
309 """
310 isoDateStr = self.popItem(md=md, key=key)
311 if isoDateStr is not None:
312 try:
313 if timesys is None:
314 timesys = md.getScalar("TIMESYS") if md.exists("TIMESYS") else "UTC"
315 if isoDateStr.endswith("Z"): # illegal in FITS
316 isoDateStr = isoDateStr[0:-1]
317 astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format="fits")
318 # DateTime uses nanosecond resolution, regardless of the
319 # resolution of the original date
320 astropyTime.precision = 9
321 # isot is ISO8601 format with "T" separating date and time and
322 # no time zone
323 return DateTime(astropyTime.tai.isot, DateTime.TAI)
324 except Exception as e:
325 self.log.warn("Could not parse {} = {} as an ISO date: {}".format(key, isoDateStr, e))
326 return BadDate
328 def popMjdDate(self, md, key, timesys=None):
329 """Get a FITS MJD date as an ``lsst.daf.base.DateTime``.
331 Parameters
332 ----------
333 md : `lsst.daf.base.PropertyList` or `PropertySet`
334 Metadata to pull `key` from.
335 key : `str`
336 Date key to read from md.
337 timesys : `str`
338 Time system as a string (not case sensitive), e.g. "UTC" or None;
339 if None then look for TIMESYS (but do NOT pop it, since it may be
340 used for more than one date) and if not found, use UTC.
342 Returns
343 -------
344 `lsst.daf.base.DateTime`
345 Value of the requested date; `DateTime()` if the key is not found.
346 """
347 mjdDate = self.popFloat(md, key)
348 try:
349 if timesys is None:
350 timesys = md.getScalar("TIMESYS") if md.exists("TIMESYS") else "UTC"
351 astropyTime = astropy.time.Time(mjdDate, format="mjd", scale=timesys.lower())
352 # DateTime uses nanosecond resolution, regardless of the resolution
353 # of the original date
354 astropyTime.precision = 9
355 # isot is ISO8601 format with "T" separating date and time and no
356 # time zone
357 return DateTime(astropyTime.tai.isot, DateTime.TAI)
358 except Exception as e:
359 self.log.warn("Could not parse {} = {} as an MJD date: {}".format(key, mjdDate, e))
360 return BadDate
362 @staticmethod
363 def eraFromLstAndLongitude(lst, longitude):
364 """
365 Return an approximate Earth Rotation Angle (afw:Angle) computed from
366 local sidereal time and longitude (both as afw:Angle; Longitude shares
367 the afw:Observatory covention: positive values are E of Greenwich).
369 NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
370 this method.
371 """
372 return lst - longitude
374 @staticmethod
375 def altitudeFromZenithDistance(zd):
376 """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
377 return 90*degrees - zd
379 @staticmethod
380 def centigradeFromKelvin(tempK):
381 """Convert temperature from Kelvin to Centigrade"""
382 return tempK - KelvinMinusCentigrade
384 @staticmethod
385 def pascalFromMBar(mbar):
386 """Convert pressure from millibars to Pascals
387 """
388 return mbar*PascalPerMillibar
390 @staticmethod
391 def pascalFromMmHg(mmHg):
392 """Convert pressure from mm Hg to Pascals
394 Notes
395 -----
396 Could use the following, but astropy.units.cds is not fully compatible
397 with Python 2 as of astropy 1.2.1 (see
398 https://github.com/astropy/astropy/issues/5350#issuecomment-248612824):
399 astropy.units.cds.mmHg.to(astropy.units.pascal, mmHg)
400 """
401 return mmHg*PascalPerMmHg
403 @staticmethod
404 def pascalFromTorr(torr):
405 """Convert pressure from torr to Pascals
406 """
407 return torr*PascalPerTorr
409 @staticmethod
410 def defaultMetadata(value, defaultValue, minimum=None, maximum=None):
411 """Return the value if it is not NaN and within min/max, otherwise
412 return defaultValue.
414 Parameters
415 ----------
416 value : `float`
417 metadata value returned by popItem, popFloat, or popAngle
418 defaultValue : `float``
419 default value to use if the metadata value is invalid
420 minimum : `float`
421 Minimum possible valid value, optional
422 maximum : `float`
423 Maximum possible valid value, optional
425 Returns
426 -------
427 `float`
428 The "validated" value.
429 """
430 if np.isnan(value):
431 retVal = defaultValue
432 else:
433 if minimum is not None and value < minimum:
434 retVal = defaultValue
435 elif maximum is not None and value > maximum:
436 retVal = defaultValue
437 else:
438 retVal = value
439 return retVal