Coverage for python/lsst/obs/lsst/translators/lsstCam.py: 42%
53 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-11 20:01 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-11 20:01 +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 the main LSST Camera"""
13__all__ = ("LsstCamTranslator", )
15import logging
16import astropy.units as u
18from astro_metadata_translator import cache_translation
19from astro_metadata_translator.translators.helpers import is_non_science
21from .lsst import LsstBaseTranslator, SIMONYI_TELESCOPE
23log = logging.getLogger(__name__)
25# Normalized name of the LSST Camera
26LSST_CAM = "LSSTCam"
29def is_non_science_or_lab(self):
30 """Pseudo method to determine whether this is a lab or non-science
31 header.
33 Raises
34 ------
35 KeyError
36 If this is a science observation and on the mountain.
37 """
38 # Return without raising if this is not a science observation
39 # since the defaults are fine.
40 try:
41 # This will raise if it is a science observation.
42 is_non_science(self)
43 return
44 except KeyError:
45 pass
47 # We are still in the lab, return and use the default.
48 if not self._is_on_mountain():
49 return
51 # This is a science observation on the mountain so we should not
52 # use defaults.
53 raise KeyError(f"{self._log_prefix}: Required key is missing and this is a mountain science observation")
56class LsstCamTranslator(LsstBaseTranslator):
57 """Metadata translation for the main LSST Camera."""
59 name = LSST_CAM
60 """Name of this translation class"""
62 supported_instrument = LSST_CAM
63 """Supports the lsstCam instrument."""
65 _const_map = {
66 "instrument": LSST_CAM,
67 "telescope": SIMONYI_TELESCOPE,
68 # Migrate these to full translations once test data appears that
69 # includes them
70 "altaz_begin": None,
71 "object": "UNKNOWN",
72 }
74 _trivial_map = {
75 "detector_group": "RAFTBAY",
76 "detector_name": "CCDSLOT",
77 "observation_id": "OBSID",
78 "exposure_time": ("EXPTIME", dict(unit=u.s)),
79 "detector_serial": "LSST_NUM",
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 @classmethod
89 def fix_header(cls, header, instrument, obsid, filename=None):
90 """Fix LSSTCam headers.
92 Notes
93 -----
94 See `~astro_metadata_translator.fix_header` for details of the general
95 process.
96 """
98 modified = False
100 # Calculate the standard label to use for log messages
101 log_label = cls._construct_log_prefix(obsid, filename)
103 if "FILTER" not in header and header.get("FILTER2") is not None:
104 ccdslot = header.get("CCDSLOT", "unknown")
105 raftbay = header.get("RAFTBAY", "unknown")
107 log.warning("%s %s_%s: No FILTER key found but FILTER2=\"%s\" (removed)",
108 log_label, raftbay, ccdslot, header["FILTER2"])
109 header["FILTER2"] = None
110 modified = True
112 if header.get("DAYOBS") in ("20231107", "20231108") and header["FILTER"] == "ph_05":
113 header["FILTER"] = "ph_5"
114 modified = True
116 return modified
118 @classmethod
119 def can_translate(cls, header, filename=None):
120 """Indicate whether this translation class can translate the
121 supplied header.
123 Parameters
124 ----------
125 header : `dict`-like
126 Header to convert to standardized form.
127 filename : `str`, optional
128 Name of file being translated.
130 Returns
131 -------
132 can : `bool`
133 `True` if the header is recognized by this class. `False`
134 otherwise.
135 """
136 # INSTRUME keyword might be of two types
137 if "INSTRUME" in header:
138 instrume = header["INSTRUME"].lower()
139 if instrume == cls.supported_instrument.lower():
140 return True
141 return False
143 @cache_translation
144 def to_physical_filter(self):
145 """Calculate the physical filter name.
147 Returns
148 -------
149 filter : `str`
150 Name of filter. Can be a combination of FILTER, FILTER1, and
151 FILTER2 headers joined by a "~". Trailing "~empty" components
152 are stripped.
153 Returns "unknown" if no filter is declared.
154 """
155 joined = super().to_physical_filter()
156 while joined.endswith("~empty"):
157 joined = joined[:-len("~empty")]
159 return joined