Coverage for python / lsst / summit / utils / dateTime.py: 44%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-22 09:41 +0000

1# This file is part of summit_utils. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://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 <https://www.gnu.org/licenses/>. 

21from __future__ import annotations 

22 

23import datetime 

24from typing import TYPE_CHECKING 

25 

26import astropy 

27from astropy import units as u 

28from astropy.time import Time, TimeDelta 

29from dateutil.tz import gettz 

30 

31if TYPE_CHECKING: 

32 from lsst.daf.butler import DimensionRecord 

33 

34 

35__all__ = [ 

36 "expRecordToTimespan", 

37 "efdTimestampToAstropy", 

38 "astropyToEfdTimestamp", 

39 "offsetDayObs", 

40 "calcNextDay", 

41 "calcPreviousDay", 

42 "calcDayOffset", 

43 "getDayObsStartTime", 

44 "getDayObsEndTime", 

45 "getDayObsForTime", 

46 "dayObsIntToString", 

47 "getCurrentDayObsDatetime", 

48 "getCurrentDayObsInt", 

49 "getCurrentDayObsHumanStr", 

50 "getExpRecordAge", 

51] 

52 

53 

54def expRecordToTimespan(expRecord: DimensionRecord) -> dict: 

55 """Get the timespan from an exposure record. 

56 

57 Returns the timespan in a format where it can be used to directly unpack 

58 into a efdClient.select_time_series() call. 

59 

60 Parameters 

61 ---------- 

62 expRecord : `lsst.daf.butler.DimensionRecord` 

63 The exposure record. 

64 

65 Returns 

66 ------- 

67 timespanDict : `dict` 

68 The timespan in a format that can be used to directly unpack into a 

69 efdClient.select_time_series() call. 

70 """ 

71 return { 

72 "begin": expRecord.timespan.begin.utc, 

73 "end": expRecord.timespan.end.utc, 

74 } 

75 

76 

77def efdTimestampToAstropy(timestamp: float) -> Time: 

78 """Get an efd timestamp as an astropy.time.Time object. 

79 

80 Parameters 

81 ---------- 

82 timestamp : `float` 

83 The timestamp, as a float. 

84 

85 Returns 

86 ------- 

87 time : `astropy.time.Time` 

88 The timestamp as an astropy.time.Time object. 

89 """ 

90 return Time(timestamp, format="unix") 

91 

92 

93def astropyToEfdTimestamp(time: Time) -> float: 

94 """Get astropy Time object as an efd timestamp 

95 

96 Parameters 

97 ---------- 

98 time : `astropy.time.Time` 

99 The time as an astropy.time.Time object. 

100 

101 Returns 

102 ------- 

103 timestamp : `float` 

104 The timestamp, in UTC, in unix seconds. 

105 """ 

106 

107 return time.utc.unix 

108 

109 

110def offsetDayObs(dayObs: int, nDays: int) -> int: 

111 """Offset a dayObs by a given number of days. 

112 

113 Parameters 

114 ---------- 

115 dayObs : `int` 

116 The dayObs, as an integer, e.g. 20231225 

117 nDays : `int` 

118 The number of days to offset the dayObs by. 

119 

120 Returns 

121 ------- 

122 newDayObs : `int` 

123 The new dayObs, as an integer, e.g. 20231225 

124 """ 

125 d1 = datetime.datetime.strptime(str(dayObs), "%Y%m%d") 

126 oneDay = datetime.timedelta(days=nDays) 

127 return int((d1 + oneDay).strftime("%Y%m%d")) 

128 

129 

130def calcNextDay(dayObs: int) -> int: 

131 """Given an integer dayObs, calculate the next integer dayObs. 

132 

133 Integers are used for dayObs, but dayObs values are therefore not 

134 contiguous due to month/year ends etc, so this utility provides a robust 

135 way to get the integer dayObs which follows the one specified. 

136 

137 Parameters 

138 ---------- 

139 dayObs : `int` 

140 The dayObs, as an integer, e.g. 20231231 

141 

142 Returns 

143 ------- 

144 nextDayObs : `int` 

145 The next dayObs, as an integer, e.g. 20240101 

146 """ 

147 return offsetDayObs(dayObs, 1) 

148 

149 

150def calcPreviousDay(dayObs: int) -> int: 

151 """Given an integer dayObs, calculate the next integer dayObs. 

152 

153 Integers are used for dayObs, but dayObs values are therefore not 

154 contiguous due to month/year ends etc, so this utility provides a robust 

155 way to get the integer dayObs which follows the one specified. 

156 

157 Parameters 

158 ---------- 

159 dayObs : `int` 

160 The dayObs, as an integer, e.g. 20231231 

161 

162 Returns 

163 ------- 

164 nextDayObs : `int` 

165 The next dayObs, as an integer, e.g. 20240101 

166 """ 

