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,94 @@
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
+
23
+ from pySimBlocks.core.block import Block
24
+
25
+
26
+ class BlockSource(Block):
27
+ """
28
+ Base class for all source blocks (Constant, Step, Ramp, Sinusoidal, ...).
29
+
30
+ Provides:
31
+ - normalization utilities for source parameters to produce 2D signals
32
+ - strict scalar-only broadcasting to a common 2D shape
33
+ - no state update by default
34
+ """
35
+
36
+ direct_feedthrough = False
37
+ is_source = True
38
+
39
+ def __init__(self, name: str, sample_time: float | None = None):
40
+ super().__init__(name, sample_time)
41
+
42
+
43
+ # --------------------------------------------------------------------------
44
+ # Public methods
45
+ # --------------------------------------------------------------------------
46
+ def state_update(self, t: float, dt: float) -> None:
47
+ pass # all sources are stateless
48
+
49
+
50
+ # --------------------------------------------------------------------------
51
+ # Private methods
52
+ # --------------------------------------------------------------------------
53
+ def _resolve_common_shape(self, params: dict[str, np.ndarray]) -> tuple[int, int]:
54
+ """
55
+ Determine the common target shape among parameters.
56
+
57
+ Policy:
58
+ - scalars (1,1) are broadcastable
59
+ - any non-scalar (2D not (1,1)) fixes the target shape
60
+ - if multiple non-scalars exist, all must have exactly the same shape
61
+ - if all are scalars -> target shape is (1,1)
62
+ """
63
+ non_scalar_shapes = {a.shape for a in params.values() if not self._is_scalar_2d(a)}
64
+
65
+ if len(non_scalar_shapes) == 0:
66
+ return (1, 1)
67
+
68
+ if len(non_scalar_shapes) == 1:
69
+ return next(iter(non_scalar_shapes))
70
+
71
+ details = ", ".join(f"{k}={v.shape}" for k, v in params.items())
72
+ raise ValueError(
73
+ f"[{self.name}] Inconsistent parameter shapes. "
74
+ f"All non-scalar parameters must have the same (m,n) shape. Got: {details}"
75
+ )
76
+
77
+ # ------------------------------------------------------------------
78
+ def _broadcast_scalar_only(self, param_name: str, arr: np.ndarray, target_shape: tuple[int, int]) -> np.ndarray:
79
+ """
80
+ Broadcast only scalar (1,1) to target_shape.
81
+ Any non-scalar must already match target_shape exactly.
82
+ """
83
+ if self._is_scalar_2d(arr):
84
+ if target_shape == (1, 1):
85
+ return arr.astype(float, copy=False)
86
+ return np.full(target_shape, float(arr[0, 0]), dtype=float)
87
+
88
+ if arr.shape != target_shape:
89
+ raise ValueError(
90
+ f"[{self.name}] '{param_name}' shape {arr.shape} is incompatible with "
91
+ f"target shape {target_shape}. Only scalar-to-shape broadcasting is allowed."
92
+ )
93
+
94
+ return arr.astype(float, copy=False)
@@ -0,0 +1,90 @@
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 dataclasses import dataclass, field
22
+ from typing import Any, Dict, List
23
+
24
+
25
+ # ---------------------------------------------------------------------
26
+ # Classes
27
+ # ---------------------------------------------------------------------
28
+ @dataclass(frozen=True)
29
+ class SimulationConfig:
30
+ """
31
+ Simulation execution configuration.
32
+
33
+ This object contains ONLY execution-related parameters.
34
+ It must not contain any model or block-specific information.
35
+ """
36
+
37
+ dt: float
38
+ T: float
39
+ t0: float = 0.0
40
+ solver: str = "fixed"
41
+ logging: List[str] = field(default_factory=list)
42
+ clock: str = "internal" # "internal" or "external"
43
+
44
+ def validate(self) -> None:
45
+ """Verify that the configuration is valid.
46
+ (ie: dt > 0, T > t0, solver is known)
47
+ """
48
+ if self.dt <= 0.0:
49
+ raise ValueError("SimulationConfig.dt must be > 0")
50
+
51
+ if self.T <= self.t0:
52
+ raise ValueError("SimulationConfig.T must be > t0")
53
+
54
+ if self.solver not in {"fixed", "variable"}:
55
+ raise ValueError(
56
+ f"Unknown solver '{self.solver}'. "
57
+ "Allowed values: {'fixed', 'variable'}"
58
+ )
59
+
60
+ if self.clock not in {"internal", "external"}:
61
+ raise ValueError(
62
+ f"Unknown clock '{self.clock}'. "
63
+ "Allowed values: {'internal', 'external'}"
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class PlotConfig:
69
+ """
70
+ Plot configuration.
71
+
72
+ Describes how logged signals should be visualized.
73
+ This object contains NO plotting logic.
74
+ """
75
+
76
+ plots: List[Dict[str, Any]]
77
+
78
+ def validate(self) -> None:
79
+ """Verify that the configuration is valid.
80
+ (ie: each plot has required fields)
81
+ """
82
+ for i, plot in enumerate(self.plots):
83
+ if "signals" not in plot:
84
+ raise ValueError(
85
+ f"Plot #{i} is missing required field 'signals'"
86
+ )
87
+ if not isinstance(plot["signals"], list):
88
+ raise ValueError(
89
+ f"'signals' in plot #{i} must be a list"
90
+ )
@@ -0,0 +1,46 @@
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 FixedStepTimeManager:
22
+ """A time manager for fixed-step simulations.
23
+ Handle multiple sample times by ensuring they are multiples of the base time step.
24
+ """
25
+ def __init__(self, dt_base: float, sample_times: list[float]):
26
+
27
+ if dt_base <= 0:
28
+ raise ValueError("Base time step must be strictly positive.")
29
+
30
+ self.dt = dt_base
31
+ self._check_sample_times(sample_times)
32
+
33
+ def _check_sample_times(self, sample_times):
34
+ """Ensure all sample times are multiples of the base time step."""
35
+ eps = 1e-12
36
+ for st in sample_times:
37
+ ratio = st / self.dt
38
+ if abs(ratio - round(ratio)) > eps:
39
+ raise ValueError(
40
+ f"In fixed-step mode, sample_time={st} "
41
+ f"is not a multiple of base dt={self.dt}."
42
+ )
43
+
44
+ def next_dt(self, t):
45
+ """Get the next time step (always fixed)."""
46
+ return self.dt
@@ -0,0 +1,250 @@
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 collections import deque
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Tuple
24
+
25
+ from pySimBlocks.core.block import Block
26
+
27
+ # A connection is:
28
+ # ( (src_block, src_port), (dst_block, dst_port) )
29
+ Connection = Tuple[Tuple[str, str], Tuple[str, str]]
30
+
31
+
32
+ class Model:
33
+ """
34
+ Discrete-time block-diagram model (Simulink-like).
35
+
36
+ Responsibilities:
37
+ - Store blocks.
38
+ - Store signal connections.
39
+ - Build execution order (topological sort).
40
+ - Provide fast access to downstream connections.
41
+
42
+ Notes:
43
+ * Topological sorting is applied only to the combinational graph.
44
+ * Blocks with state (i.e., blocks where next_state is non-empty)
45
+ are treated as "cycle breakers" (delay elements),
46
+ exactly like Simulink does for algebraic loops.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ name: str = "model",
52
+ model_data: Dict[str, Any] | None = None,
53
+ params_dir: Path | None = None,
54
+ verbose: bool = False,
55
+ ):
56
+ self.name = name
57
+ self.verbose = verbose
58
+
59
+ self.blocks: Dict[str, Block] = {}
60
+ self.connections: List[Connection] = []
61
+
62
+ self._output_execution_order: List[Block] = []
63
+ self._state_execution_order: List[Block] = []
64
+
65
+ self._downstream_map: Dict[str, List[Connection]] = {}
66
+ self._connections_dirty: bool = True
67
+
68
+ if model_data is not None:
69
+ from pySimBlocks.project.build_model import build_model_from_dict
70
+ build_model_from_dict(self, model_data, params_dir=params_dir)
71
+
72
+ # --------------------------------------------------------------------------
73
+ # Public methods
74
+ # --------------------------------------------------------------------------
75
+ def add_block(self, block: Block) -> Block:
76
+ """Add a block to the model."""
77
+ if block.name in self.blocks:
78
+ raise ValueError(f"Block name '{block.name}' already exists.")
79
+
80
+ self.blocks[block.name] = block
81
+ return block
82
+
83
+ # ------------------------------------------------------------------
84
+ def get_block_by_name(self, name: str) -> Block:
85
+ """Get a block by its name."""
86
+ if name not in self.blocks:
87
+ raise ValueError(
88
+ f"Block name '{name}' not found. Known blocks: {list(self.blocks.keys())}"
89
+ )
90
+ return self.blocks[name]
91
+
92
+ # ------------------------------------------------------------------
93
+ def connect(self, src_block: str, src_port: str,
94
+ dst_block: str, dst_port: str) -> None:
95
+ """
96
+ Connect:
97
+ blocks[src_block].outputs[src_port]
98
+ to:
99
+ blocks[dst_block].inputs[dst_port]
100
+ """
101
+ if src_block not in self.blocks:
102
+ raise ValueError(
103
+ f"Unknown source block '{src_block}'. "
104
+ f"Known blocks: {list(self.blocks.keys())}"
105
+ )
106
+ if dst_block not in self.blocks:
107
+ raise ValueError(
108
+ f"Unknown destination block '{dst_block}'. "
109
+ f"Known blocks: {list(self.blocks.keys())}"
110
+ )
111
+
112
+ self.connections.append(
113
+ ((src_block, src_port), (dst_block, dst_port))
114
+ )
115
+ self._connections_dirty = True
116
+
117
+ # ------------------------------------------------------------------
118
+ def build_execution_order(self):
119
+ """
120
+ Build Simulink-like execution order based solely on direct-feedthrough
121
+ causal dependencies (Simulink PDF p.7).
122
+ """
123
+
124
+ blocks = self.blocks
125
+ names = list(blocks.keys())
126
+
127
+ vprint = print if self.verbose else (lambda *a, **k: None)
128
+
129
+ vprint("\n================= BUILD EXECUTION ORDER =================")
130
+ vprint(f"Blocks in model: {names}")
131
+
132
+ # STEP 1 — Build dependency graph
133
+ vprint("\n--- STEP 1: CONNECTION ANALYSIS (direct-feedthrough rules) ---")
134
+
135
+ graph = {name: [] for name in names}
136
+ indegree = {name: 0 for name in names}
137
+
138
+ for (src, dst) in self.connections:
139
+ src_block, src_port = src
140
+ dst_block, dst_port = dst
141
+
142
+ block_dist = blocks[dst_block]
143
+
144
+ if block_dist.direct_feedthrough:
145
+ graph[src_block].append(dst_block)
146
+ indegree[dst_block] += 1
147
+ vprint(f" DEPENDENCY: {src_block}.{src_port} -> {dst_block}.{dst_port} "
148
+ f"(direct-feedthrough)")
149
+ else:
150
+ vprint(f" NO DEPENDENCY: {src_block}.{src_port} -> {dst_block}.{dst_port} "
151
+ f"(destination NOT direct-feedthrough)")
152
+
153
+ # Show resulting graph
154
+ vprint("\nGraph adjacency list:")
155
+ for k, v in graph.items():
156
+ vprint(f" {k}: {v}")
157
+
158
+ vprint("\nInitial indegree:")
159
+ for k, v in indegree.items():
160
+ vprint(f" {k}: {v}")
161
+
162
+ # STEP 2 — Kahn topological sort
163
+ vprint("\n--- STEP 2: TOPOLOGICAL SORT ---")
164
+
165
+ ready = deque([b for b in names if indegree[b] == 0])
166
+
167
+ vprint(f"Initial READY queue: {list(ready)}")
168
+
169
+ execution_order = []
170
+
171
+ while ready:
172
+ current = ready.popleft()
173
+ execution_order.append(current)
174
+
175
+ vprint(f"\n==> EXECUTE: '{current}'")
176
+
177
+ # Decrease indegree for successors
178
+ for succ in graph[current]:
179
+ indegree[succ] -= 1
180
+ vprint(f" indegree[{succ}] -> {indegree[succ]}")
181
+ if indegree[succ] == 0:
182
+ ready.append(succ)
183
+ vprint(f" '{succ}' added to READY")
184
+
185
+ # STEP 3 — Detect algebraic loops
186
+ if len(execution_order) != len(names):
187
+ vprint("\n!!! ALGEBRAIC LOOP DETECTED !!!")
188
+ raise RuntimeError(
189
+ "Algebraic loop detected: direct-feedthrough cycle exists."
190
+ )
191
+
192
+ # STEP 4 — Final result
193
+ vprint("\n--- FINAL SIMULINK-LIKE EXECUTION ORDER ---")
194
+ for i, b in enumerate(execution_order, 1):
195
+ vprint(f" {i}. {b}")
196
+ vprint("========================================================\n")
197
+
198
+ # Final storage
199
+ self._output_execution_order = [blocks[n] for n in execution_order]
200
+
201
+ return self._output_execution_order
202
+
203
+ # ------------------------------------------------------------------
204
+ def downstream_of(self, block_name: str):
205
+ """
206
+ Returns all connections where block_name is the source.
207
+ """
208
+ if self._connections_dirty or not self._downstream_map:
209
+ self._rebuild_downstream_map()
210
+ return self._downstream_map.get(block_name, [])
211
+
212
+ # ------------------------------------------------------------------
213
+ def execution_order(self):
214
+ """Get execution order, building it if necessary."""
215
+ if not self._output_execution_order:
216
+ return self.build_execution_order()
217
+ return self._output_execution_order
218
+
219
+ # ------------------------------------------------------------------
220
+ def predecessors_of(self, block_name):
221
+ """Get all blocks that feed into block_name."""
222
+ for (src, dst) in self.connections:
223
+ if dst[0] == block_name:
224
+ yield src[0]
225
+
226
+ # ------------------------------------------------------------------
227
+ def resolve_sample_times(self, dt):
228
+ """
229
+ Resolve effective sample times for all blocks.
230
+
231
+ Returns:
232
+ has_explicit_rate (bool): True if at least one block defines a sample_time.
233
+ """
234
+
235
+ for b in self.blocks.values():
236
+ if b.sample_time is None:
237
+ b._effective_sample_time = dt
238
+ else:
239
+ b._effective_sample_time = b.sample_time
240
+
241
+
242
+ # --------------------------------------------------------------------------
243
+ # Private methods
244
+ # --------------------------------------------------------------------------
245
+ def _rebuild_downstream_map(self) -> None:
246
+ downstream = {name: [] for name in self.blocks.keys()}
247
+ for (src, dst) in self.connections:
248
+ downstream[src[0]].append((src, dst))
249
+ self._downstream_map = downstream
250
+ self._connections_dirty = False
@@ -0,0 +1,30 @@
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.task import Task
22
+
23
+ class Scheduler:
24
+ """A simple scheduler for managing tasks based on their start times."""
25
+ def __init__(self, tasks: list[Task]):
26
+ self.tasks = sorted(tasks, key=lambda t: t.sample_time)
27
+
28
+ def active_tasks(self, t):
29
+ """Return the list of tasks that should run at time t."""
30
+ return [task for task in self.tasks if task.should_run(t)]