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 "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 @classmethod
63 def can_translate(cls, header, filename=None):
64 """Indicate whether this translation class can translate the
65 supplied header.
67 There is no ``INSTRUME`` header in TS8 data. Instead we use
68 the ``TSTAND`` header. We also look at the file name to see if
69 it starts with "ts8-".
71 Older data has no ``TSTAND`` header so we must use a combination
72 of headers.
74 Parameters
75 ----------
76 header : `dict`-like
77 Header to convert to standardized form.
78 filename : `str`, optional
79 Name of file being translated.
81 Returns
82 -------
83 can : `bool`
84 `True` if the header is recognized by this class. `False`
85 otherwise.
86 """
87 can = cls.can_translate_with_options(header, {"TSTAND": "TS8"}, filename=filename)
88 if can:
89 return True
91 if "LSST_NUM" in header and "REBNAME" in header and \
92 "CONTNUM" in header and \
93 header["CONTNUM"] in ("000018910e0c", "000018ee33b7", "000018ee0f35", "000018ee3b40",
94 "00001891fcc7", "000018edfd65", "0000123b5ba8", "000018911b05",
95 "00001891fa3e", "000018910d7f", "000018ed9f12", "000018edf4a7",
96 "000018ee34e6", "000018ef1464", "000018eda120", "000018edf8a2",
97 "000018ef3819", "000018ed9486", "000018ee02c8", "000018edfb24",
98 "000018ee34c0", "000018edfb51", "0000123b51d1", "0000123b5862",
99 "0000123b8ca9", "0000189208fa", "0000189111af", "0000189126e1",
100 "000018ee0618", "000018ee3b78", "000018ef1534"):
101 return True
103 return False
105 @staticmethod
106 def compute_exposure_id(dateobs, seqnum=0, controller=None):
107 """Helper method to calculate the TS8 exposure_id.
109 Parameters
110 ----------
111 dateobs : `str`
112 Date of observation in FITS ISO format.
113 seqnum : `int`, unused
114 Sequence number. Ignored.
115 controller : `str`, unused
116 Controller type. Ignored.
118 Returns
119 -------
120 exposure_id : `int`
121 Exposure ID.
122 """
123 # There is worry that seconds are too coarse so use 10th of second
124 # and read the first 21 characters.
125 exposure_id = re.sub(r"\D", "", dateobs[:21])
126 return int(exposure_id)
128 @cache_translation
129 def to_datetime_begin(self):
130 # Docstring will be inherited. Property defined in properties.py
131 self._used_these_cards("MJD-OBS")
132 return Time(self._header["MJD-OBS"], scale="utc", format="mjd")
134 @cache_translation
135 def to_detector_name(self):
136 # Docstring will be inherited. Property defined in properties.py
137 serial = self.to_detector_serial()
138 detector_info = self.compute_detector_info_from_serial(serial)
139 return detector_info[1]
141 def to_detector_group(self):
142 """Returns the name of the raft.
144 Extracted from RAFTNAME header.
146 Raftname should be of the form: 'LCA-11021_RTM-011-Dev' and
147 the resulting name will have the form "RTM-NNN".
149 Returns
150 -------
151 name : `str`
152 Name of raft.
153 """
154 raft_name = self._header["RAFTNAME"]
155 self._used_these_cards("RAFTNAME")
156 match = re.search(r"(RTM-\d\d\d)", raft_name)
157 if match:
158 return match.group(0)
159 raise ValueError(f"RAFTNAME has unexpected form of '{raft_name}'")
161 @cache_translation
162 def to_detector_serial(self):
163 """Returns the serial number of the detector.
165 Returns
166 -------
167 serial : `str`
168 LSST assigned serial number.
170 Notes
171 -----
172 This is the LSST assigned serial number (``LSST_NUM``), and not
173 the manufacturer's serial number (``CCD_SERN``).
174 """
175 serial = self._header["LSST_NUM"]
176 self._used_these_cards("LSST_NUM")
178 # this seems to be appended more or less at random and should be
179 # removed.
180 serial = re.sub("-Dev$", "", serial)
181 return serial
183 @cache_translation
184 def to_physical_filter(self):
185 """Return the filter name.
187 Uses the FILTPOS header.
189 Returns
190 -------
191 filter : `str`
192 The filter name. Returns "NONE" if no filter can be determined.
194 Notes
195 -----
196 The calculations here are examples rather than being accurate.
197 They need to be fixed once the camera acquisition system does
198 this properly.
199 """
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 NONE)",
206 self.to_observation_id())
207 return "NONE"
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 NONE): %d",
219 self.to_observation_id(), filter_pos)
220 return "NONE"
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 filename = self._header["FILENAME"]
246 self._used_these_cards("FILENAME")
247 return filename[:filename.rfind(".")]