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,302 @@
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 copy
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING, Any, Callable
24
+
25
+ from PySide6.QtCore import QObject, Signal, QPointF
26
+
27
+ from pySimBlocks.gui.models import (
28
+ BlockInstance,
29
+ ConnectionInstance,
30
+ PortInstance,
31
+ ProjectState,
32
+ port_instance,
33
+ )
34
+ from pySimBlocks.gui.widgets.diagram_view import DiagramView
35
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta
36
+ from pySimBlocks.gui.services.yaml_tools import cleanup_runtime_project_yaml
37
+
38
+ if TYPE_CHECKING:
39
+ from pySimBlocks.gui.services.project_loader import ProjectLoader
40
+
41
+
42
+ class ProjectController(QObject):
43
+
44
+ dirty_changed: Signal = Signal(bool)
45
+
46
+ def __init__(self,
47
+ project_state: ProjectState,
48
+ view: DiagramView,
49
+ resolve_block_meta: Callable[[str, str], BlockMeta]
50
+ ):
51
+ super().__init__()
52
+ self.project_state = project_state
53
+ self.resolve_block_meta = resolve_block_meta
54
+ self.view = view
55
+
56
+ self.is_dirty: bool = False
57
+
58
+ # --------------------------------------------------------------------------
59
+ # Blocks methods
60
+ # --------------------------------------------------------------------------
61
+ def add_block(self, category: str,
62
+ block_type: str,
63
+ block_layout: dict | None = None) -> BlockInstance:
64
+ block_meta = self.resolve_block_meta(category, block_type)
65
+ block_instance = BlockInstance(block_meta)
66
+ return self._add_block(block_instance, block_layout)
67
+
68
+ # ------------------------------------------------------------------
69
+ def add_copy_block(self, block_instance: BlockInstance) -> BlockInstance:
70
+ copy = BlockInstance.copy(block_instance)
71
+ return self._add_block(copy)
72
+
73
+ # ------------------------------------------------------------------
74
+ def _add_block(self, block_instance: BlockInstance,
75
+ block_layout: dict | None = None) -> BlockInstance:
76
+ self.make_dirty()
77
+ block_instance.name = self.make_unique_name(block_instance.name)
78
+ block_instance.resolve_ports()
79
+ self.project_state.add_block(block_instance)
80
+ self.view.add_block(block_instance, block_layout)
81
+
82
+ return block_instance
83
+
84
+ # ------------------------------------------------------------------
85
+ def rename_block(self, block_instance: BlockInstance, new_name: str):
86
+ old_name = block_instance.name
87
+
88
+ if old_name == new_name:
89
+ return
90
+
91
+ self.make_dirty()
92
+ new_name = self.make_unique_name(new_name)
93
+
94
+ block_instance.name = new_name
95
+ prefix_old = f"{old_name}.outputs."
96
+ prefix_new = f"{new_name}.outputs."
97
+
98
+ self.project_state.logging = [
99
+ s.replace(prefix_old, prefix_new)
100
+ if s.startswith(prefix_old) else s
101
+ for s in self.project_state.logging
102
+ ]
103
+
104
+ for plot in self.project_state.plots:
105
+ plot["signals"] = [
106
+ s.replace(prefix_old, prefix_new)
107
+ if s.startswith(prefix_old) else s
108
+ for s in plot["signals"]
109
+ ]
110
+
111
+ # ------------------------------------------------------------------
112
+ def update_block_param(self, block_instance: BlockInstance, params: dict[str, Any]):
113
+
114
+ self.rename_block(block_instance, params.pop("name", block_instance.name))
115
+
116
+ if params == block_instance.parameters:
117
+ return
118
+
119
+ block_instance.update_params(params)
120
+ block_instance.resolve_ports()
121
+ self._remove_connection_if_port_disapear(block_instance)
122
+ self.view.refresh_block_port(block_instance)
123
+ self.make_dirty()
124
+
125
+ # ------------------------------------------------------------------
126
+ def remove_block(self, block_instance: BlockInstance):
127
+ self.make_dirty()
128
+
129
+ # remove connections
130
+ for connection in self.project_state.get_connections_of_block(block_instance):
131
+ self.remove_connection(connection)
132
+
133
+ # delete all outputs signal from logging
134
+ removed_signals = [
135
+ f"{block_instance.name}.outputs.{p.name}"
136
+ for p in block_instance.ports if p.direction == "output"
137
+ ]
138
+ remaining_signals = [
139
+ s for s in self.project_state.logging
140
+ if s not in removed_signals
141
+ ]
142
+ self.set_logged_signals(remaining_signals)
143
+
144
+ # remove signals from plots and delete empty plot
145
+ for i in reversed(range(len(self.project_state.plots))):
146
+ plot = self.project_state.plots[i]
147
+ # Keep only signals that are not removed
148
+ plot["signals"] = [s for s in plot["signals"] if s not in removed_signals]
149
+ # Delete the plot if it has no signals left
150
+ if not plot["signals"]:
151
+ self.delete_plot(i)
152
+
153
+ # remove block
154
+ self.project_state.remove_block(block_instance)
155
+ self.view.remove_block(block_instance)
156
+
157
+ # ------------------------------------------------------------------
158
+ def make_unique_name(self, base_name: str) -> str:
159
+ existing = {b.name for b in self.project_state.blocks}
160
+
161
+ if base_name not in existing:
162
+ return base_name
163
+
164
+ i = 1
165
+ while f"{base_name}_{i}" in existing:
166
+ i += 1
167
+
168
+ return f"{base_name}_{i}"
169
+
170
+ # ------------------------------------------------------------------
171
+ def is_name_available(self, name: str, current=None) -> bool:
172
+ for b in self.project_state.blocks:
173
+ if b is current:
174
+ continue
175
+ if b.name == name:
176
+ return False
177
+ return True
178
+
179
+ # --------------------------------------------------------------------------
180
+ # connection methods
181
+ # --------------------------------------------------------------------------
182
+ def add_connection(self,
183
+ port1: PortInstance,
184
+ port2: PortInstance,
185
+ points: list[QPointF] | None = None):
186
+
187
+ if not port1.is_compatible(port2):
188
+ return
189
+
190
+ src_port, dst_port = (
191
+ (port1, port2) if port1.direction == "output" else (port2, port1)
192
+ )
193
+
194
+ port_dst_connections = self.project_state.get_connections_of_port(dst_port)
195
+
196
+ if not dst_port.can_accept_connection(port_dst_connections):
197
+ return
198
+
199
+ connection_instance = ConnectionInstance(src_port, dst_port)
200
+
201
+ self.project_state.add_connection(connection_instance)
202
+ self.view.add_connection(connection_instance, points)
203
+ self.make_dirty()
204
+
205
+ # ------------------------------------------------------------------
206
+
207
+ def _remove_connection_if_port_disapear(self, block_instance: BlockInstance) -> None:
208
+
209
+ for connection in self.project_state.get_connections_of_block(block_instance):
210
+
211
+ src_exists = connection.src_port in connection.src_block().ports
212
+ dst_exists = connection.dst_port in connection.dst_block().ports
213
+ if not (src_exists and dst_exists):
214
+ self.remove_connection(connection)
215
+
216
+ # ------------------------------------------------------------------
217
+ def remove_connection(self, connection: ConnectionInstance):
218
+
219
+ self.project_state.remove_connection(connection)
220
+ self.view.remove_connection(connection)
221
+ self.make_dirty()
222
+
223
+ # --------------------------------------------------------------------------
224
+ # Project methods
225
+ # --------------------------------------------------------------------------
226
+ def make_dirty(self):
227
+ if not self.is_dirty:
228
+ self.is_dirty = True
229
+ self.dirty_changed.emit(True)
230
+
231
+ # ------------------------------------------------------------------
232
+ def clear_dirty(self):
233
+ if self.is_dirty:
234
+ self.is_dirty = False
235
+ self.dirty_changed.emit(False)
236
+
237
+ # ------------------------------------------------------------------
238
+ def clear(self):
239
+ self.project_state.clear()
240
+ self.view.clear_scene()
241
+
242
+ # ------------------------------------------------------------------
243
+ def update_project_param(self, new_path: Path, ext: str):
244
+ cleanup_runtime_project_yaml(self.project_state.directory_path)
245
+ if new_path != self.project_state.directory_path:
246
+ self.make_dirty()
247
+ self.project_state.directory_path = new_path
248
+
249
+ if ext != self.project_state.external:
250
+ self.make_dirty()
251
+ self.project_state.external = None if ext == "" else ext
252
+
253
+ # ------------------------------------------------------------------
254
+ def load_project(self, loader: 'ProjectLoader'):
255
+ loader.load(self, self.project_state.directory_path)
256
+
257
+ # --------------------------------------------------------------------------
258
+ # Plot methods
259
+ # --------------------------------------------------------------------------
260
+ def create_plot(self, title: str, signals: list[str]) -> None:
261
+ self._ensure_logged(signals)
262
+ self.project_state.plots.append({
263
+ "title": title,
264
+ "signals": list(signals),
265
+ })
266
+ self.make_dirty()
267
+
268
+ # ------------------------------------------------------------------
269
+ def update_plot(self, index: int, title: str, signals: list[str]) -> None:
270
+ self._ensure_logged(signals)
271
+ plot = self.project_state.plots[index]
272
+ if plot["signals"] == signals and plot["title"] == title:
273
+ return
274
+ plot["title"] = title
275
+ plot["signals"] = list(signals)
276
+ self.make_dirty()
277
+
278
+ # ------------------------------------------------------------------
279
+ def delete_plot(self, index: int) -> None:
280
+ del self.project_state.plots[index]
281
+ self.make_dirty()
282
+
283
+ # ------------------------------------------------------------------
284
+ def _ensure_logged(self, signals: list[str]):
285
+ for sig in signals:
286
+ if sig not in self.project_state.logging:
287
+ self.project_state.logging.append(sig)
288
+
289
+ # ------------------------------------------------------------------
290
+ def update_simulation_params(self, params: dict[str, float | str]):
291
+ if self.project_state.simulation.__dict__ == params:
292
+ return
293
+ self.project_state.load_simulation(params)
294
+ self.make_dirty()
295
+
296
+ # ------------------------------------------------------------------
297
+ def set_logged_signals(self, signals: list[str]):
298
+ new_logging = list(dict.fromkeys(signals))
299
+ if set(self.project_state.logging) == set(new_logging):
300
+ return
301
+ self.project_state.logging = new_logging
302
+ self.make_dirty()
@@ -0,0 +1,20 @@
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
+