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,329 @@
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 typing import Dict, List
22
+
23
+ import numpy as np
24
+
25
+ from pySimBlocks.core.config import SimulationConfig
26
+ from pySimBlocks.core.fixed_time_manager import FixedStepTimeManager
27
+ from pySimBlocks.core.model import Model
28
+ from pySimBlocks.core.scheduler import Scheduler
29
+ from pySimBlocks.core.task import Task
30
+
31
+
32
+ class Simulator:
33
+ """
34
+ Discrete-time simulator with strict Simulink-like semantics:
35
+
36
+ For each step k:
37
+
38
+ 1) PHASE 1: output_update(t)
39
+ Blocks compute outputs y[k] from x[k] and u[k].
40
+
41
+ 2) Propagate all outputs to downstream inputs.
42
+
43
+ 3) PHASE 2: state_update(t, dt)
44
+ Blocks compute x[k+1] from x[k] and u[k].
45
+
46
+ 4) Commit x[k+1] -> x[k].
47
+
48
+ This guarantees:
49
+ - Proper separation of outputs and state transitions.
50
+ - Correct causal behavior for feedback loops.
51
+ - Algebraic loop detection through the Model's topo ordering.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ model: Model,
57
+ sim_cfg: SimulationConfig,
58
+ verbose: bool = False,
59
+ ):
60
+
61
+ self.model = model
62
+ self.sim_cfg = sim_cfg
63
+ self.verbose = verbose
64
+ self.model.verbose = verbose
65
+
66
+ self.sim_cfg.validate()
67
+ self._compile()
68
+
69
+ self.logs: Dict[str, List[np.ndarray]] = {"time": []}
70
+
71
+
72
+ # --------------------------------------------------------------------------
73
+ # Public methods
74
+ # --------------------------------------------------------------------------
75
+ def initialize(self, t0: float = 0.0):
76
+ """Initialize all blocks and propagate initial outputs."""
77
+ self.t = float(t0)
78
+ self.t_step = float(t0)
79
+ self.logs = {"time": []}
80
+ self._log_shapes: Dict[str, tuple[int, int]] = {}
81
+
82
+ # Initialisation bloc par bloc + propagation
83
+ for block in self.output_order:
84
+ try:
85
+ block.initialize(self.t)
86
+ self._propagate_from(block)
87
+ except Exception as e:
88
+ raise RuntimeError(
89
+ f"Error during initialization of block '{block.name}': {e}"
90
+ ) from e
91
+ for task in self.tasks:
92
+ task.update_state_blocks()
93
+
94
+ # ------------------------------------------------------------------
95
+ def step(self, dt_override: float | None = None) -> None:
96
+ """
97
+ Perform one simulation step.
98
+
99
+ If dt_override is provided, the simulator time advance is driven by this
100
+ external dt (real-time clock). Otherwise, dt is provided by the internal
101
+ time manager (fixed-step).
102
+ """
103
+ # 0) Choose dt for this tick
104
+ if self.sim_cfg.clock == "external":
105
+ if dt_override is None:
106
+ raise RuntimeError(
107
+ "[Simulator] dt_override must be provided when using external clock."
108
+ )
109
+ else:
110
+ dt_scheduler = float(dt_override)
111
+ if dt_scheduler <= 0.0:
112
+ raise ValueError(f"[Simulator] dt_override must be > 0. Got {dt_scheduler}")
113
+ else: # internal clock
114
+ if dt_override is not None:
115
+ raise RuntimeError(
116
+ "[Simulator] dt_override should not be provided when using internal clock."
117
+ )
118
+ dt_scheduler = self.time_manager.next_dt(self.t)
119
+
120
+ active_tasks = self.scheduler.active_tasks(self.t)
121
+
122
+ # PHASE 1 — outputs
123
+ for task in active_tasks:
124
+ dt_task = task.get_dt(self.t)
125
+ for block in task.output_blocks:
126
+ block.output_update(self.t, dt_task)
127
+ self._propagate_from(block)
128
+
129
+ # PHASE 2 — states
130
+ for task in active_tasks:
131
+ dt_task = task.get_dt(self.t)
132
+ for block in task.state_blocks:
133
+ block.state_update(self.t, dt_task)
134
+
135
+ for task in active_tasks:
136
+ for block in task.state_blocks:
137
+ block.commit_state()
138
+
139
+ for task in active_tasks:
140
+ task.advance()
141
+
142
+ self.t_step = self.t
143
+ self.t += dt_scheduler
144
+
145
+ # ------------------------------------------------------------------
146
+ def run(
147
+ self,
148
+ T: float | None = None,
149
+ t0: float | None = None,
150
+ logging: list[str] | None = None,
151
+ ):
152
+ """Run the simulation from t0 to T.
153
+ If T, t0 or logging are not provided, use the simulator's config.
154
+ Returns:
155
+ logs (Dict[str, List[np.ndarray]]): Logged variables over time.
156
+ """
157
+ if self.sim_cfg.clock == "external":
158
+ raise RuntimeError("Simulator.run() is not supported with external clock. Use step(dt_override=...)")
159
+
160
+ sim_duration = T if T is not None else self.sim_cfg.T
161
+ t0_run = t0 if t0 is not None else self.sim_cfg.t0
162
+ logging_run = logging if logging is not None else self.sim_cfg.logging
163
+
164
+ self.initialize(t0_run)
165
+
166
+ # Main loop (with a small epsilon to avoid floating-point issues)
167
+ eps = 1e-12
168
+ while self.t_step < sim_duration - eps:
169
+ self.step()
170
+ self._log(logging_run)
171
+
172
+ if self.verbose:
173
+ print(f"\nTime: {self.t_step}/{sim_duration}")
174
+ for variable in logging_run:
175
+ print(f"{variable}: {self.logs[variable][-1]}")
176
+
177
+
178
+
179
+ for block in self.model.blocks.values():
180
+ try:
181
+ block.finalize()
182
+ except Exception as e:
183
+ print(f"[WARNING] finalize() failed for block {block.name}: {e}")
184
+
185
+
186
+ return self.logs
187
+
188
+ # ------------------------------------------------------------------
189
+ def get_data(self,
190
+ variable: str | None = None,
191
+ block:str | None = None,
192
+ port: str | None = None) -> np.ndarray:
193
+ """Retrieve logged data for a specific variable, block output, or state.
194
+ Provide either:
195
+ - variable: the full variable name as logged (e.g., "BlockName.outputs.Port)
196
+ - block and port: to specify an output variable (e.g., block="BlockName", port="Port")
197
+ """
198
+ if variable is not None:
199
+ var_name = variable
200
+ elif block is not None and port is not None:
201
+ var_name = f"{block}.outputs.{port}"
202
+ else:
203
+ raise ValueError("Either variable or (block, port) must be provided.")
204
+
205
+ if var_name not in self.logs:
206
+ raise ValueError(f"Variable '{var_name}' is not logged. Available logs: {list(self.logs.keys())}")
207
+
208
+ data = self.logs.get(var_name)
209
+ if data is None:
210
+ raise ValueError(f"No data found for variable '{var_name}'.")
211
+ length = len(data)
212
+ if length == 0:
213
+ raise ValueError(f"Log for variable '{var_name}' is empty.")
214
+ shape = data[0].shape
215
+ try:
216
+ data_array = np.array(data).reshape(length, *shape)
217
+ except Exception as e:
218
+ raise ValueError(f"Failed to convert log data for variable '{var_name}' to numpy array: {e}") from e
219
+
220
+ return data_array
221
+
222
+ # --------------------------------------------------------------------------
223
+ # Private methods
224
+ # --------------------------------------------------------------------------
225
+ def _compile(self):
226
+ """Prepare the simulator for execution.
227
+ - Build execution order.
228
+ - Group blocks into tasks by sample time.
229
+ - Initialize the scheduler and time manager.
230
+ """
231
+ self.output_order = self.model.build_execution_order()
232
+ self.model.resolve_sample_times(self.sim_cfg.dt)
233
+ self.model._rebuild_downstream_map()
234
+ sample_times = [b._effective_sample_time for b in self.model.blocks.values()]
235
+
236
+ # regroup blocks by sample time
237
+ tasks_by_ts = {}
238
+ for b in self.model.blocks.values():
239
+ sample_time = b._effective_sample_time
240
+ tasks_by_ts.setdefault(sample_time, []).append(b)
241
+
242
+ self.tasks = [
243
+ Task(sample_time, blocks, self.output_order)
244
+ for sample_time, blocks in tasks_by_ts.items()
245
+ ]
246
+
247
+ self._single_task = (len(self.tasks) == 1)
248
+ if self._single_task:
249
+ self._task0 = self.tasks[0]
250
+
251
+ self.scheduler = Scheduler(self.tasks)
252
+
253
+ if self.sim_cfg.solver == "fixed":
254
+ self.time_manager = FixedStepTimeManager(
255
+ dt_base=self.sim_cfg.dt,
256
+ sample_times=list(set(sample_times))
257
+ )
258
+ elif self.sim_cfg.solver == "variable":
259
+ raise NotImplementedError(
260
+ "Variable-step simulation is not implemented yet."
261
+ )
262
+ else:
263
+ raise ValueError(
264
+ f"Unknown simulation mode '{self.sim_cfg.solver}'. "
265
+ "Supported modes are: 'fixed', 'variable'."
266
+ )
267
+
268
+
269
+ # ------------------------------------------------------------------
270
+ def _propagate_from(self, block):
271
+ """
272
+ Propagate outputs of `block` to its direct downstream blocks.
273
+ """
274
+ blocks = self.model.blocks
275
+ for (src, dst) in self.model.downstream_of(block.name):
276
+ _, src_port = src
277
+ dst_block, dst_port = dst
278
+ value = block.outputs[src_port]
279
+ if value is not None:
280
+ blocks[dst_block].inputs[dst_port] = value
281
+
282
+ # ------------------------------------------------------------------
283
+ def _log(self, variables_to_log):
284
+ """Log specified variables at the current time step.
285
+
286
+ Enforces:
287
+ - logged values must be 2D numpy arrays
288
+ - shape must stay constant over time for each logged variable
289
+ """
290
+ for var in variables_to_log:
291
+ block_name, container, key = var.split(".")
292
+ block = self.model.blocks[block_name]
293
+
294
+ if container == "outputs":
295
+ value = block.outputs[key]
296
+ elif container == "state":
297
+ value = block.state[key]
298
+ else:
299
+ raise ValueError(f"Unknown container '{container}' in '{var}'.")
300
+
301
+ if value is None:
302
+ raise RuntimeError(
303
+ f"[Simulator] Cannot log '{var}' at t={self.t_step}: value is None."
304
+ )
305
+
306
+ arr = np.asarray(value)
307
+
308
+ if arr.ndim != 2:
309
+ raise RuntimeError(
310
+ f"[Simulator] Cannot log '{var}' at t={self.t_step}: expected a 2D array, "
311
+ f"got ndim={arr.ndim} with shape {arr.shape}."
312
+ )
313
+
314
+ # Enforce constant shape over time for this variable
315
+ if var not in self._log_shapes:
316
+ self._log_shapes[var] = arr.shape
317
+ else:
318
+ expected_shape = self._log_shapes[var]
319
+ if arr.shape != expected_shape:
320
+ raise RuntimeError(
321
+ f"[Simulator] Logged signal '{var}' changed shape over time at t={self.t_step}: "
322
+ f"expected {expected_shape}, got {arr.shape}."
323
+ )
324
+
325
+ if var not in self.logs:
326
+ self.logs[var] = []
327
+ self.logs[var].append(np.copy(arr))
328
+
329
+ self.logs["time"].append(np.array([self.t_step]))
@@ -0,0 +1,73 @@
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
+ class Task:
22
+ """A task represents a group of blocks that share the same sample time."""
23
+ def __init__(self, sample_time, blocks, global_output_order):
24
+ self.sample_time = sample_time
25
+ self.next_activation = 0.0
26
+ self.last_activation = None
27
+
28
+ self.output_blocks = [
29
+ b for b in global_output_order
30
+ if b in blocks
31
+ ]
32
+ self.state_blocks = []
33
+
34
+ def update_state_blocks(self):
35
+ """Update the list of blocks with state within this task."""
36
+ self.state_blocks = [
37
+ b for b in self.output_blocks
38
+ if b.has_state
39
+ ]
40
+
41
+ def should_run(self, t, eps=1e-12):
42
+ """Check if the task should run at time t."""
43
+ return t + eps >= self.next_activation
44
+
45
+ def get_dt(self, t):
46
+ """Get the time step for this task at time t."""
47
+ if self.last_activation is None:
48
+ return self.sample_time
49
+ return t - self.last_activation
50
+
51
+ def advance(self):
52
+ """Advance the task's activation times."""
53
+ self.last_activation = self.next_activation
54
+ self.next_activation += self.sample_time
55
+
56
+ def run_outputs(self, t: float, dt: float, propagate_cb):
57
+ for block in self.output_blocks:
58
+ block.output_update(t, dt)
59
+ propagate_cb(block)
60
+
61
+ def run_states(self, t: float, dt: float):
62
+ for block in self.state_blocks:
63
+ block.state_update(t, dt)
64
+
65
+ def commit_states(self):
66
+ for block in self.state_blocks:
67
+ block.commit_state()
68
+
69
+ def tick(self, t: float, dt: float, propagate_cb):
70
+ # Optionnel : si tu veux une API unique, mais attention :
71
+ # tu ne peux PAS commit ici si tu veux respecter le "all state_update before any commit"
72
+ self.run_outputs(t, dt, propagate_cb)
73
+ self.run_states(t, dt)
@@ -0,0 +1,78 @@
1
+ # PID Controller
2
+
3
+ ## Description
4
+
5
+ The PID block implements a discrete-time proportional–integral–derivative
6
+ controller in parallel form. It is strictly single-input single-output (SISO)
7
+ and follows Simulink-like execution semantics.
8
+
9
+ The controller structure can be configured to enable or disable individual
10
+ terms (P, I, D), and the integration method can be selected.
11
+
12
+ ---
13
+
14
+ ## Mathematical Formulation
15
+
16
+ The control law is given by:
17
+
18
+ $$
19
+ u[k] = K_p e[k] + x_i[k] + K_d \frac{e[k] - e[k-1]}{dt}
20
+ $$
21
+
22
+ where the integral state evolves according to:
23
+
24
+ ### Euler forward integration
25
+
26
+ $$
27
+ x_i[k+1] = x_i[k] + K_i e[k] \, dt
28
+ $$
29
+
30
+ ### Euler backward integration
31
+
32
+ $$
33
+ x_i[k+1] = u[k] - \left( K_p e[k] + K_d \frac{e[k] - e[k-1]}{dt} \right)
34
+ $$
35
+
36
+ ---
37
+
38
+ ## Parameters
39
+
40
+ | Name | Type | Description | Optional |
41
+ |------------|-------------|-------------|-------------|
42
+ | `controller` | string | Controller structure: `P`, `I`, `PI`, `PD`, or `PID`. | False |
43
+ | `Kp` | scalar | Proportional gain. | False |
44
+ | `Ki` | scalar | Integral gain. | False |
45
+ | `Kd` | scalar | Derivative gain. | False |
46
+ | `integration_method` | string | Numerical integration scheme for the integral term: `euler forward` (default) or `euler backward`. | True |
47
+ | `u_min` | scalar | Minimum output saturation bound. | True |
48
+ | `u_max` | scalar | Maximum output saturation bound. | True |
49
+ | `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True |
50
+
51
+ ---
52
+
53
+ ## Inputs
54
+
55
+ | Port | Description |
56
+ |------|------------|
57
+ | `e` | Control error. |
58
+
59
+ ---
60
+
61
+ ## Outputs
62
+
63
+
64
+ | Port | Description |
65
+ |------|------------|
66
+ | `u` | Control command. |
67
+
68
+ ---
69
+
70
+ ## Notes
71
+
72
+ - The block is strictly SISO.
73
+ - Dimension checks are enforced at runtime.
74
+ - Direct feedthrough depends on the controller structure and integration method.
75
+
76
+
77
+ ---
78
+ © 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later
@@ -0,0 +1,60 @@
1
+ # StateFeedback
2
+
3
+ ## Summary
4
+
5
+ The **StateFeedback** block implements a discrete-time static state-feedback
6
+ controller combining reference feedforward and state feedback.
7
+
8
+ ---
9
+
10
+ ## Mathematical definition
11
+
12
+ The control law is defined as:
13
+
14
+ $$
15
+ u[k] = G r[k] - K x[k]
16
+ $$
17
+
18
+ where:
19
+ - $r[k]$ is the reference vector,
20
+ - $x[k]$ is the measured state vector,
21
+ - $u[k]$ is the control input.
22
+
23
+ ---
24
+
25
+ ## Parameters
26
+
27
+ | Name | Type | Description | Optional |
28
+ |------------|-------------|-------------|-------------|
29
+ | `K` | array | State feedback gain matrix. | False |
30
+ | `G` | array | Reference feedforward gain matrix. | False |
31
+ | `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True |
32
+
33
+ ---
34
+
35
+ ## Inputs
36
+
37
+ | Port | Description |
38
+ |------|------------|
39
+ | `r` | Reference vector. |
40
+ | `x` | State measurement vector. |
41
+
42
+ ---
43
+
44
+ ## Outputs
45
+
46
+ | Port | Description |
47
+ |------|------------|
48
+ | `u` | Control input vector. |
49
+
50
+ ---
51
+
52
+ ## Notes
53
+
54
+ - The controller has no internal state.
55
+ - The control law is evaluated at each simulation step.
56
+ - Both reference and state inputs must be connected.
57
+
58
+
59
+ ---
60
+ © 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later
@@ -0,0 +1,39 @@
1
+ # External Input Block
2
+
3
+ ## Description
4
+
5
+ The External Input block represents a data input point in a simulation model. It allows external signals to be fed into the model during simulation. The block outputs the value of the external signal at each simulation time step.
6
+
7
+ ---
8
+
9
+ ## Parameters
10
+
11
+ | Name | Type | Description | Optional |
12
+ |------------|-------------|-------------|-------------|
13
+ | `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True |
14
+
15
+ ---
16
+
17
+ ## Inputs
18
+
19
+ | Port | Description |
20
+ |------|------------|
21
+ | `in` | External data signal $u$. |
22
+
23
+ ---
24
+
25
+ ## Outputs
26
+
27
+ | Port | Description |
28
+ |------|------------|
29
+ | `out` | Model data signal. |
30
+
31
+ ---
32
+
33
+ ## Notes
34
+
35
+ - The External Input block is **stateless**.
36
+
37
+
38
+ ---
39
+ © 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later
@@ -0,0 +1,39 @@
1
+ # External Output Block
2
+
3
+ ## Description
4
+
5
+ The External Output block represents a data output point in a simulation model. It allows the model to send signals to an external environment during simulation. The block outputs the value of the internal signal at each simulation time step.
6
+
7
+ ---
8
+
9
+ ## Parameters
10
+
11
+ | Name | Type | Description | Optional |
12
+ |------------|-------------|-------------|-------------|
13
+ | `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True |
14
+
15
+ ---
16
+
17
+ ## Inputs
18
+
19
+ | Port | Description |
20
+ |------|------------|
21
+ | `in` | Model data signal $u$. |
22
+
23
+ ---
24
+
25
+ ## Outputs
26
+
27
+ | Port | Description |
28
+ |------|------------|
29
+ | `out` | External data signal. |
30
+
31
+ ---
32
+
33
+ ## Notes
34
+
35
+ - The External Output block is **stateless**.
36
+
37
+
38
+ ---
39
+ © 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later
@@ -0,0 +1,70 @@
1
+ # Luenberger
2
+
3
+ ## Summary
4
+
5
+ The **Luenberger** block implements a discrete-time state observer that
6
+ estimates the system state from input and output measurements.
7
+
8
+ ---
9
+
10
+ ## Mathematical definition
11
+
12
+ The observer is defined by the equations:
13
+
14
+ $$
15
+ x̂[k+1] = A x̂[k] + B u[k] + L (y[k] - C x̂[k])
16
+ $$
17
+
18
+ $$
19
+ ŷ[k] = C x̂[k]
20
+ $$
21
+
22
+ where:
23
+ - $x̂[k]$ is the estimated state,
24
+ - $u[k]$ is the control input,
25
+ - $y[k]$ is the measured output,
26
+ - $ŷ[k]$ is the estimated output.
27
+
28
+ ---
29
+
30
+ ## Parameters
31
+
32
+ | Name | Type | Description | Optional |
33
+ |------------|-------------|-------------|-------------|
34
+ | `A` | array | System state matrix. | False |
35
+ | `B` | array | Input matrix. | False |
36
+ | `C` | array | Output matrix. | False |
37
+ | `L` | array | Observer gain matrix. | False |
38
+ | `x0` | array | Initial estimated state. If omitted, the estimate is initialized to zero. | True |
39
+ | `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True |
40
+
41
+ ---
42
+
43
+ ## Inputs
44
+
45
+ | Port | Description |
46
+ |------|------------|
47
+ | `u` | Control input. |
48
+ | `y` | Measured output. |
49
+
50
+ ---
51
+
52
+ ## Outputs
53
+
54
+ | Port | Description |
55
+ |------|------------|
56
+ | `x_hat` | Estimated state. |
57
+ | `y_hat` | Estimated output. |
58
+
59
+ ---
60
+
61
+ ## Notes
62
+
63
+ - The block has internal state.
64
+ - The observer has no direct feedthrough.
65
+ - Matrix $D$ is intentionally not supported to avoid algebraic loops.
66
+ - The state estimate is updated once per simulation step.
67
+
68
+
69
+ ---
70
+ © 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later