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,306 @@
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 multiprocessing import Process, Pipe
23
+ import numpy as np
24
+ from typing import Any, Dict, List
25
+ from pySimBlocks.core.block import Block
26
+
27
+
28
+ def sofa_worker(conn, scene_file, input_keys, output_keys):
29
+ import os
30
+ import sys
31
+ import Sofa
32
+ import importlib.util
33
+
34
+ scene_dir = os.path.dirname(os.path.abspath(scene_file))
35
+ if scene_dir not in sys.path:
36
+ sys.path.insert(0, scene_dir)
37
+
38
+ # 1. Import scene
39
+ spec = importlib.util.spec_from_file_location("scene", scene_file)
40
+ mod = importlib.util.module_from_spec(spec)
41
+ spec.loader.exec_module(mod)
42
+
43
+ root = Sofa.Core.Node("root")
44
+ root, controller = mod.createScene(root)
45
+
46
+ sofa_outputs_keys = set(controller.outputs.keys())
47
+ if not set(output_keys).issubset(sofa_outputs_keys):
48
+ conn.send({
49
+ "cmd": "error",
50
+ "message": (
51
+ f"\n[pySimBlocks] ERROR: Output key not found in controller outputs.\n"
52
+ f"Available keys: {sofa_outputs_keys}\n"
53
+ f"Provided keys: {set(output_keys)}\n"
54
+ f"Check the 'output_keys' parameter in your project.yaml."
55
+ )
56
+ })
57
+ conn.close()
58
+ return
59
+
60
+ sofa_inputs_keys = set(controller.inputs.keys())
61
+ if not set(input_keys).issubset(sofa_inputs_keys):
62
+ conn.send({
63
+ "cmd": "error",
64
+ "message": (
65
+ f"[pySimBlocks] ERROR: Input key not found in controller inputs.\n"
66
+ f"Available keys: {sofa_inputs_keys}\n"
67
+ f"Provided keys: {set(input_keys)}\n"
68
+ f"Check the 'input_keys' parameter in your project.yaml."
69
+ )
70
+ })
71
+ conn.close()
72
+ return
73
+
74
+ controller.SOFA_MASTER = False
75
+ Sofa.Simulation.initRoot(root)
76
+
77
+ dt = float(root.dt.value)
78
+
79
+ while not controller.IS_READY:
80
+ controller.prepare_scene()
81
+ if controller.IS_READY:
82
+ break
83
+ Sofa.Simulation.animate(root, dt)
84
+
85
+ # Send initial outputs
86
+ try:
87
+ controller.get_outputs()
88
+ initial = {k: np.asarray(controller.outputs[k]).reshape(-1,1) for k in output_keys}
89
+ conn.send(initial)
90
+ except Exception as e:
91
+ conn.send({
92
+ "cmd": "error",
93
+ "message":
94
+ f"[pySimBlocks] ERROR: Failed to get initial outputs.\n{e}"})
95
+ conn.close()
96
+ return
97
+
98
+ # 2. Main loop
99
+ while True:
100
+ msg = conn.recv()
101
+
102
+ if msg["cmd"] == "step":
103
+ try:
104
+ for key, val in msg["inputs"].items():
105
+ controller.inputs[key] = val
106
+
107
+ controller.set_inputs()
108
+ Sofa.Simulation.animate(root, dt)
109
+ controller.get_outputs()
110
+
111
+ outputs = {k: np.asarray(controller.outputs[k]).reshape(-1,1)
112
+ for k in output_keys}
113
+ conn.send(outputs)
114
+ except Exception as e:
115
+ conn.send({
116
+ "cmd": "error",
117
+ "message": f"[pySimBlocks] ERROR during step execution.\n{e}"})
118
+ break
119
+
120
+ elif msg["cmd"] == "stop":
121
+ break
122
+
123
+ conn.close()
124
+
125
+
126
+ class SofaPlant(Block):
127
+ """
128
+ SOFA-based dynamic plant block.
129
+
130
+ Summary:
131
+ Executes a SOFA simulation as a dynamic system driven by pySimBlocks.
132
+ At each control step, inputs are sent to SOFA, the scene advances,
133
+ and updated outputs are returned.
134
+
135
+ Parameters (overview):
136
+ scene_file : str
137
+ Path to the SOFA scene file.
138
+ input_keys : list[str]
139
+ Names of input signals exchanged with SOFA.
140
+ output_keys : list[str]
141
+ Names of output signals produced by SOFA.
142
+ sample_time : float, optional
143
+ Block execution period.
144
+
145
+ I/O:
146
+ Inputs:
147
+ Defined dynamically by input_keys.
148
+ Outputs:
149
+ Defined dynamically by output_keys.
150
+
151
+ Notes:
152
+ - This block runs SOFA in a separate worker process.
153
+ - The block has internal state and no direct feedthrough.
154
+ """
155
+
156
+ direct_feedthrough = False
157
+ need_first = True
158
+
159
+ def __init__(self,
160
+ name: str,
161
+ scene_file: str,
162
+ input_keys: list[str],
163
+ output_keys: list[str],
164
+ slider_params: Dict[str, List[float]] | None = None,
165
+ sample_time: float | None = None
166
+ ):
167
+ super().__init__(name, sample_time)
168
+
169
+ self.scene_file = scene_file
170
+ self.input_keys = input_keys
171
+ self.output_keys = output_keys
172
+ self.slider_params = slider_params
173
+
174
+ for k in input_keys:
175
+ self.inputs[k] = None
176
+ self.next_outputs = {}
177
+ for k in output_keys:
178
+ self.outputs[k] = None
179
+ self.state[k] = None
180
+ self.next_state[k] = None
181
+
182
+ self.process = None
183
+ self.conn = None
184
+
185
+
186
+ # --------------------------------------------------------------------------
187
+ # Class Methods
188
+ # --------------------------------------------------------------------------
189
+ @classmethod
190
+ def adapt_params(cls,
191
+ params: Dict[str, Any],
192
+ params_dir: Path | None = None) -> Dict[str, Any]:
193
+ """
194
+ Adapt parameters from yaml format to class constructor format.
195
+ Adapt function file and name in a yaml format into callable.
196
+ """
197
+ if params_dir is None:
198
+ raise ValueError("params_dir must be provided for SofaPlant adaptation")
199
+
200
+ scene_file = params.get("scene_file")
201
+ if scene_file is None:
202
+ raise ValueError("Missing 'scene_file' parameter")
203
+
204
+ path = Path(scene_file).expanduser()
205
+ if not path.is_absolute():
206
+ path = (params_dir / path).resolve()
207
+
208
+ adapted = dict(params)
209
+ adapted["scene_file"] = str(path)
210
+
211
+ return adapted
212
+
213
+
214
+ # --------------------------------------------------------------------------
215
+ # Public Methods
216
+ # --------------------------------------------------------------------------
217
+ def initialize(self, t0: float):
218
+
219
+ # Start worker
220
+ parent_conn, child_conn = Pipe()
221
+ self.conn = parent_conn
222
+
223
+ self.process = Process(
224
+ target=sofa_worker,
225
+ args=(child_conn, self.scene_file,
226
+ self.input_keys, self.output_keys)
227
+ )
228
+ self.process.start()
229
+
230
+ # Receive initial outputs
231
+ initial_outputs = self.conn.recv()
232
+
233
+ if isinstance(initial_outputs, dict) and initial_outputs.get("cmd") == "error":
234
+ raise RuntimeError(initial_outputs["message"])
235
+
236
+ for k in self.output_keys:
237
+ self.outputs[k] = initial_outputs[k]
238
+ self.state[k] = initial_outputs[k]
239
+ self.next_state[k] = initial_outputs[k]
240
+
241
+
242
+ # ------------------------------------------------------------------
243
+ def output_update(self, t: float, dt: float):
244
+ """
245
+ Outputs were already updated during the previous state_update().
246
+ This block retrieves outputs from an external SOFA worker process,
247
+ so no computation is required here.
248
+ """
249
+ for key in self.output_keys:
250
+ self.outputs[key] = self.state[key]
251
+
252
+
253
+ # ------------------------------------------------------------------
254
+ def state_update(self, t: float, dt: float):
255
+
256
+ # Send inputs
257
+ msg = {"cmd": "step", "inputs": {}}
258
+ for k in self.input_keys:
259
+ val = self.inputs[k]
260
+ if val is None:
261
+ raise RuntimeError(
262
+ f"[{self.name}] Input '{k}' is missing at time {t}."
263
+ )
264
+ msg["inputs"][k] = val
265
+
266
+ self.conn.send(msg)
267
+
268
+ # Receive outputs
269
+ outputs = self.conn.recv()
270
+ if isinstance(outputs, dict) and outputs.get("cmd") == "error":
271
+ raise RuntimeError(outputs["message"])
272
+
273
+ for k in self.output_keys:
274
+ self.next_state[k] = outputs[k]
275
+
276
+
277
+ # ------------------------------------------------------------------
278
+ def finalize(self):
279
+ """Ensure worker process is shutdown cleanly."""
280
+ if self.conn:
281
+ try:
282
+ self.conn.send({"cmd": "stop"})
283
+ except:
284
+ pass
285
+ try:
286
+ self.conn.close()
287
+ except:
288
+ pass
289
+
290
+ if self.process:
291
+ self.process.join(timeout=1.0)
292
+ if self.process.is_alive():
293
+ self.process.kill()
294
+
295
+
296
+ # --------------------------------------------------------------------------
297
+ # Destructor
298
+ # --------------------------------------------------------------------------
299
+ def __del__(self):
300
+ if self.conn:
301
+ try:
302
+ self.conn.send({"cmd": "stop"})
303
+ except:
304
+ pass
305
+ if self.process:
306
+ self.process.join(timeout=0.5)
pySimBlocks/cli.py ADDED
@@ -0,0 +1,127 @@
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
+ import argparse
22
+ import sys
23
+ from pathlib import Path
24
+
25
+
26
+ def _run_gui(project_dir: str | None) -> None:
27
+ try:
28
+ from pySimBlocks.gui.editor import run_app
29
+ except ImportError as e:
30
+ print(f"Error importing GUI modules: {e}")
31
+ print("Make sure PySide6 is installed to use the GUI editor.")
32
+ sys.exit(1)
33
+
34
+ path = Path(project_dir).resolve() if project_dir else Path.cwd().resolve()
35
+ run_app(path)
36
+
37
+
38
+ def _run_export(args: argparse.Namespace) -> None:
39
+ project_yaml = Path(args.project_file) if args.project_file else None
40
+ project_dir = Path(args.project_dir) if args.project_dir else Path(".")
41
+ output = Path(args.out) if args.out else None
42
+
43
+ if args.sofa_controller:
44
+ try:
45
+ from pySimBlocks.project.generate_sofa_controller import generate_sofa_controller
46
+ generate_sofa_controller(project_dir=project_dir, project_yaml=project_yaml)
47
+ except Exception as e:
48
+ print(f"Error generating SOFA controller: {e}")
49
+ print("See SOFA integration documentation for troubleshooting.")
50
+ sys.exit(1)
51
+ else:
52
+ from pySimBlocks.project import generate_run_script
53
+
54
+ generate_run_script(project_dir=project_dir, project_yaml=project_yaml, output=output)
55
+
56
+
57
+ def _run_update() -> None:
58
+ from pySimBlocks.tools.generate_blocks_index import generate_blocks_index
59
+
60
+ print("Running pySimBlocks index update...")
61
+ generate_blocks_index()
62
+ print("pySimBlocks update complete.")
63
+
64
+
65
+ def _build_parser() -> argparse.ArgumentParser:
66
+ parser = argparse.ArgumentParser(
67
+ prog="pysimblocks",
68
+ description=(
69
+ "pySimBlocks command line interface."
70
+ ),
71
+ )
72
+
73
+ subparsers = parser.add_subparsers(dest="command")
74
+
75
+ gui_parser = subparsers.add_parser("gui", help="Launch the GUI editor.")
76
+ gui_parser.add_argument(
77
+ "project_dir",
78
+ nargs="?",
79
+ help="Project directory to open. Defaults to current directory.",
80
+ )
81
+
82
+ export_parser = subparsers.add_parser(
83
+ "export",
84
+ help="Generate run.py or a SOFA controller from project.yaml.",
85
+ )
86
+ source_group = export_parser.add_mutually_exclusive_group()
87
+ source_group.add_argument(
88
+ "-f",
89
+ "--file",
90
+ "--project",
91
+ dest="project_file",
92
+ help="Path to project.yaml",
93
+ )
94
+ source_group.add_argument(
95
+ "-d",
96
+ "--directory",
97
+ dest="project_dir",
98
+ help="Project directory containing project.yaml",
99
+ )
100
+ export_parser.add_argument("-o", "--out", help="Output run.py path")
101
+ export_parser.add_argument(
102
+ "-s",
103
+ "--sofa-controller",
104
+ action="store_true",
105
+ help="Update SOFA controller from project.yaml instead of generating run.py.",
106
+ )
107
+
108
+ subparsers.add_parser("update", help="Regenerate pySimBlocks blocks index.")
109
+ return parser
110
+
111
+
112
+ def main(argv: list[str] | None = None) -> None:
113
+ args_list = list(sys.argv[1:] if argv is None else argv)
114
+ parser = _build_parser()
115
+ args = parser.parse_args(args_list)
116
+ if args.command == "gui":
117
+ _run_gui(project_dir=args.project_dir)
118
+ elif args.command == "export":
119
+ _run_export(args)
120
+ elif args.command == "update":
121
+ _run_update()
122
+ else:
123
+ parser.print_help()
124
+
125
+
126
+ if __name__ == "__main__":
127
+ main()
@@ -0,0 +1,32 @@
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 pySimBlocks.core.block import Block
22
+ from pySimBlocks.core.config import SimulationConfig, PlotConfig
23
+ from pySimBlocks.core.model import Model
24
+ from pySimBlocks.core.simulator import Simulator
25
+
26
+ __all__ = [
27
+ "Block",
28
+ "Model",
29
+ "PlotConfig",
30
+ "SimulationConfig",
31
+ "Simulator"
32
+ ]
@@ -0,0 +1,167 @@
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 abc import ABC, abstractmethod
22
+ from pathlib import Path
23
+ from typing import Any, Dict
24
+
25
+ import numpy as np
26
+
27
+
28
+ class Block(ABC):
29
+ """
30
+ Base class for all discrete-time blocks (Simulink-like).
31
+
32
+ A block follows two-phase execution:
33
+
34
+ 1) output_update(t, dt):
35
+ Computes outputs y[k] from:
36
+ - current state x[k]
37
+ - current inputs u[k]
38
+
39
+ 2) state_update(t, dt):
40
+ Computes next state x[k+1] from:
41
+ - current state x[k]
42
+ - current inputs u[k]
43
+ """
44
+
45
+ direct_feedthrough = True
46
+ is_source = False
47
+
48
+ def __init__(self, name: str, sample_time: float | None = None):
49
+ self.name = name
50
+
51
+ if sample_time is not None and sample_time <= 0:
52
+ raise ValueError(f"[{self.name}] sample_time must be > 0.")
53
+ self.sample_time = sample_time
54
+
55
+ # Dict[str -> np.ndarray]
56
+ self.inputs = {} # ports set by the simulator
57
+ self.outputs = {} # ports produced at each step
58
+
59
+ # Internal states:
60
+ # state: x[k] (committed state)
61
+ # next_state: x[k+1] (to commit at end of step)
62
+ self.state = {}
63
+ self.next_state = {}
64
+
65
+ self._effective_sample_time = 0.
66
+
67
+
68
+ # --------------------------------------------------------------------------
69
+ # Class Methods
70
+ # --------------------------------------------------------------------------
71
+ @classmethod
72
+ def adapt_params(cls,
73
+ params: Dict[str, Any],
74
+ params_dir: Path | None = None) -> Dict[str, Any]:
75
+ """
76
+ Adapt parameters from yaml format to class constructor format.
77
+ By default, does nothing.
78
+ """
79
+ return params
80
+
81
+
82
+ # --------------------------------------------------------------------------
83
+ # Public methods
84
+ # --------------------------------------------------------------------------
85
+ @property
86
+ def has_state(self) -> bool:
87
+ """Specify if block is stateful."""
88
+ return bool(self.state) or bool(self.next_state)
89
+
90
+ # ------------------------------------------------------------------
91
+ @abstractmethod
92
+ def initialize(self, t0: float):
93
+ """
94
+ Initialize internal state x[0] and outputs y[0].
95
+ Must fill:
96
+ - self.state[...] (initial state)
97
+ - self.outputs[...] (initial outputs)
98
+ """
99
+
100
+ # ------------------------------------------------------------------
101
+ @abstractmethod
102
+ def output_update(self, t: float, dt: float):
103
+ """
104
+ Compute outputs y[k] from x[k] and inputs u[k].
105
+ Called before state_update.
106
+ Must write to self.outputs[...].
107
+ """
108
+
109
+ # ------------------------------------------------------------------
110
+ @abstractmethod
111
+ def state_update(self, t: float, dt: float):
112
+ """
113
+ Compute next state x[k+1] from x[k] and inputs u[k].
114
+ Must write to self.next_state[...].
115
+ """
116
+
117
+ # ------------------------------------------------------------------
118
+ def commit_state(self):
119
+ """
120
+ Finalize the step by copying x[k+1] into x[k].
121
+ Called by the simulator after all blocks completed state_update().
122
+ """
123
+ for key, value in self.next_state.items():
124
+ self.state[key] = np.copy(value)
125
+
126
+ # ------------------------------------------------------------------
127
+ def finalize(self):
128
+ """
129
+ Optional cleanup method called at the end of the simulation.
130
+ """
131
+
132
+
133
+ # --------------------------------------------------------------------------
134
+ # Private methods
135
+ # --------------------------------------------------------------------------
136
+ @staticmethod
137
+ def _is_scalar_2d(arr: np.ndarray) -> bool:
138
+ return arr.shape == (1, 1)
139
+
140
+ # ------------------------------------------------------------------
141
+ def _to_2d_array(self, param_name: str, value, *, dtype=float) -> np.ndarray:
142
+ """
143
+ Normalize into a 2D NumPy array.
144
+
145
+ Rules:
146
+ - scalar -> (1,1)
147
+ - 1D -> (n,1) (column vector convention)
148
+ - 2D -> preserved as-is (m,n)
149
+ - ndim > 2 -> rejected
150
+ """
151
+ arr = np.asarray(value, dtype=dtype)
152
+
153
+ if arr.ndim == 0:
154
+ return arr.reshape(1, 1)
155
+
156
+ if arr.ndim == 1:
157
+ return arr.reshape(-1, 1)
158
+
159
+ if arr.ndim == 2:
160
+ if arr.shape[0] == 1 and arr.shape[1] != 1:
161
+ return arr.reshape(-1, 1)
162
+ return arr
163
+
164
+ raise ValueError(
165
+ f"[{self.name}] '{param_name}' must be scalar, 1D, or 2D array-like. "
166
+ f"Got ndim={arr.ndim} with shape {arr.shape}."
167
+ )