167 return offsetDayObs(dayObs, -1) 

168 

169 

170def calcDayOffset(startDay: int, endDay: int) -> int: 

171 """Calculate the number of days between two dayObs values. 

172 

173 Positive if endDay is after startDay, negative if before, zero if equal. 

174 

175 Parameters 

176 ---------- 

177 startDay : `int` 

178 The starting dayObs, e.g. 20231225. 

179 endDay : `int` 

180 The ending dayObs, e.g. 20240101. 

181 

182 Returns 

183 ------- 

184 offset : `int` 

185 The number of days from startDay to endDay. 

186 """ 

187 dStart = datetime.datetime.strptime(str(startDay), "%Y%m%d") 

188 dEnd = datetime.datetime.strptime(str(endDay), "%Y%m%d") 

189 return (dEnd - dStart).days 

190 

191 

192def getDayObsStartTime(dayObs: int) -> astropy.time.Time: 

193 """Get the start of the given dayObs as an astropy.time.Time object. 

194 

195 The observatory rolls the date over at UTC-12. 

196 

197 Parameters 

198 ---------- 

199 dayObs : `int` 

200 The dayObs, as an integer, e.g. 20231225 

201 

202 Returns 

203 ------- 

204 time : `astropy.time.Time` 

205 The start of the dayObs as an astropy.time.Time object. 

206 """ 

207 pythonDateTime = datetime.datetime.strptime(str(dayObs), "%Y%m%d") 

208 return Time(pythonDateTime) + 12 * u.hour 

209 

210 

211def getDayObsEndTime(dayObs: int) -> Time: 

212 """Get the end of the given dayObs as an astropy.time.Time object. 

213 

214 Parameters 

215 ---------- 

216 dayObs : `int` 

217 The dayObs, as an integer, e.g. 20231225 

218 

219 Returns 

220 ------- 

221 time : `astropy.time.Time` 

222 The end of the dayObs as an astropy.time.Time object. 

223 """ 

224 return getDayObsStartTime(dayObs) + 24 * u.hour 

225 

226 

227def getDayObsForTime(time: Time) -> int: 

228 """Get the dayObs in which an astropy.time.Time object falls. 

229 

230 Parameters 

231 ---------- 

232 time : `astropy.time.Time` 

233 The time. 

234 

235 Returns 

236 ------- 

237 dayObs : `int` 

238 The dayObs, as an integer, e.g. 20231225 

239 """ 

240 twelveHours = datetime.timedelta(hours=-12) 

241 offset = TimeDelta(twelveHours, format="datetime") 

242 return int((time + offset).utc.isot[:10].replace("-", "")) 

243 

244 

245def dayObsIntToString(dayObs: int) -> str: 

246 """Convert an integer dayObs to a dash-delimited string. 

247 

248 e.g. convert the hard to read 20210101 to 2021-01-01 

249 

250 Parameters 

251 ---------- 

252 dayObs : `int` 

253 The dayObs. 

254 

255 Returns 

256 ------- 

257 dayObs : `str` 

258 The dayObs as a string. 

259 """ 

260 assert isinstance(dayObs, int) 

261 dStr = str(dayObs) 

262 assert len(dStr) == 8 

263 return "-".join([dStr[0:4], dStr[4:6], dStr[6:8]]) 

264 

265 

266def getCurrentDayObsDatetime() -> datetime.date: 

267 """Get the current day_obs - the observatory rolls the date over at UTC-12 

268 

269 Returned as datetime.date(2022, 4, 28) 

270 """ 

271 utc = gettz("UTC") 

272 nowUtc = datetime.datetime.now().astimezone(utc) 

273 offset = datetime.timedelta(hours=-12) 

274 dayObs = (nowUtc + offset).date() 

275 return dayObs 

276 

277 

278def getCurrentDayObsInt() -> int: 

279 """Return the current dayObs as an int in the form 20220428""" 

280 return int(getCurrentDayObsDatetime().strftime("%Y%m%d")) 

281 

282 

283def getCurrentDayObsHumanStr() -> str: 

284 """Return the current dayObs as a string in the form '2022-04-28'""" 

285 return dayObsIntToString(getCurrentDayObsInt()) 

286 

287 

288def getExpRecordAge(expRecord: DimensionRecord) -> float: 

289 """Get the time, in seconds, since the end of exposure. 

290 

291 Parameters 

292 ---------- 

293 expRecord : `lsst.daf.butler.DimensionRecord` 

294 The exposure record. 

295 

296 Returns 

297 ------- 

298 age : `float` 

299 The age of the exposure, in seconds. 

300 """ 

301 return -1 * (expRecord.timespan.end - Time.now()).sec