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

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 <http://www.gnu.org/licenses/>. 

21 

22import re 

23from lsst.pipe.tasks.ingest import ParseTask 

24from lsst.pipe.tasks.ingestCalibs import CalibsParseTask 

25from astro_metadata_translator import ObservationInfo 

26import lsst.log as lsstLog 

27from .translators.lsst import ROLLOVERTIME 

28from .translators import LsstCamTranslator 

29from .lsstCamMapper import LsstCamMapper 

30from ._fitsHeader import readRawFitsHeader 

31 

32EXTENSIONS = ["fits", "gz", "fz"] # Filename extensions to strip off 

33 

34__all__ = ["LsstCamParseTask"] 

35 

36 

37class LsstCamParseTask(ParseTask): 

38 """Parser suitable for lsstCam data. 

39 

40 See `LCA-13501 <https://ls.st/LCA-13501>`_ and 

41 `LSE-400 <https://ls.st/LSE-400>`_. 

42 """ 

43 

44 _mapperClass = LsstCamMapper 

45 _translatorClass = LsstCamTranslator 

46 

47 def __init__(self, config, *args, **kwargs): 

48 super().__init__(config, *args, **kwargs) 

49 

50 self.observationInfo = None 

51 

52 def getInfo(self, filename): 

53 """Get information about the image from the filename and its contents 

54 

55 Here, we open the image and parse the header. 

56 

57 Parameters 

58 ---------- 

59 filename : `str` 

60 Name of file to inspect 

61 

62 Returns 

63 ------- 

64 info : `dict` 

65 File properties 

66 linfo : `list` of `dict` 

67 List of file properties. Always contains the same as ``info`` 

68 because no extensions are read. 

69 """ 

70 md = readRawFitsHeader(filename, translator_class=self._translatorClass) 

71 phuInfo = self.getInfoFromMetadata(md) 

72 # No extensions to worry about 

73 return phuInfo, [phuInfo] 

74 

75 def getInfoFromMetadata(self, md, info=None): 

76 """Attempt to pull the desired information out of the header. 

77 

78 Parameters 

79 ---------- 

80 md : `lsst.daf.base.PropertyList` 

81 FITS header. 

82 info : `dict`, optional 

83 File properties, to be updated by this routine. If `None` 

84 it will be created. 

85 

86 Returns 

87 ------- 

88 info : `dict` 

89 Translated information from the metadata. Updated form of the 

90 input parameter. 

91 

92 Notes 

93 ----- 

94 

95 This is done through two mechanisms: 

96 

97 * translation: a property is set directly from the relevant header 

98 keyword. 

99 * translator: a property is set with the result of calling a method. 

100 

101 The translator methods receive the header metadata and should return 

102 the appropriate value, or None if the value cannot be determined. 

103 

104 This implementation constructs an 

105 `~astro_metadata_translator.ObservationInfo` object prior to calling 

106 each translator method, making the translated information available 

107 through the ``observationInfo`` attribute. 

108 

109 """ 

110 # Always calculate a new ObservationInfo since getInfo calls 

111 # this method repeatedly for each header. 

112 self.observationInfo = ObservationInfo(md, translator_class=self._translatorClass, 

113 pedantic=False) 

114 

115 info = super().getInfoFromMetadata(md, info) 

116 

117 # Ensure that the translated ObservationInfo is cleared. 

118 # This avoids possible confusion. 

119 self.observationInfo = None 

120 return info 

121 

122 def translate_wavelength(self, md): 

123 """Translate wavelength provided by teststand readout. 

124 

125 The teststand driving script asks for a wavelength, and then reads the 

126 value back to ensure that the correct position was moved to. This 

127 number is therefore read back with sub-nm precision. Typically the 

128 position is within 0.005nm of the desired position, so we warn if it's 

129 not very close to an integer value. 

130 

131 Future users should be aware that the ``HIERARCH MONOCH-WAVELENG`` key 

132 is NOT the requested value, and therefore cannot be used as a 

133 cross-check that the wavelength was close to the one requested. 

134 The only record of the wavelength that was set is in the original 

135 filename. 

136 

137 Parameters 

138 ---------- 

139 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet` 

140 Image metadata. 

141 

142 Returns 

143 ------- 

144 wavelength : `int` 

145 The recorded wavelength in nanometers as an `int`. 

146 """ 

147 bad_wl = -666 # Bad value for wavelength 

148 if "MONOWL" not in md: 

149 return bad_wl 

150 

151 raw_wl = float(md.getScalar("MONOWL")) 

152 

153 # Negative wavelengths are bad so normalize the bad value 

154 if raw_wl < 0: 

155 return bad_wl 

156 

157 wl = int(round(raw_wl)) 

158 if abs(raw_wl-wl) >= 0.1: 

159 logger = lsstLog.Log.getLogger('obs.lsst.ingest') 

160 logger.warn( 

161 'Translated significantly non-integer wavelength; ' 

162 '%s is more than 0.1nm from an integer value', raw_wl) 

163 return wl 

164 

165 def translate_dateObs(self, md): 

166 """Retrieve the date of observation as an ISO format string. 

167 

168 Parameters 

169 ---------- 

170 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet` 

171 Image metadata. 

172 

173 Returns 

174 ------- 

175 dateObs : `str` 

176 The date that the data was taken in FITS ISO format, 

177 e.g. ``2018-08-20T21:56:24.608``. 

178 """ 

179 dateObs = self.observationInfo.datetime_begin 

