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,45 @@
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 Tuple
23
+
24
+ from pySimBlocks.core.config import PlotConfig
25
+ from pySimBlocks.core.model import Model
26
+ from pySimBlocks.core.simulator import Simulator
27
+ from pySimBlocks.project.build_model import build_model_from_dict
28
+ from pySimBlocks.project.load_project_config import load_project_config
29
+
30
+
31
+ def load_simulator_from_project(
32
+ project_yaml: str | Path,
33
+ ) -> Tuple[Simulator, PlotConfig | None]:
34
+ """
35
+ Build and return a ready-to-run Simulator from a unified project.yaml.
36
+ """
37
+ sim_cfg, model_dict, plot_cfg, project_name, params_dir = load_project_config(
38
+ project_yaml
39
+ )
40
+
41
+ model = Model(name=project_name)
42
+ build_model_from_dict(model, model_dict, params_dir=params_dir)
43
+
44
+ sim = Simulator(model, sim_cfg)
45
+ return sim, plot_cfg
@@ -0,0 +1,174 @@
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 numpy as np
22
+ import matplotlib.pyplot as plt
23
+
24
+ from pySimBlocks.core.config import PlotConfig
25
+
26
+
27
+ def _stack_logged_signal(logs: dict, sig: str) -> np.ndarray:
28
+ """
29
+ Stack a logged signal over time, preserving its 2D shape.
30
+
31
+ Returns:
32
+ data: np.ndarray of shape (T, m, n)
33
+
34
+ Raises:
35
+ ValueError: if the signal is not a list of 2D arrays with consistent shape.
36
+ """
37
+ samples = logs[sig]
38
+ if not isinstance(samples, list) or len(samples) == 0:
39
+ raise ValueError(f"Signal '{sig}' has no samples in logs.")
40
+
41
+ # Find first non-None sample to define shape
42
+ first = None
43
+ for s in samples:
44
+ if s is not None:
45
+ first = np.asarray(s)
46
+ break
47
+
48
+ if first is None:
49
+ raise ValueError(f"Signal '{sig}' is always None; cannot plot.")
50
+
51
+ if first.ndim != 2:
52
+ raise ValueError(f"Signal '{sig}' must be 2D. Got ndim={first.ndim} with shape {first.shape}.")
53
+
54
+ shape0 = first.shape
55
+
56
+ stacked = []
57
+ for k, s in enumerate(samples):
58
+ if s is None:
59
+ raise ValueError(f"Signal '{sig}' contains None at index {k}; cannot plot.")
60
+ a = np.asarray(s)
61
+ if a.ndim != 2:
62
+ raise ValueError(
63
+ f"Signal '{sig}' sample {k} must be 2D. Got ndim={a.ndim} with shape {a.shape}."
64
+ )
65
+ if a.shape != shape0:
66
+ raise ValueError(
67
+ f"Signal '{sig}' shape changed over time: expected {shape0}, got {a.shape} at sample {k}."
68
+ )
69
+ stacked.append(a)
70
+
71
+ data = np.stack(stacked, axis=0) # (T, m, n)
72
+ return data
73
+
74
+
75
+ def plot_from_config(
76
+ logs: dict,
77
+ plot_cfg: PlotConfig | None,
78
+ show: bool = True,
79
+ block: bool = True
80
+ ):
81
+ """
82
+ Plot logged simulation signals according to a PlotConfig.
83
+
84
+ Supports 2D signals:
85
+ - scalar (1,1)
86
+ - column vectors (n,1)
87
+ - matrices (m,n) with n>1
88
+
89
+ Plot semantics:
90
+ - each component is plotted as a separate curve
91
+ - labels:
92
+ scalar: sig
93
+ vector: sig[i]
94
+ matrix: sig[r,c]
95
+ """
96
+ if plot_cfg is None:
97
+ return
98
+
99
+ # ------------------------------------------------------------
100
+ # Global validation: all plotted signals must be logged
101
+ # ------------------------------------------------------------
102
+ requested_signals = set()
103
+ for plot in plot_cfg.plots:
104
+ requested_signals.update(plot["signals"])
105
+
106
+ available_signals = set(logs.keys())
107
+ available_signals.discard("time")
108
+
109
+ missing = sorted(requested_signals - available_signals)
110
+ if missing:
111
+ raise KeyError(
112
+ "The following signals are requested for plotting but were not logged:\n"
113
+ + "\n".join(f" - {sig}" for sig in missing)
114
+ + "\n\nAvailable logged signals:\n"
115
+ + "\n".join(f" - {sig}" for sig in sorted(available_signals))
116
+ )
117
+
118
+ if "time" not in logs:
119
+ raise KeyError("Logs must contain a 'time' entry.")
120
+
121
+ # ------------------------------------------------------------
122
+ # Time base
123
+ # Your simulator logs time as np.array([t_step]) each step,
124
+ # so flatten() is appropriate.
125
+ # ------------------------------------------------------------
126
+ time = np.asarray(logs["time"]).flatten()
127
+ T = len(time)
128
+ if T == 0:
129
+ return
130
+
131
+ # ------------------------------------------------------------
132
+ # Plotting
133
+ # ------------------------------------------------------------
134
+ for plot in plot_cfg.plots:
135
+ title = plot.get("title", "")
136
+ signals = plot["signals"]
137
+
138
+ plt.figure()
139
+
140
+ for sig in signals:
141
+ data = _stack_logged_signal(logs, sig) # (T, m, n)
142
+
143
+ if data.shape[0] != T:
144
+ raise ValueError(
145
+ f"Time length mismatch for '{sig}': time has {T} samples but signal has {data.shape[0]}."
146
+ )
147
+
148
+ m, n = data.shape[1], data.shape[2]
149
+
150
+ # scalar
151
+ if (m, n) == (1, 1):
152
+ plt.step(time, data[:, 0, 0], where="post", label=sig)
153
+ continue
154
+
155
+ # vector column (n,1) -> label sig[i]
156
+ if n == 1:
157
+ for i in range(m):
158
+ plt.step(time, data[:, i, 0], where="post", label=f"{sig}[{i}]")
159
+ continue
160
+
161
+ # matrix (m,n) -> label sig[r,c]
162
+ for r in range(m):
163
+ for c in range(n):
164
+ plt.step(time, data[:, r, c], where="post", label=f"{sig}[{r},{c}]")
165
+
166
+ plt.xlabel("Time [s]")
167
+ plt.grid(True)
168
+ plt.legend()
169
+
170
+ if title:
171
+ plt.title(title)
172
+
173
+ if show:
174
+ plt.show(block=block)
@@ -0,0 +1,106 @@
1
+ controllers:
2
+ pid:
3
+ class: Pid
4
+ module: pySimBlocks.blocks.controllers.pid
5
+ state_feedback:
6
+ class: StateFeedback
7
+ module: pySimBlocks.blocks.controllers.state_feedback
8
+ interfaces:
9
+ external_input:
10
+ class: ExternalInput
11
+ module: pySimBlocks.blocks.interfaces.external_input
12
+ external_output:
13
+ class: ExternalOutput
14
+ module: pySimBlocks.blocks.interfaces.external_output
15
+ observers:
16
+ luenberger:
17
+ class: Luenberger
18
+ module: pySimBlocks.blocks.observers.luenberger
19
+ operators:
20
+ algebraic_function:
21
+ class: AlgebraicFunction
22
+ module: pySimBlocks.blocks.operators.algebraic_function
23
+ dead_zone:
24
+ class: DeadZone
25
+ module: pySimBlocks.blocks.operators.dead_zone
26
+ delay:
27
+ class: Delay
28
+ module: pySimBlocks.blocks.operators.delay
29
+ demux:
30
+ class: Demux
31
+ module: pySimBlocks.blocks.operators.demux
32
+ discrete_derivator:
33
+ class: DiscreteDerivator
34
+ module: pySimBlocks.blocks.operators.discrete_derivator
35
+ discrete_integrator:
36
+ class: DiscreteIntegrator
37
+ module: pySimBlocks.blocks.operators.discrete_integrator
38
+ gain:
39
+ class: Gain
40
+ module: pySimBlocks.blocks.operators.gain
41
+ mux:
42
+ class: Mux
43
+ module: pySimBlocks.blocks.operators.mux
44
+ product:
45
+ class: Product
46
+ module: pySimBlocks.blocks.operators.product
47
+ rate_limiter:
48
+ class: RateLimiter
49
+ module: pySimBlocks.blocks.operators.rate_limiter
50
+ saturation:
51
+ class: Saturation
52
+ module: pySimBlocks.blocks.operators.saturation
53
+ sum:
54
+ class: Sum
55
+ module: pySimBlocks.blocks.operators.sum
56
+ zero_order_hold:
57
+ class: ZeroOrderHold
58
+ module: pySimBlocks.blocks.operators.zero_order_hold
59
+ optimizers:
60
+ quadratic_program:
61
+ class: QuadraticProgram
62
+ module: pySimBlocks.blocks.optimizers.quadratic_program
63
+ sources:
64
+ chirp:
65
+ class: Chirp
66
+ module: pySimBlocks.blocks.sources.chirp
67
+ constant:
68
+ class: Constant
69
+ module: pySimBlocks.blocks.sources.constant
70
+ file_source:
71
+ class: FileSource
72
+ module: pySimBlocks.blocks.sources.file_source
73
+ function_source:
74
+ class: FunctionSource
75
+ module: pySimBlocks.blocks.sources.function_source
76
+ ramp:
77
+ class: Ramp
78
+ module: pySimBlocks.blocks.sources.ramp
79
+ sinusoidal:
80
+ class: Sinusoidal
81
+ module: pySimBlocks.blocks.sources.sinusoidal
82
+ step:
83
+ class: Step
84
+ module: pySimBlocks.blocks.sources.step
85
+ white_noise:
86
+ class: WhiteNoise
87
+ module: pySimBlocks.blocks.sources.white_noise
88
+ systems:
89
+ linear_state_space:
90
+ class: LinearStateSpace
91
+ module: pySimBlocks.blocks.systems.linear_state_space
92
+ non_linear_state_space:
93
+ class: NonLinearStateSpace
94
+ module: pySimBlocks.blocks.systems.non_linear_state_space
95
+ polytopic_state_space:
96
+ class: PolytopicStateSpace
97
+ module: pySimBlocks.blocks.systems.polytopic_state_space
98
+ sofa_controller:
99
+ class: SofaPysimBlocksController
100
+ module: pySimBlocks.blocks.systems.sofa.sofa_controller
101
+ sofa_exchange_i_o:
102
+ class: SofaExchangeIO
103
+ module: pySimBlocks.blocks.systems.sofa.sofa_exchange_i_o
104
+ sofa_plant:
105
+ class: SofaPlant
106
+ module: pySimBlocks.blocks.systems.sofa.sofa_plant
@@ -0,0 +1,23 @@
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.real_time.real_time_runner import RealTimeRunner
22
+
23
+ __all__ = ["RealTimeRunner"]
@@ -0,0 +1,148 @@
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 time
22
+ import numpy as np
23
+ from typing import Any, Dict, List, Optional
24
+ from pySimBlocks.core.simulator import Simulator
25
+
26
+
27
+ class RealTimeRunner:
28
+ """
29
+ Generic real-time runner for pySimBlocks.
30
+
31
+ Responsibilities:
32
+ - Provide an external clock (dt measured or provided).
33
+ - Push external inputs into ExternalInput blocks.
34
+ - Call Simulator.step(dt_override=...).
35
+ - Pull outputs from ExternalOutput blocks.
36
+
37
+ Notes:
38
+ - No threading, no I/O drivers here. The application owns that.
39
+ - Designed to be embedded in user-specific real-time apps.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ sim: Simulator,
45
+ input_blocks: List[str],
46
+ output_blocks: List[str],
47
+ *,
48
+ target_dt: Optional[float] = None,
49
+ time_source: str = "perf_counter", # "perf_counter" | "time"
50
+ ):
51
+ """
52
+ Parameters
53
+ ----------
54
+ sim:
55
+ Initialized Simulator instance (model already compiled).
56
+ input_blocks:
57
+ Mapping external input name -> block name in model
58
+ Example: {"camera": "Camera", "ref": "Reference"}
59
+ output_blocks:
60
+ Mapping external output name -> block name in model
61
+ Example: {"motor": "Motor"}
62
+ target_dt:
63
+ If provided, runner will pace the loop to approximately this period
64
+ (best effort). If None, no pacing.
65
+ time_source:
66
+ Timer function selection.
67
+ """
68
+ self.sim = sim
69
+ self.input_blocks = {block_name: sim.model.get_block_by_name(block_name) for block_name in input_blocks}
70
+ self.output_blocks = {block_name: sim.model.get_block_by_name(block_name) for block_name in output_blocks}
71
+ self.target_dt = target_dt
72
+
73
+ if time_source == "perf_counter":
74
+ self._now = time.perf_counter
75
+ elif time_source == "time":
76
+ self._now = time.time
77
+ else:
78
+ raise ValueError("time_source must be 'perf_counter' or 'time'")
79
+
80
+ self._t_prev: Optional[float] = None
81
+
82
+ def initialize(self, t0: float = 0.0) -> None:
83
+ self.sim.initialize(t0)
84
+ self._t_prev = self._now()
85
+
86
+ def tick(
87
+ self,
88
+ inputs: Dict[str, Any],
89
+ *,
90
+ dt: Optional[float] = None,
91
+ pace: bool = False,
92
+ ) -> Dict[str, np.ndarray]:
93
+ """
94
+ Execute one real-time tick.
95
+
96
+ Parameters
97
+ ----------
98
+ inputs:
99
+ External inputs keyed by input_blocks mapping key.
100
+ Example: {"camera": markers_pos, "ref": ref_value}
101
+ dt:
102
+ Optional explicit dt_override. If None, dt is measured from wall clock.
103
+ pace:
104
+ If True and target_dt is set, sleep to approximate target period.
105
+
106
+ Returns
107
+ -------
108
+ outputs:
109
+ Dict mapping output key -> np.ndarray (n,1)
110
+ """
111
+ if self._t_prev is None:
112
+ self._t_prev = self._now()
113
+
114
+ t_now = self._now()
115
+ dt_meas = t_now - self._t_prev
116
+ dt_used = float(dt) if dt is not None else float(dt_meas)
117
+
118
+ # warning if dt is much larger than target_dt
119
+ if self.target_dt is not None and dt_used > 1.5 * self.target_dt:
120
+ print(f"[RealTimeRunner] Warning: dt={dt_used:.3f}s exceeds target_dt={self.target_dt:.3f}s")
121
+
122
+ # 1) push inputs
123
+ for block_name, block in self.input_blocks.items():
124
+ if block_name not in inputs:
125
+ raise KeyError(f"[RealTimeRunner] Missing input '{block_name}'")
126
+ block.inputs["in"] = inputs[block_name]
127
+
128
+ # 2) step with external dt
129
+ self.sim.step(dt_override=dt_used)
130
+
131
+ # 3) pull outputs
132
+ outputs: Dict[str, np.ndarray] = {}
133
+ for block_name, block in self.output_blocks.items():
134
+ y = block.outputs["out"]
135
+ if y is None:
136
+ raise RuntimeError(f"[RealTimeRunner] Output 'out' of block '{block_name}' is None")
137
+ outputs[block_name] = np.asarray(y, dtype=float).reshape(-1, 1)
138
+
139
+ # 4) bookkeeping + pacing
140
+ self._t_prev = t_now
141
+
142
+ if pace and self.target_dt is not None:
143
+ elapsed = self._now() - t_now
144
+ sleep_time = self.target_dt - elapsed
145
+ if sleep_time > 0:
146
+ time.sleep(sleep_time)
147
+
148
+ return outputs
@@ -0,0 +1,19 @@
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
+ # ******************************************************************************
@@ -0,0 +1,141 @@
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 __future__ import annotations
22
+
23
+ import importlib
24
+ import inspect
25
+ from pathlib import Path
26
+ from typing import Dict, Optional
27
+
28
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta
29
+
30
+ BlockRegistry = Dict[str, Dict[str, BlockMeta]]
31
+
32
+ def load_block_registry(
33
+ metadata_root: Path | str | None = None,
34
+ ) -> BlockRegistry:
35
+
36
+ if metadata_root is None:
37
+ metadata_root = Path(__file__).parents[1] / "gui" / "blocks"
38
+ else:
39
+ metadata_root = Path(metadata_root).resolve()
40
+
41
+ if not metadata_root.exists():
42
+ raise FileNotFoundError(f"blocks_metadata directory not found: {metadata_root}")
43
+
44
+ registry: BlockRegistry = {}
45
+
46
+ for py_path in metadata_root.rglob("*.py"):
47
+ _register_block_from_py(py_path, registry)
48
+
49
+ return registry
50
+
51
+ def _register_block_from_py(
52
+ py_path: Path,
53
+ registry: BlockRegistry,
54
+ ) -> None:
55
+ """
56
+ Import a *.py file and register all BlockMeta subclasses inside.
57
+ """
58
+
59
+ module_name = _path_to_module(py_path)
60
+
61
+ module = importlib.import_module(module_name)
62
+
63
+ doc_path = _resolve_doc_path(py_path)
64
+
65
+ for _, obj in inspect.getmembers(module, inspect.isclass):
66
+ if not issubclass(obj, BlockMeta):
67
+ continue
68
+ if obj is BlockMeta:
69
+ continue
70
+
71
+ meta: BlockMeta = obj()
72
+
73
+ meta.doc_path = doc_path
74
+
75
+ category = meta.category
76
+ block_type = meta.type
77
+
78
+ registry.setdefault(category, {})
79
+
80
+ if block_type in registry[category]:
81
+ raise ValueError(
82
+ f"Duplicate block type '{block_type}' in category '{category}'.\n"
83
+ f"Conflict in module: {module_name}"
84
+ )
85
+
86
+ registry[category][block_type] = meta
87
+
88
+ def _path_to_module(py_path: Path) -> str:
89
+ """
90
+ Convert a file path to a Python module path.
91
+
92
+ Example:
93
+ pySimBlocks/blocks_metadata/operators/sum_meta.py
94
+ -> pySimBlocks.blocks_metadata.operators.sum_meta
95
+ """
96
+
97
+ py_path = py_path.with_suffix("")
98
+
99
+ package_root = Path(__file__).parents[1] # pySimBlocks/
100
+
101
+ try:
102
+ rel_path = py_path.relative_to(package_root)
103
+ except ValueError:
104
+ raise RuntimeError(
105
+ f"File {py_path} is not inside package root {package_root}"
106
+ )
107
+
108
+ module_name = (
109
+ package_root.name
110
+ + "."
111
+ + rel_path.as_posix().replace("/", ".")
112
+ )
113
+
114
+ return module_name
115
+
116
+ def _resolve_doc_path(py_path: Path) -> Optional[Path]:
117
+ """
118
+ Resolve the documentation markdown file corresponding to a YAML metadata file.
119
+
120
+ Example:
121
+ blocks_metadata/systems/sofa/sofa_plant.yaml
122
+ -> docs/blocks_metadata/systems/sofa/sofa_plant.md
123
+ """
124
+
125
+ try:
126
+ parts = list(py_path.parts)
127
+ idx = parts.index("gui")
128
+ except ValueError:
129
+ return None
130
+
131
+ doc_root = Path(*parts[:idx]) / "docs"
132
+
133
+ rel = Path(*parts[idx + 1 :]).with_suffix(".md")
134
+
135
+ doc_path = doc_root / rel
136
+
137
+ return doc_path if doc_path.exists() else None
138
+
139
+
140
+ if __name__ == "__main__":
141
+ registry = load_block_registry()