Coverage for python/astro_metadata_translator/translators/sdss.py: 40%

103 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-09 02:48 -0800

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 

14from __future__ import annotations 

15 

16__all__ = ("SdssTranslator",) 

17 

18import posixpath 

19from typing import TYPE_CHECKING, Any, MutableMapping, Optional 

20 

21import astropy.units as u 

22from astropy.coordinates import AltAz, Angle, EarthLocation 

23 

24from ..translator import CORRECTIONS_RESOURCE_ROOT, cache_translation 

25from .fits import FitsTranslator 

26from .helpers import tracking_from_degree_headers 

27 

28if TYPE_CHECKING: 28 ↛ 29line 28 didn't jump to line 29, because the condition on line 28 was never true

29 import astropy.coordinates 

30 import astropy.time 

31 

32 

33class SdssTranslator(FitsTranslator): 

34 """Metadata translator for SDSS standard headers. 

35 NB: calibration data is not handled as calibration frames were 

36 not available to me at time of writing. 

37 """ 

38 

39 name = "SDSS" 

40 """Name of this translation class""" 

41 

42 supported_instrument = "Imager" 

43 """Supports the SDSS imager instrument.""" 

44 

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

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

47 

48 # SDSS has has a rotator, but in drift scan mode, the instrument 

49 # angle on sky is set to +X=East, +Y=North which we define as a 

50 # 0 degree rotation. 

51 _const_map = { 

52 "boresight_rotation_angle": Angle(0 * u.deg), 

53 "boresight_rotation_coord": "sky", 

54 "dark_time": 0.0 * u.s, # Drift scan implies no dark time 

55 "instrument": "Imager on SDSS 2.5m", # We only ever ingest data from the imager 

56 "telescope": "SDSS 2.5m", # Value of TELESCOP in header is ambiguous 

57 "relative_humidity": None, 

58 "temperature": None, 

59 "pressure": None, 

60 "detector_serial": "UNKNOWN", 

61 } 

62 

63 _trivial_map = { 

64 "exposure_time": ("EXPTIME", dict(unit=u.s)), 

65 "object": "OBJECT", 

66 "physical_filter": "FILTER", 

67 "exposure_id": "RUN", 

68 "visit_id": "RUN", 

69 "science_program": "OBJECT", # This is the closest I can think of to a useful program 

70 "detector_name": "CCDLOC", # This is a numeric incoding of the "slot", i.e. filter+camcol 

71 } 

72 

73 # Need a mapping from unique name to index. The order is arbitrary. 

74 detector_name_id_map = { 

75 "g1": 0, 

76 "z1": 1, 

77 "u1": 2, 

78 "i1": 3, 

79 "r1": 4, 

80 "g2": 5, 

81 "z2": 6, 

82 "u2": 7, 

83 "i2": 8, 

84 "r2": 9, 

85 "g3": 10, 

86 "z3": 11, 

87 "u3": 12, 

88 "i3": 13, 

89 "r3": 14, 

90 "g4": 15, 

91 "z4": 16, 

92 "u4": 17, 

93 "i4": 18, 

94 "r4": 19, 

95 "g5": 20, 

96 "z5": 21, 

97 "u5": 22, 

98 "i5": 23, 

99 "r5": 24, 

100 "g6": 25, 

101 "z6": 26, 

102 "u6": 27, 

103 "i6": 28, 

104 "r6": 29, 

105 } 

106 

107 @classmethod 

108 def can_translate(cls, header: MutableMapping[str, Any], filename: Optional[str] = None) -> bool: 

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

110 supplied header. 

111 

112 Parameters 

113 ---------- 

114 header : `dict`-like 

115 Header to convert to standardized form. 

116 filename : `str`, optional 

117 Name of file being translated. 

118 

119 Returns 

120 ------- 

121 can : `bool` 

122 `True` if the header is recognized by this class. `False` 

123 otherwise. 

124 """ 

125 if ( 

126 cls.is_keyword_defined(header, "ORIGIN") 

127 and cls.is_keyword_defined(header, "CCDMODE") 

128 and cls.is_keyword_defined(header, "TELESCOP") 

129 and "2.5m" in header["TELESCOP"] 

130 and "SDSS" in header["ORIGIN"] 

131 and "DRIFT" in header["CCDMODE"] 

132 ): 

133 return True 

134 return False 

135 

136 @cache_translation 

137 def to_detector_unique_name(self) -> str: 

138 # Docstring will be inherited. Property defined in properties.py 

139 if self.is_key_ok("CAMCOL"): 

140 return self.to_physical_filter() + str(self._header["CAMCOL"]) 

141 else: 

142 raise ValueError(f"{self._log_prefix}: CAMCOL key is not definded") 

143 

144 @cache_translation 

145 def to_detector_num(self) -> int: 

146 # Docstring will be inherited. Property defined in properties.py 

147 return self.detector_name_id_map[self.to_detector_unique_name()] 

148 

149 @cache_translation 

150 def to_observation_id(self) -> str: 

151 """Calculate the observation ID. 

152 

153 Returns 

154 ------- 

155 observation_id : `str` 

156 A string uniquely describing the observation. 

157 This incorporates the run, camcol, filter and frame. 

158 """ 

159 return " ".join([str(self._header[el]) for el in ["RUN", "CAMCOL", "FILTER", "FRAME"]]) 

160 

161 @cache_translation 

162 def to_datetime_begin(self) -> astropy.time.Time: 

163 # Docstring will be inherited. Property defined in properties.py 

164 # We know it is UTC 

165 value = self._from_fits_date_string( 

166 self._header["DATE-OBS"], time_str=self._header["TAIHMS"], scale="tai" 

167 ) 

168 self._used_these_cards("DATE-OBS", "TAIHMS") 

169 return value 

170 

171 @cache_translation 

172 def to_datetime_end(self) -> astropy.time.Time: 

173 # Docstring will be inherited. Property defined in properties.py 

174 return self.to_datetime_begin() + self.to_exposure_time() 

175 

176 @cache_translation 

177 def to_location(self) -> EarthLocation: 

178 """Calculate the observatory location. 

179 

180 Returns 

181 ------- 

182 location : `astropy.coordinates.EarthLocation` 

183 An object representing the location of the telescope. 

184 """ 

185 

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

187 value = EarthLocation.of_site("apo") 

188 

189 return value 

190 

191 @cache_translation 

192 def to_observation_type(self) -> str: 

193 """Calculate the observation type. 

194 

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 

206 

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) 

213 

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) 

233 

234 @cache_translation 

235 def to_boresight_airmass(self) -> Optional[float]: 

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 

241 

242 @cache_translation 

243 def to_detector_exposure_id(self) -> Optional[int]: 

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) 

258 

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