pySimBlocks 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. examples/advanced/hardware/block_diagram.py +162 -0
  2. examples/advanced/hardware/camera.py +93 -0
  3. examples/advanced/hardware/gui_emio.py +231 -0
  4. examples/advanced/hardware/lmi.py +69 -0
  5. examples/advanced/hardware/main.py +57 -0
  6. examples/advanced/hardware/parameters.py +54 -0
  7. examples/advanced/qp_problem/params.py +10 -0
  8. examples/advanced/qp_problem/run.py +14 -0
  9. examples/advanced/sofa/gui/finger/Finger.py +108 -0
  10. examples/advanced/sofa/gui/finger/FingerController.py +53 -0
  11. examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
  12. examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
  13. examples/advanced/sofa/python/Finger.py +107 -0
  14. examples/advanced/sofa/python/FingerController.py +48 -0
  15. examples/advanced/sofa/python/closedloop.py +64 -0
  16. examples/advanced/sofa/python/openloop.py +58 -0
  17. examples/basics/algebraic_function/g.py +7 -0
  18. examples/basics/algebraic_function/run.py +14 -0
  19. examples/basics/dc_motor/gui/run.py +14 -0
  20. examples/basics/dc_motor/multiple_order.py +91 -0
  21. examples/basics/dc_motor/openloop.py +62 -0
  22. examples/basics/dc_motor/pi.py +99 -0
  23. examples/basics/dc_motor/pid.py +118 -0
  24. examples/basics/dc_motor/pid_block.py +82 -0
  25. examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
  26. examples/basics/external/parameters.py +4 -0
  27. examples/basics/external/run.py +14 -0
  28. examples/basics/file_sources/temp.py +16 -0
  29. examples/basics/observer.py +76 -0
  30. examples/basics/state_feedback/gui/parameters.py +9 -0
  31. examples/basics/state_feedback/gui/run.py +14 -0
  32. examples/basics/state_feedback/state_feedback.py +102 -0
  33. examples/basics/state_space/linear/gui/run.py +14 -0
  34. examples/basics/state_space/linear/ss_block.py +89 -0
  35. examples/basics/state_space/linear/ss_manual.py +105 -0
  36. examples/basics/state_space/non_linear/nlss.py +47 -0
  37. examples/basics/state_space/non_linear/run.py +14 -0
  38. examples/basics/state_space/polytope/params.py +39 -0
  39. examples/basics/state_space/polytope/weights.py +12 -0
  40. examples/comparison/simulink/convert.py +29 -0
  41. examples/comparison/simulink/openloop.py +127 -0
  42. examples/comparison/simulink/pi.py +148 -0
  43. examples/comparison/simulink/pid.py +154 -0
  44. examples/comparison/simulink/pid_backward.py +155 -0
  45. examples/quick_start/filter.py +35 -0
  46. examples/quick_start/gui/run.py +14 -0
  47. examples/tutorials/tutorial_1_python/main.py +99 -0
  48. examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
  49. examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
  50. pySimBlocks/__init__.py +35 -0
  51. pySimBlocks/blocks/__init__.py +68 -0
  52. pySimBlocks/blocks/controllers/__init__.py +27 -0
  53. pySimBlocks/blocks/controllers/pid.py +250 -0
  54. pySimBlocks/blocks/controllers/state_feedback.py +141 -0
  55. pySimBlocks/blocks/interfaces/__init__.py +27 -0
  56. pySimBlocks/blocks/interfaces/external_input.py +107 -0
  57. pySimBlocks/blocks/interfaces/external_output.py +113 -0
  58. pySimBlocks/blocks/observers/__init__.py +23 -0
  59. pySimBlocks/blocks/observers/luenberger.py +175 -0
  60. pySimBlocks/blocks/operators/__init__.py +47 -0
  61. pySimBlocks/blocks/operators/algebraic_function.py +258 -0
  62. pySimBlocks/blocks/operators/dead_zone.py +186 -0
  63. pySimBlocks/blocks/operators/delay.py +254 -0
  64. pySimBlocks/blocks/operators/demux.py +123 -0
  65. pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
  66. pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
  67. pySimBlocks/blocks/operators/gain.py +294 -0
  68. pySimBlocks/blocks/operators/mux.py +131 -0
  69. pySimBlocks/blocks/operators/product.py +194 -0
  70. pySimBlocks/blocks/operators/rate_limiter.py +214 -0
  71. pySimBlocks/blocks/operators/saturation.py +178 -0
  72. pySimBlocks/blocks/operators/sum.py +188 -0
  73. pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
  74. pySimBlocks/blocks/optimizers/__init__.py +23 -0
  75. pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
  76. pySimBlocks/blocks/sources/__init__.py +37 -0
  77. pySimBlocks/blocks/sources/chirp.py +168 -0
  78. pySimBlocks/blocks/sources/constant.py +82 -0
  79. pySimBlocks/blocks/sources/file_source.py +294 -0
  80. pySimBlocks/blocks/sources/function_source.py +197 -0
  81. pySimBlocks/blocks/sources/ramp.py +99 -0
  82. pySimBlocks/blocks/sources/sinusoidal.py +113 -0
  83. pySimBlocks/blocks/sources/step.py +138 -0
  84. pySimBlocks/blocks/sources/white_noise.py +107 -0
  85. pySimBlocks/blocks/systems/__init__.py +27 -0
  86. pySimBlocks/blocks/systems/linear_state_space.py +180 -0
  87. pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
  88. pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
  89. pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
  90. pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
  91. pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
  92. pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
  93. pySimBlocks/cli.py +127 -0
  94. pySimBlocks/core/__init__.py +32 -0
  95. pySimBlocks/core/block.py +167 -0
  96. pySimBlocks/core/block_source.py +94 -0
  97. pySimBlocks/core/config.py +90 -0
  98. pySimBlocks/core/fixed_time_manager.py +46 -0
  99. pySimBlocks/core/model.py +250 -0
  100. pySimBlocks/core/scheduler.py +30 -0
  101. pySimBlocks/core/simulator.py +329 -0
  102. pySimBlocks/core/task.py +73 -0
  103. pySimBlocks/docs/blocks/controllers/pid.md +78 -0
  104. pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
  105. pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
  106. pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
  107. pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
  108. pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
  109. pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
  110. pySimBlocks/docs/blocks/operators/delay.md +76 -0
  111. pySimBlocks/docs/blocks/operators/demux.md +64 -0
  112. pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
  113. pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
  114. pySimBlocks/docs/blocks/operators/gain.md +97 -0
  115. pySimBlocks/docs/blocks/operators/mux.md +64 -0
  116. pySimBlocks/docs/blocks/operators/product.md +51 -0
  117. pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
  118. pySimBlocks/docs/blocks/operators/saturation.md +62 -0
  119. pySimBlocks/docs/blocks/operators/sum.md +49 -0
  120. pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
  121. pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
  122. pySimBlocks/docs/blocks/sources/chirp.md +102 -0
  123. pySimBlocks/docs/blocks/sources/constant.md +53 -0
  124. pySimBlocks/docs/blocks/sources/file_source.md +51 -0
  125. pySimBlocks/docs/blocks/sources/function_source.md +52 -0
  126. pySimBlocks/docs/blocks/sources/ramp.md +58 -0
  127. pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
  128. pySimBlocks/docs/blocks/sources/step.md +63 -0
  129. pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
  130. pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
  131. pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
  132. pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
  133. pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
  134. pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
  135. pySimBlocks/gui/__init__.py +20 -0
  136. pySimBlocks/gui/addons/__init__.py +19 -0
  137. pySimBlocks/gui/addons/sofa/__init__.py +19 -0
  138. pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
  139. pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
  140. pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
  141. pySimBlocks/gui/blocks/block_meta.py +408 -0
  142. pySimBlocks/gui/blocks/controllers/pid.py +116 -0
  143. pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
  144. pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
  145. pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
  146. pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
  147. pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
  148. pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
  149. pySimBlocks/gui/blocks/operators/delay.py +79 -0
  150. pySimBlocks/gui/blocks/operators/demux.py +98 -0
  151. pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
  152. pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
  153. pySimBlocks/gui/blocks/operators/gain.py +79 -0
  154. pySimBlocks/gui/blocks/operators/mux.py +100 -0
  155. pySimBlocks/gui/blocks/operators/product.py +109 -0
  156. pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
  157. pySimBlocks/gui/blocks/operators/saturation.py +72 -0
  158. pySimBlocks/gui/blocks/operators/sum.py +91 -0
  159. pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
  160. pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
  161. pySimBlocks/gui/blocks/parameter_meta.py +34 -0
  162. pySimBlocks/gui/blocks/port_meta.py +31 -0
  163. pySimBlocks/gui/blocks/sources/chirp.py +146 -0
  164. pySimBlocks/gui/blocks/sources/constant.py +63 -0
  165. pySimBlocks/gui/blocks/sources/file_source.py +196 -0
  166. pySimBlocks/gui/blocks/sources/function_source.py +202 -0
  167. pySimBlocks/gui/blocks/sources/ramp.py +73 -0
  168. pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
  169. pySimBlocks/gui/blocks/sources/step.py +83 -0
  170. pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
  171. pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
  172. pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
  173. pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
  174. pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
  175. pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
  176. pySimBlocks/gui/dialogs/__init__.py +19 -0
  177. pySimBlocks/gui/dialogs/block_dialog.py +125 -0
  178. pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
  179. pySimBlocks/gui/dialogs/help_dialog.py +39 -0
  180. pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
  181. pySimBlocks/gui/dialogs/settings/plots.py +187 -0
  182. pySimBlocks/gui/dialogs/settings/project.py +146 -0
  183. pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
  184. pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
  185. pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
  186. pySimBlocks/gui/editor.py +49 -0
  187. pySimBlocks/gui/graphics/__init__.py +29 -0
  188. pySimBlocks/gui/graphics/block_item.py +289 -0
  189. pySimBlocks/gui/graphics/connection_item.py +266 -0
  190. pySimBlocks/gui/graphics/port_item.py +153 -0
  191. pySimBlocks/gui/graphics/theme.py +119 -0
  192. pySimBlocks/gui/main_window.py +162 -0
  193. pySimBlocks/gui/models/__init__.py +31 -0
  194. pySimBlocks/gui/models/block_instance.py +114 -0
  195. pySimBlocks/gui/models/connection_instance.py +43 -0
  196. pySimBlocks/gui/models/port_instance.py +52 -0
  197. pySimBlocks/gui/models/project_simulation_params.py +50 -0
  198. pySimBlocks/gui/models/project_state.py +114 -0
  199. pySimBlocks/gui/project_controller.py +302 -0
  200. pySimBlocks/gui/services/__init__.py +20 -0
  201. pySimBlocks/gui/services/project_loader.py +381 -0
  202. pySimBlocks/gui/services/project_saver.py +68 -0
  203. pySimBlocks/gui/services/simulation_runner.py +66 -0
  204. pySimBlocks/gui/services/yaml_tools.py +261 -0
  205. pySimBlocks/gui/widgets/__init__.py +19 -0
  206. pySimBlocks/gui/widgets/block_list.py +131 -0
  207. pySimBlocks/gui/widgets/diagram_view.py +312 -0
  208. pySimBlocks/gui/widgets/toolbar_view.py +161 -0
  209. pySimBlocks/project/__init__.py +35 -0
  210. pySimBlocks/project/build_model.py +92 -0
  211. pySimBlocks/project/generate_run_script.py +109 -0
  212. pySimBlocks/project/generate_sofa_controller.py +215 -0
  213. pySimBlocks/project/load_project_config.py +197 -0
  214. pySimBlocks/project/load_simulation_config.py +166 -0
  215. pySimBlocks/project/load_simulator.py +45 -0
  216. pySimBlocks/project/plot_from_config.py +174 -0
  217. pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
  218. pySimBlocks/real_time/__init__.py +23 -0
  219. pySimBlocks/real_time/real_time_runner.py +148 -0
  220. pySimBlocks/tools/__init__.py +19 -0
  221. pySimBlocks/tools/blocks_registry.py +141 -0
  222. pySimBlocks/tools/generate_blocks_index.py +124 -0
  223. pysimblocks-0.1.0.dist-info/METADATA +180 -0
  224. pysimblocks-0.1.0.dist-info/RECORD +262 -0
  225. pysimblocks-0.1.0.dist-info/WHEEL +5 -0
  226. pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
  227. pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
  228. pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
  229. tests/blocks/controllers/test_pid.py +143 -0
  230. tests/blocks/controllers/test_state_feedback.py +193 -0
  231. tests/blocks/observers/test_luenberger.py +225 -0
  232. tests/blocks/operators/test_algebraic_function.py +230 -0
  233. tests/blocks/operators/test_dead_zone.py +106 -0
  234. tests/blocks/operators/test_delay.py +167 -0
  235. tests/blocks/operators/test_demux.py +111 -0
  236. tests/blocks/operators/test_discrete_derivator.py +160 -0
  237. tests/blocks/operators/test_discrete_integrator.py +149 -0
  238. tests/blocks/operators/test_gain.py +182 -0
  239. tests/blocks/operators/test_mux.py +101 -0
  240. tests/blocks/operators/test_product.py +171 -0
  241. tests/blocks/operators/test_rate_limiter.py +166 -0
  242. tests/blocks/operators/test_saturation.py +157 -0
  243. tests/blocks/operators/test_sum.py +183 -0
  244. tests/blocks/operators/test_zero_order_hold.py +106 -0
  245. tests/blocks/sources/test_chirp.py +157 -0
  246. tests/blocks/sources/test_constant.py +55 -0
  247. tests/blocks/sources/test_file_source.py +185 -0
  248. tests/blocks/sources/test_function_source.py +135 -0
  249. tests/blocks/sources/test_ramp.py +110 -0
  250. tests/blocks/sources/test_sinusoidal.py +113 -0
  251. tests/blocks/sources/test_step.py +76 -0
  252. tests/blocks/sources/test_white_noise.py +108 -0
  253. tests/blocks/systems/test_linear_state_space.py +135 -0
  254. tests/blocks/systems/test_polytopic_state_space.py +133 -0
  255. tests/core/test_core_errors_and_determinism.py +82 -0
  256. tests/core/test_core_multirate_tasks.py +88 -0
  257. tests/core/test_core_two_phase_and_propagation.py +132 -0
  258. tests/gui/main_window_open_test.py +162 -0
  259. tests/gui/test_block_meta_param_cache.py +16 -0
  260. tests/gui/test_file_source_meta.py +17 -0
  261. tests/gui/test_function_source_meta.py +28 -0
  262. tests/regression/test_simulink_dc_motor.py +217 -0
@@ -0,0 +1,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