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