180 dateObs.format = "isot" 

181 return str(dateObs) 

182 

183 translate_date = translate_dateObs 

184 

185 def translate_dayObs(self, md): 

186 """Generate the day that the observation was taken. 

187 

188 Parameters 

189 ---------- 

190 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet` 

191 image metadata 

192 

193 Returns 

194 ------- 

195 dayObs : `str` 

196 The day that the data was taken, e.g. ``1958-02-05``. 

197 """ 

198 # Trust DAYOBS if it is there 

199 if "DAYOBS" in md: 

200 dayObs = str(md.getScalar("DAYOBS")) 

201 

202 if re.match(r"^\d{8}$", dayObs): 

203 dateObs = f"{dayObs[:4]}-{dayObs[4:6]}-{dayObs[6:8]}" 

204 return dateObs 

205 

206 # Try to work it out from date of observation 

207 dateObs = self.observationInfo.datetime_begin 

208 dateObs -= ROLLOVERTIME 

209 dateObs.format = "iso" 

210 dateObs.out_subfmt = "date" # YYYY-MM-DD format 

211 return str(dateObs) 

212 

213 def translate_snap(self, md): 

214 """Extract snap number from metadata. 

215 

216 Parameters 

217 ---------- 

218 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet` 

219 Image metadata. 

220 

221 Returns 

222 ------- 

223 snap : `int` 

224 Snap number (default: 0). 

225 """ 

226 try: 

227 return int(md.getScalar("SNAP")) 

228 except KeyError: 

229 return 0 

230 

231 def translate_detectorName(self, md): 

232 """Extract ccd ID from CHIPID. 

233 

234 Parameters 

235 ---------- 

236 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet` 

237 Image metadata. 

238 

239 Returns 

240 ------- 

241 ccdID : `str` 

242 Name of ccd, e.g. ``S01``. 

243 """ 

244 return self.observationInfo.detector_name 

245 

246 def translate_raftName(self, md): 

247 """Extract raft ID from CHIPID. 

248 

249 Parameters 

250 ---------- 

251 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet` 

252 Image metadata. 

253 

254 Returns 

255 ------- 

256 raftID : `str` 

257 Name of raft, e.g. ``R21``. 

258 """ 

259 return self.observationInfo.detector_group 

260 

261 def translate_detector(self, md): 

262 """Extract detector ID from metadata 

263 

264 Parameters 

265 ---------- 

266 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet` 

267 Image metadata. 

268 

269 Returns 

270 ------- 

271 detID : `int` 

272 Detector ID, e.g. ``4``. 

273 """ 

274 return self.observationInfo.detector_num 

275 

276 def translate_expTime(self, md): 

277 return self.observationInfo.exposure_time.value 

278 

279 def translate_object(self, md): 

280 return self.observationInfo.object 

281 

282 def translate_imageType(self, md): 

283 obstype = self.observationInfo.observation_type.upper() 

284 # Dictionary for obstype values is not yet clear 

285 if obstype == "SCIENCE": 

286 obstype = "SKYEXP" 

287 return obstype 

288 

289 def translate_filter(self, md): 

290 return self.observationInfo.physical_filter 

291 

292 def translate_lsstSerial(self, md): 

293 return self.observationInfo.detector_serial 

294 

295 def translate_run(self, md): 

296 return self.observationInfo.science_program 

297 

298 def translate_visit(self, md): 

299 return self.observationInfo.visit_id 

300 

301 def translate_obsid(self, md): 

302 return self.observationInfo.observation_id 

303 

304 def translate_testType(self, md): 

305 # Gen2 prefers upper case 

306 return self.observationInfo.observation_reason.upper() 

307 

308 def translate_expGroup(self, md): 

309 return self.observationInfo.exposure_group 

310 

311 def translate_expId(self, md): 

312 return self.observationInfo.exposure_id 

313 

314 def translate_controller(self, md): 

315 if "CONTRLLR" in md: 

316 if md["CONTRLLR"]: 

317 return md["CONTRLLR"] 

318 else: 

319 # Was undefined, sometimes it is in fact in the OBSID 

320 obsid = self.translate_obsid(md) 

321 components = obsid.split("_") 

322 if len(components) >= 2 and len(components[1]) == 1: 

323 # AT_C_20190319_00001 

324 return components[1] 

325 # Assume OCS control 

326 return "O" 

327 else: 

328 # Assume it is under camera control 

329 return "C" 

330 

331 

332class LsstCamCalibsParseTask(CalibsParseTask): 

333 """Parser for calibs.""" 

334 

335 def _translateFromCalibId(self, field, md): 

336 """Get a value from the CALIB_ID written by ``constructCalibs``.""" 

337 data = md.getScalar("CALIB_ID") 

338 match = re.search(r".*%s=(\S+)" % field, data) 

339 return match.groups()[0] 

340 

341 def translate_raftName(self, md): 

342 return self._translateFromCalibId("raftName", md) 

343 

344 def translate_detectorName(self, md): 

345 return self._translateFromCalibId("detectorName", md) 

346 

347 def translate_detector(self, md): 

348 # this is not a _great_ fix, but this obs_package is enforcing that 

349 # detectors be integers and there's not an elegant way of ensuring 

350 # this is the right type really 

351 return int(self._translateFromCalibId("detector", md)) 

352 

353 def translate_filter(self, md): 

354 return self._translateFromCalibId("filter", md) 

355 

356 def translate_calibDate(self, md): 

357 return self._translateFromCalibId("calibDate", md)