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 pySimBlocks.core.block import Block
23
+
24
+
25
+ class ExternalOutput(Block):
26
+ """
27
+ External output interface block.
28
+
29
+ Summary:
30
+ Pass-through block for external real-time commands/values.
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
+ Value produced by the model. Scalar, (n,), (n,1) accepted.
41
+ Output:
42
+ out: array (n,1)
43
+ Value forwarded to the external side as a column vector (n,1).
44
+
45
+ Policy:
46
+ - Accepts scalar, (n,), (n,1)
47
+ - Outputs strict (n,1)
48
+ - Once shape is known, it is frozen (cannot change)
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"] = None
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
+ # scalar
89
+ if arr.ndim == 0:
90
+ arr = arr.reshape(1, 1)
91
+
92
+ # vector (n,)
93
+ elif arr.ndim == 1:
94
+ arr = arr.reshape(-1, 1)
95
+
96
+ # column (n,1)
97
+ elif arr.ndim == 2 and arr.shape[1] == 1:
98
+ pass
99
+
100
+ else:
101
+ raise ValueError(
102
+ f"[{self.name}] Input 'in' must be scalar, (n,), or (n,1). Got shape {arr.shape}."
103
+ )
104
+
105
+ # Freeze shape
106
+ if self._resolved_shape is None:
107
+ self._resolved_shape = arr.shape
108
+ elif arr.shape != self._resolved_shape:
109
+ raise ValueError(
110
+ f"[{self.name}] Input 'in' shape changed: expected {self._resolved_shape}, got {arr.shape}."
111
+ )
112
+
113
+ return arr
@@ -0,0 +1,23 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from pySimBlocks.blocks.observers.luenberger import Luenberger
22
+
23
+ all = ["Luenberger"]
@@ -0,0 +1,175 @@
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 Luenberger(Block):
26
+ """
27
+ Discrete-time Luenberger state observer block.
28
+
29
+ Summary:
30
+ Estimates the state using:
31
+ y_hat[k] = C x_hat[k]
32
+ x_hat[k+1] = A x_hat[k] + B u[k] + L (y[k] - y_hat[k])
33
+
34
+ Parameters:
35
+ A : array-like, shape (n, n)
36
+ B : array-like, shape (n, m)
37
+ C : array-like, shape (p, n)
38
+ L : array-like, shape (n, p)
39
+ x0 : array-like, shape (n, 1), optional
40
+ sample_time : float, optional
41
+
42
+ Inputs:
43
+ u : array (m, 1)
44
+ y : array (p, 1)
45
+
46
+ Outputs:
47
+ x_hat : array (n, 1)
48
+ y_hat : array (p, 1)
49
+
50
+ Notes:
51
+ - Stateful block.
52
+ - No direct feedthrough.
53
+ - D matrix intentionally not supported.
54
+ - Input shapes are frozen once first seen.
55
+ """
56
+
57
+ direct_feedthrough = False
58
+
59
+ def __init__(
60
+ self,
61
+ name: str,
62
+ A,
63
+ B,
64
+ C,
65
+ L,
66
+ x0=None,
67
+ sample_time: float | None = None,
68
+ ):
69
+ super().__init__(name, sample_time)
70
+
71
+ # --- Matrices: strict 2D
72
+ self.A = np.asarray(A, dtype=float)
73
+ self.B = np.asarray(B, dtype=float)
74
+ self.C = np.asarray(C, dtype=float)
75
+ self.L = np.asarray(L, dtype=float)
76
+
77
+ for M_name, M in (("A", self.A), ("B", self.B), ("C", self.C), ("L", self.L)):
78
+ if M.ndim != 2:
79
+ raise ValueError(f"[{self.name}] {M_name} must be a 2D array. Got shape {M.shape}.")
80
+
81
+ n = self.A.shape[0]
82
+ if self.A.shape[1] != n:
83
+ raise ValueError(f"[{self.name}] A must be square (n,n). Got {self.A.shape}.")
84
+
85
+ if self.B.shape[0] != n:
86
+ raise ValueError(f"[{self.name}] B must have n rows to match A. Got B={self.B.shape}, A={self.A.shape}.")
87
+
88
+ if self.C.shape[1] != n:
89
+ raise ValueError(f"[{self.name}] C must have n columns to match A. Got C={self.C.shape}, A={self.A.shape}.")
90
+
91
+ p = self.C.shape[0]
92
+ m = self.B.shape[1]
93
+
94
+ if self.L.shape != (n, p):
95
+ raise ValueError(f"[{self.name}] L must have shape (n,p) = ({n},{p}). Got {self.L.shape}.")
96
+
97
+ self._n = n
98
+ self._m = m
99
+ self._p = p
100
+
101
+ # --- Initial state x0: strict (n,1), no flatten
102
+ if x0 is None:
103
+ x0_arr = np.zeros((n, 1), dtype=float)
104
+ else:
105
+ x0_arr = np.asarray(x0, dtype=float)
106
+ if x0_arr.ndim != 2 or x0_arr.shape != (n, 1):
107
+ raise ValueError(f"[{self.name}] x0 must have shape ({n},1). Got {x0_arr.shape}.")
108
+
109
+ self.state["x_hat"] = x0_arr.copy()
110
+ self.next_state["x_hat"] = x0_arr.copy()
111
+
112
+ # --- Ports
113
+ self.inputs["u"] = None
114
+ self.inputs["y"] = None
115
+ self.outputs["y_hat"] = None
116
+ self.outputs["x_hat"] = None
117
+
118
+ # Freeze input shapes once first seen
119
+ self._input_shapes = {}
120
+
121
+
122
+ # --------------------------------------------------------------------------
123
+ # Public methods
124
+ # --------------------------------------------------------------------------
125
+ def initialize(self, t0: float) -> None:
126
+ x_hat = self.state["x_hat"]
127
+ self.outputs["x_hat"] = x_hat.copy()
128
+ self.outputs["y_hat"] = self.C @ x_hat
129
+ self.next_state["x_hat"] = x_hat.copy()
130
+
131
+ # ------------------------------------------------------------------
132
+ def output_update(self, t: float, dt: float) -> None:
133
+ x_hat = self.state["x_hat"]
134
+ self.outputs["x_hat"] = x_hat.copy()
135
+ self.outputs["y_hat"] = self.C @ x_hat
136
+
137
+ # ------------------------------------------------------------------
138
+ def state_update(self, t: float, dt: float) -> None:
139
+ u = self._require_col_vector("u", self._m)
140
+ y = self._require_col_vector("y", self._p)
141
+
142
+ x_hat = self.state["x_hat"]
143
+ y_hat = self.C @ x_hat
144
+
145
+ self.next_state["x_hat"] = self.A @ x_hat + self.B @ u + self.L @ (y - y_hat)
146
+
147
+
148
+ # --------------------------------------------------------------------------
149
+ # Private methods
150
+ # --------------------------------------------------------------------------
151
+ def _require_col_vector(self, port: str, expected_rows: int) -> np.ndarray:
152
+ val = self.inputs[port]
153
+ if val is None:
154
+ raise RuntimeError(f"[{self.name}] Input '{port}' is not connected or not set.")
155
+
156
+ arr = np.asarray(val, dtype=float)
157
+
158
+ # Strict: column vector only (no implicit flatten)
159
+ if arr.ndim != 2 or arr.shape[1] != 1:
160
+ raise ValueError(f"[{self.name}] Input '{port}' must be a column vector (n,1). Got {arr.shape}.")
161
+
162
+ if arr.shape[0] != expected_rows:
163
+ raise ValueError(
164
+ f"[{self.name}] Input '{port}' has wrong dimension: expected ({expected_rows},1), got {arr.shape}."
165
+ )
166
+
167
+ # Freeze shape
168
+ if port not in self._input_shapes:
169
+ self._input_shapes[port] = arr.shape
170
+ elif arr.shape != self._input_shapes[port]:
171
+ raise ValueError(
172
+ f"[{self.name}] Input '{port}' shape changed: expected {self._input_shapes[port]}, got {arr.shape}."
173
+ )
174
+
175
+ return arr
@@ -0,0 +1,47 @@
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.operators.dead_zone import DeadZone
22
+ from pySimBlocks.blocks.operators.demux import Demux
23
+ from pySimBlocks.blocks.operators.delay import Delay
24
+ from pySimBlocks.blocks.operators.discrete_derivator import DiscreteDerivator
25
+ from pySimBlocks.blocks.operators.discrete_integrator import DiscreteIntegrator
26
+ from pySimBlocks.blocks.operators.gain import Gain
27
+ from pySimBlocks.blocks.operators.mux import Mux
28
+ from pySimBlocks.blocks.operators.product import Product
29
+ from pySimBlocks.blocks.operators.rate_limiter import RateLimiter
30
+ from pySimBlocks.blocks.operators.saturation import Saturation
31
+ from pySimBlocks.blocks.operators.sum import Sum
32
+ from pySimBlocks.blocks.operators.zero_order_hold import ZeroOrderHold
33
+
34
+ __all__ = [
35
+ "DeadZone",
36
+ "Demux",
37
+ "Delay",
38
+ "DiscreteDerivator",
39
+ "DiscreteIntegrator",
40
+ "Gain",
41
+ "Mux",
42
+ "Product",
43
+ "RateLimiter",
44
+ "Saturation",
45
+ "Sum",
46
+ "ZeroOrderHold"
47
+ ]
@@ -0,0 +1,258 @@
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 importlib.util
22
+ import inspect
23
+ from pathlib import Path
24
+ from typing import Any, Callable, Dict, List
25
+
26
+ import numpy as np
27
+
28
+ from pySimBlocks.core.block import Block
29
+
30
+
31
+ class AlgebraicFunction(Block):
32
+ """
33
+ User-defined algebraic function block.
34
+
35
+ Summary:
36
+ Stateless block defined by a user-provided Python function:
37
+ y = g(t, dt, u1, u2, ...)
38
+
39
+ Parameters:
40
+ function : callable
41
+ User-defined function.
42
+ input_keys : list[str]
43
+ Names of input ports.
44
+ output_keys : list[str]
45
+ Names of output ports.
46
+ sample_time : float, optional
47
+ Block execution period.
48
+
49
+ Notes:
50
+ - Stateless.
51
+ - Function must return a dict with exactly output_keys.
52
+ - Inputs/outputs must be 2D numpy arrays (matrices allowed).
53
+ - Input/output shapes are frozen per port after first resolution.
54
+ """
55
+
56
+ direct_feedthrough = True
57
+ is_source = False
58
+
59
+ def __init__(
60
+ self,
61
+ name: str,
62
+ function: Callable,
63
+ input_keys: List[str],
64
+ output_keys: List[str],
65
+ sample_time: float | None = None,
66
+ ):
67
+ super().__init__(name=name, sample_time=sample_time)
68
+
69
+ if function is None or not callable(function):
70
+ raise TypeError(f"[{self.name}] 'function' must be callable.")
71
+
72
+ self._func = function
73
+ self.input_keys = list(input_keys)
74
+ self.output_keys = list(output_keys)
75
+
76
+ if len(self.input_keys) == 0:
77
+ raise ValueError(f"[{self.name}] input_keys cannot be empty.")
78
+ if len(self.output_keys) == 0:
79
+ raise ValueError(f"[{self.name}] output_keys cannot be empty.")
80
+
81
+ self.inputs: Dict[str, np.ndarray | None] = {k: None for k in self.input_keys}
82
+ self.outputs: Dict[str, np.ndarray | None] = {k: None for k in self.output_keys}
83
+
84
+ # Shape freeze per port
85
+ self._in_shapes: Dict[str, tuple[int, int] | None] = {k: None for k in self.input_keys}
86
+ self._out_shapes: Dict[str, tuple[int, int] | None] = {k: None for k in self.output_keys}
87
+
88
+ # --------------------------------------------------------------------------
89
+ # Class Methods
90
+ # --------------------------------------------------------------------------
91
+ @classmethod
92
+ def adapt_params(cls,
93
+ params: Dict[str, Any],
94
+ params_dir: Path | None = None) -> Dict[str, Any]:
95
+ """
96
+ Adapt parameters from yaml format to class constructor format.
97
+ Adapt function file and name in a yaml format into callable.
98
+ """
99
+ # --- 1. Extract function file and name
100
+ if params_dir is None:
101
+ raise ValueError("parameters_dir must be provided for AlgebraicFunction adapter.")
102
+ try:
103
+ file_path = params["file_path"]
104
+ func_name = params["function_name"]
105
+ except KeyError as e:
106
+ raise ValueError(
107
+ f"AlgebraicFunction adapter missing parameter: {e}"
108
+ )
109
+
110
+ # --- 2. Resolve file path (relative to project.yaml directory)
111
+ path = Path(file_path).expanduser()
112
+ if not path.is_absolute():
113
+ path = (params_dir / path).resolve()
114
+
115
+ if not path.exists():
116
+ raise FileNotFoundError(f"Function file not found: {path}")
117
+
118
+ # --- 3. Load module
119
+ spec = importlib.util.spec_from_file_location(path.stem, path)
120
+ module = importlib.util.module_from_spec(spec)
121
+ assert spec.loader is not None
122
+ spec.loader.exec_module(module)
123
+
124
+ # --- 4. Extract function
125
+ try:
126
+ func = getattr(module, func_name)
127
+ except AttributeError:
128
+ raise AttributeError(
129
+ f"Function '{func_name}' not found in {path}"
130
+ )
131
+
132
+ if not callable(func):
133
+ raise TypeError(
134
+ f"'{func_name}' in {path} is not callable"
135
+ )
136
+
137
+ # --- 5. Build adapted parameter dict
138
+ adapted = dict(params)
139
+ adapted.pop("file_path", None)
140
+ adapted.pop("function_name", None)
141
+ adapted["function"] = func
142
+
143
+ return adapted
144
+
145
+
146
+ # --------------------------------------------------------------------------
147
+ # Public Methods
148
+ # --------------------------------------------------------------------------
149
+ def initialize(self, t0: float):
150
+ self._validate_signature()
151
+
152
+ out = self._call_func(t0, 0, **self.inputs)
153
+ if not isinstance(out, dict):
154
+ raise RuntimeError(f"[{self.name}] function must return a dict.")
155
+
156
+ if set(out.keys()) != set(self.output_keys):
157
+ raise RuntimeError(
158
+ f"[{self.name}] output keys mismatch "
159
+ f"(expected {self.output_keys}, got {list(out.keys())})."
160
+ )
161
+ for k in self.output_keys:
162
+ self.outputs[k] = out[k]
163
+
164
+ # ------------------------------------------------------------------
165
+ def output_update(self, t: float, dt: float):
166
+ # collect inputs
167
+ kwargs: Dict[str, np.ndarray] = {}
168
+ for k in self.input_keys:
169
+ u = self.inputs[k]
170
+ if u is None:
171
+ raise RuntimeError(f"[{self.name}] input '{k}' is not set.")
172
+ u = np.asarray(u) # allow array-like injection, but freeze as ndarray 2D
173
+ self._check_freeze_shape("input", k, u, self._in_shapes)
174
+ kwargs[k] = u
175
+
176
+ # call function
177
+ out = self._call_func(t, dt, **kwargs)
178
+
179
+
180
+
181
+ # assign outputs
182
+ for k in self.output_keys:
183
+ y = out[k]
184
+ y = np.asarray(y)
185
+ self._check_freeze_shape("output", k, y, self._out_shapes)
186
+ self.outputs[k] = y
187
+
188
+ # ------------------------------------------------------------------
189
+ def state_update(self, t: float, dt: float):
190
+ return # stateless
191
+
192
+
193
+ # --------------------------------------------------------------------------
194
+ # Private Methods
195
+ # --------------------------------------------------------------------------
196
+ def _call_func(self, t: float, dt: float, **kwargs) -> Dict[str, np.ndarray]:
197
+ try:
198
+ out = self._func(t, dt, **kwargs)
199
+ except Exception as e:
200
+ raise RuntimeError(f"[{self.name}] function call error: {e}\n"
201
+ f"Must always return a dict with output keys: {self.output_keys}")
202
+
203
+ if not isinstance(out, dict):
204
+ raise RuntimeError(f"[{self.name}] function must return a dict.")
205
+
206
+ if not set(self.output_keys).issubset(out.keys()):
207
+ raise RuntimeError(
208
+ f"[{self.name}] missing output keys "
209
+ f"(expected {self.output_keys}, got {list(out.keys())})."
210
+ )
211
+
212
+ for k in self.output_keys:
213
+ y = out[k]
214
+ if not isinstance(y, np.ndarray):
215
+ raise RuntimeError(f"{self.name}: output '{k}' is not a numpy array")
216
+ if y.ndim > 2:
217
+ raise RuntimeError(f"{self.name}: output '{k}' must be at most 2D (got shape {y.shape})")
218
+ return out
219
+
220
+ # ------------------------------------------------------------------
221
+ def _validate_signature(self) -> None:
222
+ sig = inspect.signature(self._func)
223
+ params = list(sig.parameters.values())
224
+
225
+ if len(params) < 2:
226
+ raise ValueError(f"[{self.name}] function must have at least arguments (t, dt).")
227
+
228
+ if params[0].name != "t" or params[1].name != "dt":
229
+ raise ValueError(f"[{self.name}] first arguments must be (t, dt).")
230
+
231
+ # no *args / **kwargs / defaults
232
+ for p in params:
233
+ if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD,):
234
+ raise ValueError(f"[{self.name}] *args and **kwargs are not allowed.")
235
+
236
+ declared = [p.name for p in params[2:]]
237
+ if set(declared) != set(self.input_keys):
238
+ raise ValueError(
239
+ f"[{self.name}] function arguments mismatch.\n"
240
+ f"Expected inputs: {self.input_keys}\n"
241
+ f"Function declares: {declared}"
242
+ )
243
+
244
+ # ------------------------------------------------------------------
245
+ def _check_freeze_shape(self, which: str, key: str, arr: np.ndarray, store: Dict[str, tuple[int, int] | None]) -> None:
246
+ if not isinstance(arr, np.ndarray):
247
+ raise TypeError(f"[{self.name}] {which} '{key}' is not a numpy array.")
248
+ if arr.ndim != 2:
249
+ raise ValueError(f"[{self.name}] {which} '{key}' must be a 2D array. Got shape {arr.shape}.")
250
+
251
+ if store[key] is None:
252
+ store[key] = arr.shape
253
+ return
254
+
255
+ if arr.shape != store[key]:
256
+ raise ValueError(
257
+ f"[{self.name}] {which} '{key}' shape changed: expected {store[key]}, got {arr.shape}."
258
+ )