Coverage for python / lsst / obs / lsst / translators / ts3.py: 66%
58 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:26 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:26 +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 BNL TestStand 3 headers"""
13__all__ = ("LsstTS3Translator", )
15import logging
16import re
17import os.path
19import astropy.units as u
20from astropy.time import Time, TimeDelta
22from astro_metadata_translator import cache_translation
24from .lsst import LsstBaseTranslator, compute_detector_exposure_id_generic
26log = logging.getLogger(__name__)
28# There is only a single sensor at a time so define a
29# fixed sensor name
30_DETECTOR_NAME = "S00"
33class LsstTS3Translator(LsstBaseTranslator):
34 """Metadata translator for LSST BNL Test Stand 3 data.
35 """
37 name = "LSST-TS3"
38 """Name of this translation class"""
40 _const_map = {
41 # TS3 is not attached to a telescope so many translations are null.
42 "instrument": "LSST-TS3",
43 "telescope": None,
44 "location": None,
45 "boresight_rotation_coord": None,
46 "boresight_rotation_angle": None,
47 "boresight_airmass": None,
48 "tracking_radec": None,
49 "altaz_begin": None,
50 "object": "UNKNOWN",
51 "relative_humidity": None,
52 "temperature": None,
53 "pressure": None,
54 "detector_name": _DETECTOR_NAME, # Single sensor
55 "can_see_sky": False,
56 }
58 _trivial_map = {
59 "detector_serial": "LSST_NUM",
60 "physical_filter": "FILTER",
61 "exposure_time": ("EXPTIME", dict(unit=u.s)),
62 }
64 DETECTOR_NAME = _DETECTOR_NAME
65 """Fixed name of single sensor."""
67 cameraPolicyFile = "policy/ts3.yaml"
69 _ROLLOVER_TIME = TimeDelta(8*60*60, scale="tai", format="sec")
70 """Time delta for the definition of a Rubin Test Stand start of day."""
72 @classmethod
73 def can_translate(cls, header, filename=None):
74 """Indicate whether this translation class can translate the
75 supplied header.
77 There is no usable ``INSTRUME`` header in TS3 data. Instead we use
78 the ``TSTAND`` header.
80 Parameters
81 ----------
82 header : `dict`-like
83 Header to convert to standardized form.
84 filename : `str`, optional
85 Name of file being translated.
87 Returns
88 -------
89 can : `bool`
90 `True` if the header is recognized by this class. `False`
91 otherwise.
92 """
93 return cls.can_translate_with_options(header, {"TSTAND": "BNL-TS3-2-Janeway"}, filename=filename)
95 @staticmethod
96 def compute_exposure_id(dateobs, seqnum=0, controller=None):
97 """Helper method to calculate the TS3 exposure_id.
99 Parameters
100 ----------
101 dateobs : `str`
102 Date of observation in FITS ISO format.
103 seqnum : `int`, unused
104 Sequence number. Ignored.
105 controller : `str`, unused
106 Controller type. Ignored.
108 Returns
109 -------
110 exposure_id : `int`
111 Exposure ID.
112 """
113 # There is worry that seconds are too coarse so use 10th of second
114 # and read the first 21 characters.
115 exposure_id = re.sub(r"\D", "", dateobs[:21])
116 return int(exposure_id)
118 @classmethod
119 def compute_detector_exposure_id(cls, exposure_id, detector_num):
120 # Docstring inherited from LsstBaseTranslator.
121 return compute_detector_exposure_id_generic(exposure_id, detector_num, max_num=cls.DETECTOR_MAX)
123 @cache_translation
124 def to_datetime_begin(self):
125 # Docstring will be inherited. Property defined in properties.py
126 self._used_these_cards("MJD-OBS")
127 return Time(self._header["MJD-OBS"], scale="utc", format="mjd")
129 def to_exposure_id(self):
130 """Generate a unique exposure ID number
132 Note that SEQNUM is not unique for a given day in TS3 data
133 so instead we convert the ISO date of observation directly to an
134 integer.
136 Returns
137 -------
138 exposure_id : `int`
139 Unique exposure number.
140 """
141 iso = self._header["DATE-OBS"]
142 self._used_these_cards("DATE-OBS")
144 return self.compute_exposure_id(iso)
146 # For now assume that visit IDs and exposure IDs are identical
147 to_visit_id = to_exposure_id
149 @cache_translation
150 def to_science_program(self):
151 """Calculate the science program information.
153 There is no header recording this in TS3 data so instead return
154 the observing day in YYYY-MM-DD format.
156 Returns
157 -------
158 run : `str`
159 Observing day in YYYY-MM-DD format.
160 """
161 # Get a copy so that we can edit the default formatting
162 date = self.to_datetime_begin().copy()
163 date.format = "iso"
164 date.out_subfmt = "date" # YYYY-MM-DD format
165 return str(date)
167 @cache_translation
168 def to_observation_id(self):
169 # Docstring will be inherited. Property defined in properties.py
170 filename = self._header["FILENAME"]
171 self._used_these_cards("FILENAME")
172 return os.path.splitext(filename)[0]
174 @cache_translation
175 def to_detector_group(self):
176 # Docstring will be inherited. Property defined in properties.py
177 serial = self.to_detector_serial()
178 detector_info = self.compute_detector_info_from_serial(serial)
179 return detector_info[0]
181 @classmethod
182 def max_exposure_id(cls):
183 # Only one controller by definition and only the date matters.
184 return cls.compute_exposure_id("2050-12-31T23:59.999")