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,408 @@
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 ast
22
+ import os
23
+ from abc import ABC
24
+ from pathlib import Path
25
+ from typing import Any, Dict, Literal, Sequence
26
+
27
+ from PySide6.QtCore import Qt
28
+ from PySide6.QtWidgets import (
29
+ QComboBox,
30
+ QFileDialog,
31
+ QFormLayout,
32
+ QFrame,
33
+ QHBoxLayout,
34
+ QLabel,
35
+ QLineEdit,
36
+ QPushButton,
37
+ QSizePolicy,
38
+ QTextBrowser,
39
+ QVBoxLayout,
40
+ QWidget,
41
+ )
42
+
43
+ from pySimBlocks.gui.blocks.block_dialog_session import BlockDialogSession
44
+ from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
45
+ from pySimBlocks.gui.blocks.port_meta import PortMeta
46
+ from pySimBlocks.gui.models import BlockInstance, PortInstance
47
+
48
+
49
+ class BlockMeta(ABC):
50
+
51
+ """
52
+ Template for child class
53
+
54
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta
55
+ from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
56
+ from pySimBlocks.gui.blocks.port_meta import PortMeta
57
+
58
+ class MyBlockMeta(BlockMeta):
59
+
60
+ def __init__(self):
61
+ self.name = ""
62
+ self.category = ""
63
+ self.type = ""
64
+ self.summary = ""
65
+ self.description = (
66
+ ""
67
+ )
68
+
69
+ self.parameters = [
70
+ ParameterMeta(
71
+ name="",
72
+ type=""
73
+ ),
74
+ ]
75
+
76
+ self.inputs = [
77
+ PortMeta(
78
+ name="",
79
+ display_as=""
80
+ shape=...
81
+ ),
82
+ ]
83
+
84
+ self.outputs = [
85
+ PortMeta(
86
+ name="",
87
+ display_as=""
88
+ shape=...
89
+ ),
90
+ ]
91
+ """
92
+
93
+
94
+ # ----------- Mandatory class attributes (must be overridden) -----------
95
+ name: str
96
+ category: str
97
+ type: str
98
+ summary: str
99
+ description: str
100
+
101
+ # ----------- Optional declarations -----------
102
+ doc_path: Path | None = None
103
+ parameters: Sequence[ParameterMeta] = ()
104
+ inputs: Sequence[PortMeta] = ()
105
+ outputs: Sequence[PortMeta] = ()
106
+
107
+ # --------------------------------------------------------------------------
108
+ # Dialog session management
109
+ # --------------------------------------------------------------------------
110
+ def create_dialog_session(
111
+ self,
112
+ instance: BlockInstance,
113
+ project_dir: Path | None = None,
114
+ ) -> BlockDialogSession:
115
+ return BlockDialogSession(self, instance, project_dir)
116
+
117
+ # --------------------------------------------------------------------------
118
+ # Parameter resolution
119
+ # --------------------------------------------------------------------------
120
+ def is_parameter_active(self,
121
+ param_name: str,
122
+ instance_params: Dict[str, Any]) -> bool:
123
+ """
124
+ Default: all parameters are always active.
125
+ Children override if needed.
126
+ """
127
+ return True
128
+
129
+ # ------------------------------------------------------------
130
+ def gather_params(self, session: BlockDialogSession) -> dict[str, Any]:
131
+ # Keep full local state, including inactive params, so values are cached
132
+ # across visibility toggles and dialog reopen.
133
+ return session.local_params.copy()
134
+
135
+ # --------------------------------------------------------------------------
136
+ # Port resolution
137
+ # --------------------------------------------------------------------------
138
+ def resolve_port_group(self,
139
+ port_meta: PortMeta,
140
+ direction: Literal['input', 'output'],
141
+ instance: "BlockInstance"
142
+ ) -> list["PortInstance"]:
143
+ """
144
+ Default behavior: fixed port.
145
+ Children override for dynamic ports.
146
+ """
147
+ return [PortInstance(port_meta.name, port_meta.display_as, direction, instance)]
148
+
149
+ # ------------------------------------------------------------
150
+ def build_ports(self, instance: "BlockInstance") -> list["PortInstance"]:
151
+ ports = []
152
+
153
+ for pmeta in self.inputs:
154
+ ports.extend(self.resolve_port_group(pmeta, "input", instance))
155
+
156
+ for pmeta in self.outputs:
157
+ ports.extend(self.resolve_port_group(pmeta, "output", instance))
158
+
159
+ return ports
160
+
161
+
162
+ # --------------------------------------------------------------------------
163
+ # QT dialog display
164
+ # --------------------------------------------------------------------------
165
+ def build_description(self, form: QFormLayout):
166
+ """ Default description display. Children can override if needed. """
167
+ title = QLabel(f"<b>{self.name}</b>")
168
+ title.setAlignment(Qt.AlignmentFlag.AlignLeft)
169
+ form.addRow(title)
170
+
171
+ frame = QFrame()
172
+ frame.setFrameShape(QFrame.StyledPanel)
173
+ frame.setFrameShadow(QFrame.Raised)
174
+ frame.setLineWidth(1)
175
+
176
+ frame_layout = QVBoxLayout(frame)
177
+ frame_layout.setContentsMargins(8, 6, 8, 6)
178
+
179
+ desc = QTextBrowser()
180
+ desc.setMarkdown(self.description)
181
+ desc.setReadOnly(True)
182
+ desc.setFrameShape(QFrame.NoFrame)
183
+ desc.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
184
+ desc.document().setTextWidth(400)
185
+ desc.setFixedHeight(int(desc.document().size().height()) + 6)
186
+
187
+ frame_layout.addWidget(desc)
188
+ form.addRow(frame)
189
+
190
+
191
+ # ------------------------------------------------------
192
+ def build_pre_param(self,
193
+ session: BlockDialogSession,
194
+ form: QFormLayout,
195
+ readonly: bool = False):
196
+ """ Default: no pre-parameter widgets. Children override if needed. """
197
+ pass
198
+
199
+ # ------------------------------------------------------------
200
+ def build_param(self,
201
+ session: BlockDialogSession,
202
+ form: QFormLayout,
203
+ readonly: bool = False):
204
+ """ Default: no parameter widgets. Children override if needed. """
205
+
206
+
207
+ # --- Block name ---
208
+ name_edit = QLineEdit(session.instance.name)
209
+ name_edit.textChanged.connect(
210
+ lambda val: self._on_param_changed(val, "name", session, readonly)
211
+ )
212
+ form.addRow(QLabel("Block name:"), name_edit)
213
+ if readonly:
214
+ name_edit.setReadOnly(True)
215
+ session.name_edit = name_edit
216
+
217
+ # --- Parameters ---
218
+ for param_meta in self.parameters:
219
+ param_name = param_meta.name
220
+
221
+ label, widget = self._create_param_row(session, param_meta, readonly)
222
+ if widget is None:
223
+ continue
224
+ if readonly:
225
+ self._set_readonly_style(widget)
226
+
227
+ form.addRow(label, widget)
228
+
229
+ session.param_widgets[param_name] = widget
230
+ session.param_labels[param_name] = label
231
+
232
+
233
+ # ------------------------------------------------------------
234
+ def build_post_param(self,
235
+ session: BlockDialogSession,
236
+ form: QFormLayout,
237
+ readonly: bool = False):
238
+ """ Default: no post-parameter widgets. Children override if needed. """
239
+ pass
240
+
241
+ # ------------------------------------------------------------
242
+ def build_file_param_row(
243
+ self,
244
+ session: BlockDialogSession,
245
+ form: QFormLayout,
246
+ pmeta: ParameterMeta,
247
+ readonly: bool = False,
248
+ file_filter: str = "Python files (*.py);;All files (*)",
249
+ ) -> None:
250
+ edit = self._create_edit_widget(session, pmeta, readonly)
251
+ if readonly:
252
+ self._set_readonly_style(edit)
253
+
254
+ browse_btn = QPushButton("...")
255
+ browse_btn.setToolTip("Select file from disk")
256
+ browse_btn.setEnabled(not readonly)
257
+ browse_btn.clicked.connect(
258
+ lambda: self._browse_and_set_relative_file(edit, session.project_dir, file_filter)
259
+ )
260
+
261
+ row_widget = QWidget()
262
+ row_layout = QHBoxLayout(row_widget)
263
+ row_layout.setContentsMargins(0, 0, 0, 0)
264
+ row_layout.addWidget(edit)
265
+ row_layout.addWidget(browse_btn)
266
+
267
+ label = QLabel(f"{pmeta.name}:")
268
+ if pmeta.description:
269
+ label.setToolTip(pmeta.description)
270
+
271
+ form.addRow(label, row_widget)
272
+ session.param_widgets[pmeta.name] = row_widget
273
+ session.param_labels[pmeta.name] = label
274
+
275
+ # ------------------------------------------------------------
276
+ def refresh_form(self, session: BlockDialogSession):
277
+ """
278
+ Refresh the parameter widgets visibility based on
279
+ BlockMeta.is_parameter_active and current local_params.
280
+ """
281
+
282
+ for param_name, widget in session.param_widgets.items():
283
+ label = session.param_labels[param_name]
284
+
285
+ active = self.is_parameter_active(param_name, session.local_params)
286
+
287
+ widget.setVisible(active)
288
+ label.setVisible(active)
289
+
290
+
291
+ # --------------------------------------------------------------------------
292
+ # Private methods
293
+ # --------------------------------------------------------------------------
294
+ def _create_param_row(self,
295
+ session: BlockDialogSession,
296
+ pmeta: ParameterMeta,
297
+ readonly: bool = False
298
+ ) -> tuple[QLabel, QWidget]:
299
+
300
+ # ENUM
301
+ if pmeta.type == "enum":
302
+ widget = self._create_enum_widget(session, pmeta, readonly)
303
+
304
+ else: # Default: text edit
305
+ widget = self._create_edit_widget(session, pmeta, readonly)
306
+
307
+ label = QLabel(f"{pmeta.name}:")
308
+ if pmeta.description:
309
+ label.setToolTip(pmeta.description)
310
+
311
+ return label, widget
312
+
313
+
314
+ # ------------------------------------------------------------
315
+ def _create_edit_widget(self,
316
+ session: BlockDialogSession,
317
+ pmeta: ParameterMeta,
318
+ readonly: bool = False) -> QLineEdit:
319
+ edit = QLineEdit()
320
+ value = session.local_params.get(pmeta.name)
321
+ if value is not None:
322
+ edit.setText(str(value))
323
+ elif pmeta.default is not None:
324
+ edit.setText(str(pmeta.default))
325
+ edit.textChanged.connect(
326
+ lambda val: self._on_param_changed(val, pmeta.name, session, readonly)
327
+ )
328
+ return edit
329
+
330
+ # ------------------------------------------------------------
331
+ def _create_enum_widget(self,
332
+ session: BlockDialogSession,
333
+ pmeta: ParameterMeta,
334
+ readonly: bool = False) -> QComboBox:
335
+ combo = QComboBox()
336
+ for v in pmeta.enum:
337
+ combo.addItem(str(v), userData=v)
338
+ value = session.local_params.get(pmeta.name)
339
+ if value is not None:
340
+ combo.setCurrentText(str(value))
341
+ combo.currentTextChanged.connect(
342
+ lambda val: self._on_param_changed(val, pmeta.name, session, readonly)
343
+ )
344
+ return combo
345
+
346
+ # ------------------------------------------------------------
347
+ def _browse_and_set_relative_file(
348
+ self,
349
+ edit: QLineEdit,
350
+ project_dir: Path | None,
351
+ file_filter: str,
352
+ ) -> None:
353
+ if project_dir is None:
354
+ return
355
+
356
+ base_dir = project_dir.expanduser()
357
+ start_dir = base_dir if base_dir.is_dir() else Path.cwd()
358
+
359
+ selected_file, _ = QFileDialog.getOpenFileName(
360
+ edit,
361
+ "Select file",
362
+ str(start_dir),
363
+ file_filter,
364
+ )
365
+ if not selected_file:
366
+ return
367
+
368
+ selected_path = Path(selected_file).resolve()
369
+ base_resolved = base_dir.resolve()
370
+ try:
371
+ relative_path = selected_path.relative_to(base_resolved)
372
+ except ValueError:
373
+ try:
374
+ relative_path = Path(os.path.relpath(str(selected_path), str(base_resolved)))
375
+ except ValueError:
376
+ # Windows cross-drive case (e.g. C: -> D:): keep absolute path.
377
+ relative_path = selected_path
378
+
379
+ edit.setText(relative_path.as_posix())
380
+
381
+ # ------------------------------------------------------------
382
+ def _on_param_changed( self, val: str, name: str, session: BlockDialogSession, readonly: bool,):
383
+ if readonly:
384
+ return
385
+
386
+ if name == "name":
387
+ session.local_params["name"] = val
388
+ else:
389
+ text = str(val).strip()
390
+ try:
391
+ session.local_params[name] = ast.literal_eval(text)
392
+ except (ValueError, SyntaxError):
393
+ session.local_params[name] = text
394
+ self.refresh_form(session)
395
+
396
+ # ------------------------------------------------------------
397
+ def _set_readonly_style(self, widget: QWidget):
398
+ if isinstance(widget, QLineEdit):
399
+ widget.setReadOnly(True)
400
+ widget.setStyleSheet("""
401
+ QLineEdit {
402
+ background-color: #2b2b2b;
403
+ color: #888888;
404
+ border: 1px solid #444444;
405
+ }
406
+ """)
407
+ elif isinstance(widget, QComboBox):
408
+ widget.setEnabled(False)
@@ -0,0 +1,116 @@
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 typing import Any, Dict
22
+
23
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta
24
+ from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
25
+ from pySimBlocks.gui.blocks.port_meta import PortMeta
26
+
27
+
28
+ class PIDMeta(BlockMeta):
29
+
30
+ def __init__(self):
31
+ self.name = "PID"
32
+ self.category = "controllers"
33
+ self.type = "pid"
34
+ self.summary = "Discrete SISO PID controller."
35
+ self.description = (
36
+ "$$\n"
37
+ "u[k] = K_p e[k] + K_i x_i[k] + K_d \frac{e[k]-e[k-1]}{dt}\n"
38
+ "$$\n"
39
+ )
40
+
41
+ self.parameters = [
42
+ ParameterMeta(
43
+ name="controller",
44
+ type="enum",
45
+ required=True,
46
+ autofill=True,
47
+ default="PID",
48
+ enum=["P", "I", "PI", "PD", "PID"]
49
+ ),
50
+ ParameterMeta(
51
+ name="Kp",
52
+ type="scalar",
53
+ autofill=True,
54
+ default=1.0,
55
+ description="Proportionnal gain."
56
+ ),
57
+ ParameterMeta(
58
+ name="Ki",
59
+ type="scalar",
60
+ default=1.0,
61
+ description="Integral gain."
62
+ ),
63
+ ParameterMeta(
64
+ name="Kd",
65
+ type="scalar",
66
+ default=1.0,
67
+ description="Derivative gain."
68
+ ),
69
+ ParameterMeta(
70
+ name="integration_method",
71
+ type="enum",
72
+ default="euler forward",
73
+ enum=["euler forward", "euler backward"]
74
+ ),
75
+ ParameterMeta(
76
+ name="u_min",
77
+ type="scalar"
78
+ ),
79
+ ParameterMeta(
80
+ name="u_max",
81
+ type="scalar"
82
+ ),
83
+ ParameterMeta(
84
+ name="sample_time",
85
+ type="float"
86
+ )
87
+ ]
88
+
89
+ self.inputs = [
90
+ PortMeta(
91
+ name="e",
92
+ display_as="e",
93
+ shape=[1, 1],
94
+ description="Error signal."
95
+ )
96
+ ]
97
+
98
+ self.outputs = [
99
+ PortMeta(
100
+ name="u",
101
+ display_as="u",
102
+ shape=[1, 1],
103
+ description="Control command."
104
+ )
105
+ ]
106
+
107
+ def is_parameter_active(self, param_name: str, instance_params: Dict[str, Any]) -> bool:
108
+
109
+ if param_name == "Kp":
110
+ return instance_params["controller"] in ["P", "PI", "PD", "PID"]
111
+ elif param_name == "Ki":
112
+ return instance_params["controller"] in ["I", "PI", "PID"]
113
+ elif param_name == "Kd":
114
+ return instance_params["controller"] in ["PD", "PID"]
115
+
116
+ return super().is_parameter_active(param_name, instance_params)
@@ -0,0 +1,80 @@
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 StateFeedBackMeta(BlockMeta):
27
+ def __init__(self):
28
+ self.name = "StateFeedback"
29
+ self.category = "controllers"
30
+ self.type = "state_feedback"
31
+ self.summary = "Discrete-time state-feedback controller."
32
+ self.description = (
33
+ "Implements the static control law:\n"
34
+ "$$\n"
35
+ "u[k] = G r[k] - K x[k]\n"
36
+ "$$\n"
37
+ )
38
+
39
+ self.parameters = [
40
+ ParameterMeta(
41
+ name="K",
42
+ type="matrix",
43
+ required=True,
44
+ description="State feedback gain matrix."
45
+ ),
46
+ ParameterMeta(
47
+ name="G",
48
+ type="matrix",
49
+ required=True,
50
+ description="Reference feedforward gain matrix."
51
+ ),
52
+ ParameterMeta(
53
+ name="sample_time",
54
+ type="float",
55
+ description="Block execution period."
56
+ ),
57
+ ]
58
+
59
+ self.inputs = [
60
+ PortMeta(
61
+ name="r",
62
+ display_as="r",
63
+ shape=["p", 1]
64
+ ),
65
+ PortMeta(
66
+ name="x",
67
+ display_as="x",
68
+ shape=["n", 1],
69
+ description="State measurement vector."
70
+ )
71
+ ]
72
+
73
+ self.outputs = [
74
+ PortMeta(
75
+ name="u",
76
+ display_as="u",
77
+ shape=["m", 1],
78
+ description="Control input vector."
79
+ ),
80
+ ]
@@ -0,0 +1,64 @@
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 ExternalInputMeta(BlockMeta):
27
+
28
+ def __init__(self):
29
+ self.name = "ExternalInput"
30
+ self.category = "interfaces"
31
+ self.type = "external_input"
32
+ self.summary = "Pass-through block for external real-time measurements."
33
+ self.description = (
34
+ "Pass-through block intended to inject measurements coming from an\n"
35
+ "external real-time application (camera, robot sensors, network, etc.)\n"
36
+ "into a pySimBlocks model.\n\n"
37
+ "This block does not perform any synchronization. It only enforces the\n"
38
+ "pySimBlocks signal convention (column vectors of shape (n,1))."
39
+ )
40
+
41
+ self.parameters = [
42
+ ParameterMeta(
43
+ name="sample_time",
44
+ type="float"
45
+ )
46
+ ]
47
+
48
+ self.inputs = [
49
+ PortMeta(
50
+ name="in",
51
+ display_as="in",
52
+ shape=["n", 1],
53
+ description="External input signal."
54
+ )
55
+ ]
56
+
57
+ self.outputs = [
58
+ PortMeta(
59
+ name="out",
60
+ display_as="out",
61
+ shape=["m", 1],
62
+ description="Model data signal."
63
+ )
64
+ ]