physioblocks 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. physioblocks/__init__.py +37 -0
  2. physioblocks/base/__init__.py +27 -0
  3. physioblocks/base/operators.py +176 -0
  4. physioblocks/base/registers.py +108 -0
  5. physioblocks/computing/__init__.py +47 -0
  6. physioblocks/computing/assembling.py +291 -0
  7. physioblocks/computing/models.py +811 -0
  8. physioblocks/computing/quantities.py +354 -0
  9. physioblocks/configuration/__init__.py +38 -0
  10. physioblocks/configuration/aliases.py +203 -0
  11. physioblocks/configuration/base.py +123 -0
  12. physioblocks/configuration/computing/__init__.py +27 -0
  13. physioblocks/configuration/computing/quantities.py +56 -0
  14. physioblocks/configuration/constants.py +121 -0
  15. physioblocks/configuration/description/__init__.py +33 -0
  16. physioblocks/configuration/description/blocks.py +239 -0
  17. physioblocks/configuration/description/nets.py +155 -0
  18. physioblocks/configuration/functions.py +695 -0
  19. physioblocks/configuration/simulation/__init__.py +32 -0
  20. physioblocks/configuration/simulation/simulations.py +280 -0
  21. physioblocks/description/__init__.py +34 -0
  22. physioblocks/description/blocks.py +418 -0
  23. physioblocks/description/flux.py +157 -0
  24. physioblocks/description/nets.py +746 -0
  25. physioblocks/io/__init__.py +29 -0
  26. physioblocks/io/aliases.py +73 -0
  27. physioblocks/io/configuration.py +125 -0
  28. physioblocks/launcher/__main__.py +285 -0
  29. physioblocks/launcher/configuration.py +231 -0
  30. physioblocks/launcher/configure/__main__.py +99 -0
  31. physioblocks/launcher/constants.py +105 -0
  32. physioblocks/launcher/files.py +150 -0
  33. physioblocks/launcher/series.py +165 -0
  34. physioblocks/library/__init__.py +27 -0
  35. physioblocks/library/aliases/blocks/c_block.json +5 -0
  36. physioblocks/library/aliases/blocks/rc_block.json +5 -0
  37. physioblocks/library/aliases/blocks/rcr_block.json +5 -0
  38. physioblocks/library/aliases/blocks/spherical_cavity_block.json +5 -0
  39. physioblocks/library/aliases/blocks/valve_rl_block.json +5 -0
  40. physioblocks/library/aliases/flux/heart_flux_dof_couples.jsonc +4 -0
  41. physioblocks/library/aliases/model_components/active_law_macro_huxley_two_moments.json +5 -0
  42. physioblocks/library/aliases/model_components/rheology_fiber_additive.json +5 -0
  43. physioblocks/library/aliases/model_components/spherical_dynamics.json +5 -0
  44. physioblocks/library/aliases/model_components/velocity_law_hht.json +5 -0
  45. physioblocks/library/aliases/nets/circulation_alone_net.json +31 -0
  46. physioblocks/library/aliases/nets/spherical_heart_net.json +93 -0
  47. physioblocks/library/aliases/simulations/circulation_alone_forward_simulation.jsonc +55 -0
  48. physioblocks/library/aliases/simulations/default_forward_simulation.jsonc +7 -0
  49. physioblocks/library/aliases/simulations/default_time.jsonc +8 -0
  50. physioblocks/library/aliases/simulations/newton_method_solver.json +5 -0
  51. physioblocks/library/aliases/simulations/spherical_heart_forward_simulation.jsonc +157 -0
  52. physioblocks/library/aliases/simulations/spherical_heart_with_respiration_forward_simulation.jsonc +45 -0
  53. physioblocks/library/blocks/__init__.py +27 -0
  54. physioblocks/library/blocks/capacitances.py +516 -0
  55. physioblocks/library/blocks/cavity.py +192 -0
  56. physioblocks/library/blocks/valves.py +281 -0
  57. physioblocks/library/functions/__init__.py +27 -0
  58. physioblocks/library/functions/base_operations.py +129 -0
  59. physioblocks/library/functions/first_order.py +113 -0
  60. physioblocks/library/functions/piecewise.py +271 -0
  61. physioblocks/library/functions/trigonometric.py +78 -0
  62. physioblocks/library/functions/watchers.py +113 -0
  63. physioblocks/library/model_components/__init__.py +27 -0
  64. physioblocks/library/model_components/active_law.py +345 -0
  65. physioblocks/library/model_components/dynamics.py +986 -0
  66. physioblocks/library/model_components/rheology.py +160 -0
  67. physioblocks/library/model_components/velocity_law.py +169 -0
  68. physioblocks/references/circulation_alone_sim.jsonc +24 -0
  69. physioblocks/references/spherical_heart_respiration_sim.jsonc +33 -0
  70. physioblocks/references/spherical_heart_sim.jsonc +29 -0
  71. physioblocks/registers/__init__.py +32 -0
  72. physioblocks/registers/load_function_register.py +93 -0
  73. physioblocks/registers/save_function_register.py +106 -0
  74. physioblocks/registers/type_register.py +97 -0
  75. physioblocks/simulation/__init__.py +48 -0
  76. physioblocks/simulation/constants.py +30 -0
  77. physioblocks/simulation/functions.py +71 -0
  78. physioblocks/simulation/runtime.py +484 -0
  79. physioblocks/simulation/saved_quantities.py +129 -0
  80. physioblocks/simulation/setup.py +576 -0
  81. physioblocks/simulation/solvers.py +235 -0
  82. physioblocks/simulation/state.py +340 -0
  83. physioblocks/simulation/time_manager.py +354 -0
  84. physioblocks/utils/__init__.py +27 -0
  85. physioblocks/utils/dynamic_import_utils.py +150 -0
  86. physioblocks/utils/exceptions_utils.py +115 -0
  87. physioblocks/utils/gradient_test_utils.py +337 -0
  88. physioblocks/utils/math_utils.py +109 -0
  89. physioblocks-1.0.0.dist-info/METADATA +127 -0
  90. physioblocks-1.0.0.dist-info/RECORD +93 -0
  91. physioblocks-1.0.0.dist-info/WHEEL +4 -0
  92. physioblocks-1.0.0.dist-info/licenses/licenses/GPL-3.0-only.txt +674 -0
  93. physioblocks-1.0.0.dist-info/licenses/licenses/LGPL-3.0-only.txt +165 -0
