Coverage for python/lsst/obs/lsst/translators/lsst_ucdcam.py: 45%
85 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-10 12:12 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-10 12: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 UCDavis Test Stand headers"""
13__all__ = ("LsstUCDCamTranslator", )
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
26log = logging.getLogger(__name__)
28# There is only one detector name used
29_DETECTOR_NAME = "S00"
32class LsstUCDCamTranslator(LsstBaseTranslator):
33 """Metadata translator for LSST UC Davis test camera data.
35 This instrument is a test system for individual LSST CCDs.
36 To fit this instrument into the standard naming convention for LSST
37 instruments we use a fixed detector name (S00) and assign a different
38 raft name to each detector. The detector number changes for each CCD.
39 """
41 name = "LSST-UCDCam"
42 """Name of this translation class"""
44 _const_map = {
45 # UCDCam is not attached to a telescope so many translations are null.
46 "instrument": "LSST-UCDCam",
47 "telescope": None,
48 "location": None,
49 "boresight_rotation_coord": None,
50 "boresight_rotation_angle": None,
51 "boresight_airmass": None,
52 "tracking_radec": None,
53 "altaz_begin": None,
54 "object": "UNKNOWN",
55 "relative_humidity": None,
56 "temperature": None,
57 "pressure": None,
58 "detector_name": _DETECTOR_NAME,
59 }
61 _trivial_map = {
62 "exposure_time": ("EXPTIME", dict(unit=u.s)),
63 "detector_serial": "LSST_NUM",
64 }
66 DETECTOR_NAME = _DETECTOR_NAME
67 """Fixed name of single sensor in raft."""
69 _detector_map = {
70 "E2V-CCD250-112-04": (0, "R00"),
71 "ITL-3800C-029": (1, "R01"),
72 "ITL-3800C-002": (2, "R02"),
73 "E2V-CCD250-112-09": (0, "R03"),
74 }
75 """Map detector serial to raft and detector number. Eventually the
76 detector number will come out of the policy camera definition."""
78 DETECTOR_MAX = 10
79 """Maximum number of detectors to use when calculating the
80 detector_exposure_id.
82 This is rounded up to a power of ten to make those IDs human-decodable.
83 """
85 _ROLLOVER_TIME = TimeDelta(8*60*60, scale="tai", format="sec")
86 """Time delta for the definition of a Rubin Test Stand start of day."""
88 @classmethod
89 def can_translate(cls, header, filename=None):
90 """Indicate whether this translation class can translate the
91 supplied header.
93 Parameters
94 ----------
95 header : `dict`-like
96 Header to convert to standardized form.
97 filename : `str`, optional
98 Name of file being translated.
100 Returns
101 -------
102 can : `bool`
103 `True` if the header is recognized by this class. `False`
104 otherwise.
105 """
106 # Check 3 headers that all have to match
107 for k, v in (("ORIGIN", "UCDAVIS"), ("INSTRUME", "SAO"), ("TSTAND", "LSST_OPTICAL_SIMULATOR")):
108 if k not in header:
109 return False
110 if header[k] != v:
111 return False
112 return True
114 @classmethod
115 def compute_detector_num_from_name(cls, detector_group, detector_name):
116 """Helper method to return the detector number from the name.
118 Parameters
119 ----------
120 detector_group : `str`
121 Detector group name. This is generally the raft name.
122 detector_name : `str`
123 Detector name. Checked to ensure it is the expected name.
125 Returns
126 -------
127 num : `int`
128 Detector number.
130 Raises
131 ------
132 ValueError
133 The supplied name is not known.
134 """
135 if detector_name != cls.DETECTOR_NAME:
136 raise ValueError(f"Detector {detector_name} is not known to UCDCam")
137 for num, group in cls._detector_map.values():
138 if group == detector_group:
139 return num
140 raise ValueError(f"Detector {detector_group}_{detector_name} is not known to UCDCam")
142 @classmethod
143 def compute_detector_group_from_num(cls, detector_num):
144 """Helper method to return the detector group from the number.
146 Parameters
147 ----------
148 detector_num : `int`
149 Detector number.
151 Returns
152 -------
153 group : `str`
154 Detector group.
156 Raises
157 ------
158 ValueError
159 The supplied number is not known.
160 """
161 for num, group in cls._detector_map.values():
162 if num == detector_num:
163 return group
164 raise ValueError(f"Detector {detector_num} is not known for UCDCam")
166 @staticmethod
167 def compute_exposure_id(dateobs, seqnum=0, controller=None):
168 """Helper method to calculate the exposure_id.
170 Parameters
171 ----------
172 dateobs : `str`
173 Date of observation in FITS ISO format.
174 seqnum : `int`, unused
175 Sequence number. Ignored.
176 controller : `str`, unused
177 Controller type. Ignored.
179 Returns
180 -------
181 exposure_id : `int`
182 Exposure ID.
183 """
184 # Use 1 second resolution
185 exposure_id = re.sub(r"\D", "", dateobs[:19])
186 return int(exposure_id)
188 @cache_translation
189 def to_datetime_begin(self):
190 # Docstring will be inherited. Property defined in properties.py
191 self._used_these_cards("MJD")
192 mjd = float(self._header["MJD"]) # Header uses a FITS string
193 return Time(mjd, scale="utc", format="mjd")
195 @cache_translation
196 def to_detector_num(self):
197 """Determine the number associated with this detector.
199 Returns
200 -------
201 num : `int`
202 The number of the detector. Each CCD gets a different number.
203 """
204 serial = self.to_detector_serial()
205 return self._detector_map[serial][0]
207 @cache_translation
208 def to_detector_group(self):
209 """Determine the pseudo raft name associated with this detector.
211 Returns
212 -------
213 raft : `str`
214 The name of the raft. The raft is derived from the serial number
215 of the detector.
216 """
217 serial = self.to_detector_serial()
218 return self._detector_map[serial][1]
220 @cache_translation
221 def to_physical_filter(self):
222 """Return the filter name.
224 Uses the FILTER header.
226 Returns
227 -------
228 filter : `str`
229 The filter name. Returns "NONE" if no filter can be determined.
230 """
232 if "FILTER" in self._header:
233 self._used_these_cards("FILTER")
234 return self._header["FILTER"].lower()
235 else:
236 log.warning("%s: FILTER key not found in header (assuming NONE)",
237 self._log_prefix)
238 return "NONE"
240 def to_exposure_id(self):
241 """Generate a unique exposure ID number
243 Note that SEQNUM is not unique for a given day
244 so instead we convert the ISO date of observation directly to an
245 integer.
247 Returns
248 -------
249 exposure_id : `int`
250 Unique exposure number.
251 """
252 date = self.to_datetime_begin()
253 return self.compute_exposure_id(date.isot)
255 # For now assume that visit IDs and exposure IDs are identical
256 to_visit_id = to_exposure_id
258 @cache_translation
259 def to_observation_id(self):
260 # Docstring will be inherited. Property defined in properties.py
261 filename = self._header["FILENAME"]
262 self._used_these_cards("FILENAME")
263 return os.path.splitext(os.path.basename(filename))[0]
265 @cache_translation
266 def to_science_program(self):
267 """Calculate the run number for this observation.
269 There is no explicit run header, so instead treat each day
270 as the run in YYYY-MM-DD format.
272 Returns
273 -------
274 run : `str`
275 YYYY-MM-DD string corresponding to the date of observation.
276 """
277 # Get a copy so that we can edit the default formatting
278 date = self.to_datetime_begin().copy()
279 date.format = "iso"
280 date.out_subfmt = "date" # YYYY-MM-DD format
281 return str(date)