Coverage for python/lsst/obs/lsst/translators/ts8.py : 38%

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 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 DETECTOR_MAX = 250
57 """Maximum number of detectors to use when calculating the
58 detector_exposure_id."""
60 cameraPolicyFile = "policy/ts8.yaml"
62 _ROLLOVER_TIME = TimeDelta(8*60*60, scale="tai", format="sec")
63 """Time delta for the definition of a Rubin Test Stand start of day."""
65 @classmethod
66 def can_translate(cls, header, filename=None):
67 """Indicate whether this translation class can translate the
68 supplied header.
70 There is no ``INSTRUME`` header in TS8 data. Instead we use
71 the ``TSTAND`` header. We also look at the file name to see if
72 it starts with "ts8-".
74 Older data has no ``TSTAND`` header so we must use a combination
75 of headers.
77 Parameters
78 ----------
79 header : `dict`-like
80 Header to convert to standardized form.
81 filename : `str`, optional
82 Name of file being translated.
84 Returns
85 -------
86 can : `bool`
87 `True` if the header is recognized by this class. `False`
88 otherwise.
89 """
90 can = cls.can_translate_with_options(header, {"TSTAND": "TS8"}, filename=filename)
91 if can:
92 return True
94 if "LSST_NUM" in header and "REBNAME" in header and \
95 "CONTNUM" in header and \
96 header["CONTNUM"] in ("000018910e0c", "000018ee33b7", "000018ee0f35", "000018ee3b40",
97 "00001891fcc7", "000018edfd65", "0000123b5ba8", "000018911b05",
98 "00001891fa3e", "000018910d7f", "000018ed9f12", "000018edf4a7",
99 "000018ee34e6", "000018ef1464", "000018eda120", "000018edf8a2",
100 "000018ef3819", "000018ed9486", "000018ee02c8", "000018edfb24",
101 "000018ee34c0", "000018edfb51", "0000123b51d1", "0000123b5862",
102 "0000123b8ca9", "0000189208fa", "0000189111af", "0000189126e1",
103 "000018ee0618", "000018ee3b78", "000018ef1534"):
104 return True
106 return False
108 @staticmethod
109 def compute_exposure_id(dateobs, seqnum=0, controller=None):
110 """Helper method to calculate the TS8 exposure_id.
112 Parameters
113 ----------
114 dateobs : `str`
115 Date of observation in FITS ISO format.
116 seqnum : `int`, unused
117 Sequence number. Ignored.
118 controller : `str`, unused
119 Controller type. Ignored.
121 Returns
122 -------
123 exposure_id : `int`
124 Exposure ID.
125 """
126 # There is worry that seconds are too coarse so use 10th of second
127 # and read the first 21 characters.
128 exposure_id = re.sub(r"\D", "", dateobs[:21])
129 return int(exposure_id)
131 @cache_translation
132 def to_datetime_begin(self):
133 # Docstring will be inherited. Property defined in properties.py
134 self._used_these_cards("MJD-OBS")
135 return Time(self._header["MJD-OBS"], scale="utc", format="mjd")
137 @cache_translation
138 def to_detector_name(self):
139 # Docstring will be inherited. Property defined in properties.py
140 serial = self.to_detector_serial()
141 detector_info = self.compute_detector_info_from_serial(serial)
142 return detector_info[1]
144 def to_detector_group(self):
145 """Returns the name of the raft.
147 Extracted from RAFTNAME header.
149 Raftname should be of the form: 'LCA-11021_RTM-011-Dev' and
150 the resulting name will have the form "RTM-NNN".
152 Returns
153 -------
154 name : `str`
155 Name of raft.
156 """
157 raft_name = self._header["RAFTNAME"]
158 self._used_these_cards("RAFTNAME")
159 match = re.search(r"(RTM-\d\d\d)", raft_name)
160 if match:
161 return match.group(0)
162 raise ValueError(f"{self._log_prefix}: RAFTNAME has unexpected form of '{raft_name}'")
164 @cache_translation
165 def to_detector_serial(self):
166 """Returns the serial number of the detector.
168 Returns
169 -------
170 serial : `str`
171 LSST assigned serial number.
173 Notes
174 -----
175 This is the LSST assigned serial number (``LSST_NUM``), and not
176 the manufacturer's serial number (``CCD_SERN``).
177 """
178 serial = self._header["LSST_NUM"]
179 self._used_these_cards("LSST_NUM")
181 # this seems to be appended more or less at random and should be
182 # removed.
183 serial = re.sub("-Dev$", "", serial)
184 return serial
186 @cache_translation
187 def to_physical_filter(self):
188 """Return the filter name.
190 Uses the FILTPOS header.
192 Returns
193 -------
194 filter : `str`
195 The filter name. Returns "NONE" if no filter can be determined.
197 Notes
198 -----
199 The calculations here are examples rather than being accurate.
200 They need to be fixed once the camera acquisition system does
201 this properly.
202 """
204 try:
205 filter_pos = self._header["FILTPOS"]
206 self._used_these_cards("FILTPOS")
207 except KeyError:
208 log.warning("%s: FILTPOS key not found in header (assuming NONE)",
209 self._log_prefix)
210 return "NONE"
212 try:
213 return {
214 2: 'g',
215 3: 'r',
216 4: 'i',
217 5: 'z',
218 6: 'y',
219 }[filter_pos]
220 except KeyError:
221 log.warning("%s: Unknown filter position (assuming NONE): %d",
222 self._log_prefix, filter_pos)
223 return "NONE"
225 def to_exposure_id(self):
226 """Generate a unique exposure ID number
228 Note that SEQNUM is not unique for a given day in TS8 data
229 so instead we convert the ISO date of observation directly to an
230 integer.
232 Returns
233 -------
234 exposure_id : `int`
235 Unique exposure number.
236 """
237 iso = self._header["DATE-OBS"]
238 self._used_these_cards("DATE-OBS")
240 return self.compute_exposure_id(iso)
242 # For now assume that visit IDs and exposure IDs are identical
243 to_visit_id = to_exposure_id
245 @cache_translation
246 def to_observation_id(self):
247 # Docstring will be inherited. Property defined in properties.py
248 filename = self._header["FILENAME"]
249 self._used_these_cards("FILENAME")
250 return filename[:filename.rfind(".")]