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,113 @@
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
+ from numpy.typing import ArrayLike
23
+ from pySimBlocks.core.block_source import BlockSource
24
+
25
+
26
+ class Sinusoidal(BlockSource):
27
+ """
28
+ Multi-dimensional sinusoidal signal source block (Option B).
29
+
30
+ Summary:
31
+ Generates sinusoidal signals element-wise on a 2D output array:
32
+ y(t) = amplitude * sin(2*pi*frequency*t + phase) + offset
33
+
34
+ Parameters may be scalars, vectors, or matrices. Only scalar-to-shape
35
+ broadcasting is allowed; all non-scalar parameters must share the same
36
+ shape.
37
+
38
+ Parameters (overview):
39
+ amplitude : float or array-like
40
+ Sinusoidal amplitude.
41
+ frequency : float or array-like
42
+ Sinusoidal frequency in Hertz.
43
+ phase : float or array-like, optional
44
+ Phase shift in radians.
45
+ offset : float or array-like, optional
46
+ Constant offset added to the signal.
47
+ sample_time : float, optional
48
+ Block execution period.
49
+
50
+ Outputs:
51
+ out : sinusoidal output signal (2D ndarray)
52
+
53
+ Notes:
54
+ - Stateless.
55
+ - Normalization:
56
+ scalar -> (1,1), 1D -> (n,1), 2D -> (m,n)
57
+ - Broadcasting:
58
+ Only (1,1) scalars are broadcast to the common shape.
59
+ No NumPy broadcasting beyond that.
60
+ - No implicit flattening is performed.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ name: str,
66
+ amplitude: ArrayLike,
67
+ frequency: ArrayLike,
68
+ offset: ArrayLike = 0.0,
69
+ phase: ArrayLike = 0.0,
70
+ sample_time: float | None = None,
71
+ ):
72
+ super().__init__(name, sample_time)
73
+
74
+ A = self._to_2d_array("amplitude", amplitude, dtype=float)
75
+ F = self._to_2d_array("frequency", frequency, dtype=float)
76
+ O = self._to_2d_array("offset", offset, dtype=float)
77
+ P = self._to_2d_array("phase", phase, dtype=float)
78
+
79
+ target_shape = self._resolve_common_shape({
80
+ "amplitude": A,
81
+ "frequency": F,
82
+ "offset": O,
83
+ "phase": P,
84
+ })
85
+
86
+ self.amplitude = self._broadcast_scalar_only("amplitude", A, target_shape)
87
+ self.frequency = self._broadcast_scalar_only("frequency", F, target_shape)
88
+ self.offset = self._broadcast_scalar_only("offset", O, target_shape)
89
+ self.phase = self._broadcast_scalar_only("phase", P, target_shape)
90
+
91
+ self.outputs["out"] = np.zeros(target_shape, dtype=float)
92
+
93
+
94
+ # --------------------------------------------------------------------------
95
+ # Public methods
96
+ # --------------------------------------------------------------------------
97
+ def initialize(self, t0: float) -> None:
98
+ self._compute_output(t0)
99
+
100
+ # ------------------------------------------------------------------
101
+ def output_update(self, t: float, dt: float) -> None:
102
+ self._compute_output(t)
103
+
104
+
105
+ # --------------------------------------------------------------------------
106
+ # Private methods
107
+ # --------------------------------------------------------------------------
108
+ def _compute_output(self, t: float) -> None:
109
+ self.outputs["out"] = (
110
+ self.amplitude
111
+ * np.sin(2.0 * np.pi * self.frequency * t + self.phase)
112
+ + self.offset
113
+ )
@@ -0,0 +1,138 @@
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
+ from numpy.typing import ArrayLike
23
+ from pySimBlocks.core.block_source import BlockSource
24
+
25
+
26
+ class Step(BlockSource):
27
+ """
28
+ Step signal source block.
29
+
30
+ Summary:
31
+ Generates a step signal that switches from an initial value to a final
32
+ value at a specified time.
33
+
34
+ Parameters (overview):
35
+ value_before : float or array-like
36
+ Output value before the step time. Scalar, vector, or matrix.
37
+ value_after : float or array-like
38
+ Output value after the step time. Scalar, vector, or matrix.
39
+ start_time : float
40
+ Time at which the step occurs.
41
+ sample_time : float, optional
42
+ Block execution period.
43
+
44
+ I/O:
45
+ Outputs:
46
+ out : step output signal.
47
+
48
+ Notes:
49
+ - The block has no internal state.
50
+ - Signals are normalized to 2D arrays:
51
+ scalar -> (1,1), 1D -> (n,1), 2D -> (m,n).
52
+ - If one value is scalar (1,1) and the other is not, scalar is broadcast
53
+ to match the other shape.
54
+ - EPS compensates floating-point rounding to ensure consistent behavior
55
+ on discrete time grids.
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ name: str,
61
+ value_before: ArrayLike = 0.0,
62
+ value_after: ArrayLike = 1.0,
63
+ start_time: float = 1.0,
64
+ sample_time: float | None = None,
65
+ eps: float = 1e-12,
66
+ ):
67
+ super().__init__(name, sample_time)
68
+
69
+ vb = self._to_2d_array("value_before", value_before, dtype=float)
70
+ va = self._to_2d_array("value_after", value_after, dtype=float)
71
+
72
+ vb, va = self._match_shapes_with_scalar_broadcast(vb, va)
73
+
74
+ # --- Validate start_time ---
75
+ if not isinstance(start_time, (float, int)):
76
+ raise TypeError(f"[{self.name}] start_time must be a float or int.")
77
+ self.start_time = float(start_time)
78
+
79
+ self.value_before = vb
80
+ self.value_after = va
81
+
82
+ self.outputs["out"] = None
83
+ self.EPS = float(eps)
84
+
85
+
86
+ # --------------------------------------------------------------------------
87
+ # Public methods
88
+ # --------------------------------------------------------------------------
89
+ def initialize(self, t0: float) -> None:
90
+ self.outputs["out"] = (
91
+ self.value_before.copy()
92
+ if t0 < self.start_time - self.EPS
93
+ else self.value_after.copy()
94
+ )
95
+
96
+ # ------------------------------------------------------------------
97
+ def output_update(self, t: float, dt: float) -> None:
98
+ self.outputs["out"] = (
99
+ self.value_before.copy()
100
+ if t < self.start_time - self.EPS
101
+ else self.value_after.copy()
102
+ )
103
+
104
+
105
+ # --------------------------------------------------------------------------
106
+ # Private methods
107
+ # --------------------------------------------------------------------------
108
+ @staticmethod
109
+ def _is_scalar_2d(arr: np.ndarray) -> bool:
110
+ return arr.shape == (1, 1)
111
+
112
+ # ------------------------------------------------------------------
113
+ def _match_shapes_with_scalar_broadcast(
114
+ self,
115
+ a: np.ndarray,
116
+ b: np.ndarray,
117
+ ) -> tuple[np.ndarray, np.ndarray]:
118
+ """
119
+ If shapes differ:
120
+ - allow scalar (1,1) to broadcast to the other's shape
121
+ - otherwise raise ValueError
122
+ """
123
+ if a.shape == b.shape:
124
+ return a, b
125
+
126
+ a_is_scalar = self._is_scalar_2d(a)
127
+ b_is_scalar = self._is_scalar_2d(b)
128
+
129
+ if a_is_scalar and not b_is_scalar:
130
+ return np.full(b.shape, float(a[0, 0])), b
131
+
132
+ if b_is_scalar and not a_is_scalar:
133
+ return a, np.full(a.shape, float(b[0, 0]))
134
+
135
+ raise ValueError(
136
+ f"[{self.name}] 'value_before' and 'value_after' must have compatible shapes. "
137
+ f"Got {a.shape} vs {b.shape}. Only scalar-to-shape broadcasting is allowed."
138
+ )
@@ -0,0 +1,107 @@
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
+ from numpy.typing import ArrayLike
23
+ from pySimBlocks.core.block_source import BlockSource
24
+
25
+
26
+ class WhiteNoise(BlockSource):
27
+ """
28
+ Multi-dimensional Gaussian white noise source block (Option B).
29
+
30
+ Summary:
31
+ Generates independent Gaussian noise samples at each simulation step,
32
+ element-wise on a 2D output array:
33
+ y = mean + std * N(0,1)
34
+
35
+ Parameters may be scalars, vectors, or matrices. Only scalar-to-shape
36
+ broadcasting is allowed; all non-scalar parameters must share the same
37
+ shape.
38
+
39
+ Parameters (overview):
40
+ mean : float or array-like, optional
41
+ Mean value of the noise.
42
+ std : float or array-like, optional
43
+ Standard deviation of the noise (must be >= 0 element-wise).
44
+ seed : int, optional
45
+ Random seed for reproducibility.
46
+ sample_time : float, optional
47
+ Block execution period.
48
+
49
+ Outputs:
50
+ out : noise output signal (2D ndarray)
51
+
52
+ Notes:
53
+ - Stateless (but uses an internal RNG).
54
+ - Normalization:
55
+ scalar -> (1,1), 1D -> (n,1), 2D -> (m,n)
56
+ - Broadcasting:
57
+ Only (1,1) scalars are broadcast to the common shape.
58
+ No NumPy broadcasting beyond that.
59
+ - No implicit flattening is performed.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ name: str,
65
+ mean: ArrayLike = 0.0,
66
+ std: ArrayLike = 1.0,
67
+ seed: int | None = None,
68
+ sample_time: float | None = None,
69
+ ):
70
+ super().__init__(name, sample_time)
71
+
72
+ M = self._to_2d_array("mean", mean, dtype=float)
73
+ S = self._to_2d_array("std", std, dtype=float)
74
+
75
+ target_shape = self._resolve_common_shape({
76
+ "mean": M,
77
+ "std": S,
78
+ })
79
+
80
+ self.mean = self._broadcast_scalar_only("mean", M, target_shape)
81
+ self.std = self._broadcast_scalar_only("std", S, target_shape)
82
+
83
+ # Validate std >= 0 element-wise
84
+ if np.any(self.std < 0.0):
85
+ raise ValueError(f"[{self.name}] std must be >= 0 (element-wise).")
86
+
87
+ self.rng = np.random.default_rng(seed)
88
+
89
+ self.outputs["out"] = np.zeros(target_shape, dtype=float)
90
+
91
+
92
+ # --------------------------------------------------------------------------
93
+ # Public methods
94
+ # --------------------------------------------------------------------------
95
+ def initialize(self, t0: float) -> None:
96
+ self.outputs["out"] = self._draw()
97
+
98
+ # ------------------------------------------------------------------
99
+ def output_update(self, t: float, dt: float) -> None:
100
+ self.outputs["out"] = self._draw()
101
+
102
+
103
+ # --------------------------------------------------------------------------
104
+ # Private methods
105
+ # --------------------------------------------------------------------------
106
+ def _draw(self) -> np.ndarray:
107
+ return self.mean + self.std * self.rng.standard_normal(self.mean.shape)
@@ -0,0 +1,27 @@
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.blocks.systems.linear_state_space import LinearStateSpace
22
+ from pySimBlocks.blocks.systems.polytopic_state_space import PolytopicStateSpace
23
+
24
+ __all__ = [
25
+ "LinearStateSpace",
26
+ "PolytopicStateSpace",
27
+ ]
@@ -0,0 +1,180 @@
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
+ from numpy.typing import ArrayLike
23
+ from pySimBlocks.core.block import Block
24
+
25
+
26
+ class LinearStateSpace(Block):
27
+ """
28
+ Discrete-time linear state-space system block.
29
+
30
+ Summary:
31
+ Implements a discrete-time linear state-space system without direct
32
+ feedthrough, defined by state and output equations.
33
+
34
+ Parameters (overview):
35
+ A : array-like
36
+ State transition matrix.
37
+ B : array-like
38
+ Input matrix.
39
+ C : array-like
40
+ Output matrix.
41
+ x0 : array-like, optional
42
+ Initial state vector.
43
+ sample_time : float, optional
44
+ Block execution period.
45
+
46
+ I/O:
47
+ Inputs:
48
+ u : input vector.
49
+ Outputs:
50
+ y : output vector.
51
+ x : state vector.
52
+
53
+ Notes:
54
+ - The system is strictly proper (no direct feedthrough).
55
+ - The block has internal state.
56
+ - Matrix D is intentionally not supported to avoid algebraic loops.
57
+ - The state is updated once per simulation step.
58
+ """
59
+
60
+ direct_feedthrough = False
61
+
62
+ def __init__(
63
+ self,
64
+ name: str,
65
+ A: ArrayLike,
66
+ B: ArrayLike,
67
+ C: ArrayLike,
68
+ x0: ArrayLike | None = None,
69
+ sample_time: float | None = None,
70
+ ):
71
+ super().__init__(name, sample_time)
72
+
73
+ self.A = np.asarray(A, dtype=float)
74
+ self.B = np.asarray(B, dtype=float)
75
+ self.C = np.asarray(C, dtype=float)
76
+
77
+ # --- basic matrix checks
78
+ if self.A.ndim != 2:
79
+ raise ValueError(f"[{self.name}] A must be 2D. Got shape {self.A.shape}.")
80
+ if self.B.ndim != 2:
81
+ raise ValueError(f"[{self.name}] B must be 2D. Got shape {self.B.shape}.")
82
+ if self.C.ndim != 2:
83
+ raise ValueError(f"[{self.name}] C must be 2D. Got shape {self.C.shape}.")
84
+
85
+ n = self.A.shape[0]
86
+ if self.A.shape != (n, n):
87
+ raise ValueError(f"[{self.name}] A must be square (n,n). Got {self.A.shape}.")
88
+
89
+ if self.B.shape[0] != n:
90
+ raise ValueError(
91
+ f"[{self.name}] B must have n rows. A is {self.A.shape}, B is {self.B.shape}."
92
+ )
93
+
94
+ if self.C.shape[1] != n:
95
+ raise ValueError(
96
+ f"[{self.name}] C must have n columns. A is {self.A.shape}, C is {self.C.shape}."
97
+ )
98
+
99
+ self._n = n
100
+ self._m = self.B.shape[1]
101
+ self._p = self.C.shape[0]
102
+
103
+ # --- initial state x0
104
+ if x0 is None:
105
+ x0_arr = np.zeros((n, 1), dtype=float)
106
+ else:
107
+ x0_arr = np.asarray(x0, dtype=float)
108
+ if x0_arr.ndim == 0:
109
+ # scalar x0 is not acceptable for n>1
110
+ x0_arr = x0_arr.reshape(1, 1)
111
+ elif x0_arr.ndim == 1:
112
+ x0_arr = x0_arr.reshape(-1, 1)
113
+ elif x0_arr.ndim == 2:
114
+ pass
115
+ else:
116
+ raise ValueError(f"[{self.name}] x0 must be 1D or 2D. Got shape {x0_arr.shape}.")
117
+
118
+ if x0_arr.shape != (n, 1):
119
+ raise ValueError(f"[{self.name}] x0 must have shape ({n}, 1). Got {x0_arr.shape}.")
120
+
121
+ self.state["x"] = x0_arr.copy()
122
+ self.next_state["x"] = x0_arr.copy()
123
+
124
+ # ports
125
+ self.inputs["u"] = None
126
+ self.outputs["y"] = None
127
+ self.outputs["x"] = None
128
+
129
+
130
+ # --------------------------------------------------------------------------
131
+ # Public methods
132
+ # --------------------------------------------------------------------------
133
+ def initialize(self, t0: float) -> None:
134
+ x = self.state["x"]
135
+ self.outputs["y"] = self.C @ x
136
+ self.outputs["x"] = x.copy()
137
+ self.next_state["x"] = x.copy()
138
+
139
+ # ------------------------------------------------------------------
140
+ def output_update(self, t: float, dt: float) -> None:
141
+ x = self.state["x"]
142
+ self.outputs["y"] = self.C @ x
143
+ self.outputs["x"] = x.copy()
144
+
145
+ # ------------------------------------------------------------------
146
+ def state_update(self, t: float, dt: float) -> None:
147
+ u = self.inputs["u"]
148
+ if u is None:
149
+ raise RuntimeError(f"[{self.name}] Input 'u' is not connected or not set.")
150
+
151
+ u_vec = self._to_col_vec("u", u, self._m)
152
+ x = self.state["x"]
153
+
154
+ self.next_state["x"] = self.A @ x + self.B @ u_vec
155
+
156
+
157
+ # --------------------------------------------------------------------------
158
+ # Private methods
159
+ # --------------------------------------------------------------------------
160
+ def _to_col_vec(self, name: str, value: ArrayLike, expected_rows: int) -> np.ndarray:
161
+ arr = np.asarray(value, dtype=float)
162
+
163
+ if arr.ndim == 0:
164
+ arr = arr.reshape(1, 1)
165
+ elif arr.ndim == 1:
166
+ arr = arr.reshape(-1, 1)
167
+ elif arr.ndim == 2:
168
+ pass
169
+ else:
170
+ raise ValueError(f"[{self.name}] {name} must be 1D or 2D. Got shape {arr.shape}.")
171
+
172
+ if arr.shape[1] != 1:
173
+ raise ValueError(f"[{self.name}] {name} must be a column vector (k,1). Got {arr.shape}.")
174
+
175
+ if arr.shape[0] != expected_rows:
176
+ raise ValueError(
177
+ f"[{self.name}] {name} must have shape ({expected_rows},1). Got {arr.shape}."
178
+ )
179
+
180
+ return arr