LSSTApplications  17.0+124,17.0+14,17.0+73,18.0.0+37,18.0.0+80,18.0.0-4-g68ffd23+4,18.1.0-1-g0001055+12,18.1.0-1-g03d53ef+5,18.1.0-1-g1349e88+55,18.1.0-1-g2505f39+44,18.1.0-1-g5315e5e+4,18.1.0-1-g5e4b7ea+14,18.1.0-1-g7e8fceb+4,18.1.0-1-g85f8cd4+48,18.1.0-1-g8ff0b9f+4,18.1.0-1-ga2c679d+1,18.1.0-1-gd55f500+35,18.1.0-10-gb58edde+2,18.1.0-11-g0997b02+4,18.1.0-13-gfe4edf0b+12,18.1.0-14-g259bd21+21,18.1.0-19-gdb69f3f+2,18.1.0-2-g5f9922c+24,18.1.0-2-gd3b74e5+11,18.1.0-2-gfbf3545+32,18.1.0-26-g728bddb4+5,18.1.0-27-g6ff7ca9+2,18.1.0-3-g52aa583+25,18.1.0-3-g8ea57af+9,18.1.0-3-gb69f684+42,18.1.0-3-gfcaddf3+6,18.1.0-32-gd8786685a,18.1.0-4-gf3f9b77+6,18.1.0-5-g1dd662b+2,18.1.0-5-g6dbcb01+41,18.1.0-6-gae77429+3,18.1.0-7-g9d75d83+9,18.1.0-7-gae09a6d+30,18.1.0-9-gc381ef5+4,w.2019.45
LSSTDataManagementBasePackage
makeRawVisitInfo.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2016 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
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  """
70 
71  def __init__(self, log=None):
72  if log is None:
73  log = Log.getLogger("MakeRawVisitInfo")
74  self.log = log
75 
76  def __call__(self, md, exposureId):
77  """Construct a VisitInfo and strip associated data from the metadata.
78 
79  Parameters
80  ----------
81  md : `lsst.daf.base.PropertyList` or `lsst.daf.base.PropertySet`
82  Metadata to pull from.
83  Items that are used are stripped from the metadata (except TIMESYS,
84  because it may apply to other keywords).
85  exposureId : `int`
86  exposure ID
87 
88  Notes
89  -----
90  The basic implementation sets `date` and `exposureTime` using typical values
91  found in FITS files and logs a warning if neither can be set.
92  """
93  argDict = dict(exposureId=exposureId)
94  self.setArgDict(md, argDict)
95  for key in list(argDict.keys()): # use a copy because we may delete items
96  if argDict[key] is None:
97  self.log.warn("argDict[{}] is None; stripping".format(key, argDict[key]))
98  del argDict[key]
99  return VisitInfo(**argDict)
100 
101  def setArgDict(self, md, argDict):
102  """Fill an argument dict with arguments for VisitInfo and pop associated metadata
103 
104  Subclasses are expected to override this method, though the override
105  may wish to call this default implementation, which:
106 
107  - sets exposureTime from "EXPTIME"
108  - sets date by calling getDateAvg
109 
110  Parameters
111  ----------
112  md : `lsst.daf.base.PropertyList` or `PropertySet`
113  Metadata to pull from.
114  Items that are used are stripped from the metadata (except TIMESYS,
115  because it may apply to other keywords).
116  argdict : `dict`
117  dict of arguments
118 
119  Notes
120  -----
121  Subclasses should expand this or replace it.
122  """
123  argDict["exposureTime"] = self.popFloat(md, "EXPTIME")
124  argDict["date"] = self.getDateAvg(md=md, exposureTime=argDict["exposureTime"])
125 
126  def getDateAvg(self, md, exposureTime):
127  """Return date at the middle of the exposure.
128 
129  Parameters
130  ----------
131  md : `lsst.daf.base.PropertyList` or `PropertySet`
132  Metadata to pull from.
133  Items that are used are stripped from the metadata (except TIMESYS,
134  because it may apply to other keywords).
135  exposureTime : `float`
136  Exposure time (sec)
137 
138  Notes
139  -----
140  Subclasses must override. Here is a typical implementation::
141 
142  dateObs = self.popIsoDate(md, "DATE-OBS")
143  return self.offsetDate(dateObs, 0.5*exposureTime)
144  """
145  raise NotImplementedError()
146 
147  def getDarkTime(self, argDict):
148  """Get the darkTime from the DARKTIME keyword, else expTime, else NaN,
149 
150  If dark time is available then subclasses should call this method by
151  putting the following in their `__init__` method::
152 
153  argDict['darkTime'] = self.getDarkTime(argDict)
154 
155  Parameters
156  ----------
157  argdict : `dict`
158  Dict of arguments.
159 
160  Returns
161  -------
162  `float`
163  Dark time, as inferred from the metadata.
164  """
165  darkTime = argDict.get("darkTime", NaN)
166  if np.isfinite(darkTime):
167  return darkTime
168 
169  self.log.info("darkTime is NaN/Inf; using exposureTime")
170  exposureTime = argDict.get("exposureTime", NaN)
171  if not np.isfinite(exposureTime):
172  raise RuntimeError("Tried to substitute exposureTime for darkTime but it is not available")
173 
174  return exposureTime
175 
176  def offsetDate(self, date, offsetSec):
177  """Return a date offset by a specified number of seconds.
178 
179  date : `lsst.daf.base.DateTime`
180  Date baseline to offset from.
181  offsetSec : `float`
182  Offset, in seconds.
183 
184  Returns
185  -------
186  `lsst.daf.base.DateTime`
187  The offset date.
188  """
189  if not date.isValid():
190  self.log.warn("date is invalid; cannot offset it")
191  return date
192  if math.isnan(offsetSec):
193  self.log.warn("offsetSec is invalid; cannot offset date")
194  return date
195  dateNSec = date.nsecs(DateTime.TAI)
196  return DateTime(dateNSec + int(offsetSec*1.0e9), DateTime.TAI)
197 
198  def popItem(self, md, key, default=None):
199  """Remove an item of metadata and return the value.
200 
201  Log a warning if the key is not found.
202 
203  Parameters
204  ----------
205  md : `lsst.daf.base.PropertyList` or `PropertySet`
206  Metadata to pull `key` from and remove.
207  key : `str`
208  Metadata key to extract.
209  default : `object`
210  Value to return if key not found.
211 
212  Returns
213  -------
214  `object`
215  The value of the specified key, using whatever type md.getScalar(key)
216  returns.
217  """
218  try:
219  if not md.exists(key):
220  self.log.warn("Key=\"{}\" not in metadata".format(key))
221  return default
222  val = md.getScalar(key)
223  md.remove(key)
224  return val
225  except Exception as e:
226  # this should never happen, but is a last ditch attempt to avoid exceptions
227  self.log.warn('Could not read key="{}" in metadata: {}'.format(key, e))
228  return default
229 
230  def popFloat(self, md, key):
231  """Pop a float with a default of NaN.
232 
233  Parameters
234  ----------
235  md : `lsst.daf.base.PropertyList` or `PropertySet`
236  Metadata to pull `key` from and remove.
237  key : `str`
238  Key to read and remove from md.
239 
240  Returns
241  -------
242  `float`
243  Value of the requested key as a float; float("nan") if the key is
244  not found.
245  """
246  val = self.popItem(md, key, default=NaN)
247  try:
248  return float(val)
249  except Exception as e:
250  self.log.warn("Could not interpret {} value {} as a float: {}".format(key, repr(val), e))
251  return NaN
252 
253  def popAngle(self, md, key, units=astropy.units.deg):
254  """Pop an lsst.afw.geom.Angle, whose metadata is in the specified units, with a default of Nan
255 
256  The angle may be specified as a float or sexagesimal string with 1-3 fields.
257 
258  Parameters
259  ----------
260  md : `lsst.daf.base.PropertyList` or `PropertySet`
261  Metadata to pull `key` from and remove.
262  key : `str`
263  Key to read and remove from md.
264 
265  Returns
266  -------
267  `lsst.afw.geom.Angle`
268  Value of the requested key as an angle; Angle(NaN) if the key is
269  not found.
270  """
271  angleStr = self.popItem(md, key, default=None)
272  if angleStr is not None:
273  try:
274  return (astropy.coordinates.Angle(angleStr, unit=units).deg)*degrees
275  except Exception as e:
276  self.log.warn("Could not intepret {} value {} as an angle: {}".format(key, repr(angleStr), e))
277  return NaN*degrees
278 
279  def popIsoDate(self, md, key, timesys=None):
280  """Pop a FITS ISO date as an lsst.daf.base.DateTime
281 
282  Parameters
283  ----------
284  md : `lsst.daf.base.PropertyList` or `PropertySet`
285  Metadata to pull `key` from and remove.
286  key : `str`
287  Date key to read and remove from md.
288  timesys : `str`
289  Time system as a string (not case sensitive), e.g. "UTC" or None;
290  if None then look for TIMESYS (but do NOT pop it, since it may be
291  used for more than one date) and if not found, use UTC.
292 
293  Returns
294  -------
295  `lsst.daf.base.DateTime`
296  Value of the requested date; `DateTime()` if the key is not found.
297  """
298  isoDateStr = self.popItem(md=md, key=key)
299  if isoDateStr is not None:
300  try:
301  if timesys is None:
302  timesys = md.getScalar("TIMESYS") if md.exists("TIMESYS") else "UTC"
303  if isoDateStr.endswith("Z"): # illegal in FITS
304  isoDateStr = isoDateStr[0:-1]
305  astropyTime = astropy.time.Time(isoDateStr, scale=timesys.lower(), format="fits")
306  # DateTime uses nanosecond resolution, regardless of the resolution of the original date
307  astropyTime.precision = 9
308  # isot is ISO8601 format with "T" separating date and time and no time zone
309  return DateTime(astropyTime.tai.isot, DateTime.TAI)
310  except Exception as e:
311  self.log.warn("Could not parse {} = {} as an ISO date: {}".format(key, isoDateStr, e))
312  return BadDate
313 
314  def popMjdDate(self, md, key, timesys=None):
315  """Get a FITS MJD date as an ``lsst.daf.base.DateTime``.
316 
317  Parameters
318  ----------
319  md : `lsst.daf.base.PropertyList` or `PropertySet`
320  Metadata to pull `key` from and remove.
321  key : `str`
322  Date key to read and remove from md.
323  timesys : `str`
324  Time system as a string (not case sensitive), e.g. "UTC" or None;
325  if None then look for TIMESYS (but do NOT pop it, since it may be
326  used for more than one date) and if not found, use UTC.
327 
328  Returns
329  -------
330  `lsst.daf.base.DateTime`
331  Value of the requested date; `DateTime()` if the key is not found.
332  """
333  mjdDate = self.popFloat(md, key)
334  try:
335  if timesys is None:
336  timesys = md.getScalar("TIMESYS") if md.exists("TIMESYS") else "UTC"
337  astropyTime = astropy.time.Time(mjdDate, format="mjd", scale=timesys.lower())
338  # DateTime uses nanosecond resolution, regardless of the resolution of the original date
339  astropyTime.precision = 9
340  # isot is ISO8601 format with "T" separating date and time and no time zone
341  return DateTime(astropyTime.tai.isot, DateTime.TAI)
342  except Exception as e:
343  self.log.warn("Could not parse {} = {} as an MJD date: {}".format(key, mjdDate, e))
344  return BadDate
345 
346  @staticmethod
347  def eraFromLstAndLongitude(lst, longitude):
348  """
349  Return an approximate Earth Rotation Angle (afw:Angle) computed from
350  local sidereal time and longitude (both as afw:Angle; Longitude shares
351  the afw:Observatory covention: positive values are E of Greenwich).
352 
353  NOTE: if we properly compute ERA via UT1 a la DM-8053, we should remove
354  this method.
355  """
356  return lst - longitude
357 
358  @staticmethod
360  """Convert zenith distance to altitude (lsst.afw.geom.Angle)"""
361  return 90*degrees - zd
362 
363  @staticmethod
365  """Convert temperature from Kelvin to Centigrade"""
366  return tempK - KelvinMinusCentigrade
367 
368  @staticmethod
369  def pascalFromMBar(mbar):
370  """Convert pressure from millibars to Pascals
371  """
372  return mbar*PascalPerMillibar
373 
374  @staticmethod
375  def pascalFromMmHg(mmHg):
376  """Convert pressure from mm Hg to Pascals
377 
378  Notes
379  -----
380  Could use the following, but astropy.units.cds is not fully compatible with Python 2
381  as of astropy 1.2.1 (see https://github.com/astropy/astropy/issues/5350#issuecomment-248612824):
382  astropy.units.cds.mmHg.to(astropy.units.pascal, mmHg)
383  """
384  return mmHg*PascalPerMmHg
385 
386  @staticmethod
387  def pascalFromTorr(torr):
388  """Convert pressure from torr to Pascals
389  """
390  return torr*PascalPerTorr
391 
392  @staticmethod
393  def defaultMetadata(value, defaultValue, minimum=None, maximum=None):
394  """Return the value if it is not NaN and within min/max, otherwise
395  return defaultValue.
396 
397  Parameters
398  ----------
399  value : `float`
400  metadata value returned by popItem, popFloat, or popAngle
401  defaultValue : `float``
402  default value to use if the metadata value is invalid
403  minimum : `float`
404  Minimum possible valid value, optional
405  maximum : `float`
406  Maximum possible valid value, optional
407 
408  Returns
409  -------
410  `float`
411  The "validated" value.
412  """
413  if np.isnan(value):
414  retVal = defaultValue
415  else:
416  if minimum is not None and value < minimum:
417  retVal = defaultValue
418  elif maximum is not None and value > maximum:
419  retVal = defaultValue
420  else:
421  retVal = value
422  return retVal
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
def popIsoDate(self, md, key, timesys=None)
Class for handling dates/times, including MJD, UTC, and TAI.
Definition: DateTime.h:64
Information about a single exposure of an imaging camera.
Definition: VisitInfo.h:68
def popAngle(self, md, key, units=astropy.units.deg)
def defaultMetadata(value, defaultValue, minimum=None, maximum=None)
Definition: Log.h:706
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...
daf::base::PropertyList * list
Definition: fits.cc:903