Coverage for python/astro_metadata_translator/translators/decam.py : 29%

Hot-keys 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 DECam FITS headers"""
14__all__ = ("DecamTranslator", )
16import re
17import posixpath
19from astropy.coordinates import EarthLocation, Angle
20import astropy.units as u
22from ..translator import cache_translation, CORRECTIONS_RESOURCE_ROOT
23from .fits import FitsTranslator
24from .helpers import altaz_from_degree_headers, is_non_science, \
25 tracking_from_degree_headers
28class DecamTranslator(FitsTranslator):
29 """Metadata translator for DECam standard headers.
30 """
32 name = "DECam"
33 """Name of this translation class"""
35 supported_instrument = "DECam"
36 """Supports the DECam instrument."""
38 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "DECam")
39 """Default resource path root to use to locate header correction files."""
41 # DECam has no rotator, and the instrument angle on sky is set to +Y=East,
42 # +X=South which we define as a 90 degree rotation and an X-flip.
43 _const_map = {"boresight_rotation_angle": Angle(90*u.deg),
44 "boresight_rotation_coord": "sky",
45 }
47 _trivial_map = {"exposure_time": ("EXPTIME", dict(unit=u.s)),
48 "dark_time": ("DARKTIME", dict(unit=u.s)),
49 "boresight_airmass": ("AIRMASS", dict(checker=is_non_science)),
50 "observation_id": "OBSID",
51 "object": "OBJECT",
52 "science_program": "PROPID",
53 "detector_num": "CCDNUM",
54 "detector_serial": "DETECTOR",
55 "detector_unique_name": "DETPOS",
56 "telescope": ("TELESCOP", dict(default="CTIO 4.0-m telescope")),
57 "instrument": ("INSTRUME", dict(default="DECam")),
58 # Ensure that reasonable values are always available
59 "relative_humidity": ("HUMIDITY", dict(default=40., minimum=0, maximum=100.)),
60 "temperature": ("OUTTEMP", dict(unit=u.deg_C, default=10., minimum=-10., maximum=40.)),
61 # Header says torr but seems to be mbar. Use hPa unit
62 # which is the SI equivalent of mbar.
63 "pressure": ("PRESSURE", dict(unit=u.hPa,
64 default=771.611, minimum=700., maximum=850.)),
65 }
67 # Unique detector names are currently not used but are read directly from
68 # header.
69 # The detector_group could be N or S with detector_name corresponding
70 # to the number in that group.
71 detector_names = {
72 1: 'S29', 2: 'S30', 3: 'S31', 4: 'S25', 5: 'S26', 6: 'S27', 7: 'S28', 8: 'S20', 9: 'S21',
73 10: 'S22', 11: 'S23', 12: 'S24', 13: 'S14', 14: 'S15', 15: 'S16', 16: 'S17', 17: 'S18',
74 18: 'S19', 19: 'S8', 20: 'S9', 21: 'S10', 22: 'S11', 23: 'S12', 24: 'S13', 25: 'S1', 26: 'S2',
75 27: 'S3', 28: 'S4', 29: 'S5', 30: 'S6', 31: 'S7', 32: 'N1', 33: 'N2', 34: 'N3', 35: 'N4',
76 36: 'N5', 37: 'N6', 38: 'N7', 39: 'N8', 40: 'N9', 41: 'N10', 42: 'N11', 43: 'N12', 44: 'N13',
77 45: 'N14', 46: 'N15', 47: 'N16', 48: 'N17', 49: 'N18', 50: 'N19', 51: 'N20', 52: 'N21',
78 53: 'N22', 54: 'N23', 55: 'N24', 56: 'N25', 57: 'N26', 58: 'N27', 59: 'N28', 60: 'N29',
79 62: 'N31'}
81 @classmethod
82 def can_translate(cls, header, filename=None):
83 """Indicate whether this translation class can translate the
84 supplied header.
86 Checks the INSTRUME and FILTER headers.
88 Parameters
89 ----------
90 header : `dict`-like
91 Header to convert to standardized form.
92 filename : `str`, optional
93 Name of file being translated.
95 Returns
96 -------
97 can : `bool`
98 `True` if the header is recognized by this class. `False`
99 otherwise.
100 """
101 # Use INSTRUME. Because of defaulting behavior only do this
102 # if we really have an INSTRUME header
103 if "INSTRUME" in header:
104 via_instrume = super().can_translate(header, filename=filename)
105 if via_instrume:
106 return via_instrume
107 if cls.is_keyword_defined(header, "FILTER") and "DECam" in header["FILTER"]:
108 return True
109 return False
111 @cache_translation
112 def to_exposure_id(self):
113 """Calculate exposure ID solely for science observations.
115 Returns
116 -------
117 id : `int`
118 ID of exposure.
119 """
120 if self.to_observation_type() != "science":
121 return None
122 value = self._header["EXPNUM"]
123 self._used_these_cards("EXPNUM")
124 return value
126 @cache_translation
127 def to_visit_id(self):
128 # Docstring will be inherited. Property defined in properties.py
129 return self.to_exposure_id()
131 @cache_translation
132 def to_datetime_end(self):
133 # Docstring will be inherited. Property defined in properties.py
134 # Instcals have no DATE-END or DTUTC
135 datetime_end = self._from_fits_date("DTUTC", scale="utc")
136 if datetime_end is None:
137 datetime_end = self.to_datetime_begin() + self.to_exposure_time()
138 return datetime_end
140 def _translate_from_calib_id(self, field):
141 """Fetch the ID from the CALIB_ID header.
143 Calibration products made with constructCalibs have some metadata
144 saved in its FITS header CALIB_ID.
145 """
146 data = self._header["CALIB_ID"]
147 match = re.search(r".*%s=(\S+)" % field, data)
148 self._used_these_cards("CALIB_ID")
149 return match.groups()[0]
151 @cache_translation
152 def to_physical_filter(self):
153 """Calculate physical filter.
155 Return `None` if the keyword FILTER does not exist in the header,
156 which can happen for some valid Community Pipeline products.
158 Returns
159 -------
160 filter : `str`
161 The full filter name.
162 """
163 if self.is_key_ok("FILTER"):
164 value = self._header["FILTER"].strip()
165 self._used_these_cards("FILTER")
166 return value
167 elif self.is_key_ok("CALIB_ID"):
168 return self._translate_from_calib_id("filter")
169 else:
170 return None
172 @cache_translation
173 def to_location(self):
174 """Calculate the observatory location.
176 Returns
177 -------
178 location : `astropy.coordinates.EarthLocation`
179 An object representing the location of the telescope.
180 """
182 if self.is_key_ok("OBS-LONG"):
183 # OBS-LONG has west-positive sign so must be flipped
184 lon = self._header["OBS-LONG"] * -1.0
185 value = EarthLocation.from_geodetic(lon, self._header["OBS-LAT"], self._header["OBS-ELEV"])
186 self._used_these_cards("OBS-LONG", "OBS-LAT", "OBS-ELEV")
187 else:
188 # Look up the value since some files do not have location
189 value = EarthLocation.of_site("ctio")
191 return value
193 @cache_translation
194 def to_observation_type(self):
195 """Calculate the observation type.
197 Returns
198 -------
199 typ : `str`
200 Observation type. Normalized to standard set.
201 """
202 if not self.is_key_ok("OBSTYPE"):
203 return "none"
204 obstype = self._header["OBSTYPE"].strip().lower()
205 self._used_these_cards("OBSTYPE")
206 if obstype == "object":
207 return "science"
208 return obstype
210 @cache_translation
211 def to_tracking_radec(self):
212 # Docstring will be inherited. Property defined in properties.py
213 radecsys = ("RADESYS",)
214 radecpairs = (("TELRA", "TELDEC"),)
215 return tracking_from_degree_headers(self, radecsys, radecpairs, unit=(u.hourangle, u.deg))
217 @cache_translation
218 def to_altaz_begin(self):
219 # Docstring will be inherited. Property defined in properties.py
220 return altaz_from_degree_headers(self, (("ZD", "AZ"),),
221 self.to_datetime_begin(), is_zd=set(["ZD"]))
223 @cache_translation
224 def to_detector_exposure_id(self):
225 # Docstring will be inherited. Property defined in properties.py
226 exposure_id = self.to_exposure_id()
227 if exposure_id is None:
228 return None
229 return int("{:07d}{:02d}".format(exposure_id, self.to_detector_num()))
231 @cache_translation
232 def to_detector_group(self):
233 # Docstring will be inherited. Property defined in properties.py
234 name = self.to_detector_unique_name()
235 return name[0]
237 @cache_translation
238 def to_detector_name(self):
239 # Docstring will be inherited. Property defined in properties.py
240 name = self.to_detector_unique_name()
241 return name[1:]