Coverage for python/astro_metadata_translator/translators/megaprime.py: 35%

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

89 statements  

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 CFHT MegaPrime FITS headers""" 

13 

14__all__ = ("MegaPrimeTranslator", ) 

15 

16import re 

17import posixpath 

18 

19from astropy.io import fits 

20from astropy.coordinates import EarthLocation, Angle 

21import astropy.units as u 

22 

23from ..translator import cache_translation, CORRECTIONS_RESOURCE_ROOT 

24from .fits import FitsTranslator 

25from .helpers import tracking_from_degree_headers, altaz_from_degree_headers 

26 

27 

28class MegaPrimeTranslator(FitsTranslator): 

29 """Metadata translator for CFHT MegaPrime standard headers. 

30 """ 

31 

32 name = "MegaPrime" 

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

34 

35 supported_instrument = "MegaPrime" 

36 """Supports the MegaPrime instrument.""" 

37 

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

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

40 

41 # CFHT Megacam has no rotator, and the instrument angle on sky is set to 

42 # +Y=N, +X=W which we define as a 0 degree rotation. 

43 _const_map = {"boresight_rotation_angle": Angle(0*u.deg), 

44 "boresight_rotation_coord": "sky", 

45 "detector_group": None} 

46 

47 _trivial_map = {"physical_filter": "FILTER", 

48 "dark_time": ("DARKTIME", dict(unit=u.s)), 

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

50 "observation_id": "OBSID", 

51 "object": "OBJECT", 

52 "science_program": "RUNID", 

53 "exposure_id": "EXPNUM", 

54 "visit_id": "EXPNUM", 

55 "detector_serial": "CCDNAME", 

56 "relative_humidity": ["RELHUMID", "HUMIDITY"], 

57 "temperature": (["TEMPERAT", "AIRTEMP"], dict(unit=u.deg_C)), 

58 "boresight_airmass": ["AIRMASS", "BORE-AIRMASS"]} 

59 

60 @cache_translation 

61 def to_datetime_begin(self): 

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

63 # We know it is UTC 

64 value = self._from_fits_date_string(self._header["DATE-OBS"], 

65 time_str=self._header["UTC-OBS"], scale="utc") 

66 self._used_these_cards("DATE-OBS", "UTC-OBS") 

67 return value 

68 

69 @cache_translation 

70 def to_datetime_end(self): 

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

72 # Older files are missing UTCEND 

73 if self.is_key_ok("UTCEND"): 

74 # We know it is UTC 

75 value = self._from_fits_date_string(self._header["DATE-OBS"], 

76 time_str=self._header["UTCEND"], scale="utc") 

77 self._used_these_cards("DATE-OBS", "UTCEND") 

78 else: 

79 # Take a guess by adding on the exposure time 

80 value = self.to_datetime_begin() + self.to_exposure_time() 

81 return value 

82 

83 @cache_translation 

84 def to_location(self): 

85 """Calculate the observatory location. 

86 

87 Returns 

88 ------- 

89 location : `astropy.coordinates.EarthLocation` 

90 An object representing the location of the telescope. 

91 """ 

92 # Height is not in some MegaPrime files. Use the value from 

93 # EarthLocation.of_site("CFHT") 

94 # Some data uses OBS-LONG, OBS-LAT, other data uses LONGITUD and 

95 # LATITUDE 

96 for long_key, lat_key in (("LONGITUD", "LATITUDE"), ("OBS-LONG", "OBS-LAT")): 

97 if self.are_keys_ok([long_key, lat_key]): 

98 value = EarthLocation.from_geodetic(self._header[long_key], self._header[lat_key], 4215.0) 

99 self._used_these_cards(long_key, lat_key) 

100 break 

101 else: 

102 value = EarthLocation.of_site("CFHT") 

103 return value 

104 

105 @cache_translation 

106 def to_detector_name(self): 

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

108 if self.is_key_ok("EXTNAME"): 

109 name = self._header["EXTNAME"] 

110 # Only valid name has form "ccdNN" 

111 if re.match(r"ccd\d+$", name): 

112 self._used_these_cards("EXTNAME") 

113 return name 

114 

115 # Dummy value, intended for PHU (need something to get filename) 

116 return "ccd99" 

117 

118 @cache_translation 

119 def to_detector_num(self): 

120 name = self.to_detector_name() 

121 return int(name[3:]) 

122 

123 @cache_translation 

124 def to_observation_type(self): 

125 """Calculate the observation type. 

