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.
Files changed (262) hide show
  1. examples/advanced/hardware/block_diagram.py +162 -0
  2. examples/advanced/hardware/camera.py +93 -0
  3. examples/advanced/hardware/gui_emio.py +231 -0
  4. examples/advanced/hardware/lmi.py +69 -0
  5. examples/advanced/hardware/main.py +57 -0
  6. examples/advanced/hardware/parameters.py +54 -0
  7. examples/advanced/qp_problem/params.py +10 -0
  8. examples/advanced/qp_problem/run.py +14 -0
  9. examples/advanced/sofa/gui/finger/Finger.py +108 -0
  10. examples/advanced/sofa/gui/finger/FingerController.py +53 -0
  11. examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
  12. examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
  13. examples/advanced/sofa/python/Finger.py +107 -0
  14. examples/advanced/sofa/python/FingerController.py +48 -0
  15. examples/advanced/sofa/python/closedloop.py +64 -0
  16. examples/advanced/sofa/python/openloop.py +58 -0
  17. examples/basics/algebraic_function/g.py +7 -0
  18. examples/basics/algebraic_function/run.py +14 -0
  19. examples/basics/dc_motor/gui/run.py +14 -0
  20. examples/basics/dc_motor/multiple_order.py +91 -0
  21. examples/basics/dc_motor/openloop.py +62 -0
  22. examples/basics/dc_motor/pi.py +99 -0
  23. examples/basics/dc_motor/pid.py +118 -0
  24. examples/basics/dc_motor/pid_block.py +82 -0
  25. examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
  26. examples/basics/external/parameters.py +4 -0
  27. examples/basics/external/run.py +14 -0
  28. examples/basics/file_sources/temp.py +16 -0
  29. examples/basics/observer.py +76 -0
  30. examples/basics/state_feedback/gui/parameters.py +9 -0
  31. examples/basics/state_feedback/gui/run.py +14 -0
  32. examples/basics/state_feedback/state_feedback.py +102 -0
  33. examples/basics/state_space/linear/gui/run.py +14 -0
  34. examples/basics/state_space/linear/ss_block.py +89 -0
  35. examples/basics/state_space/linear/ss_manual.py +105 -0
  36. examples/basics/state_space/non_linear/nlss.py +47 -0
  37. examples/basics/state_space/non_linear/run.py +14 -0
  38. examples/basics/state_space/polytope/params.py +39 -0
  39. examples/basics/state_space/polytope/weights.py +12 -0
  40. examples/comparison/simulink/convert.py +29 -0
  41. examples/comparison/simulink/openloop.py +127 -0
  42. examples/comparison/simulink/pi.py +148 -0
  43. examples/comparison/simulink/pid.py +154 -0
  44. examples/comparison/simulink/pid_backward.py +155 -0
  45. examples/quick_start/filter.py +35 -0
  46. examples/quick_start/gui/run.py +14 -0
  47. examples/tutorials/tutorial_1_python/main.py +99 -0
  48. examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
  49. examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
  50. pySimBlocks/__init__.py +35 -0
  51. pySimBlocks/blocks/__init__.py +68 -0
  52. pySimBlocks/blocks/controllers/__init__.py +27 -0
  53. pySimBlocks/blocks/controllers/pid.py +250 -0
  54. pySimBlocks/blocks/controllers/state_feedback.py +141 -0
  55. pySimBlocks/blocks/interfaces/__init__.py +27 -0
  56. pySimBlocks/blocks/interfaces/external_input.py +107 -0
  57. pySimBlocks/blocks/interfaces/external_output.py +113 -0
  58. pySimBlocks/blocks/observers/__init__.py +23 -0
  59. pySimBlocks/blocks/observers/luenberger.py +175 -0
  60. pySimBlocks/blocks/operators/__init__.py +47 -0
  61. pySimBlocks/blocks/operators/algebraic_function.py +258 -0
  62. pySimBlocks/blocks/operators/dead_zone.py +186 -0
  63. pySimBlocks/blocks/operators/delay.py +254 -0
  64. pySimBlocks/blocks/operators/demux.py +123 -0
  65. pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
  66. pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
  67. pySimBlocks/blocks/operators/gain.py +294 -0
  68. pySimBlocks/blocks/operators/mux.py +131 -0
  69. pySimBlocks/blocks/operators/product.py +194 -0
  70. pySimBlocks/blocks/operators/rate_limiter.py +214 -0
  71. pySimBlocks/blocks/operators/saturation.py +178 -0
  72. pySimBlocks/blocks/operators/sum.py +188 -0
  73. pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
  74. pySimBlocks/blocks/optimizers/__init__.py +23 -0
  75. pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
  76. pySimBlocks/blocks/sources/__init__.py +37 -0
  77. pySimBlocks/blocks/sources/chirp.py +168 -0
  78. pySimBlocks/blocks/sources/constant.py +82 -0
  79. pySimBlocks/blocks/sources/file_source.py +294 -0
  80. pySimBlocks/blocks/sources/function_source.py +197 -0
  81. pySimBlocks/blocks/sources/ramp.py +99 -0
  82. pySimBlocks/blocks/sources/sinusoidal.py +113 -0
  83. pySimBlocks/blocks/sources/step.py +138 -0
  84. pySimBlocks/blocks/sources/white_noise.py +107 -0
  85. pySimBlocks/blocks/systems/__init__.py +27 -0
  86. pySimBlocks/blocks/systems/linear_state_space.py +180 -0
  87. pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
  88. pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
  89. pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
  90. pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
  91. pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
  92. pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
  93. pySimBlocks/cli.py +127 -0
  94. pySimBlocks/core/__init__.py +32 -0
  95. pySimBlocks/core/block.py +167 -0
  96. pySimBlocks/core/block_source.py +94 -0
  97. pySimBlocks/core/config.py +90 -0
  98. pySimBlocks/core/fixed_time_manager.py +46 -0
  99. pySimBlocks/core/model.py +250 -0
  100. pySimBlocks/core/scheduler.py +30 -0
  101. pySimBlocks/core/simulator.py +329 -0
  102. pySimBlocks/core/task.py +73 -0
  103. pySimBlocks/docs/blocks/controllers/pid.md +78 -0
  104. pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
  105. pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
  106. pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
  107. pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
  108. pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
  109. pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
  110. pySimBlocks/docs/blocks/operators/delay.md +76 -0
  111. pySimBlocks/docs/blocks/operators/demux.md +64 -0
  112. pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
  113. pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
  114. pySimBlocks/docs/blocks/operators/gain.md +97 -0
  115. pySimBlocks/docs/blocks/operators/mux.md +64 -0
  116. pySimBlocks/docs/blocks/operators/product.md +51 -0
  117. pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
  118. pySimBlocks/docs/blocks/operators/saturation.md +62 -0
  119. pySimBlocks/docs/blocks/operators/sum.md +49 -0
  120. pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
  121. pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
  122. pySimBlocks/docs/blocks/sources/chirp.md +102 -0
  123. pySimBlocks/docs/blocks/sources/constant.md +53 -0
  124. pySimBlocks/docs/blocks/sources/file_source.md +51 -0
  125. pySimBlocks/docs/blocks/sources/function_source.md +52 -0
  126. pySimBlocks/docs/blocks/sources/ramp.md +58 -0
  127. pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
  128. pySimBlocks/docs/blocks/sources/step.md +63 -0
  129. pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
  130. pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
  131. pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
  132. pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
  133. pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
  134. pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
  135. pySimBlocks/gui/__init__.py +20 -0
  136. pySimBlocks/gui/addons/__init__.py +19 -0
  137. pySimBlocks/gui/addons/sofa/__init__.py +19 -0
  138. pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
  139. pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
  140. pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
  141. pySimBlocks/gui/blocks/block_meta.py +408 -0
  142. pySimBlocks/gui/blocks/controllers/pid.py +116 -0
  143. pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
  144. pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
  145. pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
  146. pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
  147. pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
  148. pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
  149. pySimBlocks/gui/blocks/operators/delay.py +79 -0
  150. pySimBlocks/gui/blocks/operators/demux.py +98 -0
  151. pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
  152. pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
  153. pySimBlocks/gui/blocks/operators/gain.py +79 -0
  154. pySimBlocks/gui/blocks/operators/mux.py +100 -0
  155. pySimBlocks/gui/blocks/operators/product.py +109 -0
  156. pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
  157. pySimBlocks/gui/blocks/operators/saturation.py +72 -0
  158. pySimBlocks/gui/blocks/operators/sum.py +91 -0
  159. pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
  160. pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
  161. pySimBlocks/gui/blocks/parameter_meta.py +34 -0
  162. pySimBlocks/gui/blocks/port_meta.py +31 -0
  163. pySimBlocks/gui/blocks/sources/chirp.py +146 -0
  164. pySimBlocks/gui/blocks/sources/constant.py +63 -0
  165. pySimBlocks/gui/blocks/sources/file_source.py +196 -0
  166. pySimBlocks/gui/blocks/sources/function_source.py +202 -0
  167. pySimBlocks/gui/blocks/sources/ramp.py +73 -0
  168. pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
  169. pySimBlocks/gui/blocks/sources/step.py +83 -0
  170. pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
  171. pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
  172. pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
  173. pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
  174. pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
  175. pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
  176. pySimBlocks/gui/dialogs/__init__.py +19 -0
  177. pySimBlocks/gui/dialogs/block_dialog.py +125 -0
  178. pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
  179. pySimBlocks/gui/dialogs/help_dialog.py +39 -0
  180. pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
  181. pySimBlocks/gui/dialogs/settings/plots.py +187 -0
  182. pySimBlocks/gui/dialogs/settings/project.py +146 -0
  183. pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
  184. pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
  185. pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
  186. pySimBlocks/gui/editor.py +49 -0
  187. pySimBlocks/gui/graphics/__init__.py +29 -0
  188. pySimBlocks/gui/graphics/block_item.py +289 -0
  189. pySimBlocks/gui/graphics/connection_item.py +266 -0
  190. pySimBlocks/gui/graphics/port_item.py +153 -0
  191. pySimBlocks/gui/graphics/theme.py +119 -0
  192. pySimBlocks/gui/main_window.py +162 -0
  193. pySimBlocks/gui/models/__init__.py +31 -0
  194. pySimBlocks/gui/models/block_instance.py +114 -0
  195. pySimBlocks/gui/models/connection_instance.py +43 -0
  196. pySimBlocks/gui/models/port_instance.py +52 -0
  197. pySimBlocks/gui/models/project_simulation_params.py +50 -0
  198. pySimBlocks/gui/models/project_state.py +114 -0
  199. pySimBlocks/gui/project_controller.py +302 -0
  200. pySimBlocks/gui/services/__init__.py +20 -0
  201. pySimBlocks/gui/services/project_loader.py +381 -0
  202. pySimBlocks/gui/services/project_saver.py +68 -0
  203. pySimBlocks/gui/services/simulation_runner.py +66 -0
  204. pySimBlocks/gui/services/yaml_tools.py +261 -0
  205. pySimBlocks/gui/widgets/__init__.py +19 -0
  206. pySimBlocks/gui/widgets/block_list.py +131 -0
  207. pySimBlocks/gui/widgets/diagram_view.py +312 -0
  208. pySimBlocks/gui/widgets/toolbar_view.py +161 -0
  209. pySimBlocks/project/__init__.py +35 -0
  210. pySimBlocks/project/build_model.py +92 -0
  211. pySimBlocks/project/generate_run_script.py +109 -0
  212. pySimBlocks/project/generate_sofa_controller.py +215 -0
  213. pySimBlocks/project/load_project_config.py +197 -0
  214. pySimBlocks/project/load_simulation_config.py +166 -0
  215. pySimBlocks/project/load_simulator.py +45 -0
  216. pySimBlocks/project/plot_from_config.py +174 -0
  217. pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
  218. pySimBlocks/real_time/__init__.py +23 -0
  219. pySimBlocks/real_time/real_time_runner.py +148 -0
  220. pySimBlocks/tools/__init__.py +19 -0
  221. pySimBlocks/tools/blocks_registry.py +141 -0
  222. pySimBlocks/tools/generate_blocks_index.py +124 -0
  223. pysimblocks-0.1.0.dist-info/METADATA +180 -0
  224. pysimblocks-0.1.0.dist-info/RECORD +262 -0
  225. pysimblocks-0.1.0.dist-info/WHEEL +5 -0
  226. pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
  227. pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
  228. pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
  229. tests/blocks/controllers/test_pid.py +143 -0
  230. tests/blocks/controllers/test_state_feedback.py +193 -0
  231. tests/blocks/observers/test_luenberger.py +225 -0
  232. tests/blocks/operators/test_algebraic_function.py +230 -0
  233. tests/blocks/operators/test_dead_zone.py +106 -0
  234. tests/blocks/operators/test_delay.py +167 -0
  235. tests/blocks/operators/test_demux.py +111 -0
  236. tests/blocks/operators/test_discrete_derivator.py +160 -0
  237. tests/blocks/operators/test_discrete_integrator.py +149 -0
  238. tests/blocks/operators/test_gain.py +182 -0
  239. tests/blocks/operators/test_mux.py +101 -0
  240. tests/blocks/operators/test_product.py +171 -0
  241. tests/blocks/operators/test_rate_limiter.py +166 -0
  242. tests/blocks/operators/test_saturation.py +157 -0
  243. tests/blocks/operators/test_sum.py +183 -0
  244. tests/blocks/operators/test_zero_order_hold.py +106 -0
  245. tests/blocks/sources/test_chirp.py +157 -0
  246. tests/blocks/sources/test_constant.py +55 -0
  247. tests/blocks/sources/test_file_source.py +185 -0
  248. tests/blocks/sources/test_function_source.py +135 -0
  249. tests/blocks/sources/test_ramp.py +110 -0
  250. tests/blocks/sources/test_sinusoidal.py +113 -0
  251. tests/blocks/sources/test_step.py +76 -0
  252. tests/blocks/sources/test_white_noise.py +108 -0
  253. tests/blocks/systems/test_linear_state_space.py +135 -0
  254. tests/blocks/systems/test_polytopic_state_space.py +133 -0
  255. tests/core/test_core_errors_and_determinism.py +82 -0
  256. tests/core/test_core_multirate_tasks.py +88 -0
  257. tests/core/test_core_two_phase_and_propagation.py +132 -0
  258. tests/gui/main_window_open_test.py +162 -0
  259. tests/gui/test_block_meta_param_cache.py +16 -0
  260. tests/gui/test_file_source_meta.py +17 -0
  261. tests/gui/test_function_source_meta.py +28 -0
  262. 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