Hide keyboard shortcuts

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. 

11 

12"""Metadata translation code for SDSS FITS headers""" 

13 

14__all__ = ("SdssTranslator", ) 

15 

16import posixpath 

17 

18from astropy.coordinates import EarthLocation, Angle, AltAz 

19import astropy.units as u 

20 

21from ..translator import cache_translation, CORRECTIONS_RESOURCE_ROOT 

22from .fits import FitsTranslator 

23from .helpers import tracking_from_degree_headers 

24 

25 

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 """ 

31 

32 name = "SDSS" 

33 """Name of this translation class""" 

34 

35 supported_instrument = "Imager" 

36 """Supports the SDSS imager instrument.""" 

37 

38 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "SDSS") 

39 """Default resource path root to use to locate header correction files.""" 

40 

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 } 

54 

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 } 

63 

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} 

69 

70 @classmethod 

71 def can_translate(cls, header, filename=None): 

72 """Indicate whether this translation class can translate the 

73 supplied header. 

74 

75 Parameters 

76 ---------- 

77 header : `dict`-like 

78 Header to convert to standardized form. 

79 filename : `str`, optional 

80 Name of file being translated. 

81 

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 

93 

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") 

101 

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()] 

106 

107 @cache_translation 

108 def to_observation_id(self): 

109 """Calculate the observation ID. 

110 

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"]]) 

119 

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 

128 

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() 

133 

134 @cache_translation 

135 def to_location(self): 

136 """Calculate the observatory location. 

137 

138 Returns 

139 ------- 

140 location : `astropy.coordinates.EarthLocation` 

141 An object representing the location of the telescope. 

142 """ 

143 

144 # Look up the value since files do not have location 

145 value = EarthLocation.of_site("apo") 

146 

147 return value 

148 

149 @cache_translation 

150 def to_observation_type(self): 

151 """Calculate the observation type. 

152 

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 

164 

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) 

171 

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) 

190 

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 

197 

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) 

216 

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")