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 

34# These constants can be used by client code. 

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

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

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

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

39stored in the database. 

40""" 

41 

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

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

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

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

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

47consideration. 

48""" 

49 

50# number of nanosecons in a day 

51_NSEC_PER_DAY = 1_000_000_000 * 24 * 3600 

52 

53_LOG = logging.getLogger(__name__) 

54 

55 

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

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

58 

59 Input time is converted to TAI scale before conversion to 

60 nanoseconds. 

61 

62 Parameters 

63 ---------- 

64 astropy_time : `astropy.time.Time` 

65 Time to be converted. 

66 

67 Returns 

68 ------- 

69 time_nsec : `int` 

70 Nanoseconds since epoch. 

71 

72 Note 

73 ---- 

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

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

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

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

78 """ 

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

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

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

82 with warnings.catch_warnings(): 

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

84 value = astropy_time.tai 

85 # anything before epoch or after MAX_TIME is truncated 

86 if value < EPOCH: 

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

88 astropy_time, EPOCH) 

89 value = EPOCH 

90 elif value > MAX_TIME: 

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

92 value, MAX_TIME) 

93 value = MAX_TIME 

94 

95 delta = value - EPOCH 

96 # Special care needed to preserve nanosecond precision. 

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

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

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

100 return value 

101 

102 

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

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

105 

106 Parameters 

107 ---------- 

108 time_nsec : `int` 

109 Nanoseconds since epoch. 

110 

111 Returns 

112 ------- 

113 astropy_time : `astropy.time.Time` 

114 Time to be converted. 

115 

116 Note 

117 ---- 

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

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

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

121 time that is outside of that range. 

122 """ 

123 jd1, jd2 = divmod(time_nsec, _NSEC_PER_DAY) 

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

125 value = EPOCH + delta 

126 return value 

127 

128 

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

130 time2: astropy.time.Time, 

131 precision_nsec: float = 1.0) -> bool: 

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

133 

134 Parameters 

135 ---------- 

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

137 Times to compare. 

138 precision_nsec : `float`, optional 

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

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

141 to/from integer nanoseconds. 

142 """ 

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

144 # bring them both to TAI scale 

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

146 # to the equality 

147 with warnings.catch_warnings(): 

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

149 time1 = time1.tai 

150 time2 = time2.tai 

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

152 delta *= _NSEC_PER_DAY 

153 return abs(delta) < precision_nsec 

154 

155 

156class _AstropyTimeToYAML: 

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

158 

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

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

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

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

163 with YAML dumper and loader classes. 

164 

165 Notes 

166 ----- 

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

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

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

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

171 it requires separate regisration step. 

172 """ 

173 

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

175 

176 @classmethod 

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

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

179 

180 Parameters 

181 ---------- 

182 dumper : `yaml.Dumper` 

183 YAML dumper instance. 

184 data : `astropy.time.Time` 

185 Data to be converted. 

186 """ 

187 if data is not None: 

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

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

190 # sure its precision is set correctly. 

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

192 data = data.to_value("iso") 

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

194 

195 @classmethod 

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

197 """Convert YAML node into astropy time 

198 

199 Parameters 

200 ---------- 

201 loader : `yaml.SafeLoader` 

202 Instance of YAML loader class. 

203 node : `yaml.ScalarNode` 

204 YAML node. 

205 

206 Returns 

207 ------- 

208 time : `astropy.time.Time` 

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

210 """ 

211 if node.value is not None: 

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

213 

214 

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

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

217 

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

219# only need SafeLoader. 

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