Coverage for physioblocks / simulation / time_manager.py: 97%

103 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-09 16:40 +0100

1# SPDX-FileCopyrightText: Copyright INRIA 

2# 

3# SPDX-License-Identifier: LGPL-3.0-only 

4# 

5# Copyright INRIA 

6# 

7# This file is part of PhysioBlocks, a library mostly developed by the 

8# [Ananke project-team](https://team.inria.fr/ananke) at INRIA. 

9# 

10# Authors: 

11# - Colin Drieu 

12# - Dominique Chapelle 

13# - François Kimmig 

14# - Philippe Moireau 

15# 

16# PhysioBlocks is free software: you can redistribute it and/or modify it under the 

17# terms of the GNU Lesser General Public License as published by the Free Software 

18# Foundation, version 3 of the License. 

19# 

20# PhysioBlocks is distributed in the hope that it will be useful, but WITHOUT ANY 

21# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 

22# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 

23# 

24# You should have received a copy of the GNU Lesser General Public License along with 

25# PhysioBlocks. If not, see <https://www.gnu.org/licenses/>. 

26 

27""" 

28Defines a Time object, and a manager to hold the simulation time. 

29""" 

30 

31from typing import Any 

32 

33import numpy as np 

34 

35from physioblocks.computing.quantities import Quantity 

36from physioblocks.registers.type_register import register_type 

37 

38# Constant for the time quantity id in the simulation 

39TIME_QUANTITY_ID = "time" 

40 

41_DEFAULT_STEP = 0.001 

42_DEFAULT_MIN_STEP = _DEFAULT_STEP / 16.0 

43 

44 

45class Time(Quantity[np.float64]): 

46 """ 

47 Extend :class:`~physioblocks.computing.quantities.Quantity` class to define 

48 simulation time. 

49 

50 Whenever the time is updated, it recomputes the difference between the new 

51 and the current value and its inverse (accessibles trough the ``dt`` and ``inv_dt`` 

52 properties) 

53 """ 

54 

55 _dt: float 

56 """Difference between ``new`` and ``current`` values""" 

57 

58 _inv_dt: float 

59 """Inverse of ``dt``""" 

60 

61 def __init__(self, value: float): 

62 """ 

63 Initialize ``dt`` and ``inv_dt`` to 0. 

64 

65 :param value: the initial time value 

66 :type value: float 

67 """ 

68 super().__init__(value) 

69 self._dt = 0.0 

70 self._inv_dt = 0.0 

71 

72 @property 

73 def dt(self) -> float: 

74 """ 

75 Get the difference between the ``new`` and the ``current`` value of the time. 

76 

77 :return: the delta time value 

78 :rtype: float 

79 """ 

80 return self._dt 

81 

82 @property 

83 def inv_dt(self) -> float: 

84 """ 

85 Get the inverse of ``dt``. 

86 

87 :return: the delta time inverse 

88 :rtype: float 

89 """ 

90 return self._inv_dt 

91 

92 def update(self, new: Any) -> None: 

93 """ 

94 Update the ``new`` value of the time and recomputes ``dt`` and ``inv_dt``. 

95 

96 .. note:: 

97 

98 If ``dt`` is 0.0, ``inv_dt`` is set to 0.0. 

99 

100 :param new: the new value to set 

101 :type new: float 

102 """ 

103 super().update(new) 

104 

105 self._dt = self._new - self._current 

106 if self._dt != 0.0: 

107 self._inv_dt = 1.0 / self._dt 

108 else: 

109 self._inv_dt = 0.0 

110 

111 def initialize(self, value: Any) -> None: 

112 """ 

113 Initialize the ``new`` and ``current`` value. 

114 

115 .. note:: 

116 

117 It set both ``dt`` and ``inv_dt`` to 0. 

118 

119 :param value: the value to set 

120 :type new: Any 

121 """ 

122 super().initialize(value) 

123 self._dt = 0.0 

124 self._inv_dt = 0.0 

125 

126 

127# Constant for time manager type 

128TIME_MANAGER_ID = "time" 

129 

130 

131@register_type(TIME_MANAGER_ID) 

132class TimeManager: 

133 """ 

134 Updates the time value during the simulation. 

135 

136 :param start: start value for the simulation time. 

137 :type start: float 

138 

139 :param end: end value for the simulation time. 

140 The time ``new`` and ``current`` value can not exceed it. 

141 :type end: float 

142 

143 :param time_step: time increment when update time is called. 

144 :type time_step: float 

145 

146 :param min_step: minimum allowed value of the time increment. 

147 :type time_step: float 

148 

149 :raise ValueError: ValueError is raised when : 

150 * the start parameter is superior to the end parameter. 

151 * the time_step parameter is negative. 

152 

153 """ 

154 

155 _time: Time 

156 """Store the current time value""" 

157 

158 start: float 

159 """The starting time of the time manager.""" 

160 

