Coverage for python/lsst/obs/lsst/translators/ts8.py: 40%
82 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-07 09:12 +0000
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-07 09:12 +0000
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, compute_detector_exposure_id_generic
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 @classmethod
128 def compute_detector_exposure_id(cls, exposure_id, detector_num):
129 # Docstring inherited from LsstBaseTranslator.
130 return compute_detector_exposure_id_generic(exposure_id, detector_num, max_num=cls.DETECTOR_MAX)
132 @cache_translation
133 def to_datetime_begin(self):
134 # Docstring will be inherited. Property defined in properties.py
135 self._used_these_cards("MJD-OBS")
136 return Time(self._header["MJD-OBS"], scale="utc", format="mjd")
138 @cache_translation
139 def to_detector_name(self):
140 # Docstring will be inherited. Property defined in properties.py
141 serial = self.to_detector_serial()
142 detector_info = self.compute_detector_info_from_serial(serial)
143 return detector_info[1]
145 def to_detector_group(self):
146 """Returns the name of the raft.
148 Extracted from RAFTNAME header.
150 Raftname should be of the form: 'LCA-11021_RTM-011-Dev' and
151 the resulting name will have the form "RTM-NNN".
153 Returns
154 -------
155 name : `str`
156 Name of raft.
157 """
158 raft_name = self._header["RAFTNAME"]
159 self._used_these_cards("RAFTNAME")
160 match = re.search(r"(RTM-\d\d\d)", raft_name)
161 if match:
162 return match.group(0)
163 raise ValueError(f"{self._log_prefix}: RAFTNAME has unexpected form of '{raft_name}'")
165 @cache_translation
166 def to_detector_serial(self):
167 """Returns the serial number of the detector.
169 Returns
170 -------
171 serial : `str`
172 LSST assigned serial number.
174 Notes
175 -----
176 This is the LSST assigned serial number (``LSST_NUM``), and not
177 the manufacturer's serial number (``CCD_SERN``).
178 """
179 serial = self._header["LSST_NUM"]
180 self._used_these_cards("LSST_NUM")
182 # this seems to be appended more or less at random and should be
183 # removed.
184 serial = re.sub("-Dev$", "", serial)
185 return serial
187 @cache_translation
188 def to_physical_filter(self):
189 """Return the filter name.
191 Uses the FILTPOS header.
193 Returns
194 -------
195 filter : `str`
196 The filter name. Returns "NONE" if no filter can be determined.
198 Notes
199 -----
200 The calculations here are examples rather than being accurate.
201 They need to be fixed once the camera acquisition system does
202 this properly.
203 """
205 default = "unknown"
206 try:
207 filter_pos = self._header["FILTPOS"]
208 self._used_these_cards("FILTPOS")
209 except KeyError:
210 log.warning("%s: FILTPOS key not found in header (assuming %s)",
211 self._log_prefix, default)
212 return default
214 try:
215 return {
216 2: 'g',
217 3: 'r',
218 4: 'i',
219 5: 'z',
220 6: 'y',
221 }[filter_pos]
222 except KeyError:
223 log.warning("%s: Unknown filter position (assuming %s): %d",
224 self._log_prefix, default, filter_pos)
225 return default
227 def to_exposure_id(self):
228 """Generate a unique exposure ID number
230 Note that SEQNUM is not unique for a given day in TS8 data
231 so instead we convert the ISO date of observation directly to an
232 integer.
234 Returns
235 -------
236 exposure_id : `int`
237 Unique exposure number.
238 """
239 iso = self._header["DATE-OBS"]
240 self._used_these_cards("DATE-OBS")
242 return self.compute_exposure_id(iso)
244 # For now assume that visit IDs and exposure IDs are identical
245 to_visit_id = to_exposure_id
247 @cache_translation
248 def to_observation_id(self):
249 # Docstring will be inherited. Property defined in properties.py
250 if self.is_key_ok("OBSID"):
251 observation_id = self._header["OBSID"]
252 self._used_these_cards("OBSID")
253 return observation_id
254 filename = self._header["FILENAME"]
255 self._used_these_cards("FILENAME")
256 return filename[:filename.rfind(".")]