Coverage for python/lsst/obs/lsst/translators/lsst_ucdcam.py: 55%
88 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-04 11:46 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-04 11:46 +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, compute_detector_exposure_id_generic
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 @classmethod
189 def compute_detector_exposure_id(cls, exposure_id, detector_num):
190 # Docstring inherited from LsstBaseTranslator.
191 return compute_detector_exposure_id_generic(exposure_id, detector_num, max_num=cls.DETECTOR_MAX)
193 @cache_translation
194 def to_datetime_begin(self):
195 # Docstring will be inherited. Property defined in properties.py
196 self._used_these_cards("MJD")
197 mjd = float(self._header["MJD"]) # Header uses a FITS string
198 return Time(mjd, scale="utc", format="mjd")
200 @cache_translation
201 def to_detector_num(self):
202 """Determine the number associated with this detector.
204 Returns
205 -------
206 num : `int`
207 The number of the detector. Each CCD gets a different number.
208 """
209 serial = self.to_detector_serial()
210 return self._detector_map[serial][0]
212 @cache_translation
213 def to_detector_group(self):
214 """Determine the pseudo raft name associated with this detector.
216 Returns
217 -------
218 raft : `str`
219 The name of the raft. The raft is derived from the serial number
220 of the detector.
221 """
222 serial = self.to_detector_serial()
223 return self._detector_map[serial][1]
225 @cache_translation
226 def to_physical_filter(self):
227 """Return the filter name.
229 Uses the FILTER header.
231 Returns
232 -------
233 filter : `str`
234 The filter name. Returns "NONE" if no filter can be determined.
235 """
237 if "FILTER" in self._header:
238 self._used_these_cards("FILTER")
239 return self._header["FILTER"].lower()
240 else:
241 log.warning("%s: FILTER key not found in header (assuming NONE)",
242 self._log_prefix)
243 return "NONE"
245 def to_exposure_id(self):
246 """Generate a unique exposure ID number
248 Note that SEQNUM is not unique for a given day
249 so instead we convert the ISO date of observation directly to an
250 integer.
252 Returns
253 -------
254 exposure_id : `int`
255 Unique exposure number.
256 """
257 date = self.to_datetime_begin()
258 return self.compute_exposure_id(date.isot)
260 # For now assume that visit IDs and exposure IDs are identical
261 to_visit_id = to_exposure_id
263 @cache_translation
264 def to_observation_id(self):
265 # Docstring will be inherited. Property defined in properties.py
266 filename = self._header["FILENAME"]
267 self._used_these_cards("FILENAME")
268 return os.path.splitext(os.path.basename(filename))[0]
270 @cache_translation
271 def to_science_program(self):
272 """Calculate the run number for this observation.
274 There is no explicit run header, so instead treat each day
275 as the run in YYYY-MM-DD format.
277 Returns
278 -------
279 run : `str`
280 YYYY-MM-DD string corresponding to the date of observation.
281 """
282 # Get a copy so that we can edit the default formatting
283 date = self.to_datetime_begin().copy()
284 date.format = "iso"
285 date.out_subfmt = "date" # YYYY-MM-DD format
286 return str(date)