Coverage for python / lsst / obs / lsst / translators / comCamSim.py: 27%
75 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:09 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:09 +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 Simulated LSST Commissioning Camera"""
13__all__ = ("LsstComCamSimTranslator", )
15import logging
16import math
18import astropy
19import astropy.units as u
20import astropy.utils.exceptions
21from astropy.coordinates import AltAz
22from astro_metadata_translator import cache_translation
24from .lsstCam import LsstCamTranslator
25from .lsst import SIMONYI_TELESCOPE
27log = logging.getLogger(__name__)
30class LsstComCamSimTranslator(LsstCamTranslator):
31 """Metadata translation for the LSST Commissioning Camera."""
33 name = "LSSTComCamSim"
34 """Name of this translation class"""
36 _const_map = {
37 "instrument": "LSSTComCamSim",
38 "has_simulated_content": True,
39 }
41 cameraPolicyFile = 'policy/comCamSim.yaml'
43 # Allowed to use obstype for can_see_sky fallback.
44 _can_check_obstype_for_can_see_sky = True
46 @classmethod
47 def can_translate(cls, header, filename=None):
48 """Indicate whether this translation class can translate the
49 supplied header.
51 Looks for "COMCAMSIM" instrument in case-insensitive manner but
52 must be on LSST telescope. This avoids confusion with other
53 telescopes using commissioning cameras.
55 Parameters
56 ----------
57 header : `dict`-like
58 Header to convert to standardized form.
59 filename : `str`, optional
60 Name of file being translated.
62 Returns
63 -------
64 can : `bool`
65 `True` if the header is recognized by this class. `False`
66 otherwise.
67 """
68 if "INSTRUME" in header and "TELESCOP" in header:
69 telescope = header["TELESCOP"]
70 instrument = header["INSTRUME"].lower()
71 if instrument == "comcamsim" and telescope in (SIMONYI_TELESCOPE, "LSST"):
72 return True
74 return False
76 @classmethod
77 def fix_header(cls, header, instrument, obsid, filename=None):
78 """Fix Simulated ComCam headers.
80 Notes
81 -----
82 Content will be added as needed.
84 Corrections are reported as debug level log messages.
86 See `~astro_metadata_translator.fix_header` for details of the general
87 process.
88 """
89 modified = False
91 # Calculate the standard label to use for log messages
92 log_label = cls._construct_log_prefix(obsid, filename)
94 # Some simulated files lack RASTART/DECSTART etc headers. Since these
95 # are simulated they can be populated directly from the RA/DEC headers.
96 synced_radec = False
97 for key in ("RA", "DEC"):
98 for time in ("START", "END"):
99 time_key = f"{key}{time}"
100 if not header.get(time_key):
101 if (value := header.get(key)):
102 header[time_key] = value
103 synced_radec = True
104 if synced_radec:
105 modified = True
106 log.debug("%s: Synced RASTART/RAEND/DECSTART/DECEND headers with RA/DEC headers", log_label)
108 if not header.get("RADESYS") and header.get("RA") and header.get("DEC"):
109 header["RADESYS"] = "ICRS"
110 log.debug("%s: Forcing undefined RADESYS to '%s'", log_label, header["RADESYS"])
111 modified = True
113 if not header.get("TELCODE"):
114 if camcode := header.get("CAMCODE"):
115 header["TELCODE"] = camcode
116 modified = True
117 log.debug("%s: Setting TELCODE header from CAMCODE header", log_label)
118 else:
119 # Get the code from the OBSID.
120 code, _ = obsid.split("_", 1)
121 header["TELCODE"] = code
122 modified = True
123 log.debug("%s: Determining telescope code of %s from OBSID", log_label, code)
125 return modified
127 def _is_on_mountain(self):
128 """Indicate whether these data are coming from the instrument
129 installed on the mountain.
130 Returns
131 -------
132 is : `bool`
133 `True` if instrument is on the mountain.
135 Notes
136 -----
137 TODO: DM-33387 This is currently a terrible hack and MUST be removed
138 once CAP-807 and CAP-808 are done.
139 Until then, ALL non-calib ComCam data will look like it is on sky.
140 """
141 return True
143 @cache_translation
144 def to_altaz_begin(self):
145 # Tries to calculate the value. Simulated files for ops-rehearsal 3
146 # did not have the AZ/EL headers defined.
147 if self.are_keys_ok(["ELSTART", "AZSTART"]):
148 return super().to_altaz_begin()
150 # The time is not consistent with HASTART/AMSTART values.
151 # This means that the elevation may well come out negative.
152 # Rather than attempting to calculate something that is already
153 # known to be junk, return a fixed value.
154 if self.are_keys_ok(["RA", "DEC"]):
156 # If there is an airmass value, use it for the elevation
157 # to try to use the available information even if inconsistent
158 # with the observing date.
159 airmass = self.to_boresight_airmass()
160 if airmass is None:
161 elevation = 45 * u.deg
162 else:
163 # The number does not have to be accurate.
164 elevation = math.asin(1 / airmass) * u.rad
166 return AltAz(
167 0. * u.deg, elevation, obstime=self.to_datetime_begin(), location=self.to_location()
168 )
170 return None
172 @cache_translation
173 def to_altaz_end(self):
174 # Tries to calculate the value. Simulated files for ops-rehearsal 3
175 # did not have the AZ/EL headers defined.
176 if self.are_keys_ok(["ELEND", "AZEND"]):
177 return super().to_altaz_end()
178 # Do not attempt to calculate anything in this situation since it is
179 # never going to be correct.
180 return None
182 @classmethod
183 def observing_date_to_offset(cls, observing_date: astropy.time.Time) -> astropy.time.TimeDelta | None:
184 # Always use the 12 hour offset.
185 return cls._ROLLOVER_TIME