Coverage for python/astro_metadata_translator/translators/sdss.py: 51%
104 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 03:54 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 03:54 -0700
1# This file is part of astro_metadata_translator.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the LICENSE file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12"""Metadata translation code for SDSS FITS headers."""
14from __future__ import annotations
16__all__ = ("SdssTranslator",)
18import posixpath
19from collections.abc import Mapping
20from typing import TYPE_CHECKING, Any
22import astropy.units as u
23from astropy.coordinates import AltAz, Angle, EarthLocation
25from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation
26from .fits import FitsTranslator
27from .helpers import tracking_from_degree_headers
29if TYPE_CHECKING: 29 ↛ 30line 29 didn't jump to line 30, because the condition on line 29 was never true
30 import astropy.coordinates
31 import astropy.time
34class SdssTranslator(FitsTranslator):
35 """Metadata translator for SDSS standard headers.
36 NB: calibration data is not handled as calibration frames were
37 not available to me at time of writing.
38 """
40 name = "SDSS"
41 """Name of this translation class"""
43 supported_instrument = "Imager"
44 """Supports the SDSS imager instrument."""
46 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "SDSS")
47 """Default resource path root to use to locate header correction files."""
49 # SDSS has has a rotator, but in drift scan mode, the instrument
50 # angle on sky is set to +X=East, +Y=North which we define as a
51 # 0 degree rotation.
52 _const_map = {
53 "boresight_rotation_angle": Angle(0 * u.deg),
54 "boresight_rotation_coord": "sky",
55 "dark_time": 0.0 * u.s, # Drift scan implies no dark time
56 "instrument": "Imager on SDSS 2.5m", # We only ever ingest data from the imager
57 "telescope": "SDSS 2.5m", # Value of TELESCOP in header is ambiguous
58 "relative_humidity": None,
59 "temperature": None,
60 "pressure": None,
61 "detector_serial": "UNKNOWN",
62 }
64 _trivial_map = {
65 "exposure_time": ("EXPTIME", dict(unit=u.s)),
66 "object": "OBJECT",
67 "physical_filter": "FILTER",
68 "exposure_id": "RUN",
69 "visit_id": "RUN",
70 "science_program": "OBJECT", # This is the closest I can think of to a useful program
71 "detector_name": "CCDLOC", # This is a numeric incoding of the "slot", i.e. filter+camcol
72 }
74 # Need a mapping from unique name to index. The order is arbitrary.
75 detector_name_id_map = {
76 "g1": 0,
77 "z1": 1,
78 "u1": 2,
79 "i1": 3,
80 "r1": 4,
81 "g2": 5,
82 "z2": 6,
83 "u2": 7,
84 "i2": 8,
85 "r2": 9,
86 "g3": 10,
87 "z3": 11,
88 "u3": 12,
89 "i3": 13,
90 "r3": 14,
91 "g4": 15,
92 "z4": 16,
93 "u4": 17,
94 "i4": 18,
95 "r4": 19,
96 "g5": 20,
97 "z5": 21,
98 "u5": 22,
99 "i5": 23,
100 "r5": 24,
101 "g6": 25,
102 "z6": 26,
103 "u6": 27,
104 "i6": 28,
105 "r6": 29,
106 }
108 @classmethod
109 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool:
110 """Indicate whether this translation class can translate the
111 supplied header.
113 Parameters
114 ----------
115 header : `dict`-like
116 Header to convert to standardized form.
117 filename : `str`, optional
118 Name of file being translated.
120 Returns
121 -------
122 can : `bool`
123 `True` if the header is recognized by this class. `False`
124 otherwise.
125 """
126 if (
127 cls.is_keyword_defined(header, "ORIGIN")
128 and cls.is_keyword_defined(header, "CCDMODE")
129 and cls.is_keyword_defined(header, "TELESCOP")
130 and "2.5m" in header["TELESCOP"]
131 and "SDSS" in header["ORIGIN"]
132 and "DRIFT" in header["CCDMODE"]
133 ):
134 return True
135 return False
137 @cache_translation
138 def to_detector_unique_name(self) -> str:
139 # Docstring will be inherited. Property defined in properties.py
140 if self.is_key_ok("CAMCOL"):
141 return self.to_physical_filter() + str(self._header["CAMCOL"])
142 else:
143 raise ValueError(f"{self._log_prefix}: CAMCOL key is not definded")
145 @cache_translation
146 def to_detector_num(self) -> int:
147 # Docstring will be inherited. Property defined in properties.py
148 return self.detector_name_id_map[self.to_detector_unique_name()]
150 @cache_translation
151 def to_observation_id(self) -> str:
152 """Calculate the observation ID.
154 Returns
155 -------
156 observation_id : `str`
157 A string uniquely describing the observation.
158 This incorporates the run, camcol, filter and frame.
159 """
160 return " ".join([str(self._header[el]) for el in ["RUN", "CAMCOL", "FILTER", "FRAME"]])
162 @cache_translation
163 def to_datetime_begin(self) -> astropy.time.Time:
164 # Docstring will be inherited. Property defined in properties.py
165 # We know it is UTC
166 value = self._from_fits_date_string(
167 self._header["DATE-OBS"], time_str=self._header["TAIHMS"], scale="tai"
168 )
169 self._used_these_cards("DATE-OBS", "TAIHMS")
170 return value
172 @cache_translation
173 def to_datetime_end(self) -> astropy.time.Time:
174 # Docstring will be inherited. Property defined in properties.py
175 return self.to_datetime_begin() + self.to_exposure_time()
177 @cache_translation
178 def to_location(self) -> EarthLocation:
179 """Calculate the observatory location.
181 Returns
182 -------
183 location : `astropy.coordinates.EarthLocation`
184 An object representing the location of the telescope.
185 """
186 # Look up the value since files do not have location
187 value = EarthLocation.of_site("apo")
189 return value
191 @cache_translation
192 def to_observation_type(self) -> str:
193 """Calculate the observation type.
195 Returns
196 -------
197 typ : `str`
198 Observation type. Normalized to standard set.
199 """
200 obstype_key = "FLAVOR"
201 if not self.is_key_ok(obstype_key):
202 return "none"
203 obstype = self._header[obstype_key].strip().lower()
204 self._used_these_cards(obstype_key)
205 return obstype
207 @cache_translation
208 def to_tracking_radec(self) -> astropy.coordinates.SkyCoord:
209 # Docstring will be inherited. Property defined in properties.py
210 radecsys = ("RADECSYS",)
211 radecpairs = (("RA", "DEC"),)
212 return tracking_from_degree_headers(self, radecsys, radecpairs, unit=u.deg)
214 @cache_translation
215 def to_altaz_begin(self) -> AltAz:
216 # Docstring will be inherited. Property defined in properties.py
217 try:
218 az = self._header["AZ"]
219 alt = self._header["ALT"]
220 # It appears SDSS defines azimuth as increasing
221 # from South through East. This translates to
222 # North through East
223 az = (-az + 180.0) % 360.0
224 altaz = AltAz(
225 az * u.deg, alt * u.deg, obstime=self.to_datetime_begin(), location=self.to_location()
226 )
227 self._used_these_cards("AZ", "ALT")
228 return altaz
229 except Exception as e:
230 if self.to_observation_type() != "science":
231 return None # Allow Alt/Az not to be set for calibrations
232 raise (e)
234 @cache_translation
235 def to_boresight_airmass(self) -> float | None:
236 # Docstring will be inherited. Property defined in properties.py
237 altaz = self.to_altaz_begin()
238 if altaz is not None:
239 return altaz.secz.value # This is an estimate
240 return None
242 @cache_translation
243 def to_detector_exposure_id(self) -> int | None:
244 # Docstring will be inherited. Property defined in properties.py
245 try:
246 frame_field_map = dict(r=0, i=2, u=4, z=6, g=8)
247 run = self._header["RUN"]
248 filt = self._header["FILTER"]
249 camcol = self._header["CAMCOL"]
250 field = self._header["FRAME"] - frame_field_map[filt]
251 self._used_these_cards("RUN", "FILTER", "CAMCOL", "FRAME")
252 except Exception as e:
253 if self.to_observation_type() != "science":
254 return None
255 raise (e)
256 filter_id_map = dict(u=0, g=1, r=2, i=3, z=4)
257 return ((int(run) * 10 + filter_id_map[filt]) * 10 + int(camcol)) * 10000 + int(field)
259 @cache_translation
260 def to_detector_group(self) -> str:
261 # Docstring will be inherited. Property defined in properties.py
262 if self.is_key_ok("CAMCOL"):
263 return str(self._header["CAMCOL"])
264 else:
265 raise ValueError(f"{self._log_prefix}: CAMCOL key is not definded")