Coverage for python/lsst/obs/lsst/translators/lsst_ucdcam.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 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 = 3
79 """Maximum number of detectors to use when calculating the
80 detector_exposure_id."""
82 _ROLLOVER_TIME = TimeDelta(8*60*60, scale="tai", format="sec")
83 """Time delta for the definition of a Rubin Test Stand start of day."""
85 @classmethod
86 def can_translate(cls, header, filename=None):
87 """Indicate whether this translation class can translate the
88 supplied header.
90 Parameters
91 ----------
92 header : `dict`-like
93 Header to convert to standardized form.
94 filename : `str`, optional
95 Name of file being translated.
97 Returns
98 -------
99 can : `bool`
100 `True` if the header is recognized by this class. `False`
101 otherwise.
102 """
103 # Check 3 headers that all have to match
104 for k, v in (("ORIGIN", "UCDAVIS"), ("INSTRUME", "SAO"), ("TSTAND", "LSST_OPTICAL_SIMULATOR")):
105 if k not in header:
106 return False
107 if header[k] != v:
108 return False
109 return True
111 @classmethod
112 def compute_detector_num_from_name(cls, detector_group, detector_name):
113 """Helper method to return the detector number from the name.
115 Parameters
116 ----------
117 detector_group : `str`
118 Detector group name. This is generally the raft name.
119 detector_name : `str`
120 Detector name. Checked to ensure it is the expected name.
122 Returns
123 -------
124 num : `int`
125 Detector number.
127 Raises
128 ------
129 ValueError
130 The supplied name is not known.
131 """
132 if detector_name != cls.DETECTOR_NAME:
133 raise ValueError(f"Detector {detector_name} is not known to UCDCam")
134 for num, group in cls._detector_map.values():
135 if group == detector_group:
136 return num
137 raise ValueError(f"Detector {detector_group}_{detector_name} is not known to UCDCam")
139 @classmethod
140 def compute_detector_group_from_num(cls, detector_num):
141 """Helper method to return the detector group from the number.
143 Parameters
144 ----------
145 detector_num : `int`
146 Detector number.
148 Returns
149 -------
150 group : `str`
151 Detector group.
153 Raises
154 ------
155 ValueError
156 The supplied number is not known.
157 """
158 for num, group in cls._detector_map.values():
159 if num == detector_num:
160 return group
161 raise ValueError(f"Detector {detector_num} is not known for UCDCam")
163 @staticmethod
164 def compute_exposure_id(dateobs, seqnum=0, controller=None):
165 """Helper method to calculate the exposure_id.
167 Parameters
168 ----------
169 dateobs : `str`
170 Date of observation in FITS ISO format.
171 seqnum : `int`, unused
172 Sequence number. Ignored.
173 controller : `str`, unused
174 Controller type. Ignored.
176 Returns
177 -------
178 exposure_id : `int`
179 Exposure ID.
180 """
181 # Use 1 second resolution
182 exposure_id = re.sub(r"\D", "", dateobs[:19])
183 return int(exposure_id)
185 @cache_translation
186 def to_datetime_begin(self):
187 # Docstring will be inherited. Property defined in properties.py
188 self._used_these_cards("MJD")
189 mjd = float(self._header["MJD"]) # Header uses a FITS string
190 return Time(mjd, scale="utc", format="mjd")
192 @cache_translation
193 def to_detector_num(self):
194 """Determine the number associated with this detector.
196 Returns
197 -------
198 num : `int`
199 The number of the detector. Each CCD gets a different number.
200 """
201 serial = self.to_detector_serial()
202 return self._detector_map[serial][0]
204 @cache_translation
205 def to_detector_group(self):
206 """Determine the pseudo raft name associated with this detector.
208 Returns
209 -------
210 raft : `str`
211 The name of the raft. The raft is derived from the serial number
212 of the detector.
213 """
214 serial = self.to_detector_serial()
215 return self._detector_map[serial][1]
217 @cache_translation
218 def to_physical_filter(self):
219 """Return the filter name.
221 Uses the FILTER header.
223 Returns
224 -------
225 filter : `str`
226 The filter name. Returns "NONE" if no filter can be determined.
227 """
229 if "FILTER" in self._header:
230 self._used_these_cards("FILTER")
231 return self._header["FILTER"].lower()
232 else:
233 log.warning("%s: FILTER key not found in header (assuming NONE)",
234 self._log_prefix)
235 return "NONE"
237 def to_exposure_id(self):
238 """Generate a unique exposure ID number
240 Note that SEQNUM is not unique for a given day
241 so instead we convert the ISO date of observation directly to an
242 integer.
244 Returns
245 -------
246 exposure_id : `int`
247 Unique exposure number.
248 """
249 date = self.to_datetime_begin()
250 return self.compute_exposure_id(date.isot)
252 # For now assume that visit IDs and exposure IDs are identical
253 to_visit_id = to_exposure_id
255 @cache_translation
256 def to_observation_id(self):
257 # Docstring will be inherited. Property defined in properties.py
258 filename = self._header["FILENAME"]
259 self._used_these_cards("FILENAME")
260 return os.path.splitext(os.path.basename(filename))[0]
262 @cache_translation
263 def to_science_program(self):
264 """Calculate the run number for this observation.
266 There is no explicit run header, so instead treat each day
267 as the run in YYYY-MM-DD format.
269 Returns
270 -------
271 run : `str`
272 YYYY-MM-DD string corresponding to the date of observation.
273 """
274 # Get a copy so that we can edit the default formatting
275 date = self.to_datetime_begin().copy()
276 date.format = "iso"
277 date.out_subfmt = "date" # YYYY-MM-DD format
278 return str(date)