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,312 @@
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 TYPE_CHECKING, Any
22
+
23
+ from PySide6.QtCore import QPointF, Qt, QTimer
24
+ from PySide6.QtGui import QGuiApplication, QPainter, QPen
25
+ from PySide6.QtWidgets import QGraphicsScene, QGraphicsView
26
+
27
+ from pySimBlocks.gui.graphics.block_item import BlockItem
28
+ from pySimBlocks.gui.graphics.connection_item import ConnectionItem, OrthogonalRoute
29
+ from pySimBlocks.gui.graphics.port_item import PortItem
30
+ from pySimBlocks.gui.graphics.theme import make_theme
31
+ from pySimBlocks.gui.models.block_instance import BlockInstance
32
+ from pySimBlocks.gui.models.connection_instance import ConnectionInstance
33
+
34
+ if TYPE_CHECKING:
35
+ from pySimBlocks.gui.project_controller import ProjectController
36
+
37
+
38
+ class DiagramView(QGraphicsView):
39
+ def __init__(self):
40
+ super().__init__()
41
+ self.diagram_scene = QGraphicsScene(self)
42
+ self.setScene(self.diagram_scene)
43
+ self.setAcceptDrops(True)
44
+
45
+ self.setRenderHint(QPainter.Antialiasing)
46
+ self.theme = make_theme()
47
+ self.diagram_scene.setBackgroundBrush(self.theme.scene_bg)
48
+ hints = QGuiApplication.styleHints()
49
+ hints.colorSchemeChanged.connect(self._on_color_scheme_changed)
50
+ app = QGuiApplication.instance()
51
+ if hasattr(app, "paletteChanged"):
52
+ app.paletteChanged.connect(lambda *_: QTimer.singleShot(0, self._apply_theme_from_system))
53
+ self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
54
+
55
+ self.pending_port: PortItem | None = None
56
+ self.temp_connection: ConnectionItem | None = None
57
+ self.copied_block: BlockItem | None = None
58
+ self.project_controller: "ProjectController" | None
59
+ self.block_items: dict[str, BlockItem] = {}
60
+ self.connections: dict[ConnectionInstance, ConnectionItem] = {}
61
+
62
+ self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
63
+ self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
64
+ self.setDragMode(QGraphicsView.RubberBandDrag)
65
+
66
+ # --------------------------------------------------------------------------
67
+ # View methods
68
+ # --------------------------------------------------------------------------
69
+ def add_block(self, block_instance: BlockInstance,
70
+ block_layout: dict[str, Any] | None = None):
71
+ block_item = BlockItem(block_instance, self.drop_event_pos, self, block_layout)
72
+ self.diagram_scene.addItem(block_item)
73
+ self.block_items[block_instance.uid] = block_item
74
+
75
+ # --------------------------------------------------------------
76
+ def refresh_block_port(self, block_instance: BlockInstance):
77
+ block_item = self.get_block_item_from_instance(block_instance)
78
+ if block_item:
79
+ block_item.refresh_ports()
80
+
81
+ # --------------------------------------------------------------
82
+ def remove_block(self, block_instance: BlockInstance):
83
+ block_item = self.block_items[block_instance.uid]
84
+ self.diagram_scene.removeItem(block_item)
85
+ self.block_items.pop(block_instance.uid, None)
86
+
87
+ # --------------------------------------------------------------
88
+ def add_connection(self,
89
+ connection_instance: ConnectionInstance,
90
+ points: list[QPointF] | None = None
91
+ ):
92
+ src_port_item = self.get_block_item_from_instance(connection_instance.src_block()).get_port_item(connection_instance.src_port.name)
93
+ dst_port_item = self.get_block_item_from_instance(connection_instance.dst_block()).get_port_item(connection_instance.dst_port.name)
94
+ connection_item = ConnectionItem(
95
+ src_port_item, dst_port_item, connection_instance, points
96
+ )
97
+ self.connections[connection_instance] = connection_item
98
+ self.diagram_scene.addItem(connection_item)
99
+
100
+ # --------------------------------------------------------------
101
+ def remove_connection(self, connection_instance: ConnectionInstance):
102
+ connection_item = self.connections.pop(connection_instance, None)
103
+ if connection_item:
104
+ self.diagram_scene.removeItem(connection_item)
105
+
106
+ # --------------------------------------------------------------
107
+ def get_block_item_from_instance(self, block_instance: BlockInstance) -> BlockItem | None:
108
+ return self.block_items.get(block_instance.uid)
109
+
110
+ # --------------------------------------------------------------
111
+ def create_connection_event(self, port: PortItem):
112
+ if not self.pending_port:
113
+ self.pending_port = port
114
+ self.temp_connection = ConnectionItem(self.pending_port, None, None)
115
+ self.diagram_scene.addItem(self.temp_connection)
116
+ return
117
+
118
+ # --------------------------------------------------------------
119
+ def update_block_param_event(self, block_instance: BlockInstance, params: dict[str, Any]):
120
+ self.project_controller.update_block_param(block_instance, params)
121
+
122
+ # --------------------------------------------------------------
123
+ def on_block_moved(self, block_item: BlockItem):
124
+ self.project_controller.make_dirty()
125
+ for conn_inst, conn_item in self.connections.items():
126
+ if conn_inst.is_block_involved(block_item.instance):
127
+ conn_item.invalidate_manual_route()
128
+ conn_item.update_position()
129
+
130
+ # --------------------------------------------------------------
131
+ def on_block_ports_refreshed(self, block_item: BlockItem):
132
+ for conn_inst, conn_item in self.connections.items():
133
+ if conn_inst.is_block_involved(block_item.instance):
134
+ conn_item.update_position()
135
+
136
+ # --------------------------------------------------------------------------
137
+ # Event handlers
138
+ # --------------------------------------------------------------------------
139
+ def dragEnterEvent(self, event):
140
+ if event.mimeData().hasText():
141
+ event.acceptProposedAction()
142
+
143
+ # --------------------------------------------------------------
144
+ def dragMoveEvent(self, event):
145
+ event.acceptProposedAction()
146
+
147
+ # --------------------------------------------------------------
148
+ def dropEvent(self, event):
149
+ self.drop_event_pos = self.mapToScene(event.position().toPoint())
150
+ category, block_type = event.mimeData().text().split(":")
151
+ self.project_controller.add_block(category, block_type)
152
+ event.acceptProposedAction()
153
+
154
+ # --------------------------------------------------------------
155
+ def keyPressEvent(self, event):
156
+ # COPY
157
+ if event.key() == Qt.Key_C and event.modifiers() & Qt.ControlModifier:
158
+ selected = [i for i in self.diagram_scene.selectedItems() if isinstance(i, BlockItem)]
159
+ if selected:
160
+ self.copied_block = selected[0]
161
+ return
162
+
163
+ # PASTE
164
+ if event.key() == Qt.Key_V and event.modifiers() & Qt.ControlModifier:
165
+ if self.copied_block:
166
+ self.drop_event_pos = self.copied_block.pos() + QPointF(30, 30)
167
+ self.project_controller.add_copy_block(self.copied_block.instance)
168
+ return
169
+
170
+ # DELETE
171
+ if event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
172
+ self.delete_selected()
173
+ return
174
+
175
+ # ZOOM IN / OUT
176
+ if event.key() in (Qt.Key_Plus, Qt.Key_Equal) and event.modifiers() & Qt.ControlModifier:
177
+ self.scale_view(1.15)
178
+ return
179
+
180
+ if event.key() == Qt.Key_Minus and event.modifiers() & Qt.ControlModifier:
181
+ self.scale_view(1 / 1.15)
182
+ return
183
+
184
+ # ROTATE BLOCK
185
+ if event.key() == Qt.Key_R and event.modifiers() & Qt.ControlModifier:
186
+ selected = [i for i in self.diagram_scene.selectedItems()
187
+ if isinstance(i, BlockItem)]
188
+ for item in selected:
189
+ item.toggle_orientation()
190
+ return
191
+
192
+ # CENTER VIEW
193
+ if event.key() == Qt.Key_Space and not event.modifiers():
194
+ self._center_on_diagram()
195
+ event.accept()
196
+ return
197
+ super().keyPressEvent(event)
198
+
199
+
200
+
201
+ # --------------------------------------------------------------
202
+ def wheelEvent(self, event):
203
+ if event.modifiers() & Qt.ControlModifier:
204
+ zoom_factor = 1.15
205
+ if event.angleDelta().y() > 0:
206
+ self.scale_view(zoom_factor)
207
+ else:
208
+ self.scale_view(1 / zoom_factor)
209
+ event.accept()
210
+ else:
211
+ super().wheelEvent(event)
212
+
213
+ # --------------------------------------------------------------
214
+ def mouseMoveEvent(self, event):
215
+ if self.temp_connection:
216
+ pos = self.mapToScene(event.position().toPoint())
217
+ self.temp_connection.update_temp_position(pos)
218
+ return
219
+ super().mouseMoveEvent(event)
220
+
221
+ # --------------------------------------------------------------
222
+ def mouseReleaseEvent(self, event):
223
+ if not self.pending_port:
224
+ super().mouseReleaseEvent(event)
225
+ return
226
+
227
+ pos = self.mapToScene(event.position().toPoint())
228
+ items = self.diagram_scene.items(pos)
229
+ port = next((i for i in items if isinstance(i, PortItem)), None)
230
+ if not port:
231
+ self._cancel_temp_connection()
232
+ return
233
+
234
+ self.project_controller.add_connection(self.pending_port.instance, port.instance)
235
+ self._cancel_temp_connection()
236
+
237
+ # --------------------------------------------------------------
238
+ def delete_selected(self):
239
+ for item in self.diagram_scene.selectedItems():
240
+ if isinstance(item, BlockItem):
241
+ self.project_controller.remove_block(item.instance)
242
+
243
+ elif isinstance(item, ConnectionItem):
244
+ self.project_controller.remove_connection(item.instance)
245
+
246
+ # --------------------------------------------------------------
247
+ def clear_scene(self):
248
+
249
+ for block in list(self.block_items.values()):
250
+ self.project_controller.remove_block(block.instance)
251
+
252
+ for connection in list(self.connections.values()):
253
+ self.project_controller.remove_connection(connection.instance)
254
+
255
+ self.pending_port = None
256
+
257
+ # --------------------------------------------------------------------------
258
+ # Private methods
259
+ # --------------------------------------------------------------------------
260
+ def _cancel_temp_connection(self):
261
+ self.diagram_scene.removeItem(self.temp_connection)
262
+ self.temp_connection = None
263
+ self.pending_port = None
264
+
265
+ # --------------------------------------------------------------
266
+ def scale_view(self, factor):
267
+ current_scale = self.transform().m11()
268
+ min_scale, max_scale = 0.2, 5.0
269
+
270
+ new_scale = current_scale * factor
271
+ if min_scale <= new_scale <= max_scale:
272
+ self.scale(factor, factor)
273
+
274
+ # --------------------------------------------------------------
275
+ def _on_color_scheme_changed(self, *_):
276
+ QTimer.singleShot(0, self._apply_theme_from_system)
277
+
278
+ # --------------------------------------------------------------
279
+ def _apply_theme_from_system(self):
280
+ self.theme = make_theme()
281
+ self.diagram_scene.setBackgroundBrush(self.theme.scene_bg)
282
+ self._refresh_theme_items()
283
+ self.viewport().update()
284
+ self.diagram_scene.update()
285
+
286
+ # --------------------------------------------------------------
287
+ def _refresh_theme_items(self):
288
+ for block in self.block_items.values():
289
+ block.update()
290
+ for port in block.port_items:
291
+ port.label.setDefaultTextColor(self.theme.text)
292
+ port.update()
293
+
294
+ for conn in self.connections.values():
295
+ conn.setPen(QPen(self.theme.wire, 2))
296
+ conn.update_position()
297
+ conn.update()
298
+
299
+ # --------------------------------------------------------------
300
+ def _center_on_diagram(self):
301
+ scene = self.diagram_scene
302
+ items_rect = scene.itemsBoundingRect()
303
+
304
+ if items_rect.isNull():
305
+ return
306
+
307
+ # Un peu de marge pour éviter que ça colle aux bords
308
+ margin = 40
309
+ items_rect.adjust(-margin, -margin, margin, margin)
310
+
311
+ scene.setSceneRect(items_rect)
312
+ self.fitInView(items_rect, Qt.KeepAspectRatio)
@@ -0,0 +1,161 @@
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 PySide6.QtWidgets import QToolBar, QMessageBox, QProgressDialog, QApplication
22
+ from PySide6.QtGui import QAction
23
+ from PySide6.QtCore import Qt
24
+
25
+ from pySimBlocks.gui.dialogs.display_yaml_dialog import DisplayYamlDialog
26
+ from pySimBlocks.gui.dialogs.plot_dialog import PlotDialog
27
+ from pySimBlocks.gui.dialogs.settings_dialog import SettingsDialog
28
+ from pySimBlocks.gui.project_controller import ProjectController
29
+ from pySimBlocks.gui.services.project_saver import ProjectSaver
30
+ from pySimBlocks.gui.services.simulation_runner import SimulationRunner
31
+
32
+ # Add ons
33
+ from pySimBlocks.gui.addons.sofa.sofa_dialog import SofaDialog
34
+ from pySimBlocks.gui.addons.sofa.sofa_service import SofaService
35
+
36
+ class ToolBarView(QToolBar):
37
+
38
+ def __init__(self,
39
+ saver: ProjectSaver,
40
+ runner: SimulationRunner,
41
+ project_controller: ProjectController):
42
+ super().__init__()
43
+
44
+ self.saver = saver
45
+ self.runner = runner
46
+ self.project_controller = project_controller
47
+
48
+ save_action = QAction("Save", self)
49
+ save_action.triggered.connect(self.on_save)
50
+ self.addAction(save_action)
51
+
52
+ export_action = QAction("Export", self)
53
+ export_action.triggered.connect(self.on_export_project)
54
+ self.addAction(export_action)
55
+
56
+ display_action = QAction("Project YAML", self)
57
+ display_action.triggered.connect(self.on_open_display_yaml)
58
+ self.addAction(display_action)
59
+
60
+ sim_settings_action = QAction("Settings", self)
61
+ sim_settings_action.triggered.connect(self.on_open_simulation_settings)
62
+ self.addAction(sim_settings_action)
63
+
64
+ run_action = QAction("Run", self)
65
+ run_action.triggered.connect(self.on_run_sim)
66
+ self.addAction(run_action)
67
+
68
+ plot_action = QAction("Plot", self)
69
+ plot_action.triggered.connect(self.on_plot_logs)
70
+ self.addAction(plot_action)
71
+
72
+ # add ons
73
+ self.sofa_service = SofaService(self.project_controller.project_state, self.project_controller)
74
+ self.sofa_action = QAction("Sofa", self)
75
+ self.sofa_action.triggered.connect(self.on_open_sofa_dialog)
76
+ self.addAction(self.sofa_action)
77
+
78
+ def on_save(self):
79
+ self.saver.save(self.project_controller.project_state, self.project_controller.view.block_items)
80
+ self.project_controller.clear_dirty()
81
+
82
+ def on_export_project(self):
83
+ window = self.parent()
84
+ if window.confirm_discard_or_save("exporting"):
85
+ self.saver.export(self.project_controller.project_state, self.project_controller.view.block_items)
86
+
87
+ def on_open_display_yaml(self):
88
+ dialog = DisplayYamlDialog(self.project_controller.project_state, self.project_controller.view)
89
+ dialog.exec()
90
+
91
+ def on_open_simulation_settings(self):
92
+ dialog = SettingsDialog(self.project_controller.project_state, self.project_controller, self.parent())
93
+ dialog.exec()
94
+
95
+ def on_run_sim(self):
96
+ dlg = QProgressDialog(self)
97
+ dlg.setWindowTitle("Simulation")
98
+ dlg.setLabelText("Running simulation...\nPlease wait.")
99
+ dlg.setRange(0, 0) # busy indicator
100
+ dlg.setCancelButton(None)
101
+ dlg.setWindowModality(Qt.ApplicationModal)
102
+ dlg.setMinimumWidth(300)
103
+ dlg.setMinimumHeight(120)
104
+ dlg.show()
105
+ QApplication.processEvents()
106
+
107
+ self.set_running(True)
108
+ logs, flag, msg = self.runner.run(self.project_controller.project_state)
109
+ dlg.close()
110
+ self.set_running(False)
111
+ self.project_controller.project_state.logs = logs
112
+
113
+ if not flag:
114
+ QMessageBox.warning(
115
+ self,
116
+ "Simulation failed with error",
117
+ msg,
118
+ QMessageBox.Ok,
119
+ )
120
+
121
+
122
+ def on_plot_logs(self):
123
+ flag, msg = self.project_controller.project_state.can_plot()
124
+ if not flag:
125
+ QMessageBox.warning(
126
+ self,
127
+ "Plot Error",
128
+ msg,
129
+ QMessageBox.Ok,
130
+ )
131
+ return
132
+ self._plot_dialog = PlotDialog(self.project_controller.project_state, self.parent()) # keep ref because of python garbage collector
133
+ self._plot_dialog.show()
134
+
135
+
136
+ def set_running(self, running: bool):
137
+ for action in self.actions():
138
+ action.setEnabled(not running)
139
+
140
+ #####################################
141
+ # Adds on
142
+ def refresh_sofa_button(self):
143
+ if self.project_controller.has_sofa_block():
144
+ if self.sofa_action not in self.actions():
145
+ self.addAction(self.sofa_action)
146
+ else:
147
+ if self.sofa_action in self.actions():
148
+ self.removeAction(self.sofa_action)
149
+
150
+ def on_open_sofa_dialog(self):
151
+ ok, msg, details = self.sofa_service.can_use_sofa()
152
+ if not ok:
153
+ QMessageBox.warning(
154
+ self,
155
+ msg,
156
+ details,
157
+ QMessageBox.Ok
158
+ )
159
+ return
160
+ dialog = SofaDialog(self.sofa_service, self.parent())
161
+ dialog.exec()
@@ -0,0 +1,35 @@
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.project.generate_run_script import generate_run_script, generate_python_content
22
+ from pySimBlocks.project.load_project_config import load_project_config
23
+ from pySimBlocks.project.load_simulator import load_simulator_from_project
24
+ from pySimBlocks.project.load_simulation_config import load_simulation_config
25
+ from pySimBlocks.project.plot_from_config import plot_from_config
26
+
27
+
28
+ __all__ = [
29
+ "generate_run_script",
30
+ "generate_python_content",
31
+ "load_project_config",
32
+ "load_simulator_from_project",
33
+ "load_simulation_config",
34
+ "plot_from_config"
35
+ ]
@@ -0,0 +1,92 @@
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
22
+ from pathlib import Path
23
+ from typing import Dict, Any
24
+ import yaml
25
+
26
+ from pySimBlocks.core.model import Model
27
+
28
+ # ============================================================
29
+ # Public API
30
+ # ============================================================
31
+
32
+
33
+ def build_model_from_dict(
34
+ model: Model,
35
+ model_data: Dict[str, Any],
36
+ params_dir: Path | None = None,
37
+ ) -> None:
38
+ """
39
+ Build a Model instance from an already loaded model dictionary.
40
+ """
41
+
42
+ # ------------------------------------------------------------
43
+ # Load block registry
44
+ # ------------------------------------------------------------
45
+ index_path = Path(__file__).parent / "pySimBlocks_blocks_index.yaml"
46
+ with index_path.open("r") as f:
47
+ blocks_index = yaml.safe_load(f) or {}
48
+
49
+ # ------------------------------------------------------------
50
+ # Instantiate blocks
51
+ # ------------------------------------------------------------
52
+ for desc in model_data.get("blocks", []):
53
+ name = desc["name"]
54
+ category = desc["category"]
55
+ block_type = desc["type"]
56
+
57
+ try:
58
+ block_info = blocks_index[category][block_type]
59
+ except KeyError:
60
+ print(f"Available blocks in category '{category}':")
61
+ for bt in blocks_index.get(category, {}):
62
+ print(f" - {bt}")
63
+ print(desc)
64
+ raise ValueError(
65
+ f"Unknown block '{block_type}' in category '{category}'."
66
+ )
67
+
68
+ # --------------------------------------------------------
69
+ # Load Python block class
70
+ # --------------------------------------------------------
71
+ module = importlib.import_module(block_info["module"])
72
+ BlockClass = getattr(module, block_info["class"])
73
+
74
+ # --------------------------------------------------------
75
+ # Load parameters
76
+ # --------------------------------------------------------
77
+ params = desc.get("parameters", {})
78
+
79
+ # --------------------------------------------------------
80
+ # Instantiate block
81
+ # --------------------------------------------------------
82
+ params = BlockClass.adapt_params(params, params_dir=params_dir)
83
+ block = BlockClass(name=name, **params)
84
+ model.add_block(block)
85
+
86
+ # ------------------------------------------------------------
87
+ # Connections
88
+ # ------------------------------------------------------------
89
+ for src, dst in model_data.get("connections", []):
90
+ src_block, src_port = src.split(".")
91
+ dst_block, dst_port = dst.split(".")
92
+ model.connect(src_block, src_port, dst_block, dst_port)