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.
- examples/advanced/hardware/block_diagram.py +162 -0
- examples/advanced/hardware/camera.py +93 -0
- examples/advanced/hardware/gui_emio.py +231 -0
- examples/advanced/hardware/lmi.py +69 -0
- examples/advanced/hardware/main.py +57 -0
- examples/advanced/hardware/parameters.py +54 -0
- examples/advanced/qp_problem/params.py +10 -0
- examples/advanced/qp_problem/run.py +14 -0
- examples/advanced/sofa/gui/finger/Finger.py +108 -0
- examples/advanced/sofa/gui/finger/FingerController.py +53 -0
- examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
- examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
- examples/advanced/sofa/python/Finger.py +107 -0
- examples/advanced/sofa/python/FingerController.py +48 -0
- examples/advanced/sofa/python/closedloop.py +64 -0
- examples/advanced/sofa/python/openloop.py +58 -0
- examples/basics/algebraic_function/g.py +7 -0
- examples/basics/algebraic_function/run.py +14 -0
- examples/basics/dc_motor/gui/run.py +14 -0
- examples/basics/dc_motor/multiple_order.py +91 -0
- examples/basics/dc_motor/openloop.py +62 -0
- examples/basics/dc_motor/pi.py +99 -0
- examples/basics/dc_motor/pid.py +118 -0
- examples/basics/dc_motor/pid_block.py +82 -0
- examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
- examples/basics/external/parameters.py +4 -0
- examples/basics/external/run.py +14 -0
- examples/basics/file_sources/temp.py +16 -0
- examples/basics/observer.py +76 -0
- examples/basics/state_feedback/gui/parameters.py +9 -0
- examples/basics/state_feedback/gui/run.py +14 -0
- examples/basics/state_feedback/state_feedback.py +102 -0
- examples/basics/state_space/linear/gui/run.py +14 -0
- examples/basics/state_space/linear/ss_block.py +89 -0
- examples/basics/state_space/linear/ss_manual.py +105 -0
- examples/basics/state_space/non_linear/nlss.py +47 -0
- examples/basics/state_space/non_linear/run.py +14 -0
- examples/basics/state_space/polytope/params.py +39 -0
- examples/basics/state_space/polytope/weights.py +12 -0
- examples/comparison/simulink/convert.py +29 -0
- examples/comparison/simulink/openloop.py +127 -0
- examples/comparison/simulink/pi.py +148 -0
- examples/comparison/simulink/pid.py +154 -0
- examples/comparison/simulink/pid_backward.py +155 -0
- examples/quick_start/filter.py +35 -0
- examples/quick_start/gui/run.py +14 -0
- examples/tutorials/tutorial_1_python/main.py +99 -0
- examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
- examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
- pySimBlocks/__init__.py +35 -0
- pySimBlocks/blocks/__init__.py +68 -0
- pySimBlocks/blocks/controllers/__init__.py +27 -0
- pySimBlocks/blocks/controllers/pid.py +250 -0
- pySimBlocks/blocks/controllers/state_feedback.py +141 -0
- pySimBlocks/blocks/interfaces/__init__.py +27 -0
- pySimBlocks/blocks/interfaces/external_input.py +107 -0
- pySimBlocks/blocks/interfaces/external_output.py +113 -0
- pySimBlocks/blocks/observers/__init__.py +23 -0
- pySimBlocks/blocks/observers/luenberger.py +175 -0
- pySimBlocks/blocks/operators/__init__.py +47 -0
- pySimBlocks/blocks/operators/algebraic_function.py +258 -0
- pySimBlocks/blocks/operators/dead_zone.py +186 -0
- pySimBlocks/blocks/operators/delay.py +254 -0
- pySimBlocks/blocks/operators/demux.py +123 -0
- pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
- pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
- pySimBlocks/blocks/operators/gain.py +294 -0
- pySimBlocks/blocks/operators/mux.py +131 -0
- pySimBlocks/blocks/operators/product.py +194 -0
- pySimBlocks/blocks/operators/rate_limiter.py +214 -0
- pySimBlocks/blocks/operators/saturation.py +178 -0
- pySimBlocks/blocks/operators/sum.py +188 -0
- pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
- pySimBlocks/blocks/optimizers/__init__.py +23 -0
- pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
- pySimBlocks/blocks/sources/__init__.py +37 -0
- pySimBlocks/blocks/sources/chirp.py +168 -0
- pySimBlocks/blocks/sources/constant.py +82 -0
- pySimBlocks/blocks/sources/file_source.py +294 -0
- pySimBlocks/blocks/sources/function_source.py +197 -0
- pySimBlocks/blocks/sources/ramp.py +99 -0
- pySimBlocks/blocks/sources/sinusoidal.py +113 -0
- pySimBlocks/blocks/sources/step.py +138 -0
- pySimBlocks/blocks/sources/white_noise.py +107 -0
- pySimBlocks/blocks/systems/__init__.py +27 -0
- pySimBlocks/blocks/systems/linear_state_space.py +180 -0
- pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
- pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
- pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
- pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
- pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
- pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
- pySimBlocks/cli.py +127 -0
- pySimBlocks/core/__init__.py +32 -0
- pySimBlocks/core/block.py +167 -0
- pySimBlocks/core/block_source.py +94 -0
- pySimBlocks/core/config.py +90 -0
- pySimBlocks/core/fixed_time_manager.py +46 -0
- pySimBlocks/core/model.py +250 -0
- pySimBlocks/core/scheduler.py +30 -0
- pySimBlocks/core/simulator.py +329 -0
- pySimBlocks/core/task.py +73 -0
- pySimBlocks/docs/blocks/controllers/pid.md +78 -0
- pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
- pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
- pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
- pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
- pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
- pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
- pySimBlocks/docs/blocks/operators/delay.md +76 -0
- pySimBlocks/docs/blocks/operators/demux.md +64 -0
- pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
- pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
- pySimBlocks/docs/blocks/operators/gain.md +97 -0
- pySimBlocks/docs/blocks/operators/mux.md +64 -0
- pySimBlocks/docs/blocks/operators/product.md +51 -0
- pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
- pySimBlocks/docs/blocks/operators/saturation.md +62 -0
- pySimBlocks/docs/blocks/operators/sum.md +49 -0
- pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
- pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
- pySimBlocks/docs/blocks/sources/chirp.md +102 -0
- pySimBlocks/docs/blocks/sources/constant.md +53 -0
- pySimBlocks/docs/blocks/sources/file_source.md +51 -0
- pySimBlocks/docs/blocks/sources/function_source.md +52 -0
- pySimBlocks/docs/blocks/sources/ramp.md +58 -0
- pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
- pySimBlocks/docs/blocks/sources/step.md +63 -0
- pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
- pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
- pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
- pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
- pySimBlocks/gui/__init__.py +20 -0
- pySimBlocks/gui/addons/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
- pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
- pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
- pySimBlocks/gui/blocks/block_meta.py +408 -0
- pySimBlocks/gui/blocks/controllers/pid.py +116 -0
- pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
- pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
- pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
- pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
- pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
- pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
- pySimBlocks/gui/blocks/operators/delay.py +79 -0
- pySimBlocks/gui/blocks/operators/demux.py +98 -0
- pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
- pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
- pySimBlocks/gui/blocks/operators/gain.py +79 -0
- pySimBlocks/gui/blocks/operators/mux.py +100 -0
- pySimBlocks/gui/blocks/operators/product.py +109 -0
- pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
- pySimBlocks/gui/blocks/operators/saturation.py +72 -0
- pySimBlocks/gui/blocks/operators/sum.py +91 -0
- pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
- pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
- pySimBlocks/gui/blocks/parameter_meta.py +34 -0
- pySimBlocks/gui/blocks/port_meta.py +31 -0
- pySimBlocks/gui/blocks/sources/chirp.py +146 -0
- pySimBlocks/gui/blocks/sources/constant.py +63 -0
- pySimBlocks/gui/blocks/sources/file_source.py +196 -0
- pySimBlocks/gui/blocks/sources/function_source.py +202 -0
- pySimBlocks/gui/blocks/sources/ramp.py +73 -0
- pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
- pySimBlocks/gui/blocks/sources/step.py +83 -0
- pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
- pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
- pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
- pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
- pySimBlocks/gui/dialogs/__init__.py +19 -0
- pySimBlocks/gui/dialogs/block_dialog.py +125 -0
- pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
- pySimBlocks/gui/dialogs/help_dialog.py +39 -0
- pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
- pySimBlocks/gui/dialogs/settings/plots.py +187 -0
- pySimBlocks/gui/dialogs/settings/project.py +146 -0
- pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
- pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
- pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
- pySimBlocks/gui/editor.py +49 -0
- pySimBlocks/gui/graphics/__init__.py +29 -0
- pySimBlocks/gui/graphics/block_item.py +289 -0
- pySimBlocks/gui/graphics/connection_item.py +266 -0
- pySimBlocks/gui/graphics/port_item.py +153 -0
- pySimBlocks/gui/graphics/theme.py +119 -0
- pySimBlocks/gui/main_window.py +162 -0
- pySimBlocks/gui/models/__init__.py +31 -0
- pySimBlocks/gui/models/block_instance.py +114 -0
- pySimBlocks/gui/models/connection_instance.py +43 -0
- pySimBlocks/gui/models/port_instance.py +52 -0
- pySimBlocks/gui/models/project_simulation_params.py +50 -0
- pySimBlocks/gui/models/project_state.py +114 -0
- pySimBlocks/gui/project_controller.py +302 -0
- pySimBlocks/gui/services/__init__.py +20 -0
- pySimBlocks/gui/services/project_loader.py +381 -0
- pySimBlocks/gui/services/project_saver.py +68 -0
- pySimBlocks/gui/services/simulation_runner.py +66 -0
- pySimBlocks/gui/services/yaml_tools.py +261 -0
- pySimBlocks/gui/widgets/__init__.py +19 -0
- pySimBlocks/gui/widgets/block_list.py +131 -0
- pySimBlocks/gui/widgets/diagram_view.py +312 -0
- pySimBlocks/gui/widgets/toolbar_view.py +161 -0
- pySimBlocks/project/__init__.py +35 -0
- pySimBlocks/project/build_model.py +92 -0
- pySimBlocks/project/generate_run_script.py +109 -0
- pySimBlocks/project/generate_sofa_controller.py +215 -0
- pySimBlocks/project/load_project_config.py +197 -0
- pySimBlocks/project/load_simulation_config.py +166 -0
- pySimBlocks/project/load_simulator.py +45 -0
- pySimBlocks/project/plot_from_config.py +174 -0
- pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
- pySimBlocks/real_time/__init__.py +23 -0
- pySimBlocks/real_time/real_time_runner.py +148 -0
- pySimBlocks/tools/__init__.py +19 -0
- pySimBlocks/tools/blocks_registry.py +141 -0
- pySimBlocks/tools/generate_blocks_index.py +124 -0
- pysimblocks-0.1.0.dist-info/METADATA +180 -0
- pysimblocks-0.1.0.dist-info/RECORD +262 -0
- pysimblocks-0.1.0.dist-info/WHEEL +5 -0
- pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
- pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
- pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
- tests/blocks/controllers/test_pid.py +143 -0
- tests/blocks/controllers/test_state_feedback.py +193 -0
- tests/blocks/observers/test_luenberger.py +225 -0
- tests/blocks/operators/test_algebraic_function.py +230 -0
- tests/blocks/operators/test_dead_zone.py +106 -0
- tests/blocks/operators/test_delay.py +167 -0
- tests/blocks/operators/test_demux.py +111 -0
- tests/blocks/operators/test_discrete_derivator.py +160 -0
- tests/blocks/operators/test_discrete_integrator.py +149 -0
- tests/blocks/operators/test_gain.py +182 -0
- tests/blocks/operators/test_mux.py +101 -0
- tests/blocks/operators/test_product.py +171 -0
- tests/blocks/operators/test_rate_limiter.py +166 -0
- tests/blocks/operators/test_saturation.py +157 -0
- tests/blocks/operators/test_sum.py +183 -0
- tests/blocks/operators/test_zero_order_hold.py +106 -0
- tests/blocks/sources/test_chirp.py +157 -0
- tests/blocks/sources/test_constant.py +55 -0
- tests/blocks/sources/test_file_source.py +185 -0
- tests/blocks/sources/test_function_source.py +135 -0
- tests/blocks/sources/test_ramp.py +110 -0
- tests/blocks/sources/test_sinusoidal.py +113 -0
- tests/blocks/sources/test_step.py +76 -0
- tests/blocks/sources/test_white_noise.py +108 -0
- tests/blocks/systems/test_linear_state_space.py +135 -0
- tests/blocks/systems/test_polytopic_state_space.py +133 -0
- tests/core/test_core_errors_and_determinism.py +82 -0
- tests/core/test_core_multirate_tasks.py +88 -0
- tests/core/test_core_two_phase_and_propagation.py +132 -0
- tests/gui/main_window_open_test.py +162 -0
- tests/gui/test_block_meta_param_cache.py +16 -0
- tests/gui/test_file_source_meta.py +17 -0
- tests/gui/test_function_source_meta.py +28 -0
- 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)
|