Coverage for python/astro_metadata_translator/translators/sdss.py: 37%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""
14__all__ = ("SdssTranslator", )
16import posixpath
18from astropy.coordinates import EarthLocation, Angle, AltAz
19import astropy.units as u
21from ..translator import cache_translation, CORRECTIONS_RESOURCE_ROOT
22from .fits import FitsTranslator
23from .helpers import tracking_from_degree_headers
26class SdssTranslator(FitsTranslator):
27 """Metadata translator for SDSS standard headers.
28 NB: calibration data is not handled as calibration frames were
29 not available to me at time of writing.
30 """
32 name = "SDSS"
33 """Name of this translation class"""
35 supported_instrument = "Imager"
36 """Supports the SDSS imager instrument."""
38 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "SDSS")
39 """Default resource path root to use to locate header correction files."""
41 # SDSS has has a rotator, but in drift scan mode, the instrument
42 # angle on sky is set to +X=East, +Y=North which we define as a
43 # 0 degree rotation.
44 _const_map = {"boresight_rotation_angle": Angle(0*u.deg),
45 "boresight_rotation_coord": "sky",
46 "dark_time": 0.0*u.s, # Drift scan implies no dark time
47 "instrument": "Imager on SDSS 2.5m", # We only ever ingest data from the imager
48 "telescope": "SDSS 2.5m", # Value of TELESCOP in header is ambiguous
49 "relative_humidity": None,
50 "temperature": None,
51 "pressure": None,
52 "detector_serial": "UNKNOWN",
53 }
55 _trivial_map = {"exposure_time": ("EXPTIME", dict(unit=u.s)),
56 "object": "OBJECT",
57 "physical_filter": "FILTER",
58 "exposure_id": "RUN",
59 "visit_id": "RUN",
60 "science_program": "OBJECT", # This is the closest I can think of to a useful program
61 "detector_name": "CCDLOC", # This is a numeric incoding of the "slot", i.e. filter+camcol
62 }
64 # Need a mapping from unique name to index. The order is arbitrary.
65 detector_name_id_map = {"g1": 0, "z1": 1, "u1": 2, "i1": 3, "r1": 4, "g2": 5, "z2": 6, "u2": 7,
66 "i2": 8, "r2": 9, "g3": 10, "z3": 11, "u3": 12, "i3": 13, "r3": 14,
67 "g4": 15, "z4": 16, "u4": 17, "i4": 18, "r4": 19, "g5": 20, "z5": 21,
68 "u5": 22, "i5": 23, "r5": 24, "g6": 25, "z6": 26, "u6": 27, "i6": 28, "r6": 29}
70 @classmethod
71 def can_translate(cls, header, filename=None):
72 """Indicate whether this translation class can translate the
73 supplied header.
75 Parameters
76 ----------
77 header : `dict`-like
78 Header to convert to standardized form.
79 filename : `str`, optional
80 Name of file being translated.
82 Returns
83 -------
84 can : `bool`
85 `True` if the header is recognized by this class. `False`
86 otherwise.
87 """
88 if (cls.is_keyword_defined(header, "ORIGIN") and cls.is_keyword_defined(header, "CCDMODE")
89 and cls.is_keyword_defined(header, "TELESCOP") and "2.5m" in header["TELESCOP"]
90 and "SDSS" in header["ORIGIN"] and "DRIFT" in header["CCDMODE"]):
91 return True
92 return False
94 @cache_translation
95 def to_detector_unique_name(self):
96 # Docstring will be inherited. Property defined in properties.py
97 if self.is_key_ok("CAMCOL"):
98 return self.to_physical_filter()+str(self._header["CAMCOL"])
99 else:
100 raise ValueError(f"{self._log_prefix}: CAMCOL key is not definded")
102 @cache_translation
103 def to_detector_num(self):
104 # Docstring will be inherited. Property defined in properties.py
105 return self.detector_name_id_map[self.to_detector_unique_name()]
107 @cache_translation
108 def to_observation_id(self):
109 """Calculate the observation ID.
111 Returns
112 -------
113 observation_id : `str`
114 A string uniquely describing the observation.
115 This incorporates the run, camcol, filter and frame.
116 """
117 return " ".join([str(self._header[el]) for el in
118 ["RUN", "CAMCOL", "FILTER", "FRAME"]])
120 @cache_translation
121 def to_datetime_begin(self):
122 # Docstring will be inherited. Property defined in properties.py
123 # We know it is UTC
124 value = self._from_fits_date_string(self._header["DATE-OBS"],
125 time_str=self._header["TAIHMS"], scale="tai")
126 self._used_these_cards("DATE-OBS", "TAIHMS")
127 return value
129 @cache_translation
130 def to_datetime_end(self):
131 # Docstring will be inherited. Property defined in properties.py
132 return self.to_datetime_begin() + self.to_exposure_time()
134 @cache_translation
135 def to_location(self):
136 """Calculate the observatory location.
138 Returns
139 -------
140 location : `astropy.coordinates.EarthLocation`
141 An object representing the location of the telescope.
142 """
144 # Look up the value since files do not have location
145 value = EarthLocation.of_site("apo")
147 return value
149 @cache_translation
150 def to_observation_type(self):
151 """Calculate the observation type.
153 Returns
154 -------
155 typ : `str`
156 Observation type. Normalized to standard set.
157 """
158 obstype_key = "FLAVOR"
159 if not self.is_key_ok(obstype_key):
160 return "none"
161 obstype = self._header[obstype_key].strip().lower()
162 self._used_these_cards(obstype_key)
163 return obstype
165 @cache_translation
166 def to_tracking_radec(self):
167 # Docstring will be inherited. Property defined in properties.py
168 radecsys = ("RADECSYS",)
169 radecpairs = (("RA", "DEC"),)
170 return tracking_from_degree_headers(self, radecsys, radecpairs, unit=u.deg)
172 @cache_translation
173 def to_altaz_begin(self):
174 # Docstring will be inherited. Property defined in properties.py
175 try:
176 az = self._header["AZ"]
177 alt = self._header["ALT"]
178 # It appears SDSS defines azimuth as increasing
179 # from South through East. This translates to
180 # North through East
181 az = (-az + 180.)%360.
182 altaz = AltAz(az * u.deg, alt * u.deg,
183 obstime=self.to_datetime_begin(), location=self.to_location())
184 self._used_these_cards("AZ", "ALT")
185 return altaz
186 except Exception as e:
187 if self.to_observation_type() != "science":
188 return None # Allow Alt/Az not to be set for calibrations
189 raise(e)
191 @cache_translation
192 def to_boresight_airmass(self):
193 # Docstring will be inherited. Property defined in properties.py
194 altaz = self.to_altaz_begin()
195 if altaz is not None:
196 return altaz.secz.value # This is an estimate
198 @cache_translation
199 def to_detector_exposure_id(self):
200 # Docstring will be inherited. Property defined in properties.py
201 try:
202 frame_field_map = dict(r=0, i=2, u=4, z=6, g=8)
203 run = self._header["RUN"]
204 filt = self._header["FILTER"]
205 camcol = self._header["CAMCOL"]
206 field = self._header["FRAME"] - frame_field_map[filt]
207 self._used_these_cards("RUN", "FILTER", "CAMCOL", "FRAME")
208 except Exception as e:
209 if self.to_observation_type() != "science":
210 return None
211 raise(e)
212 filter_id_map = dict(u=0, g=1, r=2, i=3, z=4)
213 return ((int(run) * 10
214 + filter_id_map[filt]) * 10
215 + int(camcol)) * 10000 + int(field)
217 @cache_translation
218 def to_detector_group(self):
219 # Docstring will be inherited. Property defined in properties.py
220 if self.is_key_ok("CAMCOL"):
221 return str(self._header["CAMCOL"])
222 else:
223 raise ValueError(f"{self._log_prefix}: CAMCOL key is not definded")