pySimBlocks 0.1.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.
- examples/advanced/hardware/block_diagram.py +162 -0
- examples/advanced/hardware/camera.py +93 -0
- examples/advanced/hardware/gui_emio.py +231 -0
- examples/advanced/hardware/lmi.py +69 -0
- examples/advanced/hardware/main.py +57 -0
- examples/advanced/hardware/parameters.py +54 -0
- examples/advanced/qp_problem/params.py +10 -0
- examples/advanced/qp_problem/run.py +14 -0
- examples/advanced/sofa/gui/finger/Finger.py +108 -0
- examples/advanced/sofa/gui/finger/FingerController.py +53 -0
- examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
- examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
- examples/advanced/sofa/python/Finger.py +107 -0
- examples/advanced/sofa/python/FingerController.py +48 -0
- examples/advanced/sofa/python/closedloop.py +64 -0
- examples/advanced/sofa/python/openloop.py +58 -0
- examples/basics/algebraic_function/g.py +7 -0
- examples/basics/algebraic_function/run.py +14 -0
- examples/basics/dc_motor/gui/run.py +14 -0
- examples/basics/dc_motor/multiple_order.py +91 -0
- examples/basics/dc_motor/openloop.py +62 -0
- examples/basics/dc_motor/pi.py +99 -0
- examples/basics/dc_motor/pid.py +118 -0
- examples/basics/dc_motor/pid_block.py +82 -0
- examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
- examples/basics/external/parameters.py +4 -0
- examples/basics/external/run.py +14 -0
- examples/basics/file_sources/temp.py +16 -0
- examples/basics/observer.py +76 -0
- examples/basics/state_feedback/gui/parameters.py +9 -0
- examples/basics/state_feedback/gui/run.py +14 -0
- examples/basics/state_feedback/state_feedback.py +102 -0
- examples/basics/state_space/linear/gui/run.py +14 -0
- examples/basics/state_space/linear/ss_block.py +89 -0
- examples/basics/state_space/linear/ss_manual.py +105 -0
- examples/basics/state_space/non_linear/nlss.py +47 -0
- examples/basics/state_space/non_linear/run.py +14 -0
- examples/basics/state_space/polytope/params.py +39 -0
- examples/basics/state_space/polytope/weights.py +12 -0
- examples/comparison/simulink/convert.py +29 -0
- examples/comparison/simulink/openloop.py +127 -0
- examples/comparison/simulink/pi.py +148 -0
- examples/comparison/simulink/pid.py +154 -0
- examples/comparison/simulink/pid_backward.py +155 -0
- examples/quick_start/filter.py +35 -0
- examples/quick_start/gui/run.py +14 -0
- examples/tutorials/tutorial_1_python/main.py +99 -0
- examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
- examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
- pySimBlocks/__init__.py +35 -0
- pySimBlocks/blocks/__init__.py +68 -0
- pySimBlocks/blocks/controllers/__init__.py +27 -0
- pySimBlocks/blocks/controllers/pid.py +250 -0
- pySimBlocks/blocks/controllers/state_feedback.py +141 -0
- pySimBlocks/blocks/interfaces/__init__.py +27 -0
- pySimBlocks/blocks/interfaces/external_input.py +107 -0
- pySimBlocks/blocks/interfaces/external_output.py +113 -0
- pySimBlocks/blocks/observers/__init__.py +23 -0
- pySimBlocks/blocks/observers/luenberger.py +175 -0
- pySimBlocks/blocks/operators/__init__.py +47 -0
- pySimBlocks/blocks/operators/algebraic_function.py +258 -0
- pySimBlocks/blocks/operators/dead_zone.py +186 -0
- pySimBlocks/blocks/operators/delay.py +254 -0
- pySimBlocks/blocks/operators/demux.py +123 -0
- pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
- pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
- pySimBlocks/blocks/operators/gain.py +294 -0
- pySimBlocks/blocks/operators/mux.py +131 -0
- pySimBlocks/blocks/operators/product.py +194 -0
- pySimBlocks/blocks/operators/rate_limiter.py +214 -0
- pySimBlocks/blocks/operators/saturation.py +178 -0
- pySimBlocks/blocks/operators/sum.py +188 -0
- pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
- pySimBlocks/blocks/optimizers/__init__.py +23 -0
- pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
- pySimBlocks/blocks/sources/__init__.py +37 -0
- pySimBlocks/blocks/sources/chirp.py +168 -0
- pySimBlocks/blocks/sources/constant.py +82 -0
- pySimBlocks/blocks/sources/file_source.py +294 -0
- pySimBlocks/blocks/sources/function_source.py +197 -0
- pySimBlocks/blocks/sources/ramp.py +99 -0
- pySimBlocks/blocks/sources/sinusoidal.py +113 -0
- pySimBlocks/blocks/sources/step.py +138 -0
- pySimBlocks/blocks/sources/white_noise.py +107 -0
- pySimBlocks/blocks/systems/__init__.py +27 -0
- pySimBlocks/blocks/systems/linear_state_space.py +180 -0
- pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
- pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
- pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
- pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
- pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
- pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
- pySimBlocks/cli.py +127 -0
- pySimBlocks/core/__init__.py +32 -0
- pySimBlocks/core/block.py +167 -0
- pySimBlocks/core/block_source.py +94 -0
- pySimBlocks/core/config.py +90 -0
- pySimBlocks/core/fixed_time_manager.py +46 -0
- pySimBlocks/core/model.py +250 -0
- pySimBlocks/core/scheduler.py +30 -0
- pySimBlocks/core/simulator.py +329 -0
- pySimBlocks/core/task.py +73 -0
- pySimBlocks/docs/blocks/controllers/pid.md +78 -0
- pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
- pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
- pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
- pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
- pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
- pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
- pySimBlocks/docs/blocks/operators/delay.md +76 -0
- pySimBlocks/docs/blocks/operators/demux.md +64 -0
- pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
- pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
- pySimBlocks/docs/blocks/operators/gain.md +97 -0
- pySimBlocks/docs/blocks/operators/mux.md +64 -0
- pySimBlocks/docs/blocks/operators/product.md +51 -0
- pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
- pySimBlocks/docs/blocks/operators/saturation.md +62 -0
- pySimBlocks/docs/blocks/operators/sum.md +49 -0
- pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
- pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
- pySimBlocks/docs/blocks/sources/chirp.md +102 -0
- pySimBlocks/docs/blocks/sources/constant.md +53 -0
- pySimBlocks/docs/blocks/sources/file_source.md +51 -0
- pySimBlocks/docs/blocks/sources/function_source.md +52 -0
- pySimBlocks/docs/blocks/sources/ramp.md +58 -0
- pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
- pySimBlocks/docs/blocks/sources/step.md +63 -0
- pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
- pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
- pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
- pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
- pySimBlocks/gui/__init__.py +20 -0
- pySimBlocks/gui/addons/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
- pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
- pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
- pySimBlocks/gui/blocks/block_meta.py +408 -0
- pySimBlocks/gui/blocks/controllers/pid.py +116 -0
- pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
- pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
- pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
- pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
- pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
- pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
- pySimBlocks/gui/blocks/operators/delay.py +79 -0
- pySimBlocks/gui/blocks/operators/demux.py +98 -0
- pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
- pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
- pySimBlocks/gui/blocks/operators/gain.py +79 -0
- pySimBlocks/gui/blocks/operators/mux.py +100 -0
- pySimBlocks/gui/blocks/operators/product.py +109 -0
- pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
- pySimBlocks/gui/blocks/operators/saturation.py +72 -0
- pySimBlocks/gui/blocks/operators/sum.py +91 -0
- pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
- pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
- pySimBlocks/gui/blocks/parameter_meta.py +34 -0
- pySimBlocks/gui/blocks/port_meta.py +31 -0
- pySimBlocks/gui/blocks/sources/chirp.py +146 -0
- pySimBlocks/gui/blocks/sources/constant.py +63 -0
- pySimBlocks/gui/blocks/sources/file_source.py +196 -0
- pySimBlocks/gui/blocks/sources/function_source.py +202 -0
- pySimBlocks/gui/blocks/sources/ramp.py +73 -0
- pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
- pySimBlocks/gui/blocks/sources/step.py +83 -0
- pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
- pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
- pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
- pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
- pySimBlocks/gui/dialogs/__init__.py +19 -0
- pySimBlocks/gui/dialogs/block_dialog.py +125 -0
- pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
- pySimBlocks/gui/dialogs/help_dialog.py +39 -0
- pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
- pySimBlocks/gui/dialogs/settings/plots.py +187 -0
- pySimBlocks/gui/dialogs/settings/project.py +146 -0
- pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
- pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
- pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
- pySimBlocks/gui/editor.py +49 -0
- pySimBlocks/gui/graphics/__init__.py +29 -0
- pySimBlocks/gui/graphics/block_item.py +289 -0
- pySimBlocks/gui/graphics/connection_item.py +266 -0
- pySimBlocks/gui/graphics/port_item.py +153 -0
- pySimBlocks/gui/graphics/theme.py +119 -0
- pySimBlocks/gui/main_window.py +162 -0
- pySimBlocks/gui/models/__init__.py +31 -0
- pySimBlocks/gui/models/block_instance.py +114 -0
- pySimBlocks/gui/models/connection_instance.py +43 -0
- pySimBlocks/gui/models/port_instance.py +52 -0
- pySimBlocks/gui/models/project_simulation_params.py +50 -0
- pySimBlocks/gui/models/project_state.py +114 -0
- pySimBlocks/gui/project_controller.py +302 -0
- pySimBlocks/gui/services/__init__.py +20 -0
- pySimBlocks/gui/services/project_loader.py +381 -0
- pySimBlocks/gui/services/project_saver.py +68 -0
- pySimBlocks/gui/services/simulation_runner.py +66 -0
- pySimBlocks/gui/services/yaml_tools.py +261 -0
- pySimBlocks/gui/widgets/__init__.py +19 -0
- pySimBlocks/gui/widgets/block_list.py +131 -0
- pySimBlocks/gui/widgets/diagram_view.py +312 -0
- pySimBlocks/gui/widgets/toolbar_view.py +161 -0
- pySimBlocks/project/__init__.py +35 -0
- pySimBlocks/project/build_model.py +92 -0
- pySimBlocks/project/generate_run_script.py +109 -0
- pySimBlocks/project/generate_sofa_controller.py +215 -0
- pySimBlocks/project/load_project_config.py +197 -0
- pySimBlocks/project/load_simulation_config.py +166 -0
- pySimBlocks/project/load_simulator.py +45 -0
- pySimBlocks/project/plot_from_config.py +174 -0
- pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
- pySimBlocks/real_time/__init__.py +23 -0
- pySimBlocks/real_time/real_time_runner.py +148 -0
- pySimBlocks/tools/__init__.py +19 -0
- pySimBlocks/tools/blocks_registry.py +141 -0
- pySimBlocks/tools/generate_blocks_index.py +124 -0
- pysimblocks-0.1.0.dist-info/METADATA +180 -0
- pysimblocks-0.1.0.dist-info/RECORD +262 -0
- pysimblocks-0.1.0.dist-info/WHEEL +5 -0
- pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
- pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
- pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
- tests/blocks/controllers/test_pid.py +143 -0
- tests/blocks/controllers/test_state_feedback.py +193 -0
- tests/blocks/observers/test_luenberger.py +225 -0
- tests/blocks/operators/test_algebraic_function.py +230 -0
- tests/blocks/operators/test_dead_zone.py +106 -0
- tests/blocks/operators/test_delay.py +167 -0
- tests/blocks/operators/test_demux.py +111 -0
- tests/blocks/operators/test_discrete_derivator.py +160 -0
- tests/blocks/operators/test_discrete_integrator.py +149 -0
- tests/blocks/operators/test_gain.py +182 -0
- tests/blocks/operators/test_mux.py +101 -0
- tests/blocks/operators/test_product.py +171 -0
- tests/blocks/operators/test_rate_limiter.py +166 -0
- tests/blocks/operators/test_saturation.py +157 -0
- tests/blocks/operators/test_sum.py +183 -0
- tests/blocks/operators/test_zero_order_hold.py +106 -0
- tests/blocks/sources/test_chirp.py +157 -0
- tests/blocks/sources/test_constant.py +55 -0
- tests/blocks/sources/test_file_source.py +185 -0
- tests/blocks/sources/test_function_source.py +135 -0
- tests/blocks/sources/test_ramp.py +110 -0
- tests/blocks/sources/test_sinusoidal.py +113 -0
- tests/blocks/sources/test_step.py +76 -0
- tests/blocks/sources/test_white_noise.py +108 -0
- tests/blocks/systems/test_linear_state_space.py +135 -0
- tests/blocks/systems/test_polytopic_state_space.py +133 -0
- tests/core/test_core_errors_and_determinism.py +82 -0
- tests/core/test_core_multirate_tasks.py +88 -0
- tests/core/test_core_two_phase_and_propagation.py +132 -0
- tests/gui/main_window_open_test.py +162 -0
- tests/gui/test_block_meta_param_cache.py +16 -0
- tests/gui/test_file_source_meta.py +17 -0
- tests/gui/test_function_source_meta.py +28 -0
- tests/regression/test_simulink_dc_motor.py +217 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Dict, Any, List
|
|
23
|
+
import numpy as np
|
|
24
|
+
import Sofa
|
|
25
|
+
|
|
26
|
+
from pySimBlocks import Model, Simulator
|
|
27
|
+
from pySimBlocks.project.load_project_config import load_project_config
|
|
28
|
+
from pySimBlocks.project.build_model import build_model_from_dict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import Sofa.ImGui as MyGui
|
|
33
|
+
_imgui = hasattr(MyGui, "MyRobotWindow")
|
|
34
|
+
except ImportError:
|
|
35
|
+
_imgui = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SofaPysimBlocksController(Sofa.Core.Controller):
|
|
39
|
+
"""
|
|
40
|
+
Base class for controller to enable Sofa Simulation Loop and pySimBlocks to interact.
|
|
41
|
+
|
|
42
|
+
Description:
|
|
43
|
+
- SOFA_MASTER: SOFA is the time master.
|
|
44
|
+
project_yaml required
|
|
45
|
+
pysimblocks time step must be a multiple of sofa time step.
|
|
46
|
+
At each pysimblocks step, the controller:
|
|
47
|
+
1) reads measurements from the scene
|
|
48
|
+
2) performs one pySimBlocks step
|
|
49
|
+
3) applies the computed control inputs
|
|
50
|
+
|
|
51
|
+
- Non-SOFA_MASTER: pySimBlocks is the time master.
|
|
52
|
+
The controller is used ONLY as an I/O shell.
|
|
53
|
+
No pySimBlocks model is built or executed.
|
|
54
|
+
|
|
55
|
+
Required methods:
|
|
56
|
+
- set_inputs(self)
|
|
57
|
+
- get_outputs(self)
|
|
58
|
+
|
|
59
|
+
Optional methods:
|
|
60
|
+
- build_model() : create self.model, self.sofa_block and variables_to_log
|
|
61
|
+
- print_logs() : print logs at each step (already implemented)
|
|
62
|
+
- save()
|
|
63
|
+
|
|
64
|
+
optional parameter:
|
|
65
|
+
- variables_to_log: list of signal to log
|
|
66
|
+
|
|
67
|
+
Note:
|
|
68
|
+
- get_outputs() MUST ALWAYS WORK AND RETURN CONSISTENT SHAPES.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, root: Sofa.Core.Node, name: str ="SofaControllerGui"):
|
|
72
|
+
super().__init__(name=name)
|
|
73
|
+
|
|
74
|
+
self.IS_READY = False
|
|
75
|
+
self.SOFA_MASTER = True
|
|
76
|
+
self._imgui = _imgui
|
|
77
|
+
|
|
78
|
+
# MUST be filled by child controllers
|
|
79
|
+
self.root = root
|
|
80
|
+
self.inputs: Dict[str, np.ndarray] = {}
|
|
81
|
+
self.outputs: Dict[str, np.ndarray] = {}
|
|
82
|
+
self.variables_to_log: List[str] = []
|
|
83
|
+
self.verbose = False
|
|
84
|
+
|
|
85
|
+
self.dt: float | None = None
|
|
86
|
+
self.sim: Simulator | None = None
|
|
87
|
+
self.step_index: int = 0
|
|
88
|
+
|
|
89
|
+
self.project_yaml: str | None = None
|
|
90
|
+
self._init_failed = False
|
|
91
|
+
|
|
92
|
+
# --------------------------------------------------------------------------
|
|
93
|
+
# Public methods
|
|
94
|
+
# --------------------------------------------------------------------------
|
|
95
|
+
# --- Mandatory methods. ---
|
|
96
|
+
def prepare_scene(self):
|
|
97
|
+
"""
|
|
98
|
+
Optional initialization hook executed before pySimBlocks starts.
|
|
99
|
+
|
|
100
|
+
Purpose:
|
|
101
|
+
- wait for any preparation condition (fixed number of steps,
|
|
102
|
+
convergence, stabilization, etc.),
|
|
103
|
+
- optionally record initial measurements or offsets.
|
|
104
|
+
|
|
105
|
+
The user must set `self.IS_READY = True` when the scene is ready to start
|
|
106
|
+
the pySimBlocks control loop.
|
|
107
|
+
"""
|
|
108
|
+
self.IS_READY = True
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
def set_inputs(self):
|
|
112
|
+
"""
|
|
113
|
+
Apply inputs from pySimBlocks to SOFA components.
|
|
114
|
+
Must be implemented by child classes.
|
|
115
|
+
"""
|
|
116
|
+
raise NotImplementedError("[pySimBlocks] ERROR: set_inputs() must be implemented by subclass.")
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
def get_outputs(self):
|
|
120
|
+
"""
|
|
121
|
+
Read state from SOFA components and populate self.outputs.
|
|
122
|
+
MUST ALWAYS WORK AND RETURN CONSISTENT SHAPES.
|
|
123
|
+
Must be implemented by child classes.
|
|
124
|
+
"""
|
|
125
|
+
raise NotImplementedError("[pySimBlocks] ERROR: get_outputs() must be implemented by subclass.")
|
|
126
|
+
|
|
127
|
+
# --- Optionnal methods. ---
|
|
128
|
+
def save(self):
|
|
129
|
+
"""
|
|
130
|
+
Optional: executed at each control step.
|
|
131
|
+
Override this method to save logs or export custom data.
|
|
132
|
+
The default implementation does nothing.
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def get_block(self, block_name: str):
|
|
137
|
+
"""
|
|
138
|
+
Utility method to get a block from the model by name.
|
|
139
|
+
Only works if the model has been built.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
block_name : str
|
|
144
|
+
Name of the block to retrieve.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
Block
|
|
149
|
+
The block instance with the specified name.
|
|
150
|
+
|
|
151
|
+
Raises
|
|
152
|
+
------
|
|
153
|
+
RuntimeError
|
|
154
|
+
If the model is not built or if the block is not found.
|
|
155
|
+
"""
|
|
156
|
+
if self.sim is None:
|
|
157
|
+
raise RuntimeError("[pySimBlocks] ERROR: Simulator not initialized. Cannot get block.")
|
|
158
|
+
if block_name not in self.sim.model.blocks:
|
|
159
|
+
raise RuntimeError(f"[pySimBlocks] ERROR: Block '{block_name}' not found in the model.")
|
|
160
|
+
return self.sim.model.blocks[block_name]
|
|
161
|
+
|
|
162
|
+
# ----------------------------------------------------------------------
|
|
163
|
+
# SOFA event callback
|
|
164
|
+
# ----------------------------------------------------------------------
|
|
165
|
+
def onAnimateBeginEvent(self, event):
|
|
166
|
+
"""
|
|
167
|
+
SOFA callback executed before each physical integration step.
|
|
168
|
+
|
|
169
|
+
Sequence:
|
|
170
|
+
1. Read SOFA outputs -> get_outputs()
|
|
171
|
+
2. Push them into the exchange block
|
|
172
|
+
3. Advance pySimBlocks one step -> sim.step()
|
|
173
|
+
4. Retrieve controller inputs from exchange block
|
|
174
|
+
5. Apply them to SOFA -> set_inputs()
|
|
175
|
+
"""
|
|
176
|
+
if self.SOFA_MASTER:
|
|
177
|
+
if self._init_failed:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
if self.sim is None:
|
|
181
|
+
self._prepare_pysimblocks()
|
|
182
|
+
self._get_sofa_outputs()
|
|
183
|
+
self._set_sofa_plot()
|
|
184
|
+
self._set_sofa_slider()
|
|
185
|
+
|
|
186
|
+
if not self.IS_READY:
|
|
187
|
+
self.prepare_scene()
|
|
188
|
+
|
|
189
|
+
if self.IS_READY:
|
|
190
|
+
if self.counter % self.ratio ==0:
|
|
191
|
+
|
|
192
|
+
self._get_sofa_outputs()
|
|
193
|
+
self.sim.step()
|
|
194
|
+
self.sim._log(self.sim_cfg.logging)
|
|
195
|
+
self._set_sofa_inputs()
|
|
196
|
+
|
|
197
|
+
if self.verbose:
|
|
198
|
+
self._print_logs()
|
|
199
|
+
|
|
200
|
+
self.save()
|
|
201
|
+
self._update_sofa_slider()
|
|
202
|
+
self._update_sofa_plot()
|
|
203
|
+
|
|
204
|
+
self.sim_index += 1
|
|
205
|
+
self.counter = 0
|
|
206
|
+
self.counter += 1
|
|
207
|
+
|
|
208
|
+
self.step_index += 1
|
|
209
|
+
|
|
210
|
+
# --------------------------------------------------------------------------
|
|
211
|
+
# Private methods
|
|
212
|
+
# --------------------------------------------------------------------------
|
|
213
|
+
def _build_model(self):
|
|
214
|
+
"""
|
|
215
|
+
Define the internal pySimBlocks controller model.
|
|
216
|
+
Mandatory if purpose to be used in Sofa Simulation (SOFA_MASTER)
|
|
217
|
+
|
|
218
|
+
Must create:
|
|
219
|
+
- self.model
|
|
220
|
+
- the exchange block self.sofa_block (SofaExchangeIO)
|
|
221
|
+
- self.variables_to_log
|
|
222
|
+
"""
|
|
223
|
+
project_path = self.project_yaml
|
|
224
|
+
if project_path is None:
|
|
225
|
+
raise RuntimeError("[pySimBlocks] ERROR: SOFA_MASTER=True requires project_yaml to be set.")
|
|
226
|
+
|
|
227
|
+
self.sim_cfg, model_dict, self.plot_cfg, _, params_dir = load_project_config(project_path)
|
|
228
|
+
model_dict = self._adapt_model_for_sofa(model_dict)
|
|
229
|
+
self.model = Model("sofa_model")
|
|
230
|
+
build_model_from_dict(self.model, model_dict, params_dir=params_dir)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
def _prepare_pysimblocks(self):
|
|
235
|
+
"""
|
|
236
|
+
Called once SOFA is initialized AND if SOFA is the master.
|
|
237
|
+
Initialize the pysimblock struture.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
if self.SOFA_MASTER and self.project_yaml is None:
|
|
241
|
+
self._init_failed = True
|
|
242
|
+
raise RuntimeError("[pySimBlocks] ERROR: SOFA_MASTER=True requires project_yaml.")
|
|
243
|
+
if self.dt is None:
|
|
244
|
+
self._init_failed = True
|
|
245
|
+
raise ValueError("[pySimBlocks] ERROR: SOFA_MASTER=True requires self.dt to be set to the SOFA time step.")
|
|
246
|
+
|
|
247
|
+
self._build_model()
|
|
248
|
+
self._detect_sofa_exchange_block()
|
|
249
|
+
self._secure_keys()
|
|
250
|
+
self.sim = Simulator(self.model, self.sim_cfg, verbose=self.verbose)
|
|
251
|
+
self._get_sofa_outputs()
|
|
252
|
+
self.sim.initialize()
|
|
253
|
+
self.sim_index = 0
|
|
254
|
+
|
|
255
|
+
ratio = self.sim_cfg.dt / self.dt
|
|
256
|
+
if abs(ratio - round(ratio)) > 1e-12:
|
|
257
|
+
self._init_failed = True
|
|
258
|
+
raise ValueError(
|
|
259
|
+
"[pySimBlocks] ERROR: Sample time mismatch.\n"
|
|
260
|
+
f"pySimBlocks sample time={self.sim_cfg.dt} "
|
|
261
|
+
f"is not a multiple of Sofa sample time={self.dt}."
|
|
262
|
+
)
|
|
263
|
+
self.ratio = int(round(ratio))
|
|
264
|
+
self.counter = 0
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self._init_failed = True
|
|
267
|
+
# print(f"Initialization failed: {e}")
|
|
268
|
+
raise
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------------
|
|
271
|
+
def _secure_keys(self):
|
|
272
|
+
"""
|
|
273
|
+
Ensure that the keys used for SOFA exchange are consistent between the model and the controller.
|
|
274
|
+
"""
|
|
275
|
+
model_inputs_keys = set(self._sofa_block.inputs.keys())
|
|
276
|
+
sofa_inputs_keys = set(self.inputs.keys())
|
|
277
|
+
if not model_inputs_keys.issubset(sofa_inputs_keys):
|
|
278
|
+
self._init_failed = True
|
|
279
|
+
raise RuntimeError(
|
|
280
|
+
"[pySimBlocks] ERROR: model input_keys are missing from controller inputs.\n"
|
|
281
|
+
f"SOFA controller inputs: {sofa_inputs_keys}\n"
|
|
282
|
+
f"Model block inputs: {model_inputs_keys}\n"
|
|
283
|
+
f"Ensure that the controller in the SOFA block contains at least the same input keys as the SofaExchangeIO block."
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
model_outputs_keys = set(self._sofa_block.outputs.keys())
|
|
287
|
+
sofa_outputs_keys = set(self.outputs.keys())
|
|
288
|
+
if not model_outputs_keys.issubset(sofa_outputs_keys):
|
|
289
|
+
self._init_failed = True
|
|
290
|
+
raise RuntimeError(
|
|
291
|
+
"[pySimBlocks] ERROR: model output_keys are missing from controller outputs.\n"
|
|
292
|
+
f"SOFA controller outputs: {sofa_outputs_keys}\n"
|
|
293
|
+
f"Model block outputs: {model_outputs_keys}\n"
|
|
294
|
+
f"Ensure that the controller in the SOFA block contains at least the same output keys as the SofaExchangeIO block."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ------------------------------------------------------------------
|
|
299
|
+
def _print_logs(self):
|
|
300
|
+
"""
|
|
301
|
+
Optional: print selected logged variables at each control step.
|
|
302
|
+
Already implemented
|
|
303
|
+
"""
|
|
304
|
+
print(f"\nStep: {self.sim_index}")
|
|
305
|
+
for variable in self.sim_cfg.logging:
|
|
306
|
+
print(f"{variable}: {self.sim.logs[variable][-1]}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ------------------------------------------------------------------
|
|
310
|
+
def _detect_sofa_exchange_block(self):
|
|
311
|
+
"""
|
|
312
|
+
Detect the SofaExchangeIO block inside self.model.
|
|
313
|
+
Must be called after build_model().
|
|
314
|
+
"""
|
|
315
|
+
from pySimBlocks.blocks.systems.sofa.sofa_exchange_i_o import SofaExchangeIO
|
|
316
|
+
|
|
317
|
+
candidates = [blk for blk in self.model.blocks.values() if isinstance(blk, SofaExchangeIO)]
|
|
318
|
+
|
|
319
|
+
if len(candidates) == 0:
|
|
320
|
+
self._init_failed = True
|
|
321
|
+
raise RuntimeError(
|
|
322
|
+
"[pySimBlocks] ERROR: No SofaExchangeIO block found in the model.\n"
|
|
323
|
+
"The controller must include exactly one SOFA exchange block."
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if len(candidates) > 1:
|
|
327
|
+
self._init_failed = True
|
|
328
|
+
raise RuntimeError(
|
|
329
|
+
"[pySimBlocks] ERROR: Multiple SofaExchangeIO blocks found ({len(candidates)}).\n"
|
|
330
|
+
"Only one SOFA IO block is allowed."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
self._sofa_block = candidates[0]
|
|
334
|
+
|
|
335
|
+
# ------------------------------------------------------------------
|
|
336
|
+
def _get_sofa_outputs(self):
|
|
337
|
+
"""
|
|
338
|
+
Read outputs from SOFA components and push them to pySimBlocks.
|
|
339
|
+
"""
|
|
340
|
+
self.get_outputs()
|
|
341
|
+
for keys, val in self.outputs.items():
|
|
342
|
+
self._sofa_block.outputs[keys] = val
|
|
343
|
+
|
|
344
|
+
# ------------------------------------------------------------------
|
|
345
|
+
def _set_sofa_inputs(self):
|
|
346
|
+
"""
|
|
347
|
+
Apply inputs from pySimBlocks to SOFA components.
|
|
348
|
+
"""
|
|
349
|
+
for key, val in self._sofa_block.inputs.items():
|
|
350
|
+
self.inputs[key] = val
|
|
351
|
+
self.set_inputs()
|
|
352
|
+
|
|
353
|
+
# ------------------------------------------------------------------
|
|
354
|
+
def _set_sofa_plot(self):
|
|
355
|
+
"""
|
|
356
|
+
Setup ImGui plotting for selected variables.
|
|
357
|
+
"""
|
|
358
|
+
if not self._imgui:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if self.sim is None:
|
|
362
|
+
raise RuntimeError("[pySimBlocks] ERROR: Simulator not initialized.")
|
|
363
|
+
|
|
364
|
+
self._plot_node = self.root.addChild("PLOT")
|
|
365
|
+
self._plot_data = {}
|
|
366
|
+
for plot in self.plot_cfg.plots:
|
|
367
|
+
for var in plot["signals"]:
|
|
368
|
+
block_name, _, key = var.split(".")
|
|
369
|
+
block = self.get_block(block_name)
|
|
370
|
+
self._plot_data[f"{block_name}.{key}"] = self._plot_node.addChild(f"{block_name}_{key}")
|
|
371
|
+
value = block.outputs[key].flatten()
|
|
372
|
+
for i in range(len(value)):
|
|
373
|
+
self._plot_data[f"{block_name}.{key}"].addData(name=f"value{i}", type="float", value=value[i])
|
|
374
|
+
MyGui.PlottingWindow.addData(f"{block_name}.{key}[{i}]", self._plot_data[f"{block_name}.{key}"].getData(f"value{i}"))
|
|
375
|
+
|
|
376
|
+
# ------------------------------------------------------------------
|
|
377
|
+
def _update_sofa_plot(self):
|
|
378
|
+
"""
|
|
379
|
+
Update ImGui plotting for selected variables.
|
|
380
|
+
"""
|
|
381
|
+
if not self._imgui:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
for name, node in self._plot_data.items():
|
|
385
|
+
block_name, key = name.split(".")
|
|
386
|
+
block = self.get_block(block_name)
|
|
387
|
+
value = block.outputs[key].flatten()
|
|
388
|
+
for i in range(len(value)):
|
|
389
|
+
node.getData(f"value{i}").value = float(value[i])
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
def _set_sofa_slider(self):
|
|
393
|
+
"""
|
|
394
|
+
Setup ImGui sliders for selected variables.
|
|
395
|
+
"""
|
|
396
|
+
if not self._imgui:
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
if self.sim is None:
|
|
400
|
+
raise RuntimeError("[pySimBlocks] ERROR: Simulator not initialized.")
|
|
401
|
+
|
|
402
|
+
data = self._sofa_block.slider_params
|
|
403
|
+
data = data if data is not None else {}
|
|
404
|
+
|
|
405
|
+
self._slider_node = self.root.addChild("SLIDERS")
|
|
406
|
+
self._slider_data = {}
|
|
407
|
+
for var, extremum in data.items():
|
|
408
|
+
block_name, key = var.split(".")
|
|
409
|
+
node = self._slider_node.addChild(f"{block_name}_{key}")
|
|
410
|
+
block = self.get_block(block_name)
|
|
411
|
+
value = getattr(block, key)
|
|
412
|
+
self._slider_data[f"{block_name}.{key}"] = {"node": node, "shape": value.shape}
|
|
413
|
+
value = value.flatten()
|
|
414
|
+
for i in range(len(value)):
|
|
415
|
+
d = node.addData(name=f"value{i}", type="float", value=value[i])
|
|
416
|
+
MyGui.MyRobotWindow.addSettingInGroup( f"{key}[{i}]", d, extremum[0], extremum[1], f"{block_name}")
|
|
417
|
+
|
|
418
|
+
# ------------------------------------------------------------------
|
|
419
|
+
def _update_sofa_slider(self):
|
|
420
|
+
"""
|
|
421
|
+
Update ImGui sliders for selected variables.
|
|
422
|
+
"""
|
|
423
|
+
if not self._imgui:
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
for var in self._slider_data:
|
|
427
|
+
block_name, key = var.split(".")
|
|
428
|
+
block = self.get_block(block_name)
|
|
429
|
+
node = self._slider_data[var]["node"]
|
|
430
|
+
shape = self._slider_data[var]["shape"]
|
|
431
|
+
new_values = []
|
|
432
|
+
for i in range(np.prod(shape)):
|
|
433
|
+
new_values.append(node.getData(f"value{i}").value)
|
|
434
|
+
setattr(block, key, np.array(new_values).reshape(shape))
|
|
435
|
+
|
|
436
|
+
# ------------------------------------------------------------------
|
|
437
|
+
def _adapt_model_for_sofa(self, model_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
438
|
+
"""
|
|
439
|
+
Adapt model data for SOFA execution.
|
|
440
|
+
|
|
441
|
+
This replaces any SofaPlant block by a SofaExchangeIO block,
|
|
442
|
+
while preserving block name and connections.
|
|
443
|
+
|
|
444
|
+
Parameters
|
|
445
|
+
----------
|
|
446
|
+
model_data : dict
|
|
447
|
+
Model dictionary.
|
|
448
|
+
|
|
449
|
+
Returns
|
|
450
|
+
-------
|
|
451
|
+
dict
|
|
452
|
+
Adapted model dictionary
|
|
453
|
+
"""
|
|
454
|
+
adapted = dict(model_data)
|
|
455
|
+
adapted_blocks = []
|
|
456
|
+
|
|
457
|
+
for block in model_data.get("blocks", []):
|
|
458
|
+
if not isinstance(block, dict):
|
|
459
|
+
adapted_blocks.append(block)
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
block_type = str(block.get("type", "")).lower()
|
|
463
|
+
if block_type == "sofa_plant":
|
|
464
|
+
patched = dict(block).copy()
|
|
465
|
+
patched["type"] = "sofa_exchange_i_o"
|
|
466
|
+
patched.pop("scene_file", None)
|
|
467
|
+
adapted_blocks.append(patched)
|
|
468
|
+
else:
|
|
469
|
+
adapted_blocks.append(block)
|
|
470
|
+
|
|
471
|
+
adapted["blocks"] = adapted_blocks
|
|
472
|
+
return adapted
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List
|
|
23
|
+
from pySimBlocks.core.block import Block
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SofaExchangeIO(Block):
|
|
27
|
+
"""
|
|
28
|
+
SOFA exchange interface block.
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Provides an interface between a pySimBlocks model and an external
|
|
32
|
+
SOFA controller by exposing dynamic input and output ports.
|
|
33
|
+
|
|
34
|
+
Parameters (overview):
|
|
35
|
+
input_keys : list of str
|
|
36
|
+
Names of externally provided input signals.
|
|
37
|
+
output_keys : list of str
|
|
38
|
+
Names of output signals to be consumed by SOFA.
|
|
39
|
+
scene_file : str
|
|
40
|
+
Path to the SOFA scene file, used only for automatic generation.
|
|
41
|
+
sample_time : float, optional
|
|
42
|
+
Block execution period.
|
|
43
|
+
|
|
44
|
+
I/O:
|
|
45
|
+
Inputs:
|
|
46
|
+
Defined dynamically by input_keys.
|
|
47
|
+
Outputs:
|
|
48
|
+
Defined dynamically by output_keys.
|
|
49
|
+
|
|
50
|
+
Notes:
|
|
51
|
+
- This block does not run a SOFA simulation.
|
|
52
|
+
- It acts only as a data exchange interface.
|
|
53
|
+
- The block is stateless.
|
|
54
|
+
- Outputs are produced by the pySimBlocks controller logic.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
direct_feedthrough = False
|
|
59
|
+
is_source = False
|
|
60
|
+
|
|
61
|
+
def __init__(self,
|
|
62
|
+
name: str,
|
|
63
|
+
input_keys: list[str],
|
|
64
|
+
output_keys: list[str],
|
|
65
|
+
slider_params: Dict[str, List[float]] | None = None,
|
|
66
|
+
sample_time: float | None = None
|
|
67
|
+
):
|
|
68
|
+
super().__init__(name, sample_time)
|
|
69
|
+
|
|
70
|
+
self.input_keys = input_keys
|
|
71
|
+
self.output_keys = output_keys
|
|
72
|
+
self.slider_params = slider_params
|
|
73
|
+
|
|
74
|
+
# Declare dynamic ports
|
|
75
|
+
for k in input_keys:
|
|
76
|
+
self.inputs[k] = None
|
|
77
|
+
for k in output_keys:
|
|
78
|
+
self.outputs[k] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------------------------
|
|
82
|
+
# Class Methods
|
|
83
|
+
# --------------------------------------------------------------------------
|
|
84
|
+
@classmethod
|
|
85
|
+
def adapt_params(cls,
|
|
86
|
+
params: Dict[str, Any],
|
|
87
|
+
params_dir: Path | None = None) -> Dict[str, Any]:
|
|
88
|
+
"""
|
|
89
|
+
Adapt parameters from yaml format to class constructor format.
|
|
90
|
+
Adapt function file and name in a yaml format into callable.
|
|
91
|
+
"""
|
|
92
|
+
adapted = dict(params)
|
|
93
|
+
adapted.pop("scene_file", None)
|
|
94
|
+
return adapted
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --------------------------------------------------------------------------
|
|
98
|
+
# Public Methods
|
|
99
|
+
# --------------------------------------------------------------------------
|
|
100
|
+
def initialize(self, t0: float):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
def output_update(self, t: float, dt: float):
|
|
105
|
+
"""
|
|
106
|
+
Outputs are produced by upstream blocks (controller).
|
|
107
|
+
This block itself does nothing but check validity.
|
|
108
|
+
"""
|
|
109
|
+
# Ensure inputs exist
|
|
110
|
+
for k in self.input_keys:
|
|
111
|
+
if self.inputs[k] is None:
|
|
112
|
+
raise RuntimeError(f"[{self.name}] Missing input '{k}' at time {t}.")
|
|
113
|
+
|
|
114
|
+
# Outputs are set by other blocks (controller chain) through normal propagation.
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
def state_update(self, t: float, dt: float):
|
|
119
|
+
# Stateless block: no internal state
|
|
120
|
+
pass
|