Coverage for python/lsst/obs/lsst/translators/ts8.py: 39%
79 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-26 02:57 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-26 02:57 -0700
1# This file is currently part of obs_lsst but is written to allow it
2# to be migrated to the astro_metadata_translator package at a later date.
3#
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the LICENSE file in this directory for details of code ownership.
7#
8# Use of this source code is governed by a 3-clause BSD-style
9# license that can be found in the LICENSE file.
11"""Metadata translation code for LSST TestStand 8 headers"""
13__all__ = ("LsstTS8Translator", )
15import logging
16import re
18import astropy.units as u
19from astropy.time import Time, TimeDelta
21from astro_metadata_translator import cache_translation
23from .lsst import LsstBaseTranslator
25log = logging.getLogger(__name__)
28class LsstTS8Translator(LsstBaseTranslator):
29 """Metadata translator for LSST Test Stand 8 data.
30 """
32 name = "LSST-TS8"
33 """Name of this translation class"""
35 _const_map = {
36 # TS8 is not attached to a telescope so many translations are null.
37 "instrument": "LSST-TS8",
38 "telescope": None,
39 "location": None,
40 "boresight_rotation_coord": None,
41 "boresight_rotation_angle": None,
42 "boresight_airmass": None,
43 "tracking_radec": None,
44 "altaz_begin": None,
45 "object": "UNKNOWN",
46 "relative_humidity": None,
47 "temperature": None,
48 "pressure": None,
49 }
51 _trivial_map = {
52 "science_program": "RUNNUM",
53 "exposure_time": ("EXPTIME", dict(unit=u.s)),
54 }
56 cameraPolicyFile = "policy/ts8.yaml"
58 _ROLLOVER_TIME = TimeDelta(8*60*60, scale="tai", format="sec")
59 """Time delta for the definition of a Rubin Test Stand start of day."""
61 @classmethod
62 def can_translate(cls, header, filename=None):
63 """Indicate whether this translation class can translate the
64 supplied header.
66 There is no ``INSTRUME`` header in TS8 data. Instead we use
67 the ``TSTAND`` header. We also look at the file name to see if
68 it starts with "ts8-".
70 Older data has no ``TSTAND`` header so we must use a combination
71 of headers.
73 Parameters
74 ----------
75 header : `dict`-like
76 Header to convert to standardized form.
77 filename : `str`, optional
78 Name of file being translated.
80 Returns
81 -------
82 can : `bool`
83 `True` if the header is recognized by this class. `False`
84 otherwise.
85 """
86 can = cls.can_translate_with_options(header, {"TSTAND": "TS8"}, filename=filename)
87 if can:
88 return True
90 if "LSST_NUM" in header and "REBNAME" in header and \
91 "CONTNUM" in header and \
92 header["CONTNUM"] in ("000018910e0c", "000018ee33b7", "000018ee0f35", "000018ee3b40",
93 "00001891fcc7", "000018edfd65", "0000123b5ba8", "000018911b05",
94 "00001891fa3e", "000018910d7f", "000018ed9f12", "000018edf4a7",
95 "000018ee34e6", "000018ef1464", "000018eda120", "000018edf8a2",
96 "000018ef3819", "000018ed9486", "000018ee02c8", "000018edfb24",
97 "000018ee34c0", "000018edfb51", "0000123b51d1", "0000123b5862",
98 "0000123b8ca9", "0000189208fa", "0000189111af", "0000189126e1",
99 "000018ee0618", "000018ee3b78", "000018ef1534"):
100 return True
102 return False
104 @staticmethod
105 def compute_exposure_id(dateobs, seqnum=0, controller=None):
106 """Helper method to calculate the TS8 exposure_id.
108 Parameters
109 ----------
110 dateobs : `str`
111 Date of observation in FITS ISO format.
112 seqnum : `int`, unused
113 Sequence number. Ignored.
114 controller : `str`, unused
115 Controller type. Ignored.
117 Returns
118 -------
119 exposure_id : `int`
120 Exposure ID.
121 """
122 # There is worry that seconds are too coarse so use 10th of second
123 # and read the first 21 characters.
124 exposure_id = re.sub(r"\D", "", dateobs[:21])
125 return int(exposure_id)
127 @cache_translation
128 def to_datetime_begin(self):
129 # Docstring will be inherited. Property defined in properties.py
130 self._used_these_cards("MJD-OBS")
131 return Time(self._header["MJD-OBS"], scale="utc", format="mjd")
133 @cache_translation
134 def to_detector_name(self):
135 # Docstring will be inherited. Property defined in properties.py
136 serial = self.to_detector_serial()
137 detector_info = self.compute_detector_info_from_serial(serial)
138 return detector_info[1]
140 def to_detector_group(self):
141 """Returns the name of the raft.
143 Extracted from RAFTNAME header.
145 Raftname should be of the form: 'LCA-11021_RTM-011-Dev' and
146 the resulting name will have the form "RTM-NNN".
148 Returns
149 -------
150 name : `str`
151 Name of raft.
152 """
153 raft_name = self._header["RAFTNAME"]
154 self._used_these_cards("RAFTNAME")
155 match = re.search(r"(RTM-\d\d\d)", raft_name)
156 if match:
157 return match.group(0)
158 raise ValueError(f"{self._log_prefix}: RAFTNAME has unexpected form of '{raft_name}'")
160 @cache_translation
161 def to_detector_serial(self):
162 """Returns the serial number of the detector.
164 Returns
165 -------
166 serial : `str`
167 LSST assigned serial number.
169 Notes
170 -----
171 This is the LSST assigned serial number (``LSST_NUM``), and not
172 the manufacturer's serial number (``CCD_SERN``).
173 """
174 serial = self._header["LSST_NUM"]
175 self._used_these_cards("LSST_NUM")
177 # this seems to be appended more or less at random and should be
178 # removed.
179 serial = re.sub("-Dev$", "", serial)
180 return serial
182 @cache_translation
183 def to_physical_filter(self):
184 """Return the filter name.
186 Uses the FILTPOS header.
188 Returns
189 -------
190 filter : `str`
191 The filter name. Returns "NONE" if no filter can be determined.
193 Notes
194 -----
195 The calculations here are examples rather than being accurate.
196 They need to be fixed once the camera acquisition system does
197 this properly.
198 """
200 default = "unknown"
201 try:
202 filter_pos = self._header["FILTPOS"]
203 self._used_these_cards("FILTPOS")
204 except KeyError:
205 log.warning("%s: FILTPOS key not found in header (assuming %s)",
206 self._log_prefix, default)
207 return default
209 try:
210 return {
211 2: 'g',
212 3: 'r',
213 4: 'i',
214 5: 'z',
215 6: 'y',
216 }[filter_pos]
217 except KeyError:
218 log.warning("%s: Unknown filter position (assuming %s): %d",
219 self._log_prefix, default, filter_pos)
220 return default
222 def to_exposure_id(self):
223 """Generate a unique exposure ID number
225 Note that SEQNUM is not unique for a given day in TS8 data
226 so instead we convert the ISO date of observation directly to an
227 integer.
229 Returns
230 -------
231 exposure_id : `int`
232 Unique exposure number.
233 """
234 iso = self._header["DATE-OBS"]
235 self._used_these_cards("DATE-OBS")
237 return self.compute_exposure_id(iso)
239 # For now assume that visit IDs and exposure IDs are identical
240 to_visit_id = to_exposure_id
242 @cache_translation
243 def to_observation_id(self):
244 # Docstring will be inherited. Property defined in properties.py
245 if self.is_key_ok("OBSID"):
246 observation_id = self._header["OBSID"]
247 self._used_these_cards("OBSID")
248 return observation_id
249 filename = self._header["FILENAME"]
250 self._used_these_cards("FILENAME")
251 return filename[:filename.rfind(".")]