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,235 @@
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
+ """Declare a generic **Solver** class and solver implementations"""
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ from abc import ABC, abstractmethod
33
+ from dataclasses import dataclass
34
+
35
+ import numpy as np
36
+ from numpy.typing import NDArray
37
+
38
+ from physioblocks.computing.assembling import EqSystem
39
+ from physioblocks.registers.type_register import register_type
40
+ from physioblocks.simulation.state import State
41
+ from physioblocks.utils.exceptions_utils import log_exception
42
+
43
+ _logger = logging.getLogger(__name__)
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class Solution:
48
+ """
49
+ Represent the solution return by a solver.
50
+ """
51
+
52
+ x: NDArray[np.float64]
53
+ """the actual solution"""
54
+
55
+ converged: bool
56
+ """get if the solver converged."""
57
+
58
+
59
+ class ConvergenceError(Exception):
60
+ """
61
+ Error raised when the solver did not converged.
62
+ """
63
+
64
+
65
+ class AbstractSolver(ABC):
66
+ """
67
+ Base class for solvers.
68
+ """
69
+
70
+ iteration_max: int
71
+ """the solver maximum allowed number of iterations"""
72
+
73
+ tolerance: float
74
+ """the solver tolerance"""
75
+
76
+ def __init__(
77
+ self,
78
+ tolerance: float = 1e-9,
79
+ iteration_max: int = 10,
80
+ ) -> None:
81
+ self.tolerance = tolerance
82
+ self.iteration_max = iteration_max
83
+
84
+ def _get_state_magnitude(
85
+ self, state: State, magnitudes: dict[str, float] | None = None
86
+ ) -> NDArray[np.float64]:
87
+ if magnitudes is None:
88
+ return np.ones(
89
+ state.size,
90
+ )
91
+
92
+ mag_dict = {}
93
+ for var_mag_key, var_mag_value in magnitudes.items():
94
+ var_index = state.get_variable_index(var_mag_key)
95
+ mag_dict[var_index] = var_mag_value
96
+ sorted_mag = sorted(mag_dict.items())
97
+ state_mag_list = [x[1] for x in sorted_mag]
98
+ return np.array(
99
+ state_mag_list,
100
+ )
101
+
102
+ @abstractmethod
103
+ def solve(
104
+ self,
105
+ state: State,
106
+ system: EqSystem,
107
+ magnitudes: dict[str, float] | None = None,
108
+ ) -> Solution:
109
+ """
110
+ Child classes have to override this method
111
+
112
+ :return: the solution of the solver
113
+ :rtype: _Array
114
+ """
115
+
116
+
117
+ # Type id for the Newton Solver
118
+ NEWTON_SOLVER_TYPE_ID = "newton_solver"
119
+
120
+
121
+ @register_type(NEWTON_SOLVER_TYPE_ID)
122
+ class NewtonSolver(AbstractSolver):
123
+ """
124
+ Implementation of the :class:`~.AbstractSolver` class using a **Newton method**.
125
+ """
126
+
127
+ def _compute_residual_and_gradient(
128
+ self, system: EqSystem
129
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
130
+ res = system.compute_residual()
131
+ grad = system.compute_gradient()
132
+ return res, grad
133
+
134
+ def _compute_new_state(
135
+ self,
136
+ state: State,
137
+ res: NDArray[np.float64],
138
+ grad: NDArray[np.float64],
139
+ state_mag: NDArray[np.float64],
140
+ ) -> NDArray[np.float64]:
141
+ res_grad_sol = np.linalg.solve(grad, res)
142
+ x = state.state_vector - res_grad_sol * state_mag
143
+ return x
144
+
145
+ def _compute_res_grad_mag(
146
+ self, gradient: NDArray[np.float64], state_mag: NDArray[np.float64]
147
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
148
+ state_mag_line = np.atleast_2d(state_mag)
149
+ state_mag_col = state_mag_line.T
150
+
151
+ res_mag = gradient @ state_mag_col
152
+ abs_res_mag = np.abs(res_mag)
153
+ res_mag_inv = 1.0 / abs_res_mag
154
+
155
+ grad_mag_inv = res_mag_inv @ state_mag_line
156
+ return res_mag_inv.flatten(), grad_mag_inv
157
+
158
+ def _rescale_res_grad(
159
+ self,
160
+ residual: NDArray[np.float64],
161
+ res_mag_inv: NDArray[np.float64],
162
+ gradient: NDArray[np.float64],
163
+ grad_mag_inv: NDArray[np.float64],
164
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
165
+ res_rescaled = residual * res_mag_inv
166
+ grad_rescaled = gradient * grad_mag_inv
167
+ return res_rescaled, grad_rescaled
168
+
169
+ def solve(
170
+ self,
171
+ state: State,
172
+ system: EqSystem,
173
+ magnitudes: dict[str, float] | None = None,
174
+ ) -> Solution:
175
+ """
176
+ Solve the equation system using the Newton method.
177
+
178
+ :return: the solution
179
+ :rtype: Solution
180
+ """
181
+
182
+ with np.errstate(all="raise"):
183
+ try:
184
+ i = 0
185
+ # initialize residual and magnitude
186
+ state_mag = self._get_state_magnitude(state, magnitudes)
187
+ res = np.ones(state.state_vector.shape)
188
+
189
+ # step 0 outside ou the loop to compute the residual and gradient
190
+ # magnitude
191
+ res, grad = self._compute_residual_and_gradient(system)
192
+ res_mag_inv, grad_mag_inv = self._compute_res_grad_mag(grad, state_mag)
193
+ res, grad = self._rescale_res_grad(res, res_mag_inv, grad, grad_mag_inv)
194
+ x = self._compute_new_state(state, res, grad, state_mag)
195
+ state.update_state_vector(x)
196
+
197
+ # Begin loop at iteration 1 (0 already done)
198
+ i = 1
199
+ while (
200
+ np.linalg.norm(res, ord=np.inf) > self.tolerance
201
+ and i < self.iteration_max
202
+ ):
203
+ res, grad = self._compute_residual_and_gradient(system)
204
+ res, grad = self._rescale_res_grad(
205
+ res, res_mag_inv, grad, grad_mag_inv
206
+ )
207
+ x = self._compute_new_state(state, res, grad, state_mag)
208
+ state.update_state_vector(x)
209
+ i += 1
210
+
211
+ sol = Solution(
212
+ state.state_vector,
213
+ (
214
+ bool(np.linalg.norm(res) <= self.tolerance)
215
+ and (True in np.isnan(x) or True in np.isinf(x)) is False
216
+ ),
217
+ )
218
+ except FloatingPointError as exception:
219
+ _logger.debug(
220
+ str.format(
221
+ "Solver did not converge at step {0} due to floating "
222
+ "point error. The solved property is set to False.",
223
+ i,
224
+ )
225
+ )
226
+ log_exception(
227
+ _logger,
228
+ FloatingPointError,
229
+ exception,
230
+ exception.__traceback__,
231
+ logging.DEBUG,
232
+ )
233
+ return Solution(np.empty(state.size), False)
234
+
235
+ return sol
@@ -0,0 +1,340 @@
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 the **State** that holds simulation variables.
29
+ """
30
+
31
+ from collections.abc import Callable, Generator, Mapping
32
+ from pprint import pformat
33
+ from typing import Any
34
+
35
+ import numpy as np
36
+ from numpy.typing import NDArray
37
+
38
+ from physioblocks.computing.quantities import Quantity
39
+
40
+ # Constant to identity the state in simulation
41
+ STATE_NAME_ID = "state"
42
+
43
+
44
+ class State:
45
+ """
46
+ The **State** holds the variables names, quantities and indexes during the
47
+ simulation.
48
+
49
+ Variables quantity values can be accessed individually with their names or index,
50
+ or altogether throught the **State Vector**.
51
+ """
52
+
53
+ _variables: dict[str, Quantity[Any]]
54
+ """The variables ids and quantities values"""
55
+
56
+ def __init__(self) -> None:
57
+ self._variables = {}
58
+
59
+ @property
60
+ def size(self) -> int:
61
+ """
62
+ Get the total size of the state.
63
+
64
+ :return: the size of the state
65
+ :rtype: int
66
+ """
67
+ return sum([var_qty.size for var_qty in self._variables.values()])
68
+
69
+ @property
70
+ def variables(self) -> dict[str, Quantity[Any]]:
71
+ """
72
+ Get a mapping of variables names and quantities.
73
+
74
+ :return: the variables names and quantities.
75
+ :rtype: dict[str, Quantity]
76
+ """
77
+ return self._variables.copy()
78
+
79
+ @property
80
+ def state_vector(self) -> NDArray[np.float64]:
81
+ """
82
+ Get the vector of the ``new`` values of the state variable quantities.
83
+
84
+ :return: the state vector
85
+ :rtype: NDArray[np.float64]
86
+ """
87
+ if len(self._variables) > 0:
88
+ return np.concatenate(
89
+ [var_qty.new for var_qty in self._variables.values()], axis=None
90
+ )
91
+ else:
92
+ return np.array([])
93
+
94
+ def __array__(self) -> NDArray[Any]:
95
+ return self.state_vector
96
+
97
+ def __getitem__(self, var_id: str) -> Quantity[Any]:
98
+ """
99
+ Get the variable quantity.
100
+
101
+ :param var_id: the variable id.
102
+ :type var_id: str
103
+
104
+ :return: the variable quantity
105
+ :rtype: Quantity
106
+ """
107
+ if var_id in self._variables:
108
+ return self._variables[var_id]
109
+
110
+ raise KeyError(str.format("State has no variable variable named {0}.", var_id))
111
+
112
+ def get(self, key: str) -> Quantity[Any] | None:
113
+ """
114
+ Get the variable quantity with the given key,
115
+ or ``None`` if it is not registered.
116
+
117
+ :param key: the variable key
118
+ :type key: str
119
+
120
+ :return: the variable or None
121
+ :rtype: Quantity | None
122
+ """
123
+ return self._variables.get(key)
124
+
125
+ def update(self, mapping: Mapping[str, Any]) -> None:
126
+ """
127
+ Update the state variable quantities with the values provided in the
128
+ mapping.
129
+
130
+ .. note::
131
+
132
+ New variables in the mapping are added to the state while existing
133
+ variables quantities are initialised to the given value.
134
+
135
+ :param mapping: the values to update the state.
136
+ :type mapping: str
137
+ """
138
+ for key, val in mapping.items():
139
+ self.__setitem__(key, val)
140
+
141
+ def __setitem__(self, var_id: str, value: Any) -> None:
142
+ """
143
+ Set the variable quantity value.
144
+
145
+ :param var_id: the variable name.
146
+ :type var_id: str
147
+
148
+ :param value: the variable quant.
149
+ :type var_id: Quantity
150
+
151
+ :raises ValueError: Raises a ValueError if the value is not a Quantity
152
+ or the quantity size is incorrect.
153
+ """
154
+
155
+ if var_id not in self._variables:
156
+ self.add_variable(var_id, value)
157
+
158
+ if var_id in self._variables:
159
+ if np.asarray(value).size != self._variables[var_id].size:
160
+ raise ValueError(
161
+ str.format(
162
+ "Expected size {0} for variable {1}, got {2}.",
163
+ self._variables[var_id].size,
164
+ var_id,
165
+ np.asarray(value).size,
166
+ )
167
+ )
168
+ else:
169
+ self._variables[var_id].initialize(value)
170
+
171
+ def __str__(self) -> str:
172
+ state_dict: dict[str, Any] = {}
173
+ state_dict["Variables"] = {
174
+ self.get_variable_index(var_id): (var_id, "size " + str(var_qty.size))
175
+ for var_id, var_qty in self._variables.items()
176
+ }
177
+ return pformat(state_dict, indent=2, compact=False)
178
+
179
+ @property
180
+ def indexes(self) -> dict[str, int]:
181
+ """
182
+ Get a mapping of the variables indexes with their names.
183
+
184
+ :return: the variables indexes ordered by variables ids
185
+ :rtype: dict[str, int]
186
+ """
187
+ return {var_id: self.get_variable_index(var_id) for var_id in self._variables}
188
+
189
+ def get_variable_index(self, variable_id: str) -> int:
190
+ """
191
+ Get the index of the variable with the given name
192
+
193
+ :param variable_id: the variable id
194
+ :rtype: str
195
+
196
+ :return: the variable index
197
+ :rtype: int
198
+ """
199
+
200
+ index = 0
201
+ for key, value in self._variables.items():
202
+ if variable_id == key:
203
+ return index
204
+ else:
205
+ index += value.size
206
+
207
+ raise KeyError(
208
+ str.format("State has no variable variable named {0}.", variable_id)
209
+ )
210
+
211
+ def get_variable_size(self, var_id: str) -> int:
212
+ """
213
+ Get the size of the variable with the given name.
214
+
215
+ :param var_id: the variable id
216
+ :rtype: str
217
+
218
+ :return: the size of the variable
219
+ :rtype: int
220
+ """
221
+ return self._variables[var_id].size
222
+
223
+ def get_variable_id(self, var_index: int) -> str:
224
+ """
225
+ Get the variable name with the given index.
226
+
227
+ :param var_index: the variable index
228
+ :rtype: int
229
+
230
+ :return: the variable id
231
+ :rtype: str
232
+ """
233
+ index = 0
234
+ var_id_iterator = (var_id for var_id in self._variables)
235
+ var_id = next(var_id_iterator, None)
236
+
237
+ while index != var_index and var_id is not None:
238
+ index += self._variables[var_id].size
239
+ var_id = next(var_id_iterator, None)
240
+
241
+ if var_id is not None:
242
+ return var_id
243
+
244
+ raise KeyError(str.format("No variable at index {0}", var_index))
245
+
246
+ def __iter__(self) -> Generator[str, None, None]:
247
+ """
248
+ Iterate on the variables names in the state.
249
+
250
+ :return: the variable ids
251
+ :rtype: str
252
+ """
253
+ yield from self._variables
254
+
255
+ def __contains__(self, key: str) -> bool:
256
+ """
257
+ Checks if the key is in the variables names.
258
+
259
+ :param key: The key to test
260
+ :rtype: Any
261
+ """
262
+ return key in self._variables
263
+
264
+ def update_state_vector(self, x: NDArray[np.float64]) -> None:
265
+ """
266
+ Update the ``new`` values of the state vector quantities,
267
+ with the given vector.
268
+
269
+ :param x: the vector to set.
270
+ :type x: NDArray[np.float64]
271
+
272
+ :raise ValueError:
273
+ Raise a ValueError when x and the state vector sizes don't match.
274
+ """
275
+ self.__change_state_vector(Quantity.update, x)
276
+
277
+ def reset_state_vector(self) -> None:
278
+ """
279
+ Set the new values to the current value of the state vector quantities.
280
+ """
281
+ for variable in self._variables.values():
282
+ variable.initialize(variable.current)
283
+
284
+ def set_state_vector(self, x: NDArray[np.float64]) -> None:
285
+ """
286
+ Set the ``new`` and ``current`` values of the state vector quantities
287
+ with the given vector.
288
+
289
+ :param x: the vector to set.
290
+ :type x: NDArray[np.float64]
291
+
292
+ :raise ValueError:
293
+ Raise a ValueError when x and the state vector sizes don't match.
294
+ """
295
+ self.__change_state_vector(Quantity.initialize, x)
296
+
297
+ def __change_state_vector(
298
+ self, func: Callable[[Quantity[Any], Any], None], x: NDArray[np.float64]
299
+ ) -> None:
300
+ # Checks x and state vector have the same size.
301
+ if x.size != self.size:
302
+ raise ValueError(str.format("State vector size does not match state size."))
303
+ indexes = self.indexes
304
+ for var_id, quantity in self._variables.items():
305
+ var_index = indexes[var_id]
306
+ if quantity.size == 1:
307
+ # assign scalar value
308
+ func(quantity, x[var_index])
309
+ else:
310
+ # assign vector value
311
+ quantity_part = x[var_index : var_index + quantity.size]
312
+ func(quantity, quantity_part)
313
+
314
+ def add_variable(self, var_id: str, var_value: Any) -> None:
315
+ """
316
+ Add a variable to the state.
317
+
318
+ :param var_id: the name of the variable
319
+ :type var_id: str
320
+
321
+ :param value: the initial value of the variable.
322
+ :type size: int
323
+ """
324
+
325
+ if var_id in self:
326
+ raise KeyError(str.format("{0} is already registered.", var_id))
327
+
328
+ quantity = var_value if isinstance(var_value, Quantity) else Quantity(var_value)
329
+ self._variables[var_id] = quantity
330
+
331
+ def remove_variable(self, var_id: str) -> None:
332
+ """
333
+ Remove a variable from the state
334
+
335
+ :param var_id: the name of the variable to remove.
336
+ :type var_id: str
337
+ """
338
+ # remove the variable
339
+ if var_id in self._variables:
340
+ self._variables.pop(var_id)