Coverage for physioblocks / configuration / simulation / simulations.py: 96%
97 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-09 16:40 +0100
« 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/>.
27"""
28Define the configuration of simulations
29"""
31from collections.abc import Iterable
32from os import linesep
33from typing import Any
35from physioblocks.configuration.base import Configuration, ConfigurationError
36from physioblocks.configuration.constants import (
37 INIT_VARIABLES_ID,
38 MAGNITUDES,
39 NET_ID,
40 OUTPUTS_FUNCTIONS_ID,
41 PARAMETERS_ID,
42 SOLVER_ID,
43 TIME_MANAGER_ID,
44 VARIABLES_MAGNITUDES,
45)
46from physioblocks.configuration.functions import load, save
47from physioblocks.registers.load_function_register import loads
48from physioblocks.registers.save_function_register import saves
49from physioblocks.registers.type_register import get_registered_type_id
50from physioblocks.simulation import AbstractFunction
51from physioblocks.simulation.functions import (
52 is_state_function,
53 is_time_function,
54)
55from physioblocks.simulation.runtime import AbstractSimulation
56from physioblocks.simulation.setup import SimulationFactory
57from physioblocks.simulation.state import STATE_NAME_ID
58from physioblocks.simulation.time_manager import TIME_QUANTITY_ID
61@loads(AbstractSimulation) # type: ignore
62def load_simulation_config(
63 config: Configuration,
64 configuration_type: type,
65 configuration_object: AbstractSimulation | None = None,
66 *args: Any,
67 **kwargs: Any,
68) -> AbstractSimulation:
69 """
70 Load a simulation from a configuration.
72 :param config: the configuration
73 :type config: Configuration
75 :return: the simulation
76 :rtype: AbstractSimulation
77 """
79 if configuration_object is None:
80 net = None
81 if NET_ID in config:
82 net = load(config[NET_ID])
84 # Solver
85 solver = None
86 if SOLVER_ID in config:
87 solver = load(config[SOLVER_ID])
89 # magnitudes
90 magnitudes = None
91 if VARIABLES_MAGNITUDES in config:
92 magnitudes = load(config[VARIABLES_MAGNITUDES])
94 sim_factory = SimulationFactory(
95 configuration_type,
96 solver,
97 net,
98 simulation_options={MAGNITUDES: magnitudes},
99 )
101 configuration_object = sim_factory.create_simulation()
103 _configure_simulation(config, configuration_object)
105 return configuration_object
108def _save_sim_factory(factory: SimulationFactory) -> Configuration:
109 simulation_type_id = get_registered_type_id(factory.simulation_type)
110 sim_factory_config_item = Configuration(simulation_type_id)
111 sim_factory_config_item[NET_ID] = save(factory.net)
113 return sim_factory_config_item
116@saves(AbstractSimulation)
117def save_simulation_config(
118 simulation: AbstractSimulation, *args: Any, **kwargs: Any
119) -> Configuration:
120 """
121 Save a simulation in a configuration.
123 :param sim: the simulation to save
124 :type sim: AbstractSimulation
126 :param simulation_type_id: the simulation type id
127 :type simulation_type_id: str
129 :return: the configuration
130 :rtype: Configuration
131 """
133 sim_config = _save_sim_factory(simulation.factory)
135 sim_config[TIME_MANAGER_ID] = save(simulation.time_manager)
136 sim_config[SOLVER_ID] = save(simulation.solver)
138 # State
139 variable_init_values = save(simulation.state.variables)
140 if isinstance(variable_init_values, dict):
141 sim_config[INIT_VARIABLES_ID] = variable_init_values
142 else:
143 raise ConfigurationError(
144 str.format(
145 "Expected a dict for {0} configuration, got {1}.",
146 INIT_VARIABLES_ID,
147 type(variable_init_values).__name__,
148 )
149 )
150 sim_config[VARIABLES_MAGNITUDES] = save(simulation.magnitudes)
152 # Parameters
153 # Get quantities
154 parameters: dict[str, Any] = {
155 key: qty
156 for key, qty in simulation.parameters.items()
157 if key not in simulation.update_functions
158 }
159 references: dict[str, Any] = simulation.models.copy()
160 parameters_config: dict[str, Any] = save(
161 parameters, configuration_references=references
162 )
163 references.update(parameters)
165 # update with fonctions
166 function_config = save(
167 simulation.update_functions, configuration_references=references
168 )
169 parameters_config.update(function_config)
170 sim_config[PARAMETERS_ID] = parameters_config
172 # All the quantities (with update function in the references)
173 references.update(simulation.parameters)
175 if len(simulation.outputs_functions) > 0:
176 sim_config[OUTPUTS_FUNCTIONS_ID] = save(
177 simulation.outputs_functions,
178 configuration_references=references,
179 )
181 return sim_config
184def _configure_simulation(
185 config: Configuration, simulation: AbstractSimulation
186) -> None:
187 # Set initial values
189 # ParameterRegister
190 _check_exising_config(PARAMETERS_ID, config)
191 _check_missing_keys(PARAMETERS_ID, config[PARAMETERS_ID], simulation.parameters)
193 constants_config = {
194 key: value
195 for key, value in config[PARAMETERS_ID].items()
196 if isinstance(value, Configuration) is False
197 }
198 functions_config = {
199 key: value
200 for key, value in config[PARAMETERS_ID].items()
201 if isinstance(value, Configuration) is True
202 }
204 # first load the constants
205 load(constants_config, configuration_object=simulation.parameters)
207 # then load functions
208 references: dict[str, Any] = simulation.quantities
209 references.update(simulation.models)
210 loaded_functions = load(
211 functions_config, configuration_references=references, configuration_sort=True
212 )
213 __initialize_functions(simulation, loaded_functions)
215 # Time Manager
216 _check_exising_config(TIME_MANAGER_ID, config)
217 load(config[TIME_MANAGER_ID], configuration_object=simulation.time_manager)
219 # State
220 _check_exising_config(INIT_VARIABLES_ID, config)
221 _check_missing_keys(INIT_VARIABLES_ID, config[INIT_VARIABLES_ID], simulation.state)
223 load(
224 config[INIT_VARIABLES_ID],
225 configuration_object=simulation.state,
226 configuration_references=simulation.quantities,
227 )
229 references.update(simulation.quantities)
230 references.update(simulation.models)
232 if OUTPUTS_FUNCTIONS_ID in config:
233 outputs = load(
234 config[OUTPUTS_FUNCTIONS_ID], configuration_references=references
235 )
236 __configure_outputs(simulation, outputs)
239def __initialize_functions(
240 sim: AbstractSimulation, functions_parameters: dict[str, AbstractFunction]
241) -> None:
242 for param_id, param_value in functions_parameters.items():
243 if param_id in sim.parameters:
244 arguments: dict[str, Any] = {}
245 if is_time_function(param_value):
246 sim.register_timed_parameter_update(param_id, param_value)
247 arguments[TIME_QUANTITY_ID] = sim.time_manager.time.current
248 if is_state_function(param_value):
249 arguments[STATE_NAME_ID] = sim.state
250 sim.parameters[param_id].initialize(param_value.eval(**arguments))
253def __configure_outputs(
254 sim: AbstractSimulation, config: dict[str, AbstractFunction]
255) -> None:
256 for output_id, update_func in config.items():
257 sim.register_output_function(output_id, update_func)
260def _check_exising_config(key: str, config: Configuration) -> None:
261 if key not in config:
262 raise ConfigurationError(
263 str.format(
264 "Missing key {0} in {1} configuration.",
265 key,
266 config.label,
267 )
268 )
271def _check_missing_keys(
272 name: str, tested: Iterable[str], reference: Iterable[str]
273) -> None:
274 missing_keys = [key for key in reference if key not in tested]
275 if len(missing_keys) > 0:
276 raise ConfigurationError(
277 str.format(
278 "Missing keys in {0} configuration:{2}{1}", name, missing_keys, linesep
279 )
280 )