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,381 @@
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 abc import ABC, abstractmethod
22
+ from pathlib import Path
23
+
24
+ from PySide6.QtCore import QPointF
25
+
26
+ from pySimBlocks.gui.project_controller import ProjectController
27
+ from pySimBlocks.gui.services.yaml_tools import load_yaml_file
28
+
29
+
30
+ class ProjectLoader(ABC):
31
+ @abstractmethod
32
+ def load(self, controller: ProjectController, directory: Path):
33
+ pass
34
+
35
+
36
+ class ProjectLoaderYaml(ProjectLoader):
37
+ def load(self, controller: ProjectController, directory: Path):
38
+ project_yaml = directory / "project.yaml"
39
+ project_data = load_yaml_file(str(project_yaml))
40
+
41
+ if not isinstance(project_data, dict):
42
+ raise ValueError("project.yaml is not a valid mapping.")
43
+
44
+ sim_data = project_data.get("simulation", {})
45
+ diagram_data = project_data.get("diagram", {})
46
+ gui_data = project_data.get("gui", {})
47
+
48
+ layout_blocks, layout_conns, layout_warnings = self._load_layout_data(gui_data)
49
+ for w in layout_warnings:
50
+ print(f"[Layout warning] {w}")
51
+
52
+ controller.clear()
53
+
54
+ self._load_simulation(controller, sim_data)
55
+ self._load_blocks(controller, diagram_data, layout_blocks)
56
+ self._load_connections(controller, diagram_data, layout_conns)
57
+ self._load_logging(controller, sim_data)
58
+ self._load_plots(controller, sim_data)
59
+
60
+ controller.clear_dirty()
61
+
62
+ def _load_simulation(self, controller: ProjectController, sim_data: dict):
63
+ if not isinstance(sim_data, dict):
64
+ sim_data = {}
65
+ controller.project_state.load_simulation(
66
+ sim_data, sim_data.get("external_module", None)
67
+ )
68
+
69
+ def _load_blocks(
70
+ self,
71
+ controller: ProjectController,
72
+ diagram_data: dict,
73
+ layout_blocks: dict | None = None,
74
+ ):
75
+ positions, position_warnings = self._compute_block_positions(
76
+ diagram_data, layout_blocks
77
+ )
78
+ for w in position_warnings:
79
+ print(f"[Layout blocks warning] {w}")
80
+
81
+ blocks = diagram_data.get("blocks", [])
82
+ if not isinstance(blocks, list):
83
+ raise ValueError("'diagram.blocks' must be a list.")
84
+
85
+ for desc in blocks:
86
+ if not isinstance(desc, dict):
87
+ print("[Block warning] Invalid block entry in diagram.blocks, ignored.")
88
+ continue
89
+
90
+ name = desc.get("name")
91
+ category = desc.get("category")
92
+ block_type = desc.get("type")
93
+ if not isinstance(name, str) or not isinstance(category, str) or not isinstance(block_type, str):
94
+ print("[Block warning] Block missing required fields (name/category/type), ignored.")
95
+ continue
96
+
97
+ block_layout = self._sanitize_block_layout((layout_blocks or {}).get(name, {}))
98
+
99
+ controller.view.drop_event_pos = positions.get(name, QPointF(0, 0))
100
+ block = controller.add_block(category, block_type, block_layout)
101
+ controller.rename_block(block, name)
102
+
103
+ raw_params = desc.get("parameters", {})
104
+ if not isinstance(raw_params, dict):
105
+ print(f"[Block warning] Invalid parameters for block '{name}', ignored.")
106
+ raw_params = {}
107
+
108
+ for pmeta in block.meta.parameters:
109
+ pname = pmeta.name
110
+ if pname in raw_params:
111
+ block.parameters[pname] = raw_params[pname]
112
+ elif pmeta.autofill and pmeta.default is not None:
113
+ block.parameters[pname] = pmeta.default
114
+
115
+ block.resolve_ports()
116
+ controller.view.refresh_block_port(block)
117
+
118
+ def _sanitize_block_layout(self, block_layout: dict | None) -> dict:
119
+ if not isinstance(block_layout, dict):
120
+ return {}
121
+
122
+ out = {}
123
+
124
+ orientation = block_layout.get("orientation")
125
+ if orientation in {"normal", "flipped"}:
126
+ out["orientation"] = orientation
127
+
128
+ width = block_layout.get("width")
129
+ if isinstance(width, (int, float)) and width > 0:
130
+ out["width"] = float(width)
131
+
132
+ height = block_layout.get("height")
133
+ if isinstance(height, (int, float)) and height > 0:
134
+ out["height"] = float(height)
135
+
136
+ return out
137
+
138
+ def _load_connections(
139
+ self,
140
+ controller: ProjectController,
141
+ diagram_data: dict,
142
+ layout_conns: dict | None,
143
+ ):
144
+ connections = diagram_data.get("connections", [])
145
+ if not isinstance(connections, list):
146
+ raise ValueError("'diagram.connections' must be a list.")
147
+
148
+ routes, routes_warnings = self._parse_manual_routes(diagram_data, layout_conns)
149
+ for w in routes_warnings:
150
+ print(f"[Layout connections warning] {w}")
151
+
152
+ for conn in connections:
153
+ if not isinstance(conn, dict):
154
+ print("[Connection warning] Invalid connection entry, ignored.")
155
+ continue
156
+
157
+ conn_name = conn.get("name", None)
158
+ ports = conn.get("ports", None)
159
+ if not isinstance(ports, list) or len(ports) != 2:
160
+ print("[Connection warning] Connection has invalid ports, ignored.")
161
+ continue
162
+ src, dst = ports
163
+ if not isinstance(src, str) or not isinstance(dst, str):
164
+ print("[Connection warning] Connection ports must be strings, ignored.")
165
+ continue
166
+
167
+ src_block_name, src_port_name = src.split(".")
168
+ dst_block_name, dst_port_name = dst.split(".")
169
+
170
+ src_block = controller.project_state.get_block(src_block_name)
171
+ dst_block = controller.project_state.get_block(dst_block_name)
172
+
173
+ if src_block is None or dst_block is None:
174
+ print(
175
+ f"[Connection warning] Cannot create connection {src} -> {dst}, missing block(s)."
176
+ )
177
+ continue
178
+
179
+ src_port = next((p for p in src_block.ports if p.name == src_port_name), None)
180
+ dst_port = next((p for p in dst_block.ports if p.name == dst_port_name), None)
181
+
182
+ if src_port is None or dst_port is None:
183
+ missing = []
184
+ if src_port is None:
185
+ missing.append(f"{src_block_name}.{src_port_name}")
186
+ if dst_port is None:
187
+ missing.append(f"{dst_block_name}.{dst_port_name}")
188
+ print(
189
+ f"[Connection warning] Cannot create connection {src} -> {dst}, "
190
+ f"missing port(s): {', '.join(missing)}"
191
+ )
192
+ continue
193
+
194
+ points = routes.get(conn_name, None) if isinstance(conn_name, str) else None
195
+ controller.add_connection(src_port, dst_port, points)
196
+
197
+ def _load_logging(self, controller: ProjectController, sim_data: dict):
198
+ log_data = sim_data.get("logging", [])
199
+ controller.project_state.logging = log_data if isinstance(log_data, list) else []
200
+
201
+ def _load_plots(self, controller: ProjectController, sim_data: dict):
202
+ plot_data = sim_data.get("plots", [])
203
+ controller.project_state.plots = plot_data if isinstance(plot_data, list) else []
204
+
205
+ def _load_layout_data(self, gui_data: dict) -> tuple[dict, dict, list[str]]:
206
+ """
207
+ Load layout data from:
208
+ gui.layout.blocks
209
+ gui.layout.connections
210
+ """
211
+ warnings = []
212
+
213
+ if not isinstance(gui_data, dict):
214
+ return {}, {}, warnings
215
+
216
+ layout = gui_data.get("layout", {})
217
+ if layout is None:
218
+ return {}, {}, warnings
219
+ if not isinstance(layout, dict):
220
+ warnings.append("project.yaml gui.layout is invalid, ignored.")
221
+ return {}, {}, warnings
222
+
223
+ blocks = layout.get("blocks", {})
224
+ if not isinstance(blocks, dict):
225
+ warnings.append("project.yaml gui.layout.blocks is invalid, ignored.")
226
+ return {}, {}, warnings
227
+
228
+ conns = layout.get("connections", {})
229
+ if conns is not None and not isinstance(conns, dict):
230
+ warnings.append("project.yaml gui.layout.connections is invalid, ignored.")
231
+ conns = {}
232
+
233
+ return blocks, conns, warnings
234
+
235
+ def _compute_block_positions(
236
+ self,
237
+ diagram_data: dict,
238
+ layout_blocks: dict | None,
239
+ ) -> tuple[dict[str, QPointF], list[str]]:
240
+ warnings = []
241
+ positions = {}
242
+
243
+ x, y = 0, 0
244
+ dx, dy = 180, 120
245
+
246
+ blocks = diagram_data.get("blocks", [])
247
+ if not isinstance(blocks, list):
248
+ return positions, ["'diagram.blocks' must be a list."]
249
+
250
+ model_block_names = {
251
+ b.get("name")
252
+ for b in blocks
253
+ if isinstance(b, dict) and isinstance(b.get("name"), str)
254
+ }
255
+ layout_block_names = set(layout_blocks.keys()) if layout_blocks else set()
256
+
257
+ for block in blocks:
258
+ if not isinstance(block, dict) or not isinstance(block.get("name"), str):
259
+ continue
260
+
261
+ name = block["name"]
262
+
263
+ if layout_blocks and name in layout_blocks:
264
+ entry = layout_blocks[name]
265
+ x_val = entry.get("x")
266
+ y_val = entry.get("y")
267
+
268
+ if isinstance(x_val, (int, float)) and isinstance(y_val, (int, float)):
269
+ positions[name] = QPointF(float(x_val), float(y_val))
270
+ continue
271
+ warnings.append(
272
+ f"Invalid position for block '{name}' in project.yaml gui.layout.blocks, auto-placed."
273
+ )
274
+
275
+ else:
276
+ if layout_blocks is not None:
277
+ warnings.append(
278
+ f"Block '{name}' not found in project.yaml gui.layout.blocks, auto-placed."
279
+ )
280
+
281
+ positions[name] = QPointF(x, y)
282
+ x += dx
283
+ if x > 800:
284
+ x = 0
285
+ y += dy
286
+
287
+ for name in layout_block_names - model_block_names:
288
+ warnings.append(
289
+ f"project.yaml gui.layout.blocks contains '{name}' not present in diagram.blocks."
290
+ )
291
+
292
+ return positions, warnings
293
+
294
+ def _parse_manual_routes(
295
+ self,
296
+ diagram_data: dict,
297
+ layout_connections: dict | None,
298
+ ) -> tuple[dict[str, list[QPointF]], list[str]]:
299
+ warnings = []
300
+ routes: dict[str, list[QPointF]] = {}
301
+
302
+ if not layout_connections:
303
+ return routes, warnings
304
+
305
+ blocks = diagram_data.get("blocks", [])
306
+ connections = diagram_data.get("connections", [])
307
+ if not isinstance(blocks, list) or not isinstance(connections, list):
308
+ return routes, warnings
309
+
310
+ model_block_names = {
311
+ b.get("name")
312
+ for b in blocks
313
+ if isinstance(b, dict) and isinstance(b.get("name"), str)
314
+ }
315
+ model_connections_by_name = {}
316
+ for conn in connections:
317
+ if not isinstance(conn, dict):
318
+ continue
319
+ name = conn.get("name")
320
+ ports = conn.get("ports")
321
+ if isinstance(name, str) and isinstance(ports, list) and len(ports) == 2:
322
+ model_connections_by_name[name] = ports
323
+
324
+ for conn_name, payload in layout_connections.items():
325
+ if conn_name not in model_connections_by_name:
326
+ warnings.append(
327
+ f"project.yaml gui.layout.connections contains unknown connection '{conn_name}', ignored."
328
+ )
329
+ continue
330
+
331
+ ports = model_connections_by_name[conn_name]
332
+ try:
333
+ src_block, _src_port = [s.strip() for s in ports[0].split(".", 1)]
334
+ dst_block, _dst_port = [s.strip() for s in ports[1].split(".", 1)]
335
+ except Exception:
336
+ warnings.append(
337
+ f"Invalid ports for connection '{conn_name}' in diagram.connections, ignored."
338
+ )
339
+ continue
340
+
341
+ if src_block not in model_block_names or dst_block not in model_block_names:
342
+ warnings.append(
343
+ f"project.yaml layout connection '{conn_name}' references missing block(s), ignored."
344
+ )
345
+ continue
346
+
347
+ if not isinstance(payload, dict) or "route" not in payload:
348
+ warnings.append(
349
+ f"project.yaml layout connection '{conn_name}' has no valid 'route', ignored."
350
+ )
351
+ continue
352
+
353
+ raw_route = payload["route"]
354
+ if not isinstance(raw_route, list) or len(raw_route) < 2:
355
+ warnings.append(
356
+ f"project.yaml layout connection '{conn_name}' route is invalid/too short, ignored."
357
+ )
358
+ continue
359
+
360
+ pts: list[QPointF] = []
361
+ ok = True
362
+ for pt in raw_route:
363
+ if (
364
+ not isinstance(pt, (list, tuple))
365
+ or len(pt) != 2
366
+ or not isinstance(pt[0], (int, float))
367
+ or not isinstance(pt[1], (int, float))
368
+ ):
369
+ ok = False
370
+ break
371
+ pts.append(QPointF(float(pt[0]), float(pt[1])))
372
+
373
+ if not ok:
374
+ warnings.append(
375
+ f"project.yaml layout connection '{conn_name}' route has invalid points, ignored."
376
+ )
377
+ continue
378
+
379
+ routes[conn_name] = pts
380
+
381
+ return routes, warnings
@@ -0,0 +1,68 @@
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 abc import ABC, abstractmethod
22
+
23
+ from pySimBlocks.gui.models import ProjectState
24
+ from pySimBlocks.gui.graphics import BlockItem
25
+ from pySimBlocks.gui.services.yaml_tools import save_yaml
26
+ from pySimBlocks.project.generate_run_script import generate_python_content
27
+
28
+
29
+ class ProjectSaver(ABC):
30
+
31
+ @abstractmethod
32
+ def save(self, project_state: ProjectState,
33
+ block_items: dict[str, BlockItem] | None = None):
34
+ pass
35
+
36
+ @abstractmethod
37
+ def export(self, project_state: ProjectState,
38
+ block_items: dict[str, BlockItem] | None = None):
39
+ pass
40
+
41
+ class ProjectSaverYaml(ProjectSaver):
42
+
43
+ def save(self,
44
+ project_state: ProjectState,
45
+ block_items: dict[str, BlockItem] | None = None
46
+ ):
47
+ save_yaml(
48
+ project_state,
49
+ block_items if block_items is not None else {},
50
+ )
51
+
52
+
53
+ def export(self,
54
+ project_state: ProjectState,
55
+ block_items: dict[str, BlockItem] | None = None
56
+ ):
57
+ if project_state.directory_path is None:
58
+ raise ValueError("Project directory is not set.")
59
+
60
+ save_yaml(
61
+ project_state,
62
+ block_items if block_items is not None else {},
63
+ )
64
+ run_py = project_state.directory_path / "run.py"
65
+ run_py.write_text(
66
+ generate_python_content(project_yaml_path="project.yaml")
67
+ )
68
+
@@ -0,0 +1,66 @@
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 sys
23
+ from pySimBlocks.gui.models import ProjectState
24
+ from pySimBlocks.gui.services.yaml_tools import (
25
+ cleanup_runtime_project_yaml,
26
+ runtime_project_yaml_path,
27
+ save_yaml,
28
+ )
29
+ from pySimBlocks.project.generate_run_script import generate_python_content
30
+
31
+
32
+ class SimulationRunner:
33
+ def run(self, project_state: ProjectState):
34
+ project_dir = project_state.directory_path
35
+ if project_dir is None:
36
+ return (
37
+ {},
38
+ False,
39
+ "Project directory is not set.\nPlease define it in settings.",
40
+ )
41
+
42
+ project_path = runtime_project_yaml_path(project_dir)
43
+ cleanup_runtime_project_yaml(project_dir)
44
+ save_yaml(project_state, runtime=True)
45
+
46
+ code = generate_python_content(
47
+ project_yaml_path=str(project_path),
48
+ enable_plots=False,
49
+ )
50
+
51
+ old_cwd = os.getcwd()
52
+ old_sys_path = list(sys.path)
53
+ env = {}
54
+ try:
55
+ os.chdir(project_dir)
56
+ sys.path.insert(0, str(project_dir))
57
+ exec(code, env, env)
58
+ logs = env.get("logs")
59
+ return logs, True, "Simulation success."
60
+ except Exception as e:
61
+ logs = {}
62
+ return logs, False, f"Error: {e}"
63
+ finally:
64
+ os.chdir(old_cwd)
65
+ sys.path[:] = old_sys_path
66
+ cleanup_runtime_project_yaml(project_dir)