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,68 @@
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.controllers import Pid, StateFeedback
22
+ from pySimBlocks.blocks.interfaces import ExternalInput, ExternalOutput
23
+ from pySimBlocks.blocks.observers import Luenberger
24
+ from pySimBlocks.blocks.operators import (
25
+ DeadZone, Delay, DiscreteDerivator, DiscreteIntegrator,
26
+ Gain, Mux, Product, RateLimiter, Saturation, Sum, ZeroOrderHold
27
+ )
28
+ from pySimBlocks.blocks.sources import (
29
+ Constant,
30
+ FunctionSource,
31
+ Ramp,
32
+ Sinusoidal,
33
+ Step,
34
+ WhiteNoise,
35
+ )
36
+ from pySimBlocks.blocks.systems import LinearStateSpace, PolytopicStateSpace
37
+
38
+ __all__ = [
39
+ "Pid",
40
+ "StateFeedback",
41
+
42
+ "ExternalInput",
43
+ "ExternalOutput",
44
+
45
+ "Luenberger",
46
+
47
+ "DeadZone",
48
+ "Delay",
49
+ "DiscreteDerivator",
50
+ "DiscreteIntegrator",
51
+ "Gain",
52
+ "Mux",
53
+ "Product",
54
+ "RateLimiter",
55
+ "Saturation",
56
+ "Sum",
57
+ "ZeroOrderHold",
58
+
59
+ "Constant",
60
+ "FunctionSource",
61
+ "Ramp",
62
+ "Step",
63
+ "Sinusoidal",
64
+ "WhiteNoise",
65
+
66
+ "LinearStateSpace",
67
+ "PolytopicStateSpace",
68
+ ]
@@ -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.controllers.pid import Pid
22
+ from pySimBlocks.blocks.controllers.state_feedback import StateFeedback
23
+
24
+ __all__ = [
25
+ "Pid",
26
+ "StateFeedback"
27
+ ]
@@ -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
+ import warnings
22
+
23
+ import numpy as np
24
+ from numpy.typing import ArrayLike
25
+
26
+ from pySimBlocks.core.block import Block
27
+
28
+
29
+ class Pid(Block):
30
+ """
31
+ Discrete-time PID controller block.
32
+
33
+ Summary:
34
+ Implements a single-input single-output discrete PID controller,
35
+ similar to the Simulink PID block. The controller computes a control
36
+ command from an error signal using proportional, integral and/or
37
+ derivative actions, depending on the selected control mode.
38
+
39
+ Parameters (overview):
40
+ controller : str
41
+ Control mode. One of {"P", "PI", "PD", "PID"}.
42
+ Kp : float
43
+ Proportional gain.
44
+ Ki : float
45
+ Integral gain.
46
+ Kd : float
47
+ Derivative gain.
48
+ u_min : float, optional
49
+ Minimum output saturation.
50
+ u_max : float, optional
51
+ Maximum output saturation.
52
+ sample_time : float, optional
53
+ Controller sampling period.
54
+
55
+ I/O:
56
+ Inputs:
57
+ e : error signal.
58
+ Outputs:
59
+ u : control command.
60
+
61
+ Notes:
62
+ - The block is strictly SISO.
63
+ - Integral action introduces internal state.
64
+ - If a parameter is not provided, the block falls back to its
65
+ internal default value.
66
+ - Output saturation is applied only if u_min and/or u_max are defined.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ name: str,
72
+ controller: str = "PID",
73
+ Kp: ArrayLike = 0.0,
74
+ Ki: ArrayLike = 0.0,
75
+ Kd: ArrayLike = 0.0,
76
+ u_min: ArrayLike | None = None,
77
+ u_max: ArrayLike | None = None,
78
+ integration_method: str = "euler forward",
79
+ sample_time: float | None = None,
80
+ ):
81
+ super().__init__(name, sample_time)
82
+
83
+ controller = controller.upper()
84
+ allowed_types = {"P", "I", "PI", "PD", "PID"}
85
+ if controller not in allowed_types:
86
+ raise ValueError(
87
+ f"[{self.name}] Invalid controller type '{controller}'. Allowed: {allowed_types}"
88
+ )
89
+ self.controller = controller
90
+
91
+ self.integration_method = integration_method.lower()
92
+ allowed = ("euler forward", "euler backward")
93
+ if self.integration_method not in allowed:
94
+ raise ValueError(
95
+ f"[{self.name}] Unsupported method '{self.integration_method}'. Allowed: {allowed}"
96
+ )
97
+
98
+ # Gains (SISO, (1,1))
99
+ self.Kp = self._to_siso("Kp", Kp)
100
+ self.Ki = self._to_siso("Ki", Ki)
101
+ self.Kd = self._to_siso("Kd", Kd)
102
+
103
+ self.u_min = None if u_min is None else self._to_siso("u_min", u_min)
104
+ self.u_max = None if u_max is None else self._to_siso("u_max", u_max)
105
+
106
+ if self.u_min is not None and self.u_max is not None:
107
+ if float(self.u_min[0, 0]) > float(self.u_max[0, 0]):
108
+ raise ValueError(
109
+ f"[{self.name}] u_min ({self.u_min.item()}) must be <= u_max ({self.u_max.item()})."
110
+ )
111
+
112
+ # Warnings
113
+ self._validate_gains()
114
+
115
+ # Direct feedthrough policy
116
+ has_p = "P" in self.controller
117
+ has_d = "D" in self.controller
118
+ has_i = "I" in self.controller
119
+
120
+ if has_p or has_d:
121
+ self.direct_feedthrough = True
122
+ else:
123
+ # I-only
124
+ self.direct_feedthrough = (self.integration_method == "euler backward")
125
+
126
+ # Ports
127
+ self.inputs["e"] = None
128
+ self.outputs["u"] = None
129
+
130
+ # State (SISO)
131
+ self.state["x_i"] = np.zeros((1, 1), dtype=float)
132
+ self.state["e_prev"] = np.zeros((1, 1), dtype=float)
133
+ self.next_state["x_i"] = np.zeros((1, 1), dtype=float)
134
+ self.next_state["e_prev"] = np.zeros((1, 1), dtype=float)
135
+
136
+
137
+ # --------------------------------------------------------------------------
138
+ # Public methods
139
+ # --------------------------------------------------------------------------
140
+ def initialize(self, t0: float):
141
+ # Start at zero command, keep internal states at zero.
142
+ self.outputs["u"] = np.zeros((1, 1), dtype=float)
143
+
144
+ # ------------------------------------------------------------------
145
+ def output_update(self, t: float, dt: float):
146
+ e_in = self.inputs["e"]
147
+ if e_in is None:
148
+ raise RuntimeError(f"[{self.name}] Missing input 'e'.")
149
+
150
+ e = self._to_siso("e", e_in)
151
+
152
+ x_i = self.state["x_i"]
153
+ e_prev = self.state["e_prev"]
154
+
155
+ has_p = "P" in self.controller
156
+ has_i = "I" in self.controller
157
+ has_d = "D" in self.controller
158
+
159
+ P = self.Kp * e if has_p else np.zeros((1, 1), dtype=float)
160
+
161
+ if has_i:
162
+ if self.integration_method == "euler forward":
163
+ I = x_i
164
+ else:
165
+ I = x_i + self.Ki * e * dt
166
+ else:
167
+ I = np.zeros((1, 1), dtype=float)
168
+
169
+ D = (self.Kd * (e - e_prev) / dt) if has_d else np.zeros((1, 1), dtype=float)
170
+
171
+ u = P + I + D
172
+
173
+ # Saturation
174
+ if self.u_min is not None:
175
+ u = np.maximum(u, self.u_min)
176
+ if self.u_max is not None:
177
+ u = np.minimum(u, self.u_max)
178
+
179
+ self.outputs["u"] = u
180
+
181
+ # ------------------------------------------------------------------
182
+ def state_update(self, t: float, dt: float):
183
+ e_in = self.inputs["e"]
184
+ if e_in is None:
185
+ raise RuntimeError(f"[{self.name}] Missing input 'e'.")
186
+
187
+ e = self._to_siso("e", e_in)
188
+
189
+ has_i = "I" in self.controller
190
+
191
+ # Integrator update only if I term is enabled
192
+ if has_i:
193
+ x_i_next = self.state["x_i"] + self.Ki * e * dt
194
+ else:
195
+ x_i_next = self.state["x_i"].copy()
196
+
197
+ # Anti-windup (clamp integral state if saturation is defined)
198
+ if self.u_min is not None:
199
+ x_i_next = np.maximum(x_i_next, self.u_min)
200
+ if self.u_max is not None:
201
+ x_i_next = np.minimum(x_i_next, self.u_max)
202
+
203
+ self.next_state["x_i"] = x_i_next
204
+ self.next_state["e_prev"] = e.copy()
205
+
206
+
207
+ # --------------------------------------------------------------------------
208
+ # Private methods
209
+ # --------------------------------------------------------------------------
210
+ def _to_siso(self, name: str, value: ArrayLike) -> np.ndarray:
211
+ """
212
+ Normalize scalar-like into (1,1). Reject anything else (strict SISO).
213
+ """
214
+ if np.isscalar(value):
215
+ return np.array([[float(value)]], dtype=float)
216
+
217
+ arr = np.asarray(value, dtype=float)
218
+
219
+ if arr.shape == ():
220
+ return np.array([[float(arr)]], dtype=float)
221
+ if arr.shape == (1,):
222
+ return arr.reshape(1, 1)
223
+ if arr.shape == (1, 1):
224
+ return arr
225
+
226
+ raise ValueError(
227
+ f"[{self.name}] '{name}' must be scalar-like ((), (1,), or (1,1)). Got shape {arr.shape}."
228
+ )
229
+
230
+ # ------------------------------------------------------------------
231
+ def _validate_gains(self) -> None:
232
+ kp = float(self.Kp[0, 0])
233
+ ki = float(self.Ki[0, 0])
234
+ kd = float(self.Kd[0, 0])
235
+
236
+ if "P" in self.controller and kp == 0.0:
237
+ warnings.warn(
238
+ f"[{self.name}] Kp=0 while controller '{self.controller}' includes a P term.",
239
+ UserWarning,
240
+ )
241
+ if "I" in self.controller and ki == 0.0:
242
+ warnings.warn(
243
+ f"[{self.name}] Ki=0 while controller '{self.controller}' includes an I term.",
244
+ UserWarning,
245
+ )
246
+ if "D" in self.controller and kd == 0.0:
247
+ warnings.warn(
248
+ f"[{self.name}] Kd=0 while controller '{self.controller}' includes a D term.",
249
+ UserWarning,
250
+ )
@@ -0,0 +1,141 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ import numpy as np
22
+ from pySimBlocks.core.block import Block
23
+
24
+
25
+ class StateFeedback(Block):
26
+ """
27
+ Discrete-time state-feedback controller block.
28
+
29
+ Summary:
30
+ Implements a static discrete-time state-feedback control law:
31
+ u = G @ r - K @ x
32
+
33
+ Parameters:
34
+ K : array-like, shape (m, n)
35
+ State feedback gain matrix.
36
+ G : array-like, shape (m, p)
37
+ Reference feedforward gain matrix.
38
+ sample_time : float, optional
39
+ Block execution period.
40
+
41
+ Inputs:
42
+ r : array (p, 1)
43
+ Reference vector.
44
+ x : array (n, 1)
45
+ State vector.
46
+
47
+ Outputs:
48
+ u : array (m, 1)
49
+ Control vector.
50
+
51
+ Notes:
52
+ - Stateless block.
53
+ - This block intentionally enforces column-vector inputs.
54
+ - No implicit flattening is performed.
55
+ """
56
+
57
+ direct_feedthrough = True
58
+
59
+ def __init__(self, name: str, K, G, sample_time: float | None = None):
60
+ super().__init__(name, sample_time)
61
+
62
+ self.K = np.asarray(K, dtype=float)
63
+ self.G = np.asarray(G, dtype=float)
64
+
65
+ if self.K.ndim != 2:
66
+ raise ValueError(f"[{self.name}] K must be a 2D array (m,n). Got shape {self.K.shape}.")
67
+ if self.G.ndim != 2:
68
+ raise ValueError(f"[{self.name}] G must be a 2D array (m,p). Got shape {self.G.shape}.")
69
+
70
+ m, n = self.K.shape
71
+ m2, p = self.G.shape
72
+
73
+ if m != m2:
74
+ raise ValueError(
75
+ f"[{self.name}] Inconsistent dimensions: "
76
+ f"K is {self.K.shape} while G is {self.G.shape} (first dimension must match)."
77
+ )
78
+
79
+ # cached expected sizes for input validation
80
+ self._m = m
81
+ self._n = n
82
+ self._p = p
83
+
84
+ # Ports
85
+ self.inputs["r"] = None
86
+ self.inputs["x"] = None
87
+ self.outputs["u"] = None
88
+
89
+ # freeze input shapes once seen (optional but consistent)
90
+ self._input_shapes = {}
91
+
92
+ # --------------------------------------------------------------------------
93
+ # Public methods
94
+ # --------------------------------------------------------------------------
95
+ def initialize(self, t0: float):
96
+ r = self.inputs["r"]
97
+ x = self.inputs["x"]
98
+ if r is None or x is None:
99
+ self.outputs["u"] = np.zeros((self._m, 1))
100
+ return
101
+
102
+ try:
103
+ r = self._require_col_vector("r", self._p)
104
+ x = self._require_col_vector("x", self._n)
105
+ self.outputs["u"] = self.G @ r - self.K @ x
106
+ except Exception as _:
107
+ self.outputs["u"] = np.zeros((self._m, 1))
108
+
109
+ # ------------------------------------------------------------------
110
+ def output_update(self, t: float, dt: float):
111
+ r = self._require_col_vector("r", self._p)
112
+ x = self._require_col_vector("x", self._n)
113
+
114
+ self.outputs["u"] = self.G @ r - self.K @ x
115
+
116
+ # ------------------------------------------------------------------
117
+ def state_update(self, t: float, dt: float):
118
+ pass
119
+
120
+
121
+ # --------------------------------------------------------------------------
122
+ # Private methods
123
+ # --------------------------------------------------------------------------
124
+ def _require_col_vector(self, port: str, expected_rows: int) -> np.ndarray:
125
+ u = self.inputs[port]
126
+ if u is None:
127
+ raise RuntimeError(f"[{self.name}] Input '{port}' is not connected or not set.")
128
+
129
+ arr = np.asarray(u, dtype=float)
130
+
131
+ if arr.ndim != 2 or arr.shape[1] != 1:
132
+ raise ValueError(
133
+ f"[{self.name}] Input '{port}' must be a column vector (n,1). Got shape {arr.shape}."
134
+ )
135
+
136
+ if arr.shape[0] != expected_rows:
137
+ raise ValueError(
138
+ f"[{self.name}] Input '{port}' has wrong dimension: expected ({expected_rows},1), got {arr.shape}."
139
+ )
140
+
141
+ return arr
@@ -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.interfaces.external_input import ExternalInput
22
+ from pySimBlocks.blocks.interfaces.external_output import ExternalOutput
23
+
24
+ __all__ = [
25
+ "ExternalInput",
26
+ "ExternalOutput"
27
+ ]
@@ -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 pySimBlocks.core.block import Block
23
+
24
+
25
+ class ExternalInput(Block):
26
+ """
27
+ External input interface block.
28
+
29
+ Summary:
30
+ Pass-through block for external real-time measurements.
31
+
32
+ Parameters:
33
+ sample_time: float, optional
34
+ Execution period of the block. If not specified, the simulator time
35
+ step is used.
36
+
37
+ I/O:
38
+ Input:
39
+ in: array (n,1)
40
+ External measurement value. Scalar, (n,), (n,1) accepted.
41
+ Output:
42
+ out: array (n,1)
43
+ Measurement forwarded to the model as a column vector (n,1).
44
+ Policy:
45
+ - Accepts scalar, (n,), (n,1)
46
+ - Outputs strict (n,1)
47
+ - Once shape is known, it is frozen (cannot change)
48
+ """
49
+
50
+ direct_feedthrough = True
51
+
52
+ def __init__(self, name: str, sample_time: float | None = None):
53
+ super().__init__(name, sample_time)
54
+ self.inputs["in"] = None
55
+ self.outputs["out"] = None
56
+ self._resolved_shape: tuple[int, int] | None = None
57
+
58
+
59
+ # --------------------------------------------------------------------------
60
+ # Public methods
61
+ # --------------------------------------------------------------------------
62
+ def initialize(self, t0: float) -> None:
63
+ u = self.inputs["in"]
64
+ if u is None:
65
+ self.outputs["out"] = np.zeros((1, 1))
66
+ return
67
+
68
+ self.outputs["out"] = self._to_col_vec(u)
69
+
70
+ # ------------------------------------------------------------------
71
+ def output_update(self, t: float, dt: float) -> None:
72
+ u = self.inputs["in"]
73
+ if u is None:
74
+ raise RuntimeError(f"[{self.name}] Missing input 'in'.")
75
+ self.outputs["out"] = self._to_col_vec(u)
76
+
77
+ # ------------------------------------------------------------------
78
+ def state_update(self, t: float, dt: float) -> None:
79
+ pass
80
+
81
+
82
+ # --------------------------------------------------------------------------
83
+ # Private methods
84
+ # --------------------------------------------------------------------------
85
+ def _to_col_vec(self, value) -> np.ndarray:
86
+ arr = np.asarray(value, dtype=float)
87
+
88
+ if arr.ndim == 0:
89
+ arr = arr.reshape(1, 1)
90
+ elif arr.ndim == 1:
91
+ arr = arr.reshape(-1, 1)
92
+ elif arr.ndim == 2 and arr.shape[1] == 1:
93
+ pass
94
+ else:
95
+ raise ValueError(
96
+ f"[{self.name}] Input 'in' must be scalar, (n,), or (n,1). Got shape {arr.shape}."
97
+ )
98
+
99
+ # Freeze shape
100
+ if self._resolved_shape is None:
101
+ self._resolved_shape = arr.shape
102
+ elif arr.shape != self._resolved_shape:
103
+ raise ValueError(
104
+ f"[{self.name}] Input 'in' shape changed: expected {self._resolved_shape}, got {arr.shape}."
105
+ )
106
+
107
+ return arr