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.
- physioblocks/__init__.py +37 -0
- physioblocks/base/__init__.py +27 -0
- physioblocks/base/operators.py +176 -0
- physioblocks/base/registers.py +108 -0
- physioblocks/computing/__init__.py +47 -0
- physioblocks/computing/assembling.py +291 -0
- physioblocks/computing/models.py +811 -0
- physioblocks/computing/quantities.py +354 -0
- physioblocks/configuration/__init__.py +38 -0
- physioblocks/configuration/aliases.py +203 -0
- physioblocks/configuration/base.py +123 -0
- physioblocks/configuration/computing/__init__.py +27 -0
- physioblocks/configuration/computing/quantities.py +56 -0
- physioblocks/configuration/constants.py +121 -0
- physioblocks/configuration/description/__init__.py +33 -0
- physioblocks/configuration/description/blocks.py +239 -0
- physioblocks/configuration/description/nets.py +155 -0
- physioblocks/configuration/functions.py +695 -0
- physioblocks/configuration/simulation/__init__.py +32 -0
- physioblocks/configuration/simulation/simulations.py +280 -0
- physioblocks/description/__init__.py +34 -0
- physioblocks/description/blocks.py +418 -0
- physioblocks/description/flux.py +157 -0
- physioblocks/description/nets.py +746 -0
- physioblocks/io/__init__.py +29 -0
- physioblocks/io/aliases.py +73 -0
- physioblocks/io/configuration.py +125 -0
- physioblocks/launcher/__main__.py +285 -0
- physioblocks/launcher/configuration.py +231 -0
- physioblocks/launcher/configure/__main__.py +99 -0
- physioblocks/launcher/constants.py +105 -0
- physioblocks/launcher/files.py +150 -0
- physioblocks/launcher/series.py +165 -0
- physioblocks/library/__init__.py +27 -0
- physioblocks/library/aliases/blocks/c_block.json +5 -0
- physioblocks/library/aliases/blocks/rc_block.json +5 -0
- physioblocks/library/aliases/blocks/rcr_block.json +5 -0
- physioblocks/library/aliases/blocks/spherical_cavity_block.json +5 -0
- physioblocks/library/aliases/blocks/valve_rl_block.json +5 -0
- physioblocks/library/aliases/flux/heart_flux_dof_couples.jsonc +4 -0
- physioblocks/library/aliases/model_components/active_law_macro_huxley_two_moments.json +5 -0
- physioblocks/library/aliases/model_components/rheology_fiber_additive.json +5 -0
- physioblocks/library/aliases/model_components/spherical_dynamics.json +5 -0
- physioblocks/library/aliases/model_components/velocity_law_hht.json +5 -0
- physioblocks/library/aliases/nets/circulation_alone_net.json +31 -0
- physioblocks/library/aliases/nets/spherical_heart_net.json +93 -0
- physioblocks/library/aliases/simulations/circulation_alone_forward_simulation.jsonc +55 -0
- physioblocks/library/aliases/simulations/default_forward_simulation.jsonc +7 -0
- physioblocks/library/aliases/simulations/default_time.jsonc +8 -0
- physioblocks/library/aliases/simulations/newton_method_solver.json +5 -0
- physioblocks/library/aliases/simulations/spherical_heart_forward_simulation.jsonc +157 -0
- physioblocks/library/aliases/simulations/spherical_heart_with_respiration_forward_simulation.jsonc +45 -0
- physioblocks/library/blocks/__init__.py +27 -0
- physioblocks/library/blocks/capacitances.py +516 -0
- physioblocks/library/blocks/cavity.py +192 -0
- physioblocks/library/blocks/valves.py +281 -0
- physioblocks/library/functions/__init__.py +27 -0
- physioblocks/library/functions/base_operations.py +129 -0
- physioblocks/library/functions/first_order.py +113 -0
- physioblocks/library/functions/piecewise.py +271 -0
- physioblocks/library/functions/trigonometric.py +78 -0
- physioblocks/library/functions/watchers.py +113 -0
- physioblocks/library/model_components/__init__.py +27 -0
- physioblocks/library/model_components/active_law.py +345 -0
- physioblocks/library/model_components/dynamics.py +986 -0
- physioblocks/library/model_components/rheology.py +160 -0
- physioblocks/library/model_components/velocity_law.py +169 -0
- physioblocks/references/circulation_alone_sim.jsonc +24 -0
- physioblocks/references/spherical_heart_respiration_sim.jsonc +33 -0
- physioblocks/references/spherical_heart_sim.jsonc +29 -0
- physioblocks/registers/__init__.py +32 -0
- physioblocks/registers/load_function_register.py +93 -0
- physioblocks/registers/save_function_register.py +106 -0
- physioblocks/registers/type_register.py +97 -0
- physioblocks/simulation/__init__.py +48 -0
- physioblocks/simulation/constants.py +30 -0
- physioblocks/simulation/functions.py +71 -0
- physioblocks/simulation/runtime.py +484 -0
- physioblocks/simulation/saved_quantities.py +129 -0
- physioblocks/simulation/setup.py +576 -0
- physioblocks/simulation/solvers.py +235 -0
- physioblocks/simulation/state.py +340 -0
- physioblocks/simulation/time_manager.py +354 -0
- physioblocks/utils/__init__.py +27 -0
- physioblocks/utils/dynamic_import_utils.py +150 -0
- physioblocks/utils/exceptions_utils.py +115 -0
- physioblocks/utils/gradient_test_utils.py +337 -0
- physioblocks/utils/math_utils.py +109 -0
- physioblocks-1.0.0.dist-info/METADATA +127 -0
- physioblocks-1.0.0.dist-info/RECORD +93 -0
- physioblocks-1.0.0.dist-info/WHEEL +4 -0
- physioblocks-1.0.0.dist-info/licenses/licenses/GPL-3.0-only.txt +674 -0
- 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)
|