Coverage for python/lsst/obs/fiberspectrograph/translator.py: 58%
62 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-15 04:06 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-15 04:06 -0700
1import logging
2import os
4import astropy.units as u
5from astropy.time import Time
7from astro_metadata_translator import cache_translation
8from lsst.obs.lsst.translators.lsst import SIMONYI_TELESCOPE, LsstBaseTranslator
10from lsst.utils import getPackageDir
12__all__ = ["FiberSpectrographTranslator", ]
14log = logging.getLogger(__name__)
17class FiberSpectrographTranslator(LsstBaseTranslator):
18 """Metadata translator for Rubin calibration fibre spectrographs headers"""
20 name = "FiberSpectrograph"
21 """Name of this translation class"""
23 supported_instrument = "FiberSpec"
24 """Supports the Rubin calibration fiber spectrographs."""
26 default_search_path = os.path.join(getPackageDir("obs_fiberspectrograph"), "corrections")
27 """Default search path to use to locate header correction files."""
29 default_resource_root = os.path.join(getPackageDir("obs_fiberspectrograph"), "corrections")
30 """Default resource path root to use to locate header correction files."""
32 DETECTOR_MAX = 1
34 _const_map = {
35 # TODO: DM-43041 DATE, detector name and controller should be put
36 # in file header and add to mapping
37 "detector_num": 0,
38 "detector_name": "ccd0",
39 "object": None,
40 "physical_filter": "empty",
41 "detector_group": "None",
42 "relative_humidity": None,
43 "pressure": None,
44 "temperature": None,
45 "focus_z": None,
46 "boresight_airmass": None,
47 "boresight_rotation_angle": None,
48 "tracking_radec": None,
49 "telescope": SIMONYI_TELESCOPE,
50 "observation_type": "spectrum", # IMGTYPE is ''
51 }
52 """Constant mappings"""
54 _trivial_map = {
55 "observation_id": "OBSID",
56 "science_program": ("PROGRAM", dict(default="unknown")),
57 "detector_serial": "SERIAL",
58 }
59 """One-to-one mappings"""
61 @classmethod
62 def can_translate(cls, header, filename=None):
63 """Indicate whether this translation class can translate the
64 supplied header.
66 Parameters
67 ----------
68 header : `dict`-like
69 Header to convert to standardized form.
70 filename : `str`, optional
71 Name of file being translated.
73 Returns
74 -------
75 can : `bool`
76 `True` if the header is recognized by this class. `False`
77 otherwise.
78 """
80 # TODO: DM-43041 need to be updated with new fiber spec
81 return "INSTRUME" in header and header["INSTRUME"] in ["FiberSpectrograph.Broad"]
83 @cache_translation
84 def to_instrument(self):
85 return "FiberSpec"
87 @cache_translation
88 def to_datetime_begin(self):
89 self._used_these_cards("DATE-BEG")
90 return Time(self._header["DATE-BEG"], scale="tai", format="isot")
92 @cache_translation
93 def to_exposure_time(self):
94 # Docstring will be inherited. Property defined in properties.py
95 # Some data is missing a value for EXPTIME.
96 # Have to be careful we do not have circular logic when trying to
97 # guess
98 if self.is_key_ok("EXPTIME"):
99 return self.quantity_from_card("EXPTIME", u.s)
101 # A missing or undefined EXPTIME is problematic. Set to -1
102 # to indicate that none was found.
103 log.warning("%s: Insufficient information to derive exposure time. Setting to -1.0s",
104 self._log_prefix)
105 return -1.0 * u.s
107 @cache_translation
108 def to_dark_time(self): # N.b. defining this suppresses a warning re setting from exptime
109 if "DARKTIME" in self._header:
110 darkTime = self._header["DARKTIME"]
111 self._used_these_cards("DARKTIME")
112 return (darkTime, dict(unit=u.s))
113 return self.to_exposure_time()
115 @staticmethod
116 def compute_exposure_id(dayobs, seqnum, controller=None):
117 """Helper method to calculate the exposure_id.
119 Parameters
120 ----------
121 dayobs : `str`
122 Day of observation in either YYYYMMDD or YYYY-MM-DD format.
123 If the string looks like ISO format it will be truncated before the
124 ``T`` before being handled.
125 seqnum : `int` or `str`
126 Sequence number.
127 controller : `str`, optional
128 Controller to use. If this is "O", no change is made to the
129 exposure ID. If it is "C" a 1000 is added to the year component
130 of the exposure ID. If it is "H" a 2000 is added to the year
131 component. This sequence continues with "P" and "Q" controllers.
132 `None` indicates that the controller is not relevant to the
133 exposure ID calculation (generally this is the case for test
134 stand data).
136 Returns
137 -------
138 exposure_id : `int`
139 Exposure ID in form YYYYMMDDnnnnn form.
140 """
141 if not isinstance(dayobs, int):
142 if "T" in dayobs:
143 dayobs = dayobs[:dayobs.find("T")]
145 dayobs = dayobs.replace("-", "")
147 if len(dayobs) != 8:
148 raise ValueError(f"Malformed dayobs: {dayobs}")
150 # Expect no more than 99,999 exposures in a day
151 maxdigits = 5
152 if seqnum >= 10**maxdigits:
153 raise ValueError(f"Sequence number ({seqnum}) exceeds limit")
155 # Form the number as a string zero padding the sequence number
156 idstr = f"{dayobs}{seqnum:0{maxdigits}d}"
158 # Exposure ID has to be an integer
159 return int(idstr)
161 @cache_translation
162 def to_visit_id(self):
163 """Calculate the visit associated with this exposure.
164 """
165 return self.to_exposure_id()