Hide keyboard shortcuts

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/>. 

21 

22import math 

23import numpy as np 

24 

25import astropy.coordinates 

26import astropy.time 

27import astropy.units 

28 

29from lsst.log import Log 

30from lsst.daf.base import DateTime 

31from lsst.geom import degrees 

32from lsst.afw.image import VisitInfo 

33 

34__all__ = ["MakeRawVisitInfo"] 

35 

36 

37PascalPerMillibar = 100.0 

38PascalPerMmHg = 133.322387415 # from Wikipedia; exact 

39PascalPerTorr = 101325.0/760.0 # from Wikipedia; exact 

40KelvinMinusCentigrade = 273.15 # from Wikipedia; exact 

41 

42# have these read at need, to avoid unexpected errors later 

43NaN = float("nan") 

44BadDate = DateTime() 

45 

46 

47class MakeRawVisitInfo(object): 

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[{}] is None; stripping".format(key, argDict[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 

365 def altitudeFromZenithDistance(zd): 

366 """Convert zenith distance to altitude (lsst.afw.geom.Angle)""" 

367 return 90*degrees - zd 

368 

369 @staticmethod 

370 def centigradeFromKelvin(tempK): 

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