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,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))
|