@@ -0,0 +1,484 @@
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
+ """
28
+ Defines the **Simulation** classes that define how the simulations runs
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ from abc import ABC, abstractmethod
35
+ from collections.abc import Iterable
36
+ from typing import Any, TypeAlias
37
+
38
+ import numpy as np
39
+ from numpy.typing import NDArray
40
+
41
+ from physioblocks.computing.assembling import EqSystem
42
+ from physioblocks.computing.models import ModelComponent
43
+ from physioblocks.computing.quantities import Quantity
44
+ from physioblocks.registers.type_register import register_type
45
+ from physioblocks.simulation.functions import (
46
+ AbstractFunction,
47
+ is_state_function,
48
+ is_time_function,
49
+ )
50
+ from physioblocks.simulation.saved_quantities import SavedQuantities
51
+ from physioblocks.simulation.solvers import AbstractSolver, ConvergenceError
52
+ from physioblocks.simulation.state import STATE_NAME_ID, State
53
+ from physioblocks.simulation.time_manager import TIME_QUANTITY_ID, TimeManager
54
+ from physioblocks.utils.exceptions_utils import log_exception
55
+
56
+ Parameters: TypeAlias = dict[str, Quantity[Any]]
57
+ """Type alias for quantities collection"""
58
+
59
+ Result: TypeAlias = dict[str, np.float64 | NDArray[np.float64]]
60
+ """Type alias for a single result line"""
61
+
62
+ Results: TypeAlias = list[Result]
63
+ """Type alias for all the results of the simulation"""
64
+
65
+
66
+ _logger = logging.getLogger(__name__)
67
+
68
+
69
+ class AbstractSimulation(ABC):
70
+ """
71
+ Base class for **Simulations**
72
+
73
+ .. note:: Use a :class:`~physioblocks.simulation.setup.SimulationFactory` instance
74
+ to instanciate simulations.
75
+
76
+ :param factory: the factory that created the simulation instance.
77
+ :type factory: SimulationFactory
78
+
79
+ :param time_manager: the simulation time manager
80
+ :type time_manager: TimeManager
81
+
82
+ :param solver: the solver to use for simulation steps
83
+ :type solver: AbstractSolver
84
+
85
+ :param state: the simulation state
86
+ :type state: State
87
+
88
+ :param parameters: the simulations quantities for parameters.
89
+ :type parameters: Parameters
90
+
91
+ :param saved_quantities: the **Saved Quantities** register
92
+ :type saved_quantities: SavedQuantities
93
+
94
+ :param models: the mapping of used models with their names
95
+ :type models: ModelComponent
96
+
97
+ :param eq_system: the equation system to solve at each time step
98
+ :type eq_system: EqSystem
99
+
100
+ :param magnitudes: magnitude of the state variables
101
+ :type magnitudes: dict[str, float]
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ factory: Any,
107
+ time_manager: TimeManager,
108
+ state: State,
109
+ parameters: Parameters,
110
+ saved_quantities: SavedQuantities,
111
+ models: dict[str, ModelComponent],
112
+ solver: AbstractSolver,
113
+ eq_system: EqSystem,
114
+ magnitudes: dict[str, float] | None = None,
115
+ ):
116
+ self.factory = factory
117
+ self.state = state
118
+ self.parameters = parameters
119
+ self.saved_quantities = saved_quantities
120
+ self.models = models
121
+ self.time_manager = time_manager
122
+ self.solver = solver
123
+ self.eq_system = eq_system
124
+ if magnitudes is None:
125
+ magnitudes = {}
126
+ self.magnitudes = self._check_magnitudes(magnitudes, state)
127
+ self._timed_updates: dict[str, AbstractFunction] = {}
128
+ self._output_functions_updates: dict[str, AbstractFunction] = {}
129
+
130
+ @property
131
+ def update_functions(self) -> dict[str, AbstractFunction]:
132
+ """
133
+ Get all functions to update at each time step with their matching quantity
134
+ global name.
135
+
136
+ :return: the update functions
137
+ :rtype: dict[str, AbstractFunction]
138
+ """
139
+ return self._timed_updates.copy()
140
+
141
+ @property
142
+ def outputs_functions(self) -> dict[str, AbstractFunction]:
143
+ """
144
+ Get all functions that compute the additional output after a time step
145
+ with their matching output global names.
146
+
147
+ :return: the output functions
148
+ :rtype: dict[str, AbstractFunction]
149
+ """
150
+ return self._output_functions_updates.copy()
151
+
152
+ @property
153
+ def quantities(self) -> dict[str, Quantity[Any]]:
154
+ """
155
+ Get all the quantities in the simulation from the parameters, the state
156
+ and the time manager.
157
+
158
+ :return: a dictionary containing all the simulation quantities
159
+ :rtype: dict[str, Quantity]
160
+ """
161
+ quantities: dict[str, Quantity[Any]] = {
162
+ TIME_QUANTITY_ID: self.time_manager.time
163
+ }
164
+ quantities.update(self.parameters)
165
+ quantities.update(self.state.variables)
166
+
167
+ return quantities
168
+
169
+ def register_timed_parameter_update(
170
+ self, parameter_id: str, update_function: AbstractFunction
171
+ ) -> None:
172
+ """
173
+ Register a simulation function to update the parameters with the given global
174
+ name at each time step.
175
+
176
+ :param parameter_id: the global name of the parameter to update
177
+ :type parameter_id: str
178
+
179
+ :param update_function: the function to call to evaluate the parameter value
180
+ :type update_function: AbstractFunction
181
+ """
182
+
183
+ if parameter_id not in self.parameters:
184
+ raise KeyError(str.format("{0} not found in parameters", parameter_id))
185
+
186
+ if (
187
+ isinstance(update_function, AbstractFunction) is False
188
+ or is_time_function(update_function) is False
189
+ ):
190
+ raise TypeError(
191
+ str.format(
192
+ "{0} is not a time function",
193
+ type(update_function).__name__,
194
+ )
195
+ )
196
+
197
+ self._timed_updates[parameter_id] = update_function
198
+
199
+ def unregister_timed_parameter_update(self, parameter_id: str) -> None:
200
+ """
201
+ Unegister a simulation function from the timed updates.
202
+
203
+ :param parameter_id: the global name of the parameter to unregister.
204
+ :type parameter_id: str
205
+ """
206
+ self._timed_updates.pop(parameter_id)
207
+
208
+ def register_output_function(
209
+ self, output_id: str, update_function: AbstractFunction
210
+ ) -> None:
211
+ """
212
+ Register a function that is called to compute an additional output.
213
+
214
+ :param output_id: the global name of the output in the results
215
+ :type output_id: str
216
+
217
+ :param update_function: the function to compute the output
218
+ :type output_id: AbstractFunction
219
+
220
+ :raise ValueError: Raises a value error when the output id is already defined
221
+ in the results
222
+ """
223
+ if (
224
+ output_id in self._output_functions_updates
225
+ or output_id in self.saved_quantities
226
+ or output_id in self.state
227
+ ):
228
+ raise KeyError(str.format("Output {0} is already defined.", output_id))
229
+
230
+ if isinstance(update_function, AbstractFunction) is False:
231
+ raise TypeError(
232
+ str.format(
233
+ "{0} is not a valid output function",
234
+ type(update_function).__name__,
235
+ )
236
+ )
237
+
238
+ self._output_functions_updates[output_id] = update_function
239
+
240
+ def unregister_output_function(self, output_id: str) -> None:
241
+ """
242
+ Unregister a function from the outputs updates.
243
+
244
+ :param output_id: the global name of the output.
245
+ :type output_id: str
246
+ """
247
+ self._output_functions_updates.pop(output_id)
248
+
249
+ def _initialize(self) -> Results:
250
+ """Initialize the simulation with current parameters.
251
+
252
+ This method should be called when overriding the run method.
253
+ """
254
+ self._initial_state = self.state.state_vector
255
+ _initialize_models(self.models.values())
256
+
257
+ # save the initialization
258
+ results = [self._get_current_result()]
259
+
260
+ self.time_manager.initialize()
261
+ self.time_manager.update_time()
262
+
263
+ self.state.set_state_vector(self.state.state_vector)
264
+
265
+ return results
266
+
267
+ def _finalize(self) -> None:
268
+ """Terminate the simulation reinitializing state and time to initial values.
269
+
270
+ This method should be called when overriding the run method.
271
+ """
272
+ self.time_manager.time.initialize(self.time_manager.start)
273
+ self.state.set_state_vector(self._initial_state)
274
+
275
+ def _check_magnitudes(
276
+ self, magnitudes: dict[str, float], state: State
277
+ ) -> dict[str, float]:
278
+ checked_magnitudes = {}
279
+
280
+ for variable_id in state:
281
+ if variable_id not in magnitudes:
282
+ message = str.format(
283
+ "No magnitude initialized for variable {0}. Magnitude set to 1.0",
284
+ variable_id,
285
+ )
286
+ _logger.warning(message)
287
+ checked_magnitudes[variable_id] = 1.0
288
+
289
+ elif magnitudes[variable_id] == 0.0:
290
+ message = str.format(
291
+ "Magnitude for variable {0} is initialized to 0.0. "
292
+ "Replacing with 1.0",
293
+ variable_id,
294
+ )
295
+ _logger.warning(message)
296
+ checked_magnitudes[variable_id] = 1.0
297
+ else:
298
+ checked_magnitudes[variable_id] = magnitudes[variable_id]
299
+
300
+ return checked_magnitudes
301
+
302
+ @abstractmethod
303
+ def run(self) -> Results:
304
+ """
305
+ Run the simulation, this method should be implemented in child classes.
306
+
307
+ :return: the list of solution for each time step
308
+ :rtype: list[NDArray[float64]]
309
+ """
310
+
311
+ def _update_time(self) -> None:
312
+ """
313
+ Updates all the time triggered updatable parameters.
314
+ """
315
+ for param_id, func in self._timed_updates.items():
316
+ self.parameters[param_id].initialize(
317
+ func.eval(self.time_manager.time.current)
318
+ )
319
+ self.parameters[param_id].update(func.eval(self.time_manager.time.new))
320
+
321
+ def _get_current_result(self) -> Result:
322
+ result: Result = {}
323
+
324
+ result[TIME_QUANTITY_ID] = self.time_manager.time.current
325
+ result.update(
326
+ {var_id: qty.current for var_id, qty in self.state.variables.items()}
327
+ )
328
+
329
+ self.saved_quantities.update()
330
+ result.update(
331
+ {qty_id: qty.current for qty_id, qty in self.saved_quantities.items()}
332
+ )
333
+
334
+ for output_id, update_function in self._output_functions_updates.items():
335
+ arguments: dict[str, Any] = {}
336
+ if is_time_function(update_function):
337
+ arguments[TIME_QUANTITY_ID] = self.time_manager.time.current
338
+ if is_state_function(update_function):
339
+ arguments[STATE_NAME_ID] = self.state
340
+
341
+ result[output_id] = update_function.eval(**arguments)
342
+
343
+ return result
344
+
345
+
346
+ def _initialize_models(models: Iterable[ModelComponent]) -> None:
347
+ """
348
+ Initialize all provided models
349
+
350
+ :param blocks: the blocks to initialize
351
+ :type blocks: Iterable[Block]
352
+ """
353
+ for block in models:
354
+ block.initialize()
355
+
356
+
357
+ # Forward simulation type id
358
+ FORWARD_SIM_ID = "forward_simulation"
359
+
360
+
361
+ @register_type(FORWARD_SIM_ID)
362
+ class ForwardSimulation(AbstractSimulation):
363
+ """
364
+ Extend :class:`~.AbstractSimulation` class to define a **Forward Simulation**.
365
+
366
+ The forward simulation solve the **Equation System** at each time step using
367
+ the simulation **Solver**.
368
+
369
+ If the solver did not converge at a given time step, it breaks the current time
370
+ step into smaller steps and try again.
371
+ If it still do not converge, it recursivly breaks the current time steps again and
372
+ stops if the time step is under the minimum time step allowed by the time manager.
373
+
374
+ When finding a solution for a reduced time step, the simulation
375
+ then tries to solve for the remaining time interval in the current time step.
376
+
377
+ .. note::
378
+
379
+ When breaking a simulation step, the forward simulation still only provide a
380
+ result for the time step interval given to the time manager.
381
+
382
+ """
383
+
384
+ def run(self) -> Results:
385
+ """
386
+ Solve the system for each time steps.
387
+
388
+ :return: the list of solution for each time step
389
+ :rtype: list[NDArray[float64]]
390
+
391
+ :raise SimulationError: raise a Simulation Error holding the current results
392
+ if the simulation stops before reaching the end time.
393
+ """
394
+ # initialize the simulation and save the initial results
395
+ results = self._initialize()
396
+
397
+ try:
398
+ while self.time_manager.ended is False:
399
+ next_step = self.time_manager.time.new
400
+
401
+ self._update_time()
402
+
403
+ while (
404
+ np.abs(next_step - self.time_manager.time.current)
405
+ > self.time_manager.min_step
406
+ ):
407
+ self.state.reset_state_vector()
408
+
409
+ sol = self.solver.solve(self.state, self.eq_system, self.magnitudes)
410
+
411
+ if sol.converged is False:
412
+ inter_time = 0.5 * self.time_manager.current_step_size
413
+ if inter_time < self.time_manager.min_step:
414
+ raise ConvergenceError(
415
+ str.format(
416
+ "The solver did not converge at {0}s for minimal"
417
+ "time step {1}",
418
+ self.time_manager.time.current,
419
+ self.time_manager.min_step,
420
+ )
421
+ )
422
+
423
+ self.time_manager.current_step_size = inter_time
424
+ self.time_manager.time.update(
425
+ self.time_manager.time.current
426
+ + self.time_manager.current_step_size
427
+ )
428
+ else:
429
+ self.state.set_state_vector(sol.x)
430
+
431
+ self.time_manager.update_time()
432
+ if (
433
+ np.abs(next_step - self.time_manager.time.current)
434
+ >= self.time_manager.min_step
435
+ ):
436
+ self.time_manager.current_step_size = (
437
+ next_step - self.time_manager.time.current
438
+ )
439
+ self.time_manager.time.update(next_step)
440
+ else:
441
+ self.time_manager.time.initialize(next_step)
442
+ self.time_manager.current_step_size = (
443
+ self.time_manager.step_size
444
+ )
445
+ self.time_manager.time.update(
446
+ self.time_manager.time.current
447
+ + self.time_manager.current_step_size
448
+ )
449
+
450
+ self.state.set_state_vector(sol.x)
451
+ results.append(self._get_current_result())
452
+ except Exception as exception:
453
+ log_exception(
454
+ _logger,
455
+ type(exception),
456
+ exception,
457
+ exception.__traceback__,
458
+ logging.DEBUG,
459
+ )
460
+ raise SimulationError(
461
+ str.format(
462
+ "An error caused the simulation to stop prematurely",
463
+ intermediate_results=results,
464
+ ),
465
+ results,
466
+ ) from exception
467
+
468
+ self._finalize()
469
+ return results
470
+
471
+
472
+ class SimulationError(Exception):
473
+ """
474
+ Error raised when the simulation encounter a problem.
475
+ """
476
+
477
+ intermediate_results: Results
478
+ """Results obtained before the simulation error occured"""
479
+
480
+ def __init__(
481
+ self, message: str, intermediate_results: Results, *args: Any, **kwargs: Any
482
+ ) -> None:
483
+ super().__init__(message, *args, **kwargs)
484
+ self.intermediate_results = intermediate_results
@@ -0,0 +1,129 @@
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
+ """
28
+ Define a register to hold **SavedQuantities** during the simulation.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from collections.abc import Generator
34
+ from typing import Any
35
+
36
+ from physioblocks.computing.models import Expression, ModelComponent
37
+ from physioblocks.computing.quantities import Quantity
38
+
39
+
40
+ class SavedQuantities:
41
+ """
42
+ Register holding saved quantities.
43
+ """
44
+
45
+ _saved_quantities: dict[str, Quantity[Any]]
46
+ _quantities_expressions: dict[str, tuple[Expression, ModelComponent, int, int]]
47
+
48
+ def __init__(self) -> None:
49
+ self._saved_quantities = {}
50
+ self._quantities_expressions = {}
51
+
52
+ def __contains__(self, quantity_id: str) -> bool:
53
+ return quantity_id in self._saved_quantities
54
+
55
+ def __getitem__(self, quantity_id: str) -> Quantity[Any]:
56
+ """
57
+ Get the saved quantity
58
+
59
+ :param quantity_id: the quantity global name.
60
+ :type quantity_id: str
61
+
62
+ :return: the saved quantity
63
+ :rtype: Quantity
64
+ """
65
+ return self._saved_quantities[quantity_id]
66
+
67
+ def items(self) -> Generator[tuple[str, Quantity[Any]], None, None]:
68
+ yield from self._saved_quantities.items()
69
+
70
+ def values(self) -> Generator[Quantity[Any], None, None]:
71
+ yield from self._saved_quantities.values()
72
+
73
+ def __iter__(self) -> Generator[str, None, None]:
74
+ yield from self._saved_quantities
75
+
76
+ def update(self) -> None:
77
+ """
78
+ Update all saved quantities using their
79
+ :class:`~physioblocks.computing.models.Expression` object.
80
+ """
81
+ for quantity_id, (
82
+ expression,
83
+ model,
84
+ size,
85
+ index,
86
+ ) in self._quantities_expressions.items():
87
+ if size == 1:
88
+ self._saved_quantities[quantity_id].initialize(
89
+ expression.expr_func(model)
90
+ )
91
+ else:
92
+ self._saved_quantities[quantity_id].initialize(
93
+ expression.expr_func(model)[index : index + size] # type: ignore
94
+ )
95
+
96
+ def register(
97
+ self,
98
+ quantity_id: str,
99
+ expression: Expression,
100
+ model: ModelComponent,
101
+ size: int,
102
+ index: int,
103
+ ) -> None:
104
+ """
105
+ Register a **Saved Quantity** with its expression and model.
106
+
107
+ :param quantity_id: the global saved quantity name
108
+ :type quantity_id: str
109
+
110
+ :param expression: the expression to use for the quantity
111
+ :type expression: Expression
112
+
113
+ :param model: the model declaring the expression
114
+ :type model: ModelComponent
115
+ """
116
+ self._quantities_expressions[quantity_id] = (expression, model, size, index)
117
+ # initialise quantity to 0
118
+ init_value = [0.0] * size if size > 1 else 0.0
119
+ self._saved_quantities[quantity_id] = Quantity(init_value)
120
+
121
+ def unregister(self, quantity_id: str) -> None:
122
+ """
123
+ Unregister a saved quantity
124
+
125
+ :param quantity_id: the quantity global name to unregister
126
+ :type quantity_id: str
127
+ """
128
+ self._saved_quantities.pop(quantity_id)
129
+ self._quantities_expressions.pop(quantity_id)