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

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.
115 Returns
116 -------
117 id : `int`
118 ID of exposure.
119 """
120 value = self._header["EXPNUM"]
121 self._used_these_cards("EXPNUM")
122 return value
124 @cache_translation
125 def to_observation_counter(self):
126 """Return the lifetime exposure number.
128 Returns
129 -------
130 sequence : `int`
131 The observation counter.
132 """
133 return self.to_exposure_id()
135 @cache_translation
136 def to_visit_id(self):
137 # Docstring will be inherited. Property defined in properties.py
138 return self.to_exposure_id()
140 @cache_translation
141 def to_datetime_end(self):
142 # Docstring will be inherited. Property defined in properties.py
143 # Instcals have no DATE-END or DTUTC
144 datetime_end = self._from_fits_date("DTUTC", scale="utc")
145 if datetime_end is None:
146 datetime_end = self.to_datetime_begin() + self.to_exposure_time()
147 return datetime_end
149 def _translate_from_calib_id(self, field):
150 """Fetch the ID from the CALIB_ID header.
152 Calibration products made with constructCalibs have some metadata
153 saved in its FITS header CALIB_ID.
154 """
155 data = self._header["CALIB_ID"]
156 match = re.search(r".*%s=(\S+)" % field, data)
157 self._used_these_cards("CALIB_ID")
158 return match.groups()[0]
160 @cache_translation
161 def to_physical_filter(self):
162 """Calculate physical filter.
164 Return `None` if the keyword FILTER does not exist in the header,
165 which can happen for some valid Community Pipeline products.
167 Returns
168 -------
169 filter : `str`
170 The full filter name.
171 """
172 if self.is_key_ok("FILTER"):
173 value = self._header["FILTER"].strip()
174 self._used_these_cards("FILTER")
175 return value
176 elif self.is_key_ok("CALIB_ID"):
177 return self._translate_from_calib_id("filter")
178 else:
179 return None
181 @cache_translation
182 def to_location(self):
183 """Calculate the observatory location.
185 Returns
186 -------
187 location : `astropy.coordinates.EarthLocation`
188 An object representing the location of the telescope.
189 """
191 if self.is_key_ok("OBS-LONG"):
192 # OBS-LONG has west-positive sign so must be flipped
193 lon = self._header["OBS-LONG"] * -1.0
194 value = EarthLocation.from_geodetic(lon, self._header["OBS-LAT"], self._header["OBS-ELEV"])
195 self._used_these_cards("OBS-LONG", "OBS-LAT", "OBS-ELEV")
196 else:
197 # Look up the value since some files do not have location
198 value = EarthLocation.of_site("ctio")
200 return value
202 @cache_translation
203 def to_observation_type(self):
204 """Calculate the observation type.
206 Returns
207 -------
208 typ : `str`
209 Observation type. Normalized to standard set.
210 """
211 if not self.is_key_ok("OBSTYPE"):
212 return "none"
213 obstype = self._header["OBSTYPE"].strip().lower()
214 self._used_these_cards("OBSTYPE")
215 if obstype == "object":
216 return "science"
217 return obstype
219 @cache_translation
220 def to_tracking_radec(self):
221 # Docstring will be inherited. Property defined in properties.py
222 radecsys = ("RADESYS",)
223 radecpairs = (("TELRA", "TELDEC"),)
224 return tracking_from_degree_headers(self, radecsys, radecpairs, unit=(u.hourangle, u.deg))
226 @cache_translation
227 def to_altaz_begin(self):
228 # Docstring will be inherited. Property defined in properties.py
229 return altaz_from_degree_headers(self, (("ZD", "AZ"),),
230 self.to_datetime_begin(), is_zd=set(["ZD"]))
232 @cache_translation
233 def to_detector_exposure_id(self):
234 # Docstring will be inherited. Property defined in properties.py
235 exposure_id = self.to_exposure_id()
236 if exposure_id is None:
237 return None
238 return int("{:07d}{:02d}".format(exposure_id, self.to_detector_num()))
240 @cache_translation
241 def to_detector_group(self):
242 # Docstring will be inherited. Property defined in properties.py
243 name = self.to_detector_unique_name()
244 return name[0]
246 @cache_translation
247 def to_detector_name(self):
248 # Docstring will be inherited. Property defined in properties.py
249 name = self.to_detector_unique_name()
250 return name[1:]