LSSTApplications  20.0.0
LSSTDataManagementBasePackage
makeRawVisitInfo.py
Go to the documentation of this file.
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/>.
21 
22 import math
23 import numpy as np
24 
25 import astropy.coordinates
26 import astropy.time
27 import astropy.units
28 
29 from lsst.log import Log
30 from lsst.daf.base import DateTime
31 from lsst.geom import degrees
32 from lsst.afw.image import VisitInfo
33 
34 __all__ = ["MakeRawVisitInfo"]
35 
36 
37 PascalPerMillibar = 100.0
38 PascalPerMmHg = 133.322387415 # from Wikipedia; exact
39 PascalPerTorr = 101325.0/760.0 # from Wikipedia; exact
40 KelvinMinusCentigrade = 273.15 # from Wikipedia; exact
41 
42 # have these read at need, to avoid unexpected errors later
43 NaN = float("nan")
44 BadDate = DateTime()
45 
46 
48  """Base class functor to make a VisitInfo from the FITS header of a raw image.
49 
50  A subclass will be wanted for each camera. Subclasses should override:
51 
52  - `setArgDict`, The override can call the base implementation,
53  which simply sets exposure time and date of observation
54  - `getDateAvg`
55 
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.
59 
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.
63 
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  """
72 
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
78 
79  def __call__(self, md, exposureId):
80  """Construct a VisitInfo and strip associated data from the metadata.
81 
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
90 
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)
103 
104  def setArgDict(self, md, argDict):
105  """Fill an argument dict with arguments for VisitInfo and pop associated metadata
106 
107  Subclasses are expected to override this method, though the override
108  may wish to call this default implementation, which:
109 
110  - sets exposureTime from "EXPTIME"
111  - sets date by calling getDateAvg
112 
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
121 
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"])
128 
129  def getDateAvg(self, md, exposureTime):
130  """Return date at the middle of the exposure.
131 
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)
140 
141  Notes
142  -----
143  Subclasses must override. Here is a typical implementation::
144 
145  dateObs = self.popIsoDate(md, "DATE-OBS")
146  return self.offsetDate(dateObs, 0.5*exposureTime)
147  """
148  raise NotImplementedError()
149 
150  def getDarkTime(self, argDict):
151  """Get the darkTime from the DARKTIME keyword, else expTime, else NaN,
152 
153  If dark time is available then subclasses should call this method by
154  putting the following in their `__init__` method::
155 
156  argDict['darkTime'] = self.getDarkTime(argDict)
157 
158  Parameters
159  ----------
160  argdict : `dict`
161  Dict of arguments.
162 
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
171 
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")
176 
177  return exposureTime
178 
179  def offsetDate(self, date, offsetSec):
180  """Return a date offset by a specified number of seconds.
181 
182  date : `lsst.daf.base.DateTime`
183  Date baseline to offset from.
184  offsetSec : `float`
185  Offset, in seconds.
186 
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)
200 
201  def popItem(self, md, key, default=None):
202  """Return an item of metadata.
203 
204  The item is removed if ``doStripHeader`` is ``True``.
205 
206  Log a warning if the key is not found.
207 
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.
216 
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
235 
236  def popFloat(self, md, key):
237  """Pop a float with a default of NaN.
238 
239  Parameters
240  ----------
241  md : `lsst.daf.base.PropertyList` or `PropertySet`
242  Metadata to pull `key` from.
243  key : `str`
244  Key to read.
245 
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
258 
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
261 
262  The angle may be specified as a float or sexagesimal string with 1-3 fields.
263 
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.
270 
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
284 
285  def popIsoDate(self, md, key, timesys=None):
286  """Pop a FITS ISO date as an lsst.daf.base.DateTime
287 
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.
298 
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
319 
320  def popMjdDate(self, md, key, timesys=None):
321  """Get a FITS MJD date as an ``lsst.daf.base.DateTime``.
322 
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.
333 
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
351 
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).
358 
359  NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
360  this method.
361  """
362  return lst - longitude
363 
364  @staticmethod
366  """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
367  return 90*degrees - zd
368 
369  @staticmethod
371  """Convert temperature from Kelvin to Centigrade"""
372  return tempK - KelvinMinusCentigrade
373 
374  @staticmethod
375  def pascalFromMBar(mbar):
376  """Convert pressure from millibars to Pascals
377  """
378  return mbar*PascalPerMillibar
379 
380  @staticmethod
381  def pascalFromMmHg(mmHg):
382  """Convert pressure from mm Hg to Pascals
383 
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
391 
392  @staticmethod
393  def pascalFromTorr(torr):
394  """Convert pressure from torr to Pascals
395  """
396  return torr*PascalPerTorr
397 
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.
402 
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
413 
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
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo
Definition: makeRawVisitInfo.py:47
lsst::afw::image
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Definition: imageAlgorithm.dox:1
lsst::log.log.logContinued.warn
def warn(fmt, *args)
Definition: logContinued.py:202
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.log
log
Definition: makeRawVisitInfo.py:76
lsst::log.log.logContinued.info
def info(fmt, *args)
Definition: logContinued.py:198
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.pascalFromMBar
def pascalFromMBar(mbar)
Definition: makeRawVisitInfo.py:375
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.doStripHeader
doStripHeader
Definition: makeRawVisitInfo.py:77
pex.config.history.format
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.__call__
def __call__(self, md, exposureId)
Definition: makeRawVisitInfo.py:79
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.offsetDate
def offsetDate(self, date, offsetSec)
Definition: makeRawVisitInfo.py:179
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.altitudeFromZenithDistance
def altitudeFromZenithDistance(zd)
Definition: makeRawVisitInfo.py:365
lsst::daf::base::DateTime
Class for handling dates/times, including MJD, UTC, and TAI.
Definition: DateTime.h:64
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.popMjdDate
def popMjdDate(self, md, key, timesys=None)
Definition: makeRawVisitInfo.py:320
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.popFloat
def popFloat(self, md, key)
Definition: makeRawVisitInfo.py:236
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.eraFromLstAndLongitude
def eraFromLstAndLongitude(lst, longitude)
Definition: makeRawVisitInfo.py:353
lsst::afw::image::VisitInfo
Information about a single exposure of an imaging camera.
Definition: VisitInfo.h:68
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.getDarkTime
def getDarkTime(self, argDict)
Definition: makeRawVisitInfo.py:150
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.pascalFromMmHg
def pascalFromMmHg(mmHg)
Definition: makeRawVisitInfo.py:381
lsst::log
Definition: Log.h:706
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.defaultMetadata
def defaultMetadata(value, defaultValue, minimum=None, maximum=None)
Definition: makeRawVisitInfo.py:399
object
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.popItem
def popItem(self, md, key, default=None)
Definition: makeRawVisitInfo.py:201
lsst::geom
Definition: geomOperators.dox:4
lsst::daf::base
Definition: Utils.h:47
list
daf::base::PropertyList * list
Definition: fits.cc:913
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.pascalFromTorr
def pascalFromTorr(torr)
Definition: makeRawVisitInfo.py:393
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.__init__
def __init__(self, log=None, doStripHeader=False)
Definition: makeRawVisitInfo.py:73
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.getDateAvg
def getDateAvg(self, md, exposureTime)
Definition: makeRawVisitInfo.py:129
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.setArgDict
def setArgDict(self, md, argDict)
Definition: makeRawVisitInfo.py:104
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.centigradeFromKelvin
def centigradeFromKelvin(tempK)
Definition: makeRawVisitInfo.py:370
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.popIsoDate
def popIsoDate(self, md, key, timesys=None)
Definition: makeRawVisitInfo.py:285
lsst.obs.base.makeRawVisitInfo.MakeRawVisitInfo.popAngle
def popAngle(self, md, key, units=astropy.units.deg)
Definition: makeRawVisitInfo.py:259