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

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
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 "telescope": "LSST",
38 "location": None,
39 "boresight_rotation_coord": None,
40 "boresight_rotation_angle": None,
41 "boresight_airmass": None,
42 "tracking_radec": None,
43 "altaz_begin": None,
44 "object": "UNKNOWN",
45 "relative_humidity": None,
46 "temperature": None,
47 "pressure": None,
48 }
50 _trivial_map = {
51 "science_program": "RUNNUM",
52 "exposure_time": ("EXPTIME", dict(unit=u.s)),
53 }
55 DETECTOR_MAX = 250
56 """Maximum number of detectors to use when calculating the
57 detector_exposure_id."""
59 cameraPolicyFile = "policy/ts8.yaml"
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_instrument(self):
129 """Calculate the instrument name.
131 Returns
132 -------
133 instrume : `str`
134 Name of the test stand.
135 """
136 return "LSST-TS8"
138 @cache_translation
139 def to_datetime_begin(self):
140 # Docstring will be inherited. Property defined in properties.py
141 self._used_these_cards("MJD-OBS")
142 return Time(self._header["MJD-OBS"], scale="utc", format="mjd")
144 @cache_translation
145 def to_detector_name(self):
146 # Docstring will be inherited. Property defined in properties.py
147 serial = self.to_detector_serial()
148 detector_info = self.compute_detector_info_from_serial(serial)
149 return detector_info[1]
151 def to_detector_group(self):
152 """Returns the name of the raft.
154 Extracted from RAFTNAME header.
156 Raftname should be of the form: 'LCA-11021_RTM-011-Dev' and
157 the resulting name will have the form "RTM-NNN".
159 Returns
160 -------
161 name : `str`
162 Name of raft.
163 """
164 raft_name = self._header["RAFTNAME"]
165 self._used_these_cards("RAFTNAME")
166 match = re.search(r"(RTM-\d\d\d)", raft_name)
167 if match:
168 return match.group(0)
169 raise ValueError(f"RAFTNAME has unexpected form of '{raft_name}'")
171 @cache_translation
172 def to_detector_serial(self):
173 """Returns the serial number of the detector.
175 Returns
176 -------
177 serial : `str`
178 LSST assigned serial number.
180 Notes
181 -----
182 This is the LSST assigned serial number (``LSST_NUM``), and not
183 the manufacturer's serial number (``CCD_SERN``).
184 """
185 serial = self._header["LSST_NUM"]
186 self._used_these_cards("LSST_NUM")
188 # this seems to be appended more or less at random and should be
189 # removed.
190 serial = re.sub("-Dev$", "", serial)
191 return serial
193 @cache_translation
194 def to_physical_filter(self):
195 """Return the filter name.
197 Uses the FILTPOS header.
199 Returns
200 -------
201 filter : `str`
202 The filter name. Returns "NONE" if no filter can be determined.
204 Notes
205 -----
206 The calculations here are examples rather than being accurate.
207 They need to be fixed once the camera acquisition system does
208 this properly.
209 """
211 try:
212 filter_pos = self._header["FILTPOS"]
213 self._used_these_cards("FILTPOS")
214 except KeyError:
215 log.warning("%s: FILTPOS key not found in header (assuming NONE)",
216 self.to_observation_id())
217 return "NONE"
219 try:
220 return {
221 2: 'g',
222 3: 'r',
223 4: 'i',
224 5: 'z',
225 6: 'y',
226 }[filter_pos]
227 except KeyError:
228 log.warning("%s: Unknown filter position (assuming NONE): %d",
229 self.to_observation_id(), filter_pos)
230 return "NONE"
232 def to_exposure_id(self):
233 """Generate a unique exposure ID number
235 Note that SEQNUM is not unique for a given day in TS8 data
236 so instead we convert the ISO date of observation directly to an
237 integer.
239 Returns
240 -------
241 exposure_id : `int`
242 Unique exposure number.
243 """
244 iso = self._header["DATE-OBS"]
245 self._used_these_cards("DATE-OBS")
247 return self.compute_exposure_id(iso)
249 # For now assume that visit IDs and exposure IDs are identical
250 to_visit_id = to_exposure_id
252 @cache_translation
253 def to_observation_id(self):
254 # Docstring will be inherited. Property defined in properties.py
255 filename = self._header["FILENAME"]
256 self._used_these_cards("FILENAME")
257 return filename[:filename.rfind(".")]