Coverage for C:\src\imod-python\imod\util\time.py: 93%

69 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-08 13:27 +0200

1import datetime 

2import warnings 

3 

4import cftime 

5import dateutil 

6import numpy as np 

7import pandas as pd 

8 

9DATETIME_FORMATS = { 

10 14: "%Y%m%d%H%M%S", 

11 12: "%Y%m%d%H%M", 

12 10: "%Y%m%d%H", 

13 8: "%Y%m%d", 

14 4: "%Y", 

15} 

16 

17 

18def to_datetime(s: str) -> datetime.datetime: 

19 """ 

20 Convert string to datetime. Part of the public API for backwards 

21 compatibility reasons. 

22 

23 Fast performance is important, as this function is used to parse IDF names, 

24 so it being called 100,000 times is a common usecase. Function stored 

25 previously under imod.util.to_datetime. 

26 """ 

27 try: 

28 time = datetime.datetime.strptime(s, DATETIME_FORMATS[len(s)]) 

29 except (ValueError, KeyError): # Try fullblown dateutil date parser 

30 time = dateutil.parser.parse(s) 

31 return time 

32 

33 

34def _check_year(year: int) -> None: 

35 """Check whether year is out of bounds for np.datetime64[ns]""" 

36 if year < 1678 or year > 2261: 

37 raise ValueError( 

38 "A datetime is out of bounds for np.datetime64[ns]: " 

39 "before year 1678 or after 2261. You will have to use " 

40 "cftime.datetime and xarray.CFTimeIndex in your model " 

41 "input instead of the default np.datetime64[ns] datetime " 

42 "type." 

43 ) 

44 

45 

46def to_datetime_internal( 

47 time: cftime.datetime | np.datetime64 | str, use_cftime: bool 

48) -> np.datetime64 | cftime.datetime: 

49 """ 

50 Check whether time is cftime object, else convert to datetime64 series. 

51 

52 cftime currently has no pd.to_datetime equivalent: a method that accepts a 

53 lot of different input types. Function stored previously under 

54 imod.wq.timeutil.to_datetime. 

55 

56 Parameters 

57 ---------- 

58 time : cftime object or datetime-like scalar 

59 """ 

60 if isinstance(time, cftime.datetime): 

61 return time 

62 elif isinstance(time, np.datetime64): 

63 # Extract year from np.datetime64. 

64 # First force a yearly datetime64 type, 

65 # convert to int, and add the reference year. 

66 # This appears to be the safest method 

67 # see https://stackoverflow.com/a/26895491 

68 # time.astype(object).year, produces inconsistent 

69 # results when 'time' is datetime64[d] or when it is datetime64[ns] 

70 # at least for numpy version 1.20.1 

71 year = time.astype("datetime64[Y]").astype(int) + 1970 

72 _check_year(year) 

73 # Force to nanoseconds, concurrent with xarray and pandas. 

74 return time.astype(dtype="datetime64[ns]") 

75 elif isinstance(time, str): 

76 time = to_datetime(time) 

77 if not use_cftime: 

78 _check_year(time.year) 

79 

80 if use_cftime: 

81 return cftime.DatetimeProlepticGregorian(*time.timetuple()[:6]) 

82 else: 

83 return np.datetime64(time, "ns") 

84 

85 

86def timestep_duration(times: np.array, use_cftime: bool): 

87 """ 

88 Generates dictionary containing stress period time discretization data. 

89 

90 Parameters 

91 ---------- 

92 times : np.array 

93 Array containing containing time in a datetime-like format 

94 

95 Returns 

96 ------- 

97 duration : 1D numpy array of floats 

98 stress period duration in decimal days 

99 """ 

100 if not use_cftime: 

101 times = pd.to_datetime(times) 

102 

103 timestep_duration = [] 

104 for start, end in zip(times[:-1], times[1:]): 

105 timedelta = end - start 

106 duration = timedelta.days + timedelta.seconds / 86400.0 

107 timestep_duration.append(duration) 

108 return np.array(timestep_duration) 

109 

110 

111def forcing_starts_ends(package_times: np.array, globaltimes: np.array): 

112 """ 

113 Determines the stress period numbers for start and end for a forcing defined 

114 at a starting time, until the next starting time. 

115 Numbering is inclusive, in accordance with the iMODwq runfile. 

116 

117 Parameters 

118 ---------- 

119 package_times : np.array, listlike 

120 Treated as starting time of forcing 

121 globaltimes : np.array, listlike 

122 Global times of the simulation. Defines starting time of the stress 

123 periods. 

124 

125 Returns 

126 ------- 

127 starts_ends : list of tuples 

128 For every entry in the package, return index of start and end. 

129 Numbering is inclusive. 

130 """ 

131 # From searchsorted docstring: 

132 # Find the indices into a sorted array a such that, if the corresponding 

133 # elements in v were inserted before the indices, the order of a would be 

134 # preserved. 

135 # Add one because of difference in 0 vs 1 based indexing. 

136 starts = np.searchsorted(globaltimes, package_times) + 1 

137 ends = np.append(starts[1:] - 1, len(globaltimes)) 

138 starts_ends = [ 

139 f"{start}:{end}" if (end > start) else str(start) 

140 for (start, end) in zip(starts, ends) 

141 ] 

142 return starts_ends 

143 

144 

145def _convert_datetimes(times: np.array, use_cftime: bool): 

146 """ 

147 Return times as np.datetime64[ns] or cftime.DatetimeProlepticGregorian 

148 depending on whether the dates fall within the inclusive bounds of 

149 np.datetime64[ns]: [1678-01-01 AD, 2261-12-31 AD]. 

150 

151 Alternatively, always returns as cftime.DatetimeProlepticGregorian if 

152 ``use_cf_time`` is True. 

153 """ 

154 if all(time == "steady-state" for time in times): 

155 return times, False 

156 

157 out_of_bounds = False 

158 if use_cftime: 

159 converted = [ 

160 cftime.DatetimeProlepticGregorian(*time.timetuple()[:6]) for time in times 

161 ] 

162 else: 

163 for time in times: 

164 try: 

165 _check_year(time.year) 

166 except ValueError: 

167 out_of_bounds = True 

168 break 

169 

170 if out_of_bounds: 

171 use_cftime = True 

172 msg = "Dates are outside of np.datetime64[ns] timespan. Converting to cftime.DatetimeProlepticGregorian." 

173 warnings.warn(msg) 

174 converted = [ 

175 cftime.DatetimeProlepticGregorian(*time.timetuple()[:6]) 

176 for time in times 

177 ] 

178 else: 

179 converted = [np.datetime64(time, "ns") for time in times] 

180 

181 return converted, use_cftime 

182 

183 

184def _compose_timestring( 

185 time: np.datetime64 | cftime.datetime, time_format: str = "%Y%m%d%H%M%S" 

186) -> str: 

187 """ 

188 Compose timestring from time. Function takes care of different 

189 types of available time objects. 

190 """ 

191 if time == "steady-state": 

192 return time 

193 else: 

194 if isinstance(time, np.datetime64): 

195 # The following line is because numpy.datetime64[ns] does not 

196 # support converting to datetime, but returns an integer instead. 

197 # This solution is 20 times faster than using pd.to_datetime() 

198 return time.astype("datetime64[us]").item().strftime(time_format) 

199 else: 

200 return time.strftime(time_format)