Coverage for physioblocks / computing / quantities.py: 100%

73 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""" 

28Define **Quantities** to represent discretized numeric values along with some 

29convenience functions to handle them. 

30""" 

31 

32from collections.abc import Iterable 

33from typing import Any, Generic, TypeVar 

34 

35import numpy as np 

36from numpy.typing import NDArray 

37 

38from physioblocks.base.operators import AbstractBaseOperators 

39 

40T = TypeVar("T", np.float64, NDArray[np.float64]) 

41 

42 

43class Quantity(AbstractBaseOperators, Generic[T]): 

44 """ 

45 Represent discretized values by holding their current and new numeric values. 

46 

47 Examples 

48 ^^^^^^^^ 

49 

50 >>> scalar = Quantity(0.5) 

51 >>> scalar.current == scalar.new # 0.5 

52 

53 >>> vector = Quantity([0.1, 0.2, 0.3]) 

54 >>> vector.current == vector.new # [0.1, 0.2, 0.3] 

55 

56 It can also be initialized with numpy arrays 

57 

58 >>> Quantity(np.ones(3)) # [1, 1, 1] 

59 

60 Matrixes are flattened when initializing a quantity: it can only 

61 be a scalar or vector. 

62 

63 >>> Quantity(np.ones((2, 2))) # [1, 1, 1, 1] 

64 """ 

65 

66 _current: T 

67 """Current value (at step n) of the quantity""" 

68 

69 _new: T 

70 """New value (at step n + 1) of the quantity""" 

71 

72 def __init__(self, value: Any): 

73 """ 

74 Quantity Constructor 

75 

76 :param value: Initialization value for the Quantity 

77 :type value: Any 

78 """ 

79 if isinstance(value, Quantity): 

80 self.initialize(value.current) 

81 self.update(value.new) 

82 else: 

83 self.initialize(value) 

84 

85 @property 

86 def current(self) -> T: 

87 """ 

88 Get the current value (at step n) of the quantity 

89 

90 :return: The current value 

91 :rtype: np.float64 | NDArray[np.float64] 

92 """ 

93 return self._current 

94 

95 @property 

96 def new(self) -> T: 

97 """ 

98 Get the new value (at step n + 1) of the quantity 

99 

100 :return: The new value 

101 :rtype: np.float64 | NDArray[np.float64] 

102 """ 

103 return self._new 

104 

105 @property 

106 def size(self) -> int: 

107 """ 

108 Get the vector size of the quantity value (or 1 for scalars) 

109 

110 :return: The vector size of the quantity 

111 :rtype: int 

112 """ 

113 return self.current.size 

114 

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

116 """ 

117 Set the new value (at step n + 1) of the quantity 

118 

119 :param new: The new value 

120 :type new: Any 

121 

122 :raise ValueError: Exception raised if the new value and the quantity 

123 value are not the same size 

124 

125 Example 

126 ^^^^^^^ 

127 

128 .. code:: python 

129 

130 >>> q = Quantity(0.5) 

131 >>> q.current # 0.5 

132 >>> q.new # 0.5 

133 

134 >>> q.update(0.6) 

135 >>> q.current # 0.5 

136 >>> q.new # 0.6 

137 """ 

138 new_value = np.array(new) if isinstance(new, Iterable) else np.float64(new) 

139 

140 if new_value.size != self.size: 

141 raise ValueError("New value should be the same size as the current value") 

142 

143 self._new = new_value # type: ignore 

144 

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

146 """ 

147 Set the current and new value of the quantity 

148 

149 :param value: The value to set 

150 :type value: Any 

151 

152 Example 

153 ^^^^^^^ 

154 

155 .. code:: python 

156 

157 >>> q = Quantity(0.5) 

158 >>> q.update(0.6) 

159 >>> q.current # 0.5 

160 >>> q.new # 0.6 

161 

162 >>> q.initialize(0.1) 

163 >>> q.current # 0.1 

164 >>> q.new # 0.1 

165 """ 

166 if isinstance(value, Iterable): 

167 array_value = np.array(value, np.float64).flatten() 

168 if array_value.size != 0: 

169 self._current = array_value # type: ignore 

170 else: 

171 raise ValueError( 

172 str.format("Quantity size can not be 0. Got: {0}", array_value) 

173 ) 

174 else: 

175 self._current = np.float64(value) # type: ignore 

176 

177 self._new = self._current 

178 

179 # Base operators 

180 

181 @property 

182 def operation_value(self) -> T: 