161 def __init__( 

162 self, 

163 start: float = 0.0, 

164 duration: float = _DEFAULT_STEP, 

165 step_size: float = _DEFAULT_STEP, 

166 min_step: float = _DEFAULT_MIN_STEP, 

167 ): 

168 if min_step <= 0.0: 

169 raise ValueError( 

170 str.format( 

171 "Time Manager minimum time step value can not be 0 or negative", 

172 ) 

173 ) 

174 self._min_step = min_step 

175 self._time = Time(start) 

176 self.start = start 

177 self.duration = duration 

178 self.step_size = step_size 

179 self.current_step_size = step_size 

180 

181 @property 

182 def min_step(self) -> float: 

183 """ 

184 Get the minimum time step size allowed. 

185 

186 :return: the minimum time step size. 

187 :rtype: float 

188 """ 

189 return self._min_step 

190 

191 @min_step.setter 

192 def min_step(self, value: float) -> None: 

193 """ 

194 Set the start time. 

195 

196 :param value: the start time to set 

197 :type value: float 

198 

199 :raise ValueError: Raises a ValueError if the given value is greater than the 

200 end time. 

201 """ 

202 

203 if value <= 0.0: 

204 raise ValueError( 

205 str.format( 

206 "Time Manager minimum time step value can not be 0 or negative", 

207 ) 

208 ) 

209 elif value > self.step_size: 

210 raise ValueError( 

211 str.format( 

212 "Time Manager minimum step value can not be superior to time_step", 

213 ) 

214 ) 

215 self._min_step = value 

216 

217 @property 

218 def end(self) -> float: 

219 """ 

220 Get the current end time. 

221 

222 :return: the end time 

223 :rtype: float 

224 """ 

225 return self.start + self._duration 

226 

227 @property 

228 def duration(self) -> float: 

229 """ 

230 Get the duration. 

231 

232 :return: the duration 

233 :rtype: float 

234 """ 

235 return self._duration 

236 

237 @duration.setter 

238 def duration(self, value: float) -> None: 

239 """ 

240 Set the duration 

241 

242 :return: the duration 

243 :rtype: float 

244 """ 

245 if value <= 0.0: 

246 raise ValueError( 

247 str.format( 

248 "Time Manager duration value can not be 0 or negative", 

249 ) 

250 ) 

251 self._duration = value 

252 

253 @property 

254 def step_size(self) -> float: 

255 """ 

256 Get the standard step size 

257 

258 :return: the standard step size 

259 :rtype: float 

260 """ 

261 return self._max_step 

262 

263 @step_size.setter 

264 def step_size(self, value: float) -> None: 

265 """ 

266 Get the standard step size 

267 

268 :return: the standard step size 

269 :rtype: float 

270 """ 

271 if value <= 0.0: 

272 raise ValueError( 

273 str.format( 

274 "Time Manager time step value can not be 0 or negative", 

275 ) 

276 ) 

277 self._max_step = value 

278 self.current_step_size = value 

279 

280 @property 

281 def current_step_size(self) -> float: 

282 """ 

283 Get the current time step size. 

284 

285 :return: the time step 

286 :rtype: float 

287 """ 

288 return self._time_step 

289 

290 @current_step_size.setter 

291 def current_step_size(self, value: float) -> None: 

292 """ 

293 Set the current time step size. 

294 

295 :param value: the time step to set 

296 :type value: float 

297 

298 :raise ValueError: Raises a ValueError if the given value is 

299 less than min_step or greater than max_step. 

300 """ 

301 

302 if value < self.min_step: 

303 raise ValueError( 

304 "Time Manager time step value can not be inferior to minimum step", 

305 ) 

306 elif value > self._max_step: 

307 raise ValueError( 

308 "Time Manager time step value can not be superior to default step", 

309 ) 

310 

311 self._time_step = value 

312 

313 @property 

314 def time(self) -> Time: 

315 """ 

316 Get the current :class:`~.Time` quantity 

317 

318 :return: the time quantity 

319 :rtype: Time 

320 """ 

321 return self._time 

322 

323 @property 

324 def ended(self) -> bool: 

325 """ 

326 Get if the time has reached the end time. 

327 

328 :return: True if the new time value has reached the end time, False otherwise. 

329 :rtype: bool 

330 """ 

331 return bool(self._time.new >= self.end) 

332 

333 def update_time(self) -> None: 

334 """ 

335 Set the ``current`` time value to the ``new`` time value and update the ``new`` 

336 time value with the defined time step increment. 

337 

338 If the ``end`` value is reached, the ``new`` time value is set 

339 to the ``end`` value. 

340 """ 

341 updated_time = self._time.current + self._time_step 

342 if updated_time < self.end: 

343 self._time.initialize(self._time.new) 

344 self._time.update(self._time.current + self._time_step) 

345 else: 

346 # time manager reached end time 

347 self._time.initialize(self._time.new) 

348 self._time.update(self.end) 

349 

350 def initialize(self) -> None: 

351 """ 

352 Initialize the time with the ``start`` value. 

353 """ 

354 self._time.initialize(self.start)