Coverage for python / lsst / obs / lsst / translators / phosim.py: 60%

46 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-01 08:49 +0000

1# This file is currently part of obs_lsst but is written to allow it 

2# to be migrated to the astro_metadata_translator package at a later date. 

3# 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the LICENSE file in this directory for details of code ownership. 

7# 

8# Use of this source code is governed by a 3-clause BSD-style 

9# license that can be found in the LICENSE file. 

10 

11"""Metadata translation code for LSSTCam PhoSim FITS headers""" 

12 

13__all__ = ("LsstCamPhoSimTranslator",) 

14 

15import logging 

16 

17import astropy.io.fits as fits 

18import astropy.units as u 

19import astropy.units.cds as cds 

20from astropy.coordinates import Angle 

21from astropy.time import TimeDelta 

22 

23from astro_metadata_translator import cache_translation, merge_headers 

24from astro_metadata_translator.translators.helpers import ( 

25 tracking_from_degree_headers, 

26 altaz_from_degree_headers, 

27) 

28 

29from lsst.resources import ResourcePath 

30 

31from .lsstsim import LsstSimTranslator 

32 

33log = logging.getLogger(__name__) 

34 

35 

36class LsstCamPhoSimTranslator(LsstSimTranslator): 

37 """Metadata translator for LSSTCam PhoSim data.""" 

38 

39 name = "LSSTCam-PhoSim" 

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

41 

42 _const_map = { 

43 "instrument": "LSSTCam-PhoSim", 

44 "boresight_rotation_coord": "sky", 

45 "observation_type": "science", 

46 "object": "UNKNOWN", 

47 "relative_humidity": 40.0, 

48 } 

49 

50 _trivial_map = { 

51 "detector_group": "RAFTNAME", 

52 "observation_id": "OBSID", 

53 "science_program": "RUNNUM", 

54 "exposure_id": "OBSID", 

55 "visit_id": "OBSID", 

56 "physical_filter": "FILTER", 

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

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

59 "temperature": ("TEMPERA", dict(unit=u.deg_C)), 

60 "pressure": ("PRESS", dict(unit=cds.mmHg)), 

61 "boresight_airmass": "AIRMASS", 

62 "detector_name": "SENSNAME", 

63 "detector_serial": "LSST_NUM", 

64 } 

65 

66 cameraPolicyFile = "policy/phosim.yaml" 

67 

68 _ROLLOVER_TIME = TimeDelta(0, scale="tai", format="sec") 

69 """This instrument did not offset the observing day.""" 

70 

71 @classmethod 

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

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

74 supplied header. 

75 

76 There is no ``INSTRUME`` header in PhoSim data. Instead we use 

77 the ``CREATOR`` header. 

78 

79 Parameters 

80 ---------- 

81 header : `dict`-like 

82 Header to convert to standardized form. 

83 filename : `str`, optional 

84 Name of file being translated. 

85 

86 Returns 

87 ------- 

88 can : `bool` 

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

90 otherwise. 

91 """ 

92 # Generic PhoSim data does not have an INSTRUME header. 

93 # If an INSTRUME header is present this translator class 

94 # is not suitable. 

95 if "INSTRUME" in header: 

96 return False 

97 else: 

98 return cls.can_translate_with_options( 

99 header, {"CREATOR": "PHOSIM", "TESTTYPE": "PHOSIM"}, filename=filename 

100 ) 

101 

102 @cache_translation 

103 def to_tracking_radec(self): 

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

105 radecsys = ("RADESYS",) 

106 radecpairs = ( 

107 ("RATEL", "DECTEL"), 

108 ("RA_DEG", "DEC_DEG"), 

109 ("BORE-RA", "BORE-DEC"), 

110 ) 

111 return tracking_from_degree_headers(self, radecsys, radecpairs) 

112 

113 @cache_translation 

114 def to_altaz_begin(self): 

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

116 # Fallback to the "derive from ra/dec" if keys are missing 

117 if self.are_keys_ok(["ZENITH", "AZIMUTH"]): 

118 return altaz_from_degree_headers( 

119 self, 

120 (("ZENITH", "AZIMUTH"),), 

121 self.to_datetime_begin(), 

122 is_zd=set(["ZENITH"]), 

123 ) 

124 else: 

125 return super().to_altaz_begin() 

126 

127 @cache_translation 

128 def to_boresight_rotation_angle(self): 

129 angle = Angle(90.0 * u.deg) - Angle( 

130 self.quantity_from_card(["ROTANGZ", "ROTANGLE"], u.deg) 

131 ) 

132 angle = angle.wrap_at("360d") 

133 return angle 

134 

135 @classmethod 

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

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

138 

139 Phosim splits useful metadata between the primary header and the 

140 amplifier headers. A single header is returned as a merge of the 

141 first two. 

142 

143 Parameters 

144 ---------- 

145 filename : `str` or `lsst.resources.ResourcePathExpression` 

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

147 primary : `dict`-like, optional 

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

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

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

151 ignored. 

152 

153 Yields 

154 ------ 

155 headers : iterator of `dict`-like 

156 The primary header merged with the secondary header. 

157 

158 Notes 

159 ----- 

160 This translator class is specifically tailored to raw PhoSim data 

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

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

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

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

165 translation. 

166 """ 

167 uri = ResourcePath(filename, forceDirectory=False) 

168 fs, fspath = uri.to_fsspec() 

169 with fs.open(fspath) as f, fits.open(f) as fits_file: 

170 yield merge_headers([fits_file[0].header, fits_file[1].header], 

171 mode="overwrite")