Coverage for python/lsst/obs/lsst/ingest.py : 33%

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/>.
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
32EXTENSIONS = ["fits", "gz", "fz"] # Filename extensions to strip off
34__all__ = ["LsstCamParseTask"]
37class LsstCamParseTask(ParseTask):
38 """Parser suitable for lsstCam data.
40 See `LCA-13501 <https://ls.st/LCA-13501>`_ and
41 `LSE-400 <https://ls.st/LSE-400>`_.
42 """
44 _mapperClass = LsstCamMapper
45 _translatorClass = LsstCamTranslator
47 def __init__(self, config, *args, **kwargs):
48 super().__init__(config, *args, **kwargs)
50 self.observationInfo = None
52 def getInfo(self, filename):
53 """Get information about the image from the filename and its contents
55 Here, we open the image and parse the header.
57 Parameters
58 ----------
59 filename : `str`
60 Name of file to inspect
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]
75 def getInfoFromMetadata(self, md, info=None):
76 """Attempt to pull the desired information out of the header.
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.
86 Returns
87 -------
88 info : `dict`
89 Translated information from the metadata. Updated form of the
90 input parameter.
92 Notes
93 -----
95 This is done through two mechanisms:
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.
101 The translator methods receive the header metadata and should return
102 the appropriate value, or None if the value cannot be determined.
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.
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)
115 info = super().getInfoFromMetadata(md, info)
117 # Ensure that the translated ObservationInfo is cleared.
118 # This avoids possible confusion.
119 self.observationInfo = None
120 return info
122 def translate_wavelength(self, md):
123 """Translate wavelength provided by teststand readout.
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.
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.
137 Parameters
138 ----------
139 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet`
140 Image metadata.
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
151 raw_wl = float(md.getScalar("MONOWL"))
153 # Negative wavelengths are bad so normalize the bad value
154 if raw_wl < 0:
155 return bad_wl
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
165 def translate_dateObs(self, md):
166 """Retrieve the date of observation as an ISO format string.
168 Parameters
169 ----------
170 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet`
171 Image metadata.
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)
183 translate_date = translate_dateObs
185 def translate_dayObs(self, md):
186 """Generate the day that the observation was taken.
188 Parameters
189 ----------
190 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet`
191 image metadata
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"))
202 if re.match(r"^\d{8}$", dayObs):
203 dateObs = f"{dayObs[:4]}-{dayObs[4:6]}-{dayObs[6:8]}"
204 return dateObs
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)
213 def translate_snap(self, md):
214 """Extract snap number from metadata.
216 Parameters
217 ----------
218 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet`
219 Image metadata.
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
231 def translate_detectorName(self, md):
232 """Extract ccd ID from CHIPID.
234 Parameters
235 ----------
236 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet`
237 Image metadata.
239 Returns
240 -------
241 ccdID : `str`
242 Name of ccd, e.g. ``S01``.
243 """
244 return self.observationInfo.detector_name
246 def translate_raftName(self, md):
247 """Extract raft ID from CHIPID.
249 Parameters
250 ----------
251 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet`
252 Image metadata.
254 Returns
255 -------
256 raftID : `str`
257 Name of raft, e.g. ``R21``.
258 """
259 return self.observationInfo.detector_group
261 def translate_detector(self, md):
262 """Extract detector ID from metadata
264 Parameters
265 ----------
266 md : `~lsst.daf.base.PropertyList` or `~lsst.daf.base.PropertySet`
267 Image metadata.
269 Returns
270 -------
271 detID : `int`
272 Detector ID, e.g. ``4``.
273 """
274 return self.observationInfo.detector_num
276 def translate_expTime(self, md):
277 return self.observationInfo.exposure_time.value
279 def translate_object(self, md):
280 return self.observationInfo.object
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
289 def translate_filter(self, md):
290 return self.observationInfo.physical_filter
292 def translate_lsstSerial(self, md):
293 return self.observationInfo.detector_serial
295 def translate_run(self, md):
296 return self.observationInfo.science_program
298 def translate_visit(self, md):
299 return self.observationInfo.visit_id
301 def translate_obsid(self, md):
302 return self.observationInfo.observation_id
304 def translate_testType(self, md):
305 # Gen2 prefers upper case
306 return self.observationInfo.observation_reason.upper()
308 def translate_expGroup(self, md):
309 return self.observationInfo.exposure_group
311 def translate_expId(self, md):
312 return self.observationInfo.exposure_id
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"
332class LsstCamCalibsParseTask(CalibsParseTask):
333 """Parser for calibs."""
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]
341 def translate_raftName(self, md):
342 return self._translateFromCalibId("raftName", md)
344 def translate_detectorName(self, md):
345 return self._translateFromCalibId("detectorName", md)
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))
353 def translate_filter(self, md):
354 return self._translateFromCalibId("filter", md)
356 def translate_calibDate(self, md):
357 return self._translateFromCalibId("calibDate", md)