183 """ 

184 Value used with base operators. 

185 

186 Direct operation are allowed and performed on the ``current`` value of the 

187 quantity. 

188 

189 :return: the current value 

190 :rtype: Any 

191 

192 

193 Example 

194 ^^^^^^^ 

195 

196 .. code:: python 

197 

198 >>> q1 = Quantity(1.0) 

199 >>> q2 = Quantity(2.0) 

200 >>> q1.update(1.5) 

201 

202 # Quantity sum is performed on current value: 

203 >>> q1 + q2 # 3.0 

204 

205 >>> q1.initialise(1.5) 

206 >>> q1 + q2 # 3.5 

207 

208 In place operators call ``initialise`` on the **Quantity** 

209 

210 Example 

211 ^^^^^^^ 

212 

213 .. code:: python 

214 

215 >>> q1 = Quantity(1.0) 

216 >>> q1 += 0.5 

217 >>> q1.current # 1.5 

218 >>> q1.new # 1.5 

219 

220 """ 

221 return self.current 

222 

223 # In place Operations 

224 

225 def __iadd__(self, other: Any) -> "Quantity[Any]": 

226 self.initialize(self.current + other) 

227 return self 

228 

229 def __isub__(self, other: Any) -> "Quantity[Any]": 

230 self.initialize(self.current - other) 

231 return self 

232 

233 def __imul__(self, other: Any) -> "Quantity[Any]": 

234 self.initialize(self.current * other) 

235 return self 

236 

237 def __imatmul__(self, other: Any) -> "Quantity[Any]": 

238 self.initialize(self.current @ other) 

239 return self 

240 

241 def __itruediv__(self, other: Any) -> "Quantity[Any]": 

242 self.initialize(self.current / other) 

243 return self 

244 

245 def __ifloordiv__(self, other: Any) -> "Quantity[Any]": 

246 self.initialize(self.current // other) 

247 return self 

248 

249 def __imod__(self, other: Any) -> "Quantity[Any]": 

250 self.initialize(self.current % other) 

251 return self 

252 

253 def __ipow__(self, other: Any) -> "Quantity[Any]": 

254 self.initialize(self.current**other) 

255 return self 

256 

257 

258def diff(q: Quantity[T]) -> T: 

259 """ 

260 Compute the difference between the new and the current value of the given 

261 **Quantity** 

262 

263 :param q: The quantity 

264 :type q: Quantity 

265 

266 :return: The difference betwen new and current for the quantity 

267 :rtype: np.float64 | NDArray[np.float64] 

268 

269 Examples 

270 ^^^^^^^^ 

271 

272 .. code:: python 

273 

274 >>> q = Quantity(1.0) 

275 >>> diff(q) # 0.0 

276 

277 >>> q.update(1.1) 

278 >>> diff(q) # 0.1 

279 

280 """ 

281 return q.new - q.current 

282 

283 

284def mid_alpha(q: Quantity[Any], scheme_time_shift: Any) -> Any: 

285 """ 

286 Compute the discretized value of the Quantity for a shifted time scheme. 

287 

288 .. math:: 

289 

290 x^{n+\\alpha} = (\\frac{1}{2} - \\alpha) x^{n} + (\\frac{1}{2} + 

291 \\alpha) x^{n + 1} 

292 

293 :param q: The quantity 

294 :type value: Quantity 

295 

296 :param scheme_time_shift: the time shift of the scheme. 

297 :type scheme_time_shift: float 

298 

299 :raise ValueError: If scheme_time_shift is not a scalar, 

300 exception raised if scheme_time_shift and q are not the same size. 

301 

302 :return: the discretized value of the quantity 

303 :rtype: np.float64 | NDArray[np.float64] 

304 

305 Example 

306 ^^^^^^^ 

307 

308 .. code:: python 

309 

310 >>> q = Quantity(1.0) 

311 >>> q.update(2.0) 

312 >>> mid_alpha(q, 0.25) # 1.75 

313 """ 

314 return (0.5 - scheme_time_shift) * q.current + (0.5 + scheme_time_shift) * q.new 

315 

316 

317def mid_point(q: Quantity[Any]) -> Any: 

318 """ 

319 Compute the discretized value of the Quantity for a mid point time scheme 

320 

321 .. math:: 

322 

323 x^{n + \\frac{1}{2}} = \\frac{1}{2} (x^{n} + x^{n + 1}) 

324 

325 :param q: Quantity 

326 :type q: Quantity 

327 

328 :return: Discretized value of the quantity 

329 :rtype: np.float64 | NDArray[np.float64] 

330 

331 Example 

332 ^^^^^^^ 

333 

334 .. code:: python 

335 

336 >>> q = Quantity(1.0) 

337 >>> q.update(2.0) 

338 >>> mid_point(q) # 1.5 

339 """ 

340 return 0.5 * (q.current + q.new) 

341 

342 

343def sign(q: Quantity[np.float64]) -> float: 

344 """ 

345 Get 1 if the new value is superior to current value, 

346 -1 othwerwise 

347 

348 :param q: the quantity 

349 :type q: Quantity 

350 

351 :return: 1 or -1 

352 :rtype: np.float64 

353 """ 

354 return -1.0 if diff(q) < 0.0 else 1.0