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,190 @@
|
|
|
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
|
+
from PySide6.QtWidgets import (
|
|
23
|
+
QHBoxLayout,
|
|
24
|
+
QDialog,
|
|
25
|
+
QLabel,
|
|
26
|
+
QVBoxLayout,
|
|
27
|
+
QLineEdit,
|
|
28
|
+
QPushButton,
|
|
29
|
+
QFormLayout,
|
|
30
|
+
QComboBox,
|
|
31
|
+
QMessageBox,
|
|
32
|
+
QPlainTextEdit
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from pySimBlocks.gui.addons.sofa.sofa_service import SofaService
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SofaDialog(QDialog):
|
|
39
|
+
def __init__(self, sofa_service: SofaService, parent=None):
|
|
40
|
+
super().__init__(parent)
|
|
41
|
+
self.setWindowTitle("Edit block")
|
|
42
|
+
self.setMinimumWidth(300)
|
|
43
|
+
|
|
44
|
+
self.sofa_service = sofa_service
|
|
45
|
+
|
|
46
|
+
main_layout = QVBoxLayout(self)
|
|
47
|
+
self.build_form(main_layout)
|
|
48
|
+
|
|
49
|
+
# --- Buttons row ---
|
|
50
|
+
buttons_layout = QHBoxLayout()
|
|
51
|
+
buttons_layout.addStretch()
|
|
52
|
+
|
|
53
|
+
ok_btn = QPushButton("Ok")
|
|
54
|
+
ok_btn.setDefault(True)
|
|
55
|
+
ok_btn.setAutoDefault(True)
|
|
56
|
+
ok_btn.clicked.connect(self.ok)
|
|
57
|
+
buttons_layout.addWidget(ok_btn)
|
|
58
|
+
|
|
59
|
+
apply_btn = QPushButton("Apply")
|
|
60
|
+
apply_btn.clicked.connect(self.apply)
|
|
61
|
+
buttons_layout.addWidget(apply_btn)
|
|
62
|
+
main_layout.addLayout(buttons_layout)
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------
|
|
65
|
+
# Form
|
|
66
|
+
def build_form(self, layout):
|
|
67
|
+
form = QFormLayout()
|
|
68
|
+
|
|
69
|
+
# --- Block name ---
|
|
70
|
+
self.run_edit = QLineEdit(self.sofa_service.sofa_path)
|
|
71
|
+
self.run_edit.setText(self.sofa_service.sofa_path)
|
|
72
|
+
label = QLabel("runSofa:")
|
|
73
|
+
label.setToolTip("runSofa path")
|
|
74
|
+
form.addRow(label, self.run_edit)
|
|
75
|
+
|
|
76
|
+
self.gui_combo = QComboBox()
|
|
77
|
+
self.gui_combo.addItems(["imgui", "qglviewer", "qt", "custom"])
|
|
78
|
+
self.gui_combo.setCurrentText(self.sofa_service.gui)
|
|
79
|
+
self.gui_combo.currentTextChanged.connect(lambda val: self._on_gui_changed(val))
|
|
80
|
+
form.addRow(QLabel("Sofa GUI:"), self.gui_combo)
|
|
81
|
+
|
|
82
|
+
label = QLabel("Run diagram from Sofa:")
|
|
83
|
+
label.setToolTip("Run simulation with sofa gui")
|
|
84
|
+
run_btn = QPushButton("runSofa")
|
|
85
|
+
run_btn.clicked.connect(self.run)
|
|
86
|
+
form.addRow(label, run_btn)
|
|
87
|
+
|
|
88
|
+
label = QLabel("Export Controller")
|
|
89
|
+
label.setToolTip("Modify Sofa controller to run on cli.")
|
|
90
|
+
export_btn = QPushButton("Export Controller")
|
|
91
|
+
export_btn.clicked.connect(self.export)
|
|
92
|
+
form.addRow(label, export_btn)
|
|
93
|
+
|
|
94
|
+
layout.addLayout(form)
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------
|
|
97
|
+
# Buttons
|
|
98
|
+
def apply(self):
|
|
99
|
+
sofa_path = self.run_edit.text()
|
|
100
|
+
if not Path(sofa_path).exists():
|
|
101
|
+
QMessageBox.warning(
|
|
102
|
+
self,
|
|
103
|
+
"Invalid sofa path",
|
|
104
|
+
f"The run sofa exec not exist:\n{sofa_path}",
|
|
105
|
+
)
|
|
106
|
+
return False
|
|
107
|
+
self.sofa_service.sofa_path = sofa_path
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def ok(self):
|
|
111
|
+
if not self.apply():
|
|
112
|
+
return
|
|
113
|
+
self.accept()
|
|
114
|
+
|
|
115
|
+
def run(self):
|
|
116
|
+
if not self.apply():
|
|
117
|
+
return
|
|
118
|
+
if not self._update_scene_file():
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
progress = QDialog(self)
|
|
122
|
+
progress.setWindowTitle("SOFA running")
|
|
123
|
+
progress.setModal(True)
|
|
124
|
+
progress.setMinimumWidth(300)
|
|
125
|
+
|
|
126
|
+
layout = QVBoxLayout(progress)
|
|
127
|
+
layout.addWidget(QLabel(
|
|
128
|
+
"SOFA is running.\n\n"
|
|
129
|
+
"Close the SOFA GUI to return to pySimBlocks."
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
progress.show()
|
|
133
|
+
try:
|
|
134
|
+
ok, title, details = self.sofa_service.run()
|
|
135
|
+
except Exception as e:
|
|
136
|
+
ok, title, details = False, "Error launching SOFA", str(e)
|
|
137
|
+
finally:
|
|
138
|
+
progress.close()
|
|
139
|
+
if not ok:
|
|
140
|
+
dialog = LogDialog(
|
|
141
|
+
title=f"SOFA error – {title}",
|
|
142
|
+
content=details,
|
|
143
|
+
parent=self
|
|
144
|
+
)
|
|
145
|
+
dialog.exec()
|
|
146
|
+
|
|
147
|
+
def export(self):
|
|
148
|
+
if not self.apply():
|
|
149
|
+
return
|
|
150
|
+
if not self._update_scene_file():
|
|
151
|
+
return
|
|
152
|
+
window = self.parent()
|
|
153
|
+
self.sofa_service.export_controller(window, window.saver)
|
|
154
|
+
|
|
155
|
+
# ------------------------------------------------------------
|
|
156
|
+
# internal methods
|
|
157
|
+
def _on_gui_changed(self, value):
|
|
158
|
+
self.sofa_service.gui = value
|
|
159
|
+
|
|
160
|
+
def _update_scene_file(self):
|
|
161
|
+
ok, msg, details = self.sofa_service.get_scene_file()
|
|
162
|
+
if not ok:
|
|
163
|
+
QMessageBox.warning(
|
|
164
|
+
self,
|
|
165
|
+
msg,
|
|
166
|
+
details,
|
|
167
|
+
QMessageBox.Ok
|
|
168
|
+
)
|
|
169
|
+
return ok
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class LogDialog(QDialog):
|
|
174
|
+
def __init__(self, title: str, content: str, parent=None):
|
|
175
|
+
super().__init__(parent)
|
|
176
|
+
self.setWindowTitle(title)
|
|
177
|
+
self.resize(800, 500)
|
|
178
|
+
|
|
179
|
+
layout = QVBoxLayout(self)
|
|
180
|
+
|
|
181
|
+
self.text = QPlainTextEdit()
|
|
182
|
+
self.text.setReadOnly(True)
|
|
183
|
+
self.text.setPlainText(content)
|
|
184
|
+
self.text.setLineWrapMode(QPlainTextEdit.NoWrap)
|
|
185
|
+
|
|
186
|
+
layout.addWidget(self.text)
|
|
187
|
+
|
|
188
|
+
close_btn = QPushButton("Close")
|
|
189
|
+
close_btn.clicked.connect(self.accept)
|
|
190
|
+
layout.addWidget(close_btn)
|
|
@@ -0,0 +1,209 @@
|
|
|
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 shutil
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from PySide6.QtCore import QProcess, QProcessEnvironment
|
|
26
|
+
|
|
27
|
+
from pySimBlocks.gui.models.project_state import ProjectState
|
|
28
|
+
from pySimBlocks.gui.project_controller import ProjectController
|
|
29
|
+
from pySimBlocks.gui.services.yaml_tools import (
|
|
30
|
+
cleanup_runtime_project_yaml,
|
|
31
|
+
runtime_project_yaml_path,
|
|
32
|
+
save_yaml,
|
|
33
|
+
)
|
|
34
|
+
from pySimBlocks.project.generate_sofa_controller import generate_sofa_controller
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SofaService:
|
|
38
|
+
def __init__(self, project_state: ProjectState, project_controller: ProjectController):
|
|
39
|
+
self.project_state = project_state
|
|
40
|
+
self.project_controller = project_controller
|
|
41
|
+
|
|
42
|
+
self.sofa_path = ""
|
|
43
|
+
self.gui = "imgui"
|
|
44
|
+
self.scene_file = ""
|
|
45
|
+
|
|
46
|
+
self._detect_sofa()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --------------------------------------------------------------------------
|
|
50
|
+
# Public methods
|
|
51
|
+
# --------------------------------------------------------------------------
|
|
52
|
+
def get_scene_file(self):
|
|
53
|
+
flag, msg, details = self.can_use_sofa()
|
|
54
|
+
if flag:
|
|
55
|
+
sofa_block = [b for b in self.project_state.blocks if b.meta.type in ["sofa_plant", "sofa_exchange_i_o"]]
|
|
56
|
+
scene_param = sofa_block[0].parameters.get("scene_file")
|
|
57
|
+
if not scene_param:
|
|
58
|
+
return False, "No scene file", "scene_file parameter is missing."
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
scene_path = self._resolve_scene_file(scene_param)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return False, "Invalid scene file", str(e)
|
|
64
|
+
|
|
65
|
+
if not scene_path.exists():
|
|
66
|
+
return False, "Incorrect Scene File", "The scene file does not exist."
|
|
67
|
+
|
|
68
|
+
self.scene_file = str(scene_path)
|
|
69
|
+
return True, "Scene File set", ""
|
|
70
|
+
else:
|
|
71
|
+
return flag, msg, details
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
def can_use_sofa(self):
|
|
75
|
+
sofa_block = [b for b in self.project_state.blocks if b.meta.type in ["sofa_plant", "sofa_exchange_i_o"]]
|
|
76
|
+
if len(sofa_block) == 0:
|
|
77
|
+
return False, "No SOFA block", "Please Add at least one sofa system."
|
|
78
|
+
elif len(sofa_block) > 1:
|
|
79
|
+
return False, "Multiple SOFA blocks", "Only one sofa system can be set to run sofa."
|
|
80
|
+
else:
|
|
81
|
+
return True, "Sofa can be master", "Only one system found. Diagram can be used from controller."
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
def export_controller(self, window, saver):
|
|
85
|
+
if window.confirm_discard_or_save("exporting sofa"):
|
|
86
|
+
saver.save(self.project_controller.project_state, self.project_controller.view.block_items)
|
|
87
|
+
if self.project_state.directory_path is None:
|
|
88
|
+
raise ValueError("Project directory is not set.\nPlease define it in settings.")
|
|
89
|
+
generate_sofa_controller(self.project_state.directory_path)
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
def run(self):
|
|
93
|
+
env_ok, msg = self._check_sofa_environnment()
|
|
94
|
+
if not env_ok:
|
|
95
|
+
return False, "Environment error", msg
|
|
96
|
+
|
|
97
|
+
if not self.sofa_path or not os.path.exists(self.sofa_path):
|
|
98
|
+
return False, "runSofa not found", ""
|
|
99
|
+
|
|
100
|
+
if not self.scene_file or not os.path.exists(self.scene_file):
|
|
101
|
+
return False, "scene file not found", ""
|
|
102
|
+
|
|
103
|
+
project_dir = self.project_state.directory_path
|
|
104
|
+
if project_dir is None:
|
|
105
|
+
return {}, False, "Project directory is not set.\nPlease define it in settings."
|
|
106
|
+
|
|
107
|
+
runtime_yaml = runtime_project_yaml_path(project_dir)
|
|
108
|
+
cleanup_runtime_project_yaml(project_dir)
|
|
109
|
+
save_yaml(project_state=self.project_state, runtime=True)
|
|
110
|
+
try:
|
|
111
|
+
generate_sofa_controller(project_yaml=runtime_yaml)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
cleanup_runtime_project_yaml(project_dir)
|
|
114
|
+
return False, "Could not update SOFA controller", str(e)
|
|
115
|
+
|
|
116
|
+
# set command
|
|
117
|
+
plugins = "SofaPython3"
|
|
118
|
+
if self.gui == "imgui":
|
|
119
|
+
plugins += ",SofaImgui"
|
|
120
|
+
args = ["-l", plugins, "-g", self.gui, self.scene_file]
|
|
121
|
+
|
|
122
|
+
self._full_log = ""
|
|
123
|
+
|
|
124
|
+
self.process = QProcess()
|
|
125
|
+
env = QProcessEnvironment.systemEnvironment()
|
|
126
|
+
self.process.setProcessEnvironment(env)
|
|
127
|
+
self.process.setWorkingDirectory(str(Path(self.scene_file).parent))
|
|
128
|
+
self.process.setProgram(self.sofa_path)
|
|
129
|
+
self.process.setArguments(args)
|
|
130
|
+
|
|
131
|
+
self.process.setProcessChannelMode(QProcess.MergedChannels)
|
|
132
|
+
self.process.readyReadStandardOutput.connect(
|
|
133
|
+
lambda: self._accumulate_output()
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
self.process.start()
|
|
138
|
+
if not self.process.waitForStarted():
|
|
139
|
+
return False, "Launch failed", "runSofa could not start"
|
|
140
|
+
self.process.waitForFinished(-1)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
generate_sofa_controller(project_dir)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return False, "Could not regenerate controller", "project.yaml does not exist.\n" + str(e)
|
|
146
|
+
|
|
147
|
+
# get output results
|
|
148
|
+
full_log = self._full_log
|
|
149
|
+
exit_code = self.process.exitCode()
|
|
150
|
+
if exit_code != 0:
|
|
151
|
+
return False, "SOFA exited with error", f"exit code = {exit_code}\n\n{full_log}"
|
|
152
|
+
pysimblocks_errors = [
|
|
153
|
+
line for line in full_log.splitlines()
|
|
154
|
+
if "[pySimBlocks] ERROR" in line
|
|
155
|
+
]
|
|
156
|
+
if pysimblocks_errors:
|
|
157
|
+
return False, "pySimBlocks configuration error", "\n".join(pysimblocks_errors)
|
|
158
|
+
return True, "SOFA finished", "Process terminated correctly"
|
|
159
|
+
finally:
|
|
160
|
+
cleanup_runtime_project_yaml(project_dir)
|
|
161
|
+
|
|
162
|
+
# --------------------------------------------------------------------------
|
|
163
|
+
# Internal methods
|
|
164
|
+
# --------------------------------------------------------------------------
|
|
165
|
+
def _accumulate_output(self):
|
|
166
|
+
chunk = self.process.readAllStandardOutput().data().decode()
|
|
167
|
+
print(chunk, end="")
|
|
168
|
+
self._full_log += chunk
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
def _check_sofa_environnment(self):
|
|
172
|
+
sofa_root = os.environ.get("SOFA_ROOT")
|
|
173
|
+
if not sofa_root:
|
|
174
|
+
return False, "SOFA_ROOT is not set."
|
|
175
|
+
|
|
176
|
+
return True, "OK"
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
def _detect_sofa(self):
|
|
180
|
+
detected = None
|
|
181
|
+
sofa_root = os.environ.get("SOFA_ROOT")
|
|
182
|
+
if sofa_root:
|
|
183
|
+
bin_dir = Path(sofa_root) / "bin"
|
|
184
|
+
for candidate in ("runSofa", "runSofa.exe"):
|
|
185
|
+
potential_path = bin_dir / candidate
|
|
186
|
+
if potential_path.exists():
|
|
187
|
+
detected = str(potential_path)
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if not detected:
|
|
191
|
+
detected = shutil.which("runSofa")
|
|
192
|
+
|
|
193
|
+
if not detected:
|
|
194
|
+
detected = shutil.which("runsofa")
|
|
195
|
+
|
|
196
|
+
if detected:
|
|
197
|
+
self.sofa_path = detected
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
def _resolve_scene_file(self, scene_file: str) -> Path:
|
|
201
|
+
project_dir = self.project_state.directory_path
|
|
202
|
+
if project_dir is None:
|
|
203
|
+
raise RuntimeError("Project directory is not set")
|
|
204
|
+
|
|
205
|
+
path = Path(scene_file).expanduser()
|
|
206
|
+
if not path.is_absolute():
|
|
207
|
+
path = (project_dir / path).resolve()
|
|
208
|
+
|
|
209
|
+
return path
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from PySide6.QtWidgets import QLineEdit
|
|
25
|
+
|
|
26
|
+
from pySimBlocks.gui.models.block_instance import BlockInstance
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BlockDialogSession:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
meta: "BlockMeta",
|
|
36
|
+
instance: BlockInstance,
|
|
37
|
+
project_dir: Path | None = None,
|
|
38
|
+
):
|
|
39
|
+
self.meta = meta
|
|
40
|
+
self.instance = instance
|
|
41
|
+
self.project_dir = project_dir
|
|
42
|
+
|
|
43
|
+
# --- STATE UI (par dialog) ---
|
|
44
|
+
self.local_params = dict(instance.parameters)
|
|
45
|
+
self.param_widgets = {}
|
|
46
|
+
self.param_labels = {}
|
|
47
|
+
self.name_edit: QLineEdit | None = None
|