Coverage for python/lsst/obs/lsst/translators/lsstCam.py: 43%
64 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-04 03:31 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-04 03:31 -0700
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 the main LSST Camera"""
13__all__ = ("LsstCamTranslator", )
15import logging
17import pytz
18import astropy.time
19import astropy.units as u
21from astro_metadata_translator import cache_translation
22from astro_metadata_translator.translators.helpers import is_non_science
24from .lsst import LsstBaseTranslator, SIMONYI_TELESCOPE
26log = logging.getLogger(__name__)
28# Normalized name of the LSST Camera
29LSST_CAM = "LSSTCam"
32def is_non_science_or_lab(self):
33 """Pseudo method to determine whether this is a lab or non-science
34 header.
36 Raises
37 ------
38 KeyError
39 If this is a science observation and on the mountain.
40 """
41 # Return without raising if this is not a science observation
42 # since the defaults are fine.
43 try:
44 # This will raise if it is a science observation.
45 is_non_science(self)
46 return
47 except KeyError:
48 pass
50 # We are still in the lab, return and use the default.
51 if not self._is_on_mountain():
52 return
54 # This is a science observation on the mountain so we should not
55 # use defaults.
56 raise KeyError(f"{self._log_prefix}: Required key is missing and this is a mountain science observation")
59class LsstCamTranslator(LsstBaseTranslator):
60 """Metadata translation for the main LSST Camera."""
62 name = LSST_CAM
63 """Name of this translation class"""
65 supported_instrument = LSST_CAM
66 """Supports the lsstCam instrument."""
68 _const_map = {
69 "instrument": LSST_CAM,
70 "telescope": SIMONYI_TELESCOPE,
71 }
73 _trivial_map = {
74 "detector_group": "RAFTBAY",
75 "detector_name": "CCDSLOT",
76 "observation_id": "OBSID",
77 "exposure_time": ("EXPTIME", dict(unit=u.s)),
78 "detector_serial": "LSST_NUM",
79 "object": ("OBJECT", dict(default="UNKNOWN")),
80 "science_program": (["PROGRAM", "RUNNUM"], dict(default="unknown")),
81 "boresight_rotation_angle": (["ROTPA", "ROTANGLE"], dict(checker=is_non_science_or_lab,
82 default=0.0, unit=u.deg)),
83 }
85 # Use Imsim raft definitions until a true lsstCam definition exists
86 cameraPolicyFile = "policy/lsstCam.yaml"
88 # Date (YYYYMM) the camera changes from using lab day_offset (Pacific time)
89 # to summit day_offset (12 hours).
90 _CAMERA_SHIP_DATE = 202406
92 @classmethod
93 def fix_header(cls, header, instrument, obsid, filename=None):
94 """Fix LSSTCam headers.
96 Notes
97 -----
98 See `~astro_metadata_translator.fix_header` for details of the general
99 process.
100 """
102 modified = False
104 # Calculate the standard label to use for log messages
105 log_label = cls._construct_log_prefix(obsid, filename)
107 if "FILTER" not in header and header.get("FILTER2") is not None:
108 ccdslot = header.get("CCDSLOT", "unknown")
109 raftbay = header.get("RAFTBAY", "unknown")
111 log.warning("%s %s_%s: No FILTER key found but FILTER2=\"%s\" (removed)",
112 log_label, raftbay, ccdslot, header["FILTER2"])
113 header["FILTER2"] = None
114 modified = True
116 if header.get("DAYOBS") in ("20231107", "20231108") and header["FILTER"] == "ph_05":
117 header["FILTER"] = "ph_5"
118 modified = True
120 return modified
122 @classmethod
123 def can_translate(cls, header, filename=None):
124 """Indicate whether this translation class can translate the
125 supplied header.
127 Parameters
128 ----------
129 header : `dict`-like
130 Header to convert to standardized form.
131 filename : `str`, optional
132 Name of file being translated.
134 Returns
135 -------
136 can : `bool`
137 `True` if the header is recognized by this class. `False`
138 otherwise.
139 """
140 # INSTRUME keyword might be of two types
141 if "INSTRUME" in header:
142 instrume = header["INSTRUME"].lower()
143 if instrume == cls.supported_instrument.lower():
144 return True
145 return False
147 @cache_translation
148 def to_physical_filter(self):
149 """Calculate the physical filter name.
151 Returns
152 -------
153 filter : `str`
154 Name of filter. Can be a combination of FILTER, FILTER1, and
155 FILTER2 headers joined by a "~". Trailing "~empty" components
156 are stripped.
157 Returns "unknown" if no filter is declared.
158 """
159 joined = super().to_physical_filter()
160 while joined.endswith("~empty"):
161 joined = joined.removesuffix("~empty")
163 return joined
165 @classmethod
166 def observing_date_to_offset(cls, observing_date: astropy.time.Time) -> astropy.time.TimeDelta | None:
167 """Return the offset to use when calculating the observing day.
169 Parameters
170 ----------
171 observing_date : `astropy.time.Time`
172 The date of the observation. Unused.
174 Returns
175 -------
176 offset : `astropy.time.TimeDelta`
177 The offset to apply. During lab testing the offset is Pacific
178 Time which can mean UTC-7 or UTC-8 depending on daylight savings.
179 In Chile the offset is always UTC-12.
180 """
181 # Timezone calculations are slow. Only do this if the instrument
182 # is in the lab.
183 if int(observing_date.strftime("%Y%m")) > cls._CAMERA_SHIP_DATE:
184 return cls._ROLLOVER_TIME # 12 hours in base class
186 # Convert the date to a datetime UTC.
187 pacific_tz = pytz.timezone("US/Pacific")
188 pacific_time = observing_date.utc.to_datetime(timezone=pacific_tz)
190 # We need the offset to go the other way.
191 offset = pacific_time.utcoffset() * -1
192 return astropy.time.TimeDelta(offset)