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,261 @@
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 pathlib import Path
22
+
23
+ import yaml
24
+
25
+ from pySimBlocks.gui.graphics.block_item import BlockItem
26
+ from pySimBlocks.gui.models.project_state import ProjectState
27
+
28
+
29
+ def load_yaml_file(path: str) -> dict:
30
+ with open(path, "r") as f:
31
+ return yaml.safe_load(f) or {}
32
+
33
+
34
+ class FlowStyleList(list):
35
+ """Marker class for YAML flow-style lists."""
36
+ pass
37
+
38
+
39
+ class ProjectYamlDumper(yaml.SafeDumper):
40
+ pass
41
+
42
+
43
+ def _repr_flow_list(dumper, data):
44
+ return dumper.represent_sequence(
45
+ "tag:yaml.org,2002:seq",
46
+ data,
47
+ flow_style=True,
48
+ )
49
+
50
+
51
+ class FlowMatrix(list):
52
+ """Marker type for matrices that must be dumped in YAML flow-style."""
53
+ pass
54
+
55
+
56
+ def _is_matrix(obj):
57
+ if not isinstance(obj, list):
58
+ return False
59
+ if not obj:
60
+ return False
61
+ if not all(isinstance(row, list) for row in obj):
62
+ return False
63
+ row_lengths = {len(row) for row in obj}
64
+ return len(row_lengths) == 1
65
+
66
+
67
+ def _wrap_flow_matrices(obj):
68
+ if _is_matrix(obj):
69
+ return FlowMatrix([_wrap_flow_matrices(row) for row in obj])
70
+
71
+ if isinstance(obj, list):
72
+ return [_wrap_flow_matrices(x) for x in obj]
73
+
74
+ if isinstance(obj, dict):
75
+ return {k: _wrap_flow_matrices(v) for k, v in obj.items()}
76
+
77
+ return obj
78
+
79
+
80
+ ProjectYamlDumper.add_representer(FlowMatrix, _repr_flow_list)
81
+ ProjectYamlDumper.add_representer(FlowStyleList, _repr_flow_list)
82
+
83
+
84
+ def dump_project_yaml(
85
+ project_state: ProjectState | None = None,
86
+ block_items: dict[str, BlockItem] | None = None,
87
+ raw: dict | None = None,
88
+ ) -> str:
89
+ if raw is None:
90
+ if project_state is None:
91
+ raise ValueError("project_state or raw must be set")
92
+ raw = build_project_yaml(project_state, block_items if block_items is not None else {})
93
+
94
+ data = _wrap_flow_matrices(raw)
95
+ return yaml.dump(
96
+ data,
97
+ Dumper=ProjectYamlDumper,
98
+ sort_keys=False,
99
+ )
100
+
101
+
102
+ def save_yaml(
103
+ project_state: ProjectState,
104
+ block_items: dict[str, BlockItem] | None = None,
105
+ runtime: bool = False,
106
+ ) -> None:
107
+ directory = project_state.directory_path
108
+ if directory is None:
109
+ raise ValueError("project_state.directory_path must be set")
110
+
111
+ project_raw = build_project_yaml(project_state, block_items if block_items is not None else {})
112
+ directory.mkdir(parents=True, exist_ok=True)
113
+ target = ".project.runtime.yaml" if runtime else "project.yaml"
114
+ (directory / target).write_text(dump_project_yaml(raw=project_raw))
115
+
116
+
117
+ def runtime_project_yaml_path(project_dir: Path) -> Path:
118
+ return project_dir / ".project.runtime.yaml"
119
+
120
+
121
+ def cleanup_runtime_project_yaml(project_dir: Path | None) -> None:
122
+ if project_dir is None:
123
+ return
124
+
125
+ runtime_yaml = runtime_project_yaml_path(project_dir)
126
+ if runtime_yaml.exists():
127
+ runtime_yaml.unlink(missing_ok=True)
128
+
129
+
130
+ def _build_simulation_section(project_state: ProjectState) -> dict:
131
+ simulation = project_state.simulation.__dict__.copy()
132
+ if simulation.get("clock") == "internal":
133
+ simulation.pop("clock", None)
134
+
135
+ if project_state.external is not None:
136
+ simulation["external_module"] = project_state.external
137
+
138
+ simulation["logging"] = list(project_state.logging)
139
+ simulation["plots"] = list(project_state.plots)
140
+ return simulation
141
+
142
+
143
+ def _build_blocks_section(project_state: ProjectState) -> list[dict]:
144
+ blocks = []
145
+ for b in project_state.blocks:
146
+ params = {
147
+ k: v for k, v in b.parameters.items()
148
+ if v is not None and b.meta.is_parameter_active(k, b.parameters)
149
+ }
150
+ blocks.append(
151
+ {
152
+ "name": b.name,
153
+ "category": b.meta.category,
154
+ "type": b.meta.type,
155
+ "parameters": params,
156
+ }
157
+ )
158
+ return blocks
159
+
160
+
161
+ def _build_connections_section(project_state: ProjectState) -> tuple[list[dict], dict[tuple[str, str], str]]:
162
+ connections = []
163
+ conn_name_map: dict[tuple[str, str], str] = {}
164
+
165
+ for i, c in enumerate(project_state.connections, start=1):
166
+ src = f"{c.src_block().name}.{c.src_port.name}"
167
+ dst = f"{c.dst_block().name}.{c.dst_port.name}"
168
+ conn_name = f"c{i}"
169
+ conn_name_map[(src, dst)] = conn_name
170
+
171
+ connections.append(
172
+ {
173
+ "name": conn_name,
174
+ "ports": FlowStyleList([src, dst]),
175
+ }
176
+ )
177
+ return connections, conn_name_map
178
+
179
+
180
+ def _build_layout_section(
181
+ block_items: dict[str, BlockItem],
182
+ conn_name_map: dict[tuple[str, str], str],
183
+ ) -> dict:
184
+ data: dict = {"blocks": {}}
185
+ manual_connections = {}
186
+ seen = set()
187
+
188
+ for block in block_items.values():
189
+ name = block.instance.name
190
+ pos = block.pos()
191
+ data["blocks"][name] = {
192
+ "x": float(pos.x()),
193
+ "y": float(pos.y()),
194
+ "orientation": block.orientation,
195
+ "width": float(block.rect().width()),
196
+ "height": float(block.rect().height()),
197
+ }
198
+
199
+ if not block_items:
200
+ return data
201
+
202
+ view = next(iter(block_items.values())).view
203
+ for conn in view.connections.values():
204
+ if conn in seen:
205
+ continue
206
+ seen.add(conn)
207
+ if not conn.is_manual:
208
+ continue
209
+
210
+ src = f"{conn.src_port.parent_block.instance.name}.{conn.src_port.instance.name}"
211
+ dst = f"{conn.dst_port.parent_block.instance.name}.{conn.dst_port.instance.name}"
212
+ conn_name = conn_name_map.get((src, dst), None)
213
+ if conn_name is None:
214
+ continue
215
+
216
+ manual_connections[conn_name] = {
217
+ "route": FlowStyleList([
218
+ FlowStyleList([float(p.x()), float(p.y())])
219
+ for p in conn.route.points
220
+ ])
221
+ }
222
+
223
+ if manual_connections:
224
+ data["connections"] = manual_connections
225
+
226
+ return data
227
+
228
+
229
+ def build_project_yaml(
230
+ project_state: ProjectState,
231
+ block_items: dict[str, BlockItem] | None = None,
232
+ ) -> dict:
233
+ block_items = block_items if block_items is not None else {}
234
+ project_name = (
235
+ project_state.directory_path.name
236
+ if project_state.directory_path is not None
237
+ else "project"
238
+ )
239
+
240
+ blocks = _build_blocks_section(project_state)
241
+ connections, conn_name_map = _build_connections_section(project_state)
242
+ layout = _build_layout_section(block_items, conn_name_map)
243
+
244
+ return {
245
+ "schema_version": 1,
246
+ "project": {
247
+ "name": project_name,
248
+ "metadata": {
249
+ "created_by": "pySimBlocks",
250
+ "created_at": "2026-02-18T00:00:00Z",
251
+ },
252
+ },
253
+ "simulation": _build_simulation_section(project_state),
254
+ "diagram": {
255
+ "blocks": blocks,
256
+ "connections": connections,
257
+ },
258
+ "gui": {
259
+ "layout": layout,
260
+ },
261
+ }
@@ -0,0 +1,19 @@
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
+ # ******************************************************************************
@@ -0,0 +1,131 @@
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 Callable
22
+ from PySide6.QtWidgets import QLineEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
23
+ from PySide6.QtCore import Qt, QMimeData
24
+ from PySide6.QtGui import QDrag
25
+
26
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta
27
+ from pySimBlocks.gui.dialogs.block_dialog import BlockDialog
28
+ from pySimBlocks.gui.models.block_instance import BlockInstance
29
+
30
+ class _PreviewBlock:
31
+ def __init__(self, instance):
32
+ self.instance = instance
33
+
34
+ def refresh_ports(self):
35
+ pass
36
+
37
+
38
+ class _BlockTree(QTreeWidget):
39
+ def __init__(self,
40
+ get_categories: Callable[[], list[str]],
41
+ get_blocks: Callable[[str], list[str]],
42
+ resolve_block_meta: Callable[[str, str], BlockMeta]):
43
+ super().__init__()
44
+ self.setHeaderHidden(True)
45
+ self.setDragEnabled(True)
46
+
47
+ self.get_categories = get_categories
48
+ self.get_blocks = get_blocks
49
+ self.resolve_block_meta = resolve_block_meta
50
+
51
+ for category in self.get_categories():
52
+ operators = QTreeWidgetItem(self, [category])
53
+ operators.setExpanded(False)
54
+ for block_type in self.get_blocks(category):
55
+ block = QTreeWidgetItem(operators, [block_type])
56
+ block.setData(0, Qt.UserRole, (category, block_type))
57
+
58
+ self.itemDoubleClicked.connect(self.on_item_double_clicked)
59
+
60
+ def startDrag(self, supportedActions):
61
+ item = self.currentItem()
62
+ if not item or item.childCount() > 0:
63
+ return
64
+ category, block_type = item.data(0, Qt.UserRole)
65
+ mime = QMimeData()
66
+ mime.setText(f"{category}:{block_type}")
67
+
68
+ drag = QDrag(self)
69
+ drag.setMimeData(mime)
70
+ drag.exec(Qt.MoveAction)
71
+
72
+
73
+ def on_item_double_clicked(self, item, column):
74
+ if item.childCount() > 0:
75
+ return
76
+
77
+ data = item.data(0, Qt.UserRole)
78
+ if not data:
79
+ return
80
+
81
+ category, block_type = data
82
+
83
+ meta = self.resolve_block_meta(category, block_type)
84
+
85
+ instance = BlockInstance(meta=meta)
86
+
87
+ dialog = BlockDialog(_PreviewBlock(instance), readonly=True)
88
+ dialog.exec()
89
+
90
+
91
+ class BlockList(QWidget):
92
+ def __init__(self,
93
+ get_categories: Callable[[], list[str]],
94
+ get_blocks: Callable[[str], list[str]],
95
+ resolve_block_meta: Callable[[str, str], BlockMeta]):
96
+ super().__init__()
97
+
98
+ self.search = QLineEdit(self)
99
+ self.search.setPlaceholderText("Search by category or block name...")
100
+
101
+ self.tree = _BlockTree(get_categories, get_blocks, resolve_block_meta)
102
+
103
+ layout = QVBoxLayout(self)
104
+ layout.setContentsMargins(0, 0, 0, 0)
105
+ layout.setSpacing(4)
106
+ layout.addWidget(self.search)
107
+ layout.addWidget(self.tree)
108
+
109
+ self.search.textChanged.connect(self._apply_filter)
110
+
111
+ def _apply_filter(self, text: str):
112
+ query = text.strip().lower()
113
+
114
+ for i in range(self.tree.topLevelItemCount()):
115
+ category_item = self.tree.topLevelItem(i)
116
+ category_name = category_item.text(0)
117
+ category_match = category_name.lower().startswith(query)
118
+
119
+ visible_children = 0
120
+ for j in range(category_item.childCount()):
121
+ block_item = category_item.child(j)
122
+ block_name = block_item.text(0)
123
+ block_match = block_name.lower().startswith(query)
124
+ visible = (not query) or category_match or block_match
125
+ block_item.setHidden(not visible)
126
+ if visible:
127
+ visible_children += 1
128
+
129
+ category_visible = (not query) or category_match or (visible_children > 0)
130
+ category_item.setHidden(not category_visible)
131
+ category_item.setExpanded(bool(query and category_visible))