126 

127 Returns 

128 ------- 

129 typ : `str` 

130 Observation type. Normalized to standard set. 

131 """ 

132 obstype = self._header["OBSTYPE"].strip().lower() 

133 self._used_these_cards("OBSTYPE") 

134 if obstype == "object": 

135 return "science" 

136 return obstype 

137 

138 @cache_translation 

139 def to_tracking_radec(self): 

140 """Calculate the tracking RA/Dec for this observation. 

141 

142 Currently will be `None` for geocentric apparent coordinates. 

143 Additionally, can be `None` for non-science observations. 

144 

145 The method supports multiple versions of header defining tracking 

146 coordinates. 

147 

148 Returns 

149 ------- 

150 coords : `astropy.coordinates.SkyCoord` 

151 The tracking coordinates. 

152 """ 

153 radecsys = ("RADECSYS", "OBJRADEC", "RADESYS") 

154 radecpairs = (("RA_DEG", "DEC_DEG"), ("BORE-RA", "BORE-DEC")) 

155 return tracking_from_degree_headers(self, radecsys, radecpairs) 

156 

157 @cache_translation 

158 def to_altaz_begin(self): 

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

160 return altaz_from_degree_headers(self, (("TELALT", "TELAZ"), ("BORE-ALT", "BORE-AZ")), 

161 self.to_datetime_begin()) 

162 

163 @cache_translation 

164 def to_detector_exposure_id(self): 

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

166 return self.to_exposure_id() * 36 + self.to_detector_num() 

167 

168 @cache_translation 

169 def to_pressure(self): 

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

171 # Can be either AIRPRESS in Pa or PRESSURE in mbar 

172 for key, unit in (("PRESSURE", u.hPa), ("AIRPRESS", u.Pa)): 

173 if self.is_key_ok(key): 

174 return self.quantity_from_card(key, unit) 

175 else: 

176 raise KeyError(f"{self._log_prefix}: Could not find pressure keywords in header") 

177 

178 @cache_translation 

179 def to_observation_counter(self): 

180 """Return the lifetime exposure number. 

181 

182 Returns 

183 ------- 

184 sequence : `int` 

185 The observation counter. 

186 """ 

187 return self.to_exposure_id() 

188 

189 @classmethod 

190 def determine_translatable_headers(cls, filename, primary=None): 

191 """Given a file return all the headers usable for metadata translation. 

192 

193 MegaPrime files are multi-extension FITS with a primary header and 

194 each detector stored in a subsequent extension. MegaPrime uses 

195 ``INHERIT=F`` therefore the primary header will always be ignored 

196 if given. 

197 

198 Parameters 

199 ---------- 

200 filename : `str` 

201 Path to a file in a format understood by this translator. 

202 primary : `dict`-like, optional 

203 The primary header obtained by the caller. This is sometimes 

204 already known, for example if a system is trying to bootstrap 

205 without already knowing what data is in the file. Will be 

206 ignored. 

207 

208 Yields 

209 ------ 

210 headers : iterator of `dict`-like 

211 Each detector header in turn. The supplied header will never be 

212 included. 

213 

214 Notes 

215 ----- 

216 This translator class is specifically tailored to raw MegaPrime data 

217 and is not designed to work with general FITS files. The normal 

218 paradigm is for the caller to have read the first header and then 

219 called `determine_translator()` on the result to work out which 

220 translator class to then call to obtain the real headers to be used for 

221 translation. 

222 """ 

223 # Since we want to scan many HDUs we use astropy directly to keep 

224 # the file open rather than continually opening and closing it 

225 # as we go to each HDU. 

226 with fits.open(filename) as fits_file: 

227 for hdu in fits_file: 

228 # Astropy <=4.2 strips the EXTNAME header but some CFHT data 

229 # have two EXTNAME headers and the CCD number is in the 

230 # second one. 

231 if hdu.name == "PRIMARY": 

232 continue 

233 

234 if hdu.name.startswith("ccd"): 

235 # It may only be some data files that are broken so 

236 # handle the expected form. 

237 yield hdu.header 

238 continue 

239 

240 # Some test data at least has the EXTNAME as 

241 # COMPRESSED_IMAGE but the EXTVER as the detector number. 

242 if hdu.name == "COMPRESSED_IMAGE": 

243 header = hdu.header 

244 

245 # Astropy strips EXTNAME so put it back for the translator 

246 header["EXTNAME"] = f"ccd{hdu.ver:02d}" 

247 yield header