Coverage for python/lsst/obs/lsst/translators/lsst_ucdcam.py : 36%

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
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 # TS8 is not attached to a telescope so many translations are null.
46 "telescope": None,
47 "location": None,
48 "boresight_rotation_coord": None,
49 "boresight_rotation_angle": None,
50 "boresight_airmass": None,
51 "tracking_radec": None,
52 "altaz_begin": None,
53 "object": "UNKNOWN",
54 "relative_humidity": None,
55 "temperature": None,
56 "pressure": None,
57 "detector_name": _DETECTOR_NAME,
58 }
60 _trivial_map = {
61 "exposure_time": ("EXPTIME", dict(unit=u.s)),
62 "detector_serial": "LSST_NUM",
63 }
65 DETECTOR_NAME = _DETECTOR_NAME
66 """Fixed name of single sensor in raft."""
68 _detector_map = {
69 "E2V-CCD250-112-04": (0, "R00"),
70 "ITL-3800C-029": (1, "R01"),
71 "ITL-3800C-002": (2, "R02"),
72 "E2V-CCD250-112-09": (0, "R03"),
73 }
74 """Map detector serial to raft and detector number. Eventually the
75 detector number will come out of the policy camera definition."""
77 DETECTOR_MAX = 3
78 """Maximum number of detectors to use when calculating the
79 detector_exposure_id."""
81 @classmethod
82 def can_translate(cls, header, filename=None):
83 """Indicate whether this translation class can translate the
84 supplied header.
86 Parameters
87 ----------
88 header : `dict`-like
89 Header to convert to standardized form.
90 filename : `str`, optional
91 Name of file being translated.
93 Returns
94 -------
95 can : `bool`
96 `True` if the header is recognized by this class. `False`
97 otherwise.
98 """
99 # Check 3 headers that all have to match
100 for k, v in (("ORIGIN", "UCDAVIS"), ("INSTRUME", "SAO"), ("TSTAND", "LSST_OPTICAL_SIMULATOR")):
101 if k not in header:
102 return False
103 if header[k] != v:
104 return False
105 return True
107 @classmethod
108 def compute_detector_num_from_name(cls, detector_group, detector_name):
109 """Helper method to return the detector number from the name.
111 Parameters
112 ----------
113 detector_group : `str`
114 Detector group name. This is generally the raft name.
115 detector_name : `str`
116 Detector name. Checked to ensure it is the expected name.
118 Returns
119 -------
120 num : `int`
121 Detector number.
123 Raises
124 ------
125 ValueError
126 The supplied name is not known.
127 """
128 if detector_name != cls.DETECTOR_NAME:
129 raise ValueError(f"Detector {detector_name} is not known to UCDCam")
130 for num, group in cls._detector_map.values():
131 if group == detector_group:
132 return num
133 raise ValueError(f"Detector {detector_group}_{detector_name} is not known to UCDCam")
135 @classmethod
136 def compute_detector_group_from_num(cls, detector_num):
137 """Helper method to return the detector group from the number.
139 Parameters
140 ----------
141 detector_num : `int`
142 Detector number.
144 Returns
145 -------
146 group : `str`
147 Detector group.
149 Raises
150 ------
151 ValueError
152 The supplied number is not known.
153 """
154 for num, group in cls._detector_map.values():
155 if num == detector_num:
156 return group
157 raise ValueError(f"Detector {detector_num} is not known for UCDCam")
159 @staticmethod
160 def compute_exposure_id(dateobs, seqnum=0, controller=None):
161 """Helper method to calculate the exposure_id.
163 Parameters
164 ----------
165 dateobs : `str`
166 Date of observation in FITS ISO format.
167 seqnum : `int`, unused
168 Sequence number. Ignored.
169 controller : `str`, unused
170 Controller type. Ignored.
172 Returns
173 -------
174 exposure_id : `int`
175 Exposure ID.
176 """
177 # Use 1 second resolution
178 exposure_id = re.sub(r"\D", "", dateobs[:19])
179 return int(exposure_id)
181 @cache_translation
182 def to_instrument(self):
183 """Calculate the instrument name.
185 Returns
186 -------
187 instrume : `str`
188 Name of the test stand. We do not distinguish between ITL and
189 E2V.
190 """
191 return self.name
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.to_observation_id())
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)