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,233 @@
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 os
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Literal
26
+
27
+ from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
28
+
29
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta, ParameterMeta
30
+ from pySimBlocks.gui.blocks.port_meta import PortMeta
31
+ from pySimBlocks.gui.models import BlockInstance, PortInstance
32
+
33
+
34
+ class NonLinearStateSpaceMeta(BlockMeta):
35
+
36
+ def __init__(self):
37
+ self.name = "Non Linear State Space"
38
+ self.category = "systems"
39
+ self.type = "non_linear_state_space"
40
+ self.summary = "User-defined non linear state space function implemented in Python."
41
+ self.description = (
42
+ "This block evaluates a user-provided Python function of the form:\n"
43
+ " x+ = f(t, dt, x, u1, u2, ...)\n"
44
+ " y = g(t, dt, x)\n"
45
+ "The function is loaded from an external Python file and executed at each\n"
46
+ "simulation step. Input and output ports are explicitly declared via\n"
47
+ "`input_keys` and `output_keys`.\n"
48
+ )
49
+
50
+ self.parameters = [
51
+ ParameterMeta(
52
+ name="file_path",
53
+ type="string",
54
+ required=True,
55
+ description="Path to the Python file containing the functions relative to the project.yaml file."
56
+ ),
57
+ ParameterMeta(
58
+ name="state_function_name",
59
+ type="string",
60
+ required=True,
61
+ description="Name of the state function to call inside the Python file."
62
+ ),
63
+ ParameterMeta(
64
+ name="output_function_name",
65
+ type="string",
66
+ required=True,
67
+ description="Name of the output function to call inside the Python file."
68
+ ),
69
+ ParameterMeta(
70
+ name="input_keys",
71
+ type="list[string]",
72
+ required=True,
73
+ description="List of input port names. These must match the function arguments after (t, dt)."
74
+ ),
75
+ ParameterMeta(
76
+ name="output_keys",
77
+ type="list[string]",
78
+ required=True,
79
+ description="List of output port names. The function must return a dict with exactly these keys."
80
+ ),
81
+ ParameterMeta(
82
+ name="x0",
83
+ type="vector",
84
+ required=True,
85
+ description="Initial state vector."
86
+ ),
87
+ ParameterMeta(
88
+ name="sample_time",
89
+ type="float",
90
+ description="Optional execution period of the block."
91
+ )
92
+ ]
93
+
94
+ self.inputs = [
95
+ PortMeta(
96
+ name="in",
97
+ display_as="",
98
+ shape=[] # shapes are dynamic
99
+ )
100
+ ]
101
+
102
+ self.outputs = [
103
+ PortMeta(
104
+ name="out",
105
+ display_as="",
106
+ shape=[] # shapes are dynamic
107
+ )
108
+ ]
109
+
110
+ # --------------------------------------------------------------------------
111
+ # Port resolution
112
+ # --------------------------------------------------------------------------
113
+ def resolve_port_group(
114
+ self,
115
+ port_meta: PortMeta,
116
+ direction: Literal['input', 'output'],
117
+ instance: "BlockInstance"
118
+ ) -> list["PortInstance"]:
119
+ if direction == 'input' and port_meta.name == 'in':
120
+ input_keys = instance.parameters.get("input_keys", [])
121
+ if input_keys is None:
122
+ return []
123
+ return [
124
+ PortInstance(
125
+ name=key,
126
+ display_as=key,
127
+ direction="input",
128
+ block=instance
129
+ ) for key in input_keys
130
+ ]
131
+ elif direction == 'output' and port_meta.name == 'out':
132
+ output_keys = instance.parameters.get("output_keys", [])
133
+ if output_keys is None:
134
+ return []
135
+ return [
136
+ PortInstance(
137
+ name=key,
138
+ display_as=key,
139
+ direction="output",
140
+ block=instance
141
+ ) for key in output_keys
142
+ ]
143
+ return super().resolve_port_group(port_meta, direction, instance)
144
+
145
+ # --------------------------------------------------------------------------
146
+ # Dialog methods
147
+ # --------------------------------------------------------------------------
148
+ def build_param(
149
+ self,
150
+ session,
151
+ form: QFormLayout,
152
+ readonly: bool = False,
153
+ ):
154
+ name_edit = QLineEdit(session.instance.name)
155
+ name_edit.textChanged.connect(
156
+ lambda val: self._on_param_changed(val, "name", session, readonly)
157
+ )
158
+ form.addRow(QLabel("Block name:"), name_edit)
159
+ if readonly:
160
+ name_edit.setReadOnly(True)
161
+ session.name_edit = name_edit
162
+
163
+ for pmeta in self.parameters:
164
+ if pmeta.name == "file_path":
165
+ self.build_file_param_row(
166
+ session,
167
+ form,
168
+ pmeta,
169
+ readonly=readonly,
170
+ file_filter="Python files (*.py);;All files (*)",
171
+ )
172
+ continue
173
+
174
+ label, widget = self._create_param_row(session, pmeta, readonly)
175
+ if widget is None:
176
+ continue
177
+ if readonly:
178
+ self._set_readonly_style(widget)
179
+
180
+ form.addRow(label, widget)
181
+ session.param_widgets[pmeta.name] = widget
182
+ session.param_labels[pmeta.name] = label
183
+
184
+ # ------------------------------------------------------
185
+ def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
186
+ open_btn = QPushButton("Open file")
187
+ open_btn.clicked.connect(lambda: self._open_file_from_session(session))
188
+ form.addRow(QLabel(""), open_btn)
189
+ session.open_file_btn = open_btn
190
+ self._refresh_open_button_state(session)
191
+
192
+ # ------------------------------------------------------
193
+ def refresh_form(self, session):
194
+ super().refresh_form(session)
195
+ self._refresh_open_button_state(session)
196
+
197
+ # ------------------------------------------------------
198
+ def _resolve_file_path(self, session) -> Path | None:
199
+ raw = session.local_params.get("file_path")
200
+ if not raw:
201
+ return None
202
+
203
+ path = Path(str(raw)).expanduser()
204
+ if not path.is_absolute() and session.project_dir is not None:
205
+ path = (session.project_dir / path).resolve()
206
+ return path
207
+
208
+ # ------------------------------------------------------
209
+ def _refresh_open_button_state(self, session) -> None:
210
+ btn = getattr(session, "open_file_btn", None)
211
+ if btn is None:
212
+ return
213
+
214
+ target = self._resolve_file_path(session)
215
+ exists = target is not None and target.is_file()
216
+ btn.setEnabled(exists)
217
+ if exists:
218
+ btn.setToolTip(str(target))
219
+ else:
220
+ btn.setToolTip("Set a valid existing file_path to open the file.")
221
+
222
+ # ------------------------------------------------------
223
+ def _open_file_from_session(self, session) -> None:
224
+ target = self._resolve_file_path(session)
225
+ if target is None or not target.is_file():
226
+ return
227
+
228
+ if sys.platform.startswith("darwin"):
229
+ subprocess.Popen(["open", str(target)])
230
+ elif os.name == "nt":
231
+ os.startfile(str(target))
232
+ else:
233
+ subprocess.Popen(["xdg-open", str(target)])
@@ -0,0 +1,109 @@
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.gui.blocks.block_meta import BlockMeta
22
+ from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
23
+ from pySimBlocks.gui.blocks.port_meta import PortMeta
24
+
25
+
26
+ class PolytopicStateSpaceMeta(BlockMeta):
27
+
28
+ def __init__(self):
29
+ self.name = "PolytopicStateSpace"
30
+ self.category = "systems"
31
+ self.type = "polytopic_state_space"
32
+ self.summary = "Discrete-time polytopic state-space system."
33
+ self.description = (
34
+ "Implements the discrete-time polytopic state-space equations:\n"
35
+ "$$\n"
36
+ "x[k+1] = A\\,\\mathrm{kron}(w[k], x[k]) + B\\,\\mathrm{kron}(w[k], u[k])\n"
37
+ "$$\n"
38
+ "$$\n"
39
+ "y[k] = C x[k]\n"
40
+ "$$\n"
41
+ "with $w[k]$ the vertex weight vector.\n"
42
+ )
43
+
44
+ self.parameters = [
45
+ ParameterMeta(
46
+ name="A",
47
+ type="matrix",
48
+ required=True,
49
+ autofill=True,
50
+ default=[[1.0, 0.0]],
51
+ description="Stacked polytopic state matrix of shape (nx, r*nx): [A1 ... Ar]."
52
+ ),
53
+ ParameterMeta(
54
+ name="B",
55
+ type="matrix",
56
+ required=True,
57
+ autofill=True,
58
+ default=[[1.0, 1.0]],
59
+ description="Stacked polytopic input matrix of shape (nx, r*nu): [B1 ... Br]."
60
+ ),
61
+ ParameterMeta(
62
+ name="C",
63
+ type="matrix",
64
+ required=True,
65
+ autofill=True,
66
+ default=[[1.0]],
67
+ description="Output matrix of shape (ny, nx)."
68
+ ),
69
+ ParameterMeta(
70
+ name="x0",
71
+ type="vector",
72
+ description="Initial state vector."
73
+ ),
74
+ ParameterMeta(
75
+ name="sample_time",
76
+ type="float",
77
+ description="Block execution period."
78
+ )
79
+ ]
80
+
81
+ self.inputs = [
82
+ PortMeta(
83
+ name="w",
84
+ display_as="w",
85
+ shape=["r", 1],
86
+ description="Vertex weights vector."
87
+ ),
88
+ PortMeta(
89
+ name="u",
90
+ display_as="u",
91
+ shape=["nu", 1],
92
+ description="Input vector."
93
+ )
94
+ ]
95
+
96
+ self.outputs = [
97
+ PortMeta(
98
+ name="x",
99
+ display_as="x",
100
+ shape=["nx", 1],
101
+ description="State vector."
102
+ ),
103
+ PortMeta(
104
+ name="y",
105
+ display_as="y",
106
+ shape=["ny", 1],
107
+ description="Output vector."
108
+ )
109
+ ]
@@ -0,0 +1,221 @@
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 os
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Literal
26
+
27
+ from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
28
+
29
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta
30
+ from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
31
+ from pySimBlocks.gui.blocks.port_meta import PortMeta
32
+ from pySimBlocks.gui.models import BlockInstance, PortInstance
33
+
34
+
35
+ class SofaExchangeIOMeta(BlockMeta):
36
+
37
+ def __init__(self):
38
+ self.name = "SofaExchangeIO"
39
+ self.category = "systems"
40
+ self.type = "sofa_exchange_i_o"
41
+ self.summary = "Interface block for exchanging signals between pySimBlocks and SOFA."
42
+ self.description = (
43
+ "Provides a stateless interface block that exposes dynamic input and output\n"
44
+ "ports to connect a pySimBlocks model with an external SOFA controller.\n"
45
+ "This block does not execute a SOFA simulation."
46
+ )
47
+
48
+ self.parameters = [
49
+ ParameterMeta(
50
+ name="scene_file",
51
+ type="string",
52
+ description="Path to the SOFA scene file used for automatic generation relative to project.yaml file."
53
+ ),
54
+ ParameterMeta(
55
+ name="input_keys",
56
+ type="list[string]",
57
+ required=True,
58
+ description="List of input keys corresponding to SOFA input ports."
59
+ ),
60
+ ParameterMeta(
61
+ name="output_keys",
62
+ type="list[string]",
63
+ required=True,
64
+ description="List of output keys corresponding to SOFA output ports."
65
+ ),
66
+ ParameterMeta(
67
+ name="slider_params",
68
+ type="dict",
69
+ description="Dictionary of slider parameters to be modified in the SOFA scene at runtime."
70
+ ),
71
+ ParameterMeta(
72
+ name="sample_time",
73
+ type="float"
74
+ )
75
+ ]
76
+
77
+ self.inputs = [
78
+ PortMeta(
79
+ name="sofa_inputs",
80
+ display_as="",
81
+ shape=[]
82
+ )
83
+ ]
84
+
85
+ self.outputs = [
86
+ PortMeta(
87
+ name="sofa_outputs",
88
+ display_as="",
89
+ shape=[]
90
+ )
91
+ ]
92
+
93
+ # --------------------------------------------------------------------------
94
+ # Port Resolution
95
+ # --------------------------------------------------------------------------
96
+ def resolve_port_group(
97
+ self,
98
+ port_meta: PortMeta,
99
+ direction: Literal["input", "output"],
100
+ instance: "BlockInstance"
101
+ ) -> list["PortInstance"]:
102
+
103
+ if direction == "input" and port_meta.name == "sofa_inputs":
104
+ keys = instance.parameters.get("input_keys", [])
105
+ if keys is None:
106
+ return []
107
+ return [
108
+ PortInstance(
109
+ name=key,
110
+ display_as=key,
111
+ direction="input",
112
+ block=instance
113
+ )
114
+ for key in keys
115
+ ]
116
+
117
+ if direction == "output" and port_meta.name == "sofa_outputs":
118
+ keys = instance.parameters.get("output_keys", [])
119
+ if keys is None:
120
+ return []
121
+ return [
122
+ PortInstance(
123
+ name=key,
124
+ display_as=key,
125
+ direction="output",
126
+ block=instance
127
+ )
128
+ for key in keys
129
+ ]
130
+
131
+ return super().resolve_port_group(port_meta, direction, instance)
132
+
133
+ # --------------------------------------------------------------------------
134
+ # Dialog Methods
135
+ # --------------------------------------------------------------------------
136
+ def build_param(
137
+ self,
138
+ session,
139
+ form: QFormLayout,
140
+ readonly: bool = False,
141
+ ):
142
+ name_edit = QLineEdit(session.instance.name)
143
+ name_edit.textChanged.connect(
144
+ lambda val: self._on_param_changed(val, "name", session, readonly)
145
+ )
146
+ form.addRow(QLabel("Block name:"), name_edit)
147
+ if readonly:
148
+ name_edit.setReadOnly(True)
149
+ session.name_edit = name_edit
150
+
151
+ for pmeta in self.parameters:
152
+ if pmeta.name == "scene_file":
153
+ self.build_file_param_row(
154
+ session,
155
+ form,
156
+ pmeta,
157
+ readonly=readonly,
158
+ file_filter="SOFA scene files (*.py);;All files (*)",
159
+ )
160
+ continue
161
+
162
+ label, widget = self._create_param_row(session, pmeta, readonly)
163
+ if widget is None:
164
+ continue
165
+ if readonly:
166
+ self._set_readonly_style(widget)
167
+
168
+ form.addRow(label, widget)
169
+ session.param_widgets[pmeta.name] = widget
170
+ session.param_labels[pmeta.name] = label
171
+
172
+ # ------------------------------------------------------
173
+ def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
174
+ open_btn = QPushButton("Open file")
175
+ open_btn.clicked.connect(lambda: self._open_file_from_session(session))
176
+ form.addRow(QLabel(""), open_btn)
177
+ session.open_file_btn = open_btn
178
+ self._refresh_open_button_state(session)
179
+
180
+ # ------------------------------------------------------
181
+ def refresh_form(self, session):
182
+ super().refresh_form(session)
183
+ self._refresh_open_button_state(session)
184
+
185
+ # ------------------------------------------------------
186
+ def _resolve_file_path(self, session) -> Path | None:
187
+ raw = session.local_params.get("scene_file")
188
+ if not raw:
189
+ return None
190
+
191
+ path = Path(str(raw)).expanduser()
192
+ if not path.is_absolute() and session.project_dir is not None:
193
+ path = (session.project_dir / path).resolve()
194
+ return path
195
+
196
+ # ------------------------------------------------------
197
+ def _refresh_open_button_state(self, session) -> None:
198
+ btn = getattr(session, "open_file_btn", None)
199
+ if btn is None:
200
+ return
201
+
202
+ target = self._resolve_file_path(session)
203
+ exists = target is not None and target.is_file()
204
+ btn.setEnabled(exists)
205
+ if exists:
206
+ btn.setToolTip(str(target))
207
+ else:
208
+ btn.setToolTip("Set a valid existing scene_file to open the file.")
209
+
210
+ # ------------------------------------------------------
211
+ def _open_file_from_session(self, session) -> None:
212
+ target = self._resolve_file_path(session)
213
+ if target is None or not target.is_file():
214
+ return
215
+
216
+ if sys.platform.startswith("darwin"):
217
+ subprocess.Popen(["open", str(target)])
218
+ elif os.name == "nt":
219
+ os.startfile(str(target))
220
+ else:
221
+ subprocess.Popen(["xdg-open", str(target)])