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 daf_butler. 

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 COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21from __future__ import annotations 

22 

23__all__ = ("astropy_to_nsec", "nsec_to_astropy", "times_equal") 

24 

25import logging 

26from typing import Any 

27import warnings 

28 

29import astropy.time 

30import astropy.utils.exceptions 

31import yaml 

32 

33# As of astropy 4.2, the erfa interface is shipped independently and 

34# ErfaWarning is no longer an AstropyWarning 

35try: 

36 import erfa 

37except ImportError: 

38 erfa = None 

39 

40# These constants can be used by client code. 

41# EPOCH is used to construct times as read from database, its precision is 

42# used by all those timestamps, set it to 1 microsecond. 

43EPOCH = astropy.time.Time("1970-01-01 00:00:00", format="iso", scale="tai", precision=6) 

44"""Epoch for calculating time delta, this is the minimum time that can be 

45stored in the database. 

46""" 

47 

48MAX_TIME = astropy.time.Time("2100-01-01 00:00:00", format="iso", scale="tai") 

49"""Maximum time value that we can store. Assuming 64-bit integer field we 

50can actually store higher values but we intentionally limit it to arbitrary 

51but reasonably high value. Note that this value will be stored in registry 

52database for eternity, so it should not be changed without proper 

53consideration. 

54""" 

55 

56# number of nanosecons in a day 

57_NSEC_PER_DAY = 1_000_000_000 * 24 * 3600 

58 

59_LOG = logging.getLogger(__name__) 

60 

61 

62def astropy_to_nsec(astropy_time: astropy.time.Time) -> int: 

63 """Convert astropy time to nanoseconds since epoch. 

64 

65 Input time is converted to TAI scale before conversion to 

66 nanoseconds. 

67 

68 Parameters 

69 ---------- 

70 astropy_time : `astropy.time.Time` 

71 Time to be converted. 

72 

73 Returns 

74 ------- 

75 time_nsec : `int` 

76 Nanoseconds since epoch. 

77 

78 Note 

79 ---- 

80 Only the limited range of input times is supported by this method as it 

81 is defined useful in the context of Butler and Registry. If input time is 

82 earlier than epoch time then this method returns 0. If input time comes 

83 after the max. time then it returns number corresponding to max. time. 

84 """ 

85 # sometimes comparison produces warnings if input value is in UTC 

86 # scale, transform it to TAI before doing anything but also trap 

87 # warnings in case we are dealing with simulated data from the future 

88 with warnings.catch_warnings(): 

89 warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning) 

90 if erfa is not None: 

91 warnings.simplefilter("ignore", category=erfa.ErfaWarning) 

92 value = astropy_time.tai 

93 # anything before epoch or after MAX_TIME is truncated 

94 if value < EPOCH: 

95 _LOG.warning("'%s' is earlier than epoch time '%s', epoch time will be used instead", 

96 astropy_time, EPOCH) 

97 value = EPOCH 

98 elif value > MAX_TIME: 

99 _LOG.warning("'%s' is later than max. time '%s', max. time time will be used instead", 

100 value, MAX_TIME) 

101 value = MAX_TIME 

102 

103 delta = value - EPOCH 

104 # Special care needed to preserve nanosecond precision. 

105 # Usually jd1 has no fractional part but just in case. 

106 jd1, extra_jd2 = divmod(delta.jd1, 1) 

107 value = int(jd1) * _NSEC_PER_DAY + int(round((delta.jd2 + extra_jd2)*_NSEC_PER_DAY)) 

108 return value 

109 

110 

111def nsec_to_astropy(time_nsec: int) -> astropy.time.Time: 

112 """Convert nanoseconds since epoch to astropy time. 

113 

114 Parameters 

115 ---------- 

116 time_nsec : `int` 

117 Nanoseconds since epoch. 

118 

119 Returns 

120 ------- 

121 astropy_time : `astropy.time.Time` 

122 Time to be converted. 

123 

124 Note 

125 ---- 

126 Usually the input time for this method is the number returned from 

127 `astropy_to_nsec` which has a limited range. This method does not check 

128 that the number falls in the supported range and can produce output 

129 time that is outside of that range. 

130 """ 

