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,309 @@
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 NonLinearStateSpace(Block):
32
+ """
33
+ User-defined algebraic function block.
34
+
35
+ Summary:
36
+ Stateless block defined by a user-provided Python function:
37
+ x+ = f(t, dt, x, u1, u2, ...)
38
+ y = g(t, dt, x)
39
+
40
+ Parameters:
41
+ state_function : callable
42
+ Function to compute next state.
43
+ output_function_name : callable
44
+ Function to compute outputs.
45
+ input_keys : list[str]
46
+ Names of input ports.
47
+ output_keys : list[str]
48
+ Names of output ports.
49
+ sample_time : float, optional
50
+ Block execution period.
51
+
52
+ I/O:
53
+ Inputs:
54
+ Defined dynamically by input_keys.
55
+ Outputs:
56
+ Defined dynamically by output_keys.
57
+
58
+ Notes:
59
+ - This block is stateless.
60
+ - Ports are fully declarative (V1).
61
+ - The function must return a dict with exactly output_keys.
62
+ - All inputs and outputs must be numpy arrays of shape (n,1).
63
+ """
64
+
65
+ direct_feedthrough = False
66
+ is_source = False
67
+
68
+ def __init__(
69
+ self,
70
+ name: str,
71
+ state_function: Callable,
72
+ output_function: Callable,
73
+ input_keys: List[str],
74
+ output_keys: List[str],
75
+ x0: np.ndarray,
76
+ sample_time: float | None = None,
77
+ ):
78
+ super().__init__(name=name, sample_time=sample_time)
79
+
80
+ # ---- parameters
81
+ self._state_func = state_function
82
+ self._output_func = output_function
83
+ self.input_keys = list(input_keys)
84
+ self.output_keys = list(output_keys)
85
+
86
+ # ---- initial state
87
+ if not isinstance(x0, np.ndarray):
88
+ raise TypeError(
89
+ f"{self.name}: x0 must be a numpy array"
90
+ )
91
+ if x0.ndim == 1:
92
+ x0 = x0.reshape(-1, 1)
93
+ elif x0.ndim != 2 or x0.shape[1] != 1:
94
+ raise ValueError(
95
+ f"{self.name}: x0 must have shape (n,1) or (n,)"
96
+ )
97
+ self.state["x"] = x0.copy()
98
+ self.next_state["x"] = x0.copy()
99
+
100
+ # --------------------------------------------------------------------------
101
+ # Class Methods
102
+ # --------------------------------------------------------------------------
103
+ @classmethod
104
+ def adapt_params(cls,
105
+ params: Dict[str, Any],
106
+ params_dir: Path | None = None) -> Dict[str, Any]:
107
+ """
108
+ Adapt parameters from yaml format to class constructor format.
109
+ Adapt function file and name in a yaml format into callable.
110
+ """
111
+ # --- 1. Validate required fields
112
+ if params_dir is None:
113
+ raise ValueError("parameters_dir must be provided for AlgebraicFunction adapter.")
114
+ try:
115
+ file_path = params["file_path"]
116
+ state_func_name = params["state_function_name"]
117
+ output_func_name = params["output_function_name"]
118
+ except KeyError as e:
119
+ raise ValueError(
120
+ f"NonLinearStateSpace adapter missing parameter: {e}"
121
+ )
122
+
123
+ # --- 2. Resolve file path (relative to project.yaml directory)
124
+ path = Path(file_path).expanduser()
125
+ if not path.is_absolute():
126
+ path = (params_dir / path).resolve()
127
+
128
+ if not path.exists():
129
+ raise FileNotFoundError(
130
+ f"NonLinearStateSpace function file not found: {path}"
131
+ )
132
+
133
+ # --- 3. Load module
134
+ spec = importlib.util.spec_from_file_location(path.stem, path)
135
+ module = importlib.util.module_from_spec(spec)
136
+ assert spec.loader is not None
137
+ spec.loader.exec_module(module)
138
+
139
+ # --- 4. Extract functions
140
+ try:
141
+ state_func: Callable = getattr(module, state_func_name)
142
+ except AttributeError:
143
+ raise AttributeError(
144
+ f"State function '{state_func_name}' not found in {path}"
145
+ )
146
+
147
+ try:
148
+ output_func: Callable = getattr(module, output_func_name)
149
+ except AttributeError:
150
+ raise AttributeError(
151
+ f"Output function '{output_func_name}' not found in {path}"
152
+ )
153
+
154
+ if not callable(state_func):
155
+ raise TypeError(
156
+ f"'{state_func_name}' in {path} is not callable"
157
+ )
158
+
159
+ if not callable(output_func):
160
+ raise TypeError(
161
+ f"'{output_func_name}' in {path} is not callable"
162
+ )
163
+
164
+ # --- 5. Build adapted parameter dict
165
+ adapted = dict(params)
166
+
167
+ adapted.pop("file_path", None)
168
+ adapted.pop("state_function_name", None)
169
+ adapted.pop("output_function_name", None)
170
+ adapted["state_function"] = state_func
171
+ adapted["output_function"] = output_func
172
+
173
+ return adapted
174
+
175
+
176
+ # --------------------------------------------------------------------------
177
+ # Public Methods
178
+ # --------------------------------------------------------------------------
179
+ def initialize(self, t0: float):
180
+ """
181
+ Load the user function and validate its signature.
182
+ """
183
+ self._validate_signature()
184
+
185
+ # ---- declare ports
186
+ for k in self.input_keys:
187
+ self.inputs[k] = None
188
+
189
+ for k in self.output_keys:
190
+ self.outputs[k] = None
191
+
192
+ # ------------------------------------------------------------------
193
+ def output_update(self, t: float, dt: float):
194
+ """
195
+ Compute outputs from current inputs.
196
+ """
197
+ assert self._output_func is not None
198
+
199
+ # ---- call function
200
+ x = self.state["x"]
201
+ out = self._call_output_func(t, dt, x=x)
202
+
203
+ # ---- assign outputs
204
+ for k in self.output_keys:
205
+ self.outputs[k] = out[k]
206
+
207
+ # ------------------------------------------------------------------
208
+ def state_update(self, t: float, dt: float):
209
+ """
210
+ Compute next state from current inputs.
211
+ """
212
+ assert self._output_func is not None
213
+
214
+ # ---- collect inputs
215
+ kwargs: Dict[str, np.ndarray] = {}
216
+ for k in self.input_keys:
217
+ u = self.inputs[k]
218
+ if not isinstance(u, np.ndarray):
219
+ raise TypeError(
220
+ f"{self.name}: input '{k}' is not a numpy array"
221
+ )
222
+ if u.ndim != 2 or u.shape[1] != 1:
223
+ raise ValueError(
224
+ f"{self.name}: input '{k}' must have shape (n,1)"
225
+ )
226
+ kwargs[k] = u
227
+
228
+ # ---- call function
229
+ x = self.state["x"]
230
+ out = self._state_func(t, dt, x=x, **kwargs)
231
+ self.next_state["x"] = out
232
+
233
+
234
+ # --------------------------------------------------------------------------
235
+ # Private Methods
236
+ # --------------------------------------------------------------------------
237
+ def _call_state_func(self, t, dt, x, **kwargs):
238
+ try:
239
+ out = self._state_func(t, dt, x, **kwargs)
240
+ except Exception as e:
241
+ raise RuntimeError(f"{self.name}: state function call error: {e}\n"
242
+ f"Must always return the next state as a column vector array.")
243
+
244
+ if not isinstance(out, np.ndarray):
245
+ raise RuntimeError(f"{self.name}: state function must return a numpy array")
246
+ if out.ndim != 2 or out.shape[1] != 1:
247
+ raise RuntimeError(f"{self.name}: state function must return an array of shape (n,1)")
248
+
249
+ return out
250
+
251
+ # ------------------------------------------------------------------
252
+ def _call_output_func(self, t, dt, x):
253
+ try:
254
+ out = self._output_func(t, dt, x)
255
+ except Exception as e:
256
+ raise RuntimeError(f"{self.name}: output function call error: {e}\n"
257
+ f"Must always return a dict with output keys: {self.output_keys}")
258
+
259
+ if not isinstance(out, dict):
260
+ raise RuntimeError(f"{self.name}: output function must return a dict")
261
+ if set(out.keys()) != set(self.output_keys):
262
+ raise RuntimeError(
263
+ f"{self.name}: output keys mismatch "
264
+ f"(expected {self.output_keys}, got {list(out.keys())})"
265
+ )
266
+ for k in self.output_keys:
267
+ y = out[k]
268
+ if not isinstance(y, np.ndarray):
269
+ raise RuntimeError(f"{self.name}: output '{k}' is not a numpy array")
270
+ if y.ndim != 2 or y.shape[1] != 1:
271
+ raise RuntimeError(f"{self.name}: output '{k}' must have shape (n,1)")
272
+
273
+ return out
274
+
275
+ # ------------------------------------------------------------------
276
+ def _validate_signature(self):
277
+ """
278
+ Validate function signature against input_keys.
279
+ """
280
+ assert self._state_func is not None
281
+ assert self._output_func is not None
282
+
283
+ for f in [self._state_func, self._output_func]:
284
+ sig = inspect.signature(f)
285
+ params = list(sig.parameters.values())
286
+
287
+ # ---- minimum signature: (t, dt, ...)
288
+ if len(params) < 3:
289
+ raise ValueError(
290
+ f"{self.name}: function must have at least arguments (t, dt, x)"
291
+ )
292
+
293
+ if params[0].name != "t" or params[1].name != "dt" or params[2].name != "x":
294
+ raise ValueError(
295
+ f"{self.name}: first arguments must be (t, dt, x)"
296
+ )
297
+
298
+ # ---- no *args / **kwargs / defaults
299
+ for p in params:
300
+ if p.kind not in (
301
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
302
+ ):
303
+ raise ValueError(
304
+ f"{self.name}: *args and **kwargs are not allowed"
305
+ )
306
+ if p.default is not inspect.Parameter.empty:
307
+ raise ValueError(
308
+ f"{self.name}: default arguments are not allowed"
309
+ )
@@ -0,0 +1,193 @@
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 PolytopicStateSpace(Block):
27
+ """
28
+ Discrete-time polytopic state-space block.
29
+
30
+ Summary:
31
+ Implements:
32
+ x[k+1] = sum_{i=1}^r w_i[k] (A_i x[k] + B_i u[k])
33
+ y[k] = C x[k]
34
+
35
+ Parameters (overview):
36
+ A : array-like
37
+ concatenation of A_i matrices [A_1, A_2, ..., A_r], shape (nx, r*nx).
38
+ B : array-like
39
+ concatenation of B_i matrices [B_1, B_2, ..., B_r], shape (nx, r*nu).
40
+ C : array-like
41
+ Output matrix.
42
+ x0 : array-like, optional
43
+ Initial state vector.
44
+ sample_time : float, optional
45
+ Block execution period.
46
+
47
+ I/O:
48
+ Inputs:
49
+ u : input vector.
50
+ w : vertex weight vector (must sum to 1).
51
+ Outputs:
52
+ y : output vector.
53
+ x : state vector.
54
+
55
+ Notes:
56
+ - The system is strictly proper (no direct feedthrough).
57
+ - The block has internal state.
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
+ if self.A.ndim != 2:
78
+ raise ValueError(f"[{self.name}] A must be 2D. Got shape {self.A.shape}.")
79
+ if self.B.ndim != 2:
80
+ raise ValueError(f"[{self.name}] B must be 2D. Got shape {self.B.shape}.")
81
+ if self.C.ndim != 2:
82
+ raise ValueError(f"[{self.name}] C must be 2D. Got shape {self.C.shape}.")
83
+
84
+ nx = self.A.shape[0]
85
+ if nx <= 0:
86
+ raise ValueError(f"[{self.name}] A must have at least one row.")
87
+ if self.A.shape[1] % nx != 0:
88
+ raise ValueError(
89
+ f"[{self.name}] A must have shape (nx, r*nx). Got {self.A.shape}."
90
+ )
91
+
92
+ r = self.A.shape[1] // nx
93
+ if r <= 0:
94
+ raise ValueError(f"[{self.name}] Number of vertices r must be >= 1.")
95
+
96
+ if self.B.shape[0] != nx:
97
+ raise ValueError(
98
+ f"[{self.name}] B must have nx rows. A is {self.A.shape}, B is {self.B.shape}."
99
+ )
100
+ if self.B.shape[1] % r != 0:
101
+ raise ValueError(
102
+ f"[{self.name}] B must have shape (nx, r*nu). A gives r={r}, B is {self.B.shape}."
103
+ )
104
+
105
+ nu = self.B.shape[1] // r
106
+ if nu <= 0:
107
+ raise ValueError(f"[{self.name}] Input size nu must be >= 1.")
108
+
109
+ if self.C.shape[1] != nx:
110
+ raise ValueError(
111
+ f"[{self.name}] C must have nx columns. A is {self.A.shape}, C is {self.C.shape}."
112
+ )
113
+
114
+ ny = self.C.shape[0]
115
+
116
+ self._nx = nx
117
+ self._nu = nu
118
+ self._ny = ny
119
+ self._r = r
120
+
121
+ if x0 is None:
122
+ x0_arr = np.zeros((nx, 1), dtype=float)
123
+ else:
124
+ x0_arr = self._to_col_vec("x0", x0, nx)
125
+
126
+ self.state["x"] = x0_arr.copy()
127
+ self.next_state["x"] = x0_arr.copy()
128
+
129
+ self.inputs["w"] = None
130
+ self.inputs["u"] = None
131
+ self.outputs["x"] = None
132
+ self.outputs["y"] = None
133
+
134
+ # --------------------------------------------------------------------------
135
+ # Public methods
136
+ # --------------------------------------------------------------------------
137
+ def initialize(self, t0: float) -> None:
138
+ x = self.state["x"]
139
+ self.outputs["x"] = x.copy()
140
+ self.outputs["y"] = self.C @ x
141
+ self.next_state["x"] = x.copy()
142
+
143
+ # ------------------------------------------------------------------
144
+ def output_update(self, t: float, dt: float) -> None:
145
+ x = self.state["x"]
146
+ self.outputs["x"] = x.copy()
147
+ self.outputs["y"] = self.C @ x
148
+
149
+ # ------------------------------------------------------------------
150
+ def state_update(self, t: float, dt: float) -> None:
151
+ w = self.inputs["w"]
152
+ u = self.inputs["u"]
153
+ if w is None:
154
+ raise RuntimeError(f"[{self.name}] Input 'w' is not connected or not set.")
155
+ if u is None:
156
+ raise RuntimeError(f"[{self.name}] Input 'u' is not connected or not set.")
157
+
158
+ w_vec = self._to_col_vec("w", w, self._r)
159
+ if not np.isclose(np.sum(w_vec), 1.0):
160
+ raise ValueError(f"[{self.name}] Vertex weights w must sum to 1. Got sum {np.sum(w_vec)}.")
161
+ if np.any(w_vec < 0):
162
+ raise ValueError(f"[{self.name}] Vertex weights w must be non-negative. Got {w_vec.flatten()}.")
163
+
164
+ u_vec = self._to_col_vec("u", u, self._nu)
165
+ x = self.state["x"]
166
+
167
+ x_next = self.A @ np.kron(w_vec, x) + self.B @ np.kron(w_vec, u_vec)
168
+ self.next_state["x"] = x_next
169
+
170
+ # --------------------------------------------------------------------------
171
+ # Private methods
172
+ # --------------------------------------------------------------------------
173
+ def _to_col_vec(self, name: str, value: ArrayLike, expected_rows: int) -> np.ndarray:
174
+ arr = np.asarray(value, dtype=float)
175
+
176
+ if arr.ndim == 0:
177
+ arr = arr.reshape(1, 1)
178
+ elif arr.ndim == 1:
179
+ arr = arr.reshape(-1, 1)
180
+ elif arr.ndim == 2:
181
+ pass
182
+ else:
183
+ raise ValueError(f"[{self.name}] {name} must be 1D or 2D. Got shape {arr.shape}.")
184
+
185
+ if arr.shape[1] != 1:
186
+ raise ValueError(f"[{self.name}] {name} must be a column vector (k,1). Got {arr.shape}.")
187
+
188
+ if arr.shape[0] != expected_rows:
189
+ raise ValueError(
190
+ f"[{self.name}] {name} must have shape ({expected_rows},1). Got {arr.shape}."
191
+ )
192
+
193
+ return arr
@@ -0,0 +1,30 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from pySimBlocks.blocks.systems.sofa.sofa_controller import SofaPysimBlocksController
22
+ from pySimBlocks.blocks.systems.sofa.sofa_exchange_i_o import SofaExchangeIO
23
+ from pySimBlocks.blocks.systems.sofa.sofa_plant import SofaPlant
24
+
25
+
26
+ __all__ =[
27
+ "SofaPysimBlocksController",
28
+ "SofaExchangeIO",
29
+ "SofaPlant",
30
+ ]