LSST Applications  21.0.0+75b29a8a7f,21.0.0+e70536a077,21.0.0-1-ga51b5d4+62c747d40b,21.0.0-10-gbfb87ad6+3307648ee3,21.0.0-15-gedb9d5423+47cba9fc36,21.0.0-2-g103fe59+fdf0863a2a,21.0.0-2-g1367e85+d38a93257c,21.0.0-2-g45278ab+e70536a077,21.0.0-2-g5242d73+d38a93257c,21.0.0-2-g7f82c8f+e682ffb718,21.0.0-2-g8dde007+d179fbfa6a,21.0.0-2-g8f08a60+9402881886,21.0.0-2-ga326454+e682ffb718,21.0.0-2-ga63a54e+08647d4b1b,21.0.0-2-gde069b7+26c92b3210,21.0.0-2-gecfae73+0445ed2f95,21.0.0-2-gfc62afb+d38a93257c,21.0.0-27-gbbd0d29+ae871e0f33,21.0.0-28-g5fc5e037+feb0e9397b,21.0.0-3-g21c7a62+f4b9c0ff5c,21.0.0-3-g357aad2+57b0bddf0b,21.0.0-3-g4be5c26+d38a93257c,21.0.0-3-g65f322c+3f454acf5d,21.0.0-3-g7d9da8d+75b29a8a7f,21.0.0-3-gaa929c8+9e4ef6332c,21.0.0-3-ge02ed75+4b120a55c4,21.0.0-4-g3300ddd+e70536a077,21.0.0-4-g591bb35+4b120a55c4,21.0.0-4-gc004bbf+4911b9cd27,21.0.0-4-gccdca77+f94adcd104,21.0.0-4-ge8fba5a+2b3a696ff9,21.0.0-5-gb155db7+2c5429117a,21.0.0-5-gdf36809+637e4641ee,21.0.0-6-g00874e7+c9fd7f7160,21.0.0-6-g4e60332+4b120a55c4,21.0.0-7-gc8ca178+40eb9cf840,21.0.0-8-gfbe0b4b+9e4ef6332c,21.0.0-9-g2fd488a+d83b7cd606,w.2021.05
LSST Data Management Base Package
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
49  image.
50 
51  A subclass will be wanted for each camera. Subclasses should override:
52 
53  - `setArgDict`, The override can call the base implementation,
54  which simply sets exposure time and date of observation
55  - `getDateAvg`
56 
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.
61 
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.
65 
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  """
74 
75  def __init__(self, log=None, doStripHeader=False):
76  if log is None:
77  log = Log.getLogger("MakeRawVisitInfo")
78  self.loglog = log
79  self.doStripHeaderdoStripHeader = doStripHeader
80 
81  def __call__(self, md, exposureId):
82  """Construct a VisitInfo and strip associated data from the metadata.
83 
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
92 
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.setArgDictsetArgDict(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.loglog.warn("argDict[%s] is None; stripping", key)
103  del argDict[key]
104  return VisitInfo(**argDict)
105 
106  def setArgDict(self, md, argDict):
107  """Fill an argument dict with arguments for VisitInfo and pop
108  associated metadata
109 
110  Subclasses are expected to override this method, though the override
111  may wish to call this default implementation, which:
112 
113  - sets exposureTime from "EXPTIME"
114  - sets date by calling getDateAvg
115 
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
124 
125  Notes
126  -----
127  Subclasses should expand this or replace it.
128  """
129  argDict["exposureTime"] = self.popFloatpopFloat(md, "EXPTIME")
130  argDict["date"] = self.getDateAvggetDateAvg(md=md, exposureTime=argDict["exposureTime"])
131 
132  def getDateAvg(self, md, exposureTime):
133  """Return date at the middle of the exposure.
134 
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)
143 
144  Notes
145  -----
146  Subclasses must override. Here is a typical implementation::
147 
148  dateObs = self.popIsoDate(md, "DATE-OBS")
149  return self.offsetDate(dateObs, 0.5*exposureTime)
150  """
151  raise NotImplementedError()
152 
153  def getDarkTime(self, argDict):
154  """Get the darkTime from the DARKTIME keyword, else expTime, else NaN,
155 
156  If dark time is available then subclasses should call this method by
157  putting the following in their `__init__` method::
158 
159  argDict['darkTime'] = self.getDarkTime(argDict)
160 
161  Parameters
162  ----------
163  argdict : `dict`
164  Dict of arguments.
165 
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
174 
175  self.loglog.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")
179 
180  return exposureTime
181 
182  def offsetDate(self, date, offsetSec):
183  """Return a date offset by a specified number of seconds.
184 
185  date : `lsst.daf.base.DateTime`
186  Date baseline to offset from.
187  offsetSec : `float`
188  Offset, in seconds.
189 
190  Returns
191  -------
192  `lsst.daf.base.DateTime`
193  The offset date.
194  """
195  if not date.isValid():
196  self.loglog.warn("date is invalid; cannot offset it")
197  return date
198  if math.isnan(offsetSec):
199  self.loglog.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)
203 
204  def popItem(self, md, key, default=None):
205  """Return an item of metadata.
206 
207  The item is removed if ``doStripHeader`` is ``True``.
208 
209  Log a warning if the key is not found.
210 
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.
219 
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.loglog.warn("Key=\"{}\" not in metadata".format(key))
229  return default
230  val = md.getScalar(key)
231  if self.doStripHeaderdoStripHeader:
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.loglog.warn('Could not read key="{}" in metadata: {}'.format(key, e))
238  return default
239 
240  def popFloat(self, md, key):
241  """Pop a float with a default of NaN.
242 
243  Parameters
244  ----------
245  md : `lsst.daf.base.PropertyList` or `PropertySet`
246  Metadata to pull `key` from.
247  key : `str`
248  Key to read.
249 
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.popItempopItem(md, key, default=NaN)
257  try:
258  return float(val)
259  except Exception as e:
260  self.loglog.warn("Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
261  return NaN
262 
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
266 
267  The angle may be specified as a float or sexagesimal string with 1-3
268  fields.
269 
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.
276 
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.popItempopItem(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.loglog.warn("Could not intepret {} value {} as an angle: {}".format(key, repr(angleStr), e))
289  return NaN*degrees
290 
291  def popIsoDate(self, md, key, timesys=None):
292  """Pop a FITS ISO date as an lsst.daf.base.DateTime
293 
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.
304 
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.popItempopItem(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.loglog.warn("Could not parse {} = {} as an ISO date: {}".format(key, isoDateStr, e))
326  return BadDate
327 
328  def popMjdDate(self, md, key, timesys=None):
329  """Get a FITS MJD date as an ``lsst.daf.base.DateTime``.
330 
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.
341 
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.popFloatpopFloat(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.loglog.warn("Could not parse {} = {} as an MJD date: {}".format(key, mjdDate, e))
360  return BadDate
361 
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).
368 
369  NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
370  this method.
371  """
372  return lst - longitude
373 
374  @staticmethod
376  """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
377  return 90*degrees - zd
378 
379  @staticmethod
381  """Convert temperature from Kelvin to Centigrade"""
382  return tempK - KelvinMinusCentigrade
383 
384  @staticmethod
385  def pascalFromMBar(mbar):
386  """Convert pressure from millibars to Pascals
387  """
388  return mbar*PascalPerMillibar
389 
390  @staticmethod
391  def pascalFromMmHg(mmHg):
392  """Convert pressure from mm Hg to Pascals
393 
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
402 
403  @staticmethod
404  def pascalFromTorr(torr):
405  """Convert pressure from torr to Pascals
406  """
407  return torr*PascalPerTorr
408 
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.
413 
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
424 
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
Information about a single exposure of an imaging camera.
Definition: VisitInfo.h:68
Class for handling dates/times, including MJD, UTC, and TAI.
Definition: DateTime.h:64
def popAngle(self, md, key, units=astropy.units.deg)
def defaultMetadata(value, defaultValue, minimum=None, maximum=None)
def __init__(self, log=None, doStripHeader=False)
def popIsoDate(self, md, key, timesys=None)
def popMjdDate(self, md, key, timesys=None)
def popItem(self, md, key, default=None)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Definition: Log.h:706
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
daf::base::PropertyList * list
Definition: fits.cc:913