131 jd1, jd2 = divmod(time_nsec, _NSEC_PER_DAY) 

132 delta = astropy.time.TimeDelta(float(jd1), float(jd2)/_NSEC_PER_DAY, format="jd", scale="tai") 

133 value = EPOCH + delta 

134 return value 

135 

136 

137def times_equal(time1: astropy.time.Time, 

138 time2: astropy.time.Time, 

139 precision_nsec: float = 1.0) -> bool: 

140 """Check that times are equal within specified precision. 

141 

142 Parameters 

143 ---------- 

144 time1, time2 : `astropy.time.Time` 

145 Times to compare. 

146 precision_nsec : `float`, optional 

147 Precision to use for comparison in nanoseconds, default is one 

148 nanosecond which is larger that round-trip error for conversion 

149 to/from integer nanoseconds. 

150 """ 

151 # To compare we need them in common scale, for simplicity just 

152 # bring them both to TAI scale 

153 # Hide any warnings from this conversion since they are not relevant 

154 # to the equality 

155 with warnings.catch_warnings(): 

156 warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning) 

157 if erfa is not None: 

158 warnings.simplefilter("ignore", category=erfa.ErfaWarning) 

159 time1 = time1.tai 

160 time2 = time2.tai 

161 delta = (time2.jd1 - time1.jd1) + (time2.jd2 - time1.jd2) 

162 delta *= _NSEC_PER_DAY 

163 return abs(delta) < precision_nsec 

164 

165 

166class _AstropyTimeToYAML: 

167 """Handle conversion of astropy Time to/from YAML representation. 

168 

169 This class defines methods that convert astropy Time instances to or from 

170 YAML representation. On output it converts time to string ISO format in 

171 TAI scale with maximum precision defining special YAML tag for it. On 

172 input it does inverse transformation. The methods need to be registered 

173 with YAML dumper and loader classes. 

174 

175 Notes 

176 ----- 

177 Python ``yaml`` module defines special helper base class ``YAMLObject`` 

178 that provides similar functionality but its use is complicated by the need 

179 to convert ``Time`` instances to instances of ``YAMLObject`` sub-class 

180 before saving them to YAML. This class avoids this intermediate step but 

181 it requires separate regisration step. 

182 """ 

183 

184 yaml_tag = "!butler_time/tai/iso" # YAML tag name for Time class 

185 

186 @classmethod 

187 def to_yaml(cls, dumper: yaml.Dumper, data: astropy.time.Time) -> Any: 

188 """Convert astropy Time object into YAML format. 

189 

190 Parameters 

191 ---------- 

192 dumper : `yaml.Dumper` 

193 YAML dumper instance. 

194 data : `astropy.time.Time` 

195 Data to be converted. 

196 """ 

197 if data is not None: 

198 # we store time in ISO format but we need full nanosecond 

199 # precision so we have to construct intermediate instance to make 

200 # sure its precision is set correctly. 

201 data = astropy.time.Time(data.tai, precision=9) 

202 data = data.to_value("iso") 

203 return dumper.represent_scalar(cls.yaml_tag, data) 

204 

205 @classmethod 

206 def from_yaml(cls, loader: yaml.SafeLoader, node: yaml.ScalarNode) -> astropy.time.Time: 

207 """Convert YAML node into astropy time 

208 

209 Parameters 

210 ---------- 

211 loader : `yaml.SafeLoader` 

212 Instance of YAML loader class. 

213 node : `yaml.ScalarNode` 

214 YAML node. 

215 

216 Returns 

217 ------- 

218 time : `astropy.time.Time` 

219 Time instance, can be ``None``. 

220 """ 

221 if node.value is not None: 

222 return astropy.time.Time(node.value, format="iso", scale="tai") 

223 

224 

225# Register Time -> YAML conversion method with Dumper class 

226yaml.Dumper.add_representer(astropy.time.Time, _AstropyTimeToYAML.to_yaml) 

227 

228# Register YAML -> Time conversion method with Loader, for our use case we 

229# only need SafeLoader. 

230yaml.SafeLoader.add_constructor(_AstropyTimeToYAML.yaml_tag, _AstropyTimeToYAML.from_yaml)