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,230 @@
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 numpy as np
22
+ import matplotlib.pyplot as plt
23
+
24
+ from PySide6.QtWidgets import (
25
+ QDialog, QHBoxLayout, QVBoxLayout,
26
+ QLabel, QListWidget, QListWidgetItem,
27
+ QPushButton, QSizePolicy, QMessageBox
28
+ )
29
+ from PySide6.QtCore import Qt
30
+
31
+ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
32
+ from matplotlib.figure import Figure
33
+
34
+ from pySimBlocks.gui.models.project_state import ProjectState
35
+ from pySimBlocks.core.config import PlotConfig
36
+ from pySimBlocks.project.plot_from_config import plot_from_config
37
+
38
+
39
+ class PlotDialog(QDialog):
40
+ def __init__(self, project_state: ProjectState, parent=None):
41
+ super().__init__(parent)
42
+ self.setWindowTitle("Plot signals")
43
+ self.resize(900, 500)
44
+
45
+ self.project_state = project_state
46
+ self.selected_signals: set[str] = set()
47
+
48
+ self._build_ui()
49
+ self._populate_signals()
50
+
51
+ # ------------------------------------------------------------
52
+ # UI
53
+ # ------------------------------------------------------------
54
+ def _build_ui(self):
55
+ main_layout = QHBoxLayout(self)
56
+
57
+ # ---------- Left panel ----------
58
+ left_layout = QVBoxLayout()
59
+
60
+ title = QLabel("<b>Signals (logged)</b>")
61
+ left_layout.addWidget(title)
62
+
63
+ self.signal_list = QListWidget()
64
+ self.signal_list.setSelectionMode(QListWidget.NoSelection)
65
+ self.signal_list.itemChanged.connect(self._on_signal_toggled)
66
+ left_layout.addWidget(self.signal_list)
67
+
68
+ self.plot_defined_btn = QPushButton("Plot defined plots")
69
+ self.plot_defined_btn.clicked.connect(self._plot_defined_plots)
70
+ left_layout.addWidget(self.plot_defined_btn)
71
+
72
+ main_layout.addLayout(left_layout, 0)
73
+
74
+ # ---------- Plot preview ----------
75
+ self.figure = Figure()
76
+ self.canvas = FigureCanvasQTAgg(self.figure)
77
+ self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
78
+
79
+ main_layout.addWidget(self.canvas, 1)
80
+
81
+ # ------------------------------------------------------------
82
+ # Populate signals
83
+ # ------------------------------------------------------------
84
+ def _populate_signals(self):
85
+ self.signal_list.clear()
86
+
87
+ for sig in sorted(self.project_state.logs.keys()):
88
+ if sig == "time":
89
+ continue
90
+
91
+ item = QListWidgetItem(sig)
92
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
93
+ item.setCheckState(Qt.Unchecked)
94
+ self.signal_list.addItem(item)
95
+
96
+ # ------------------------------------------------------------
97
+ # Interactive plot
98
+ # ------------------------------------------------------------
99
+ def _on_signal_toggled(self, item: QListWidgetItem):
100
+ sig = item.text()
101
+
102
+ if item.checkState() == Qt.Checked:
103
+ self.selected_signals.add(sig)
104
+ else:
105
+ self.selected_signals.discard(sig)
106
+
107
+ self._update_preview_plot()
108
+
109
+
110
+ def _stack_logged_signal_2d(self, sig: str) -> np.ndarray:
111
+ """
112
+ Stack a logged signal over time, preserving its 2D shape.
113
+
114
+ Returns:
115
+ data: np.ndarray of shape (T, m, n)
116
+
117
+ Raises:
118
+ ValueError if samples are not 2D arrays of consistent shape.
119
+ """
120
+ samples = self.project_state.logs.get(sig, None)
121
+ if not isinstance(samples, list) or len(samples) == 0:
122
+ raise ValueError(f"Signal '{sig}' has no samples in logs.")
123
+
124
+ # Find first non-None sample to define shape
125
+ first = None
126
+ for s in samples:
127
+ if s is not None:
128
+ first = np.asarray(s)
129
+ break
130
+
131
+ if first is None:
132
+ raise ValueError(f"Signal '{sig}' is always None; cannot plot.")
133
+
134
+ if first.ndim != 2:
135
+ raise ValueError(f"Signal '{sig}' must be 2D. Got ndim={first.ndim} with shape {first.shape}.")
136
+
137
+ shape0 = first.shape
138
+
139
+ stacked = []
140
+ for k, s in enumerate(samples):
141
+ if s is None:
142
+ raise ValueError(f"Signal '{sig}' contains None at index {k}; cannot plot.")
143
+ a = np.asarray(s)
144
+ if a.ndim != 2:
145
+ raise ValueError(
146
+ f"Signal '{sig}' sample {k} must be 2D. Got ndim={a.ndim} with shape {a.shape}."
147
+ )
148
+ if a.shape != shape0:
149
+ raise ValueError(
150
+ f"Signal '{sig}' shape changed over time: expected {shape0}, got {a.shape} at sample {k}."
151
+ )
152
+ stacked.append(a)
153
+
154
+ return np.stack(stacked, axis=0) # (T, m, n)
155
+
156
+
157
+ def _update_preview_plot(self):
158
+ self.figure.clear()
159
+ ax = self.figure.add_subplot(111)
160
+
161
+ if not self.selected_signals:
162
+ self.canvas.draw()
163
+ return
164
+
165
+ time = np.asarray(self.project_state.logs["time"]).flatten()
166
+ T = len(time)
167
+
168
+ try:
169
+ for sig in sorted(self.selected_signals):
170
+ data = self._stack_logged_signal_2d(sig) # (T, m, n)
171
+
172
+ if data.shape[0] != T:
173
+ raise ValueError(
174
+ f"Time length mismatch for '{sig}': time has {T} samples but signal has {data.shape[0]}."
175
+ )
176
+
177
+ m, n = data.shape[1], data.shape[2]
178
+
179
+ # scalar
180
+ if (m, n) == (1, 1):
181
+ ax.step(time, data[:, 0, 0], where="post", label=sig)
182
+ continue
183
+
184
+ # vector column (m,1)
185
+ if n == 1:
186
+ for i in range(m):
187
+ ax.step(time, data[:, i, 0], where="post", label=f"{sig}[{i}]")
188
+ continue
189
+
190
+ # matrix (m,n)
191
+ for r in range(m):
192
+ for c in range(n):
193
+ ax.step(time, data[:, r, c], where="post", label=f"{sig}[{r},{c}]")
194
+
195
+ ax.set_xlabel("Time [s]")
196
+ ax.grid(True)
197
+ ax.legend()
198
+
199
+ except Exception as e:
200
+ # Keep the UI responsive; show the error inside the plot area.
201
+ ax.text(
202
+ 0.01, 0.99,
203
+ f"Plot preview error:\n{e}",
204
+ transform=ax.transAxes,
205
+ va="top",
206
+ ha="left",
207
+ wrap=True,
208
+ )
209
+ ax.set_axis_off()
210
+
211
+ self.canvas.draw()
212
+
213
+ # ------------------------------------------------------------
214
+ # Plot defined plots (matplotlib windows)
215
+ # ------------------------------------------------------------
216
+ def _plot_defined_plots(self):
217
+ if not self.project_state.plots:
218
+ QMessageBox.information(
219
+ self,
220
+ "No plots defined",
221
+ "No plots are defined in the project settings."
222
+ )
223
+ return
224
+
225
+ plot_from_config(
226
+ logs=self.project_state.logs,
227
+ plot_cfg=PlotConfig(self.project_state.plots),
228
+ show=True,
229
+ block=False
230
+ )
@@ -0,0 +1,187 @@
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 PySide6.QtWidgets import (
22
+ QWidget, QHBoxLayout, QVBoxLayout, QListWidget, QListWidgetItem,
23
+ QLabel, QLineEdit, QPushButton, QMessageBox
24
+ )
25
+ from PySide6.QtCore import Qt
26
+
27
+ from pySimBlocks.gui.models.project_state import ProjectState
28
+ from pySimBlocks.gui.project_controller import ProjectController
29
+
30
+
31
+ class PlotSettingsWidget(QWidget):
32
+ def __init__(self, project_state: ProjectState, project_controller: ProjectController):
33
+ super().__init__()
34
+ self.project_state = project_state
35
+ self.project_controller = project_controller
36
+ self.edit_index = None
37
+
38
+ main = QHBoxLayout(self)
39
+
40
+ # ==================================================
41
+ # Left: plot list + actions
42
+ # ==================================================
43
+ left = QVBoxLayout()
44
+
45
+ left.addWidget(QLabel("Plots"))
46
+
47
+ self.plot_list = QListWidget()
48
+ self.plot_list.currentRowChanged.connect(self.load_plot)
49
+ left.addWidget(self.plot_list)
50
+
51
+ self.new_btn = QPushButton("New")
52
+ self.save_btn = QPushButton("Save")
53
+ self.del_btn = QPushButton("Delete")
54
+
55
+ self.new_btn.clicked.connect(self.new_plot)
56
+ self.save_btn.clicked.connect(self.save_plot)
57
+ self.del_btn.clicked.connect(self.delete_plot)
58
+
59
+ left.addWidget(self.new_btn)
60
+ left.addWidget(self.save_btn)
61
+ left.addWidget(self.del_btn)
62
+
63
+ main.addLayout(left, 1)
64
+
65
+ # ==================================================
66
+ # Right: editor
67
+ # ==================================================
68
+ right = QVBoxLayout()
69
+
70
+ right.addWidget(QLabel("Title:"))
71
+ self.title_edit = QLineEdit()
72
+ right.addWidget(self.title_edit)
73
+
74
+ right.addWidget(QLabel("Signals:"))
75
+ self.signal_list = QListWidget()
76
+ self.signal_list.setSelectionMode(QListWidget.NoSelection)
77
+ right.addWidget(self.signal_list)
78
+
79
+ main.addLayout(right, 2)
80
+
81
+ self.refresh_plot_list()
82
+ self.populate_signal_list()
83
+ self.update_buttons_state()
84
+
85
+ # ==================================================
86
+ # Helpers
87
+ # ==================================================
88
+ def refresh_from_project(self):
89
+ """Synchronize the plot editor with the current project state."""
90
+ self.edit_index = None
91
+ self.refresh_plot_list()
92
+ self.plot_list.clearSelection()
93
+ self.populate_signal_list()
94
+ self.title_edit.clear()
95
+ self.update_buttons_state()
96
+
97
+ def refresh_plot_list(self):
98
+ self.plot_list.clear()
99
+ for plot in self.project_state.plots:
100
+ self.plot_list.addItem(plot["title"])
101
+
102
+ def populate_signal_list(self, checked=None):
103
+ """
104
+ Populate signal list.
105
+ checked: optional set/list of signals to check.
106
+ """
107
+ self.signal_list.clear()
108
+ checked = set(checked or [])
109
+
110
+ for sig in self.project_state.get_output_signals():
111
+ item = QListWidgetItem(sig)
112
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
113
+ item.setCheckState(Qt.Checked if sig in checked else Qt.Unchecked)
114
+ self.signal_list.addItem(item)
115
+
116
+ def collect_selected_signals(self) -> list[str]:
117
+ return [
118
+ self.signal_list.item(i).text()
119
+ for i in range(self.signal_list.count())
120
+ if self.signal_list.item(i).checkState() == Qt.Checked
121
+ ]
122
+
123
+ def reset_form(self):
124
+ self.title_edit.clear()
125
+ self.populate_signal_list()
126
+
127
+ def update_buttons_state(self):
128
+ has_selection = self.plot_list.currentRow() >= 0
129
+ self.del_btn.setEnabled(has_selection)
130
+
131
+ # ==================================================
132
+ # Selection handling
133
+ # ==================================================
134
+ def load_plot(self, index):
135
+ if index < 0:
136
+ self.edit_index = None
137
+ self.reset_form()
138
+ self.update_buttons_state()
139
+ return
140
+
141
+ self.edit_index = index
142
+ plot = self.project_state.plots[index]
143
+ self.title_edit.setText(plot["title"])
144
+ self.populate_signal_list(plot["signals"])
145
+ self.update_buttons_state()
146
+
147
+
148
+ # ==================================================
149
+ # Actions
150
+ # ==================================================
151
+ def new_plot(self):
152
+ self.edit_index = None
153
+ self.plot_list.clearSelection()
154
+ self.reset_form()
155
+ self.update_buttons_state()
156
+
157
+ def save_plot(self):
158
+ title = self.title_edit.text().strip()
159
+ if not title:
160
+ QMessageBox.warning(self, "Invalid plot", "Plot title cannot be empty.")
161
+ return
162
+
163
+ signals = self.collect_selected_signals()
164
+ if not signals:
165
+ QMessageBox.warning(self, "Invalid plot", "No signal selected.")
166
+ return
167
+
168
+ if self.edit_index is None:
169
+ self.project_controller.create_plot(title, signals)
170
+ self.refresh_plot_list()
171
+ else:
172
+ self.project_controller.update_plot(self.edit_index, title, signals)
173
+ self.plot_list.item(self.edit_index).setText(title)
174
+
175
+ self.update_buttons_state()
176
+
177
+
178
+ def delete_plot(self):
179
+ if self.edit_index is None:
180
+ return
181
+
182
+ self.project_controller.delete_plot(self.edit_index)
183
+ self.edit_index = None
184
+ self.refresh_plot_list()
185
+ self.plot_list.clearSelection()
186
+ self.reset_form()
187
+ self.update_buttons_state()
@@ -0,0 +1,146 @@
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
+ import os
23
+ from PySide6.QtWidgets import (
24
+ QWidget, QFormLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QFileDialog, QHBoxLayout
25
+ )
26
+
27
+ from pySimBlocks.gui.models.project_state import ProjectState
28
+ from pySimBlocks.gui.project_controller import ProjectController
29
+ from pySimBlocks.gui.services.project_loader import ProjectLoaderYaml
30
+
31
+
32
+ class ProjectSettingsWidget(QWidget):
33
+
34
+ def __init__(self, project_state: ProjectState, project_controller: ProjectController, settings_dialg):
35
+ super().__init__()
36
+ self.project_state = project_state
37
+ self.project_controller = project_controller
38
+ self.settings_dialog = settings_dialg
39
+
40
+ layout = QFormLayout(self)
41
+ layout.addRow(QLabel("<b>Project Settings</b>"))
42
+
43
+ self.dir_edit = QLineEdit(str(project_state.directory_path))
44
+ self.dir_browse_btn = QPushButton("...")
45
+ self.dir_browse_btn.setToolTip("Select a project directory from disk")
46
+ self.dir_browse_btn.clicked.connect(self.browse_project_directory)
47
+
48
+ dir_layout = QHBoxLayout()
49
+ dir_layout.setContentsMargins(0, 0, 0, 0)
50
+ dir_layout.addWidget(self.dir_edit)
51
+ dir_layout.addWidget(self.dir_browse_btn)
52
+
53
+ layout.addRow("Directory path:", dir_layout)
54
+
55
+ load_btn = QPushButton("Load")
56
+ load_btn.clicked.connect(self.load_project)
57
+ label = QLabel("Load Project:")
58
+ label.setToolTip("Auto load project from directory containing project.yaml.")
59
+ layout.addRow(label, load_btn)
60
+
61
+ ext = project_state.external or ""
62
+ self.external_edit = QLineEdit(ext)
63
+ self.external_browse_btn = QPushButton("...")
64
+ self.external_browse_btn.setToolTip("Select a Python file from disk")
65
+ self.external_browse_btn.clicked.connect(self.browse_external_file)
66
+
67
+ external_layout = QHBoxLayout()
68
+ external_layout.setContentsMargins(0, 0, 0, 0)
69
+ external_layout.addWidget(self.external_edit)
70
+ external_layout.addWidget(self.external_browse_btn)
71
+
72
+ label = QLabel("Python file:")
73
+ label.setToolTip("Relative path from project directory")
74
+ layout.addRow(label, external_layout)
75
+
76
+
77
+
78
+ def apply(self) -> bool:
79
+ path = Path(self.dir_edit.text())
80
+ if not path.exists():
81
+ QMessageBox.warning(
82
+ self,
83
+ "Invalid directory",
84
+ f"The directory does not exist:\n{path}",
85
+ )
86
+ return False
87
+ ext = self.external_edit.text().strip()
88
+ self.project_controller.update_project_param(path, ext)
89
+ return True
90
+
91
+ def browse_external_file(self):
92
+ base_dir = Path(self.dir_edit.text()).expanduser()
93
+ if not base_dir.is_dir():
94
+ QMessageBox.warning(
95
+ self,
96
+ "Invalid directory",
97
+ f"The directory does not exist:\n{base_dir}",
98
+ )
99
+ return
100
+
101
+ selected_file, _ = QFileDialog.getOpenFileName(
102
+ self,
103
+ "Select Python file",
104
+ str(base_dir),
105
+ "Python files (*.py);;All files (*)",
106
+ )
107
+
108
+ if not selected_file:
109
+ return
110
+
111
+ selected_path = Path(selected_file).resolve()
112
+ try:
113
+ relative_path = selected_path.relative_to(base_dir.resolve())
114
+ except ValueError:
115
+ try:
116
+ relative_path = Path(os.path.relpath(str(selected_path), str(base_dir.resolve())))
117
+ except ValueError:
118
+ # Windows cross-drive case (e.g. C: -> D:): keep absolute path.
119
+ relative_path = selected_path
120
+
121
+ self.external_edit.setText(relative_path.as_posix())
122
+
123
+ def browse_project_directory(self):
124
+ current_dir = Path(self.dir_edit.text()).expanduser()
125
+ start_dir = current_dir if current_dir.is_dir() else Path.cwd()
126
+
127
+ selected_dir = QFileDialog.getExistingDirectory(
128
+ self,
129
+ "Select project directory",
130
+ str(start_dir),
131
+ )
132
+
133
+ if not selected_dir:
134
+ return
135
+
136
+ self.dir_edit.setText(str(Path(selected_dir).resolve()))
137
+
138
+ def load_project(self):
139
+ main_window = self.settings_dialog.parent()
140
+ if not main_window.confirm_discard_or_save("loading a new project"):
141
+ return
142
+ self.apply()
143
+ self.project_controller.load_project(ProjectLoaderYaml())
144
+ ext = self.project_state.external
145
+ self.external_edit.setText("" if ext is None else ext)
146
+ self.settings_dialog.refresh_tabs_from_project()
@@ -0,0 +1,137 @@
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 PySide6.QtWidgets import (
22
+ QWidget, QFormLayout, QLabel, QLineEdit, QComboBox,
23
+ QListWidget, QListWidgetItem, QMessageBox
24
+ )
25
+ from PySide6.QtCore import Qt
26
+
27
+ from pySimBlocks.gui.models.project_state import ProjectState
28
+ from pySimBlocks.gui.project_controller import ProjectController
29
+
30
+
31
+ class SimulationSettingsWidget(QWidget):
32
+ def __init__(self, project_state: ProjectState, project_controller: ProjectController):
33
+ super().__init__()
34
+ self.project_state = project_state
35
+ self.project_controller = project_controller
36
+ self._logs_dirty = False
37
+
38
+ layout = QFormLayout(self)
39
+ layout.addRow(QLabel("<b>Simulation Settings</b>"))
40
+
41
+ self.dt_edit = QLineEdit(str(project_state.simulation.dt))
42
+ layout.addRow("Step time:", self.dt_edit)
43
+
44
+
45
+ self.T_edit = QLineEdit(str(project_state.simulation.T))
46
+ layout.addRow("Stop time:", self.T_edit)
47
+
48
+ self.solver_combo = QComboBox()
49
+ self.solver_combo.addItems(["fixed", "variable"])
50
+ self.solver_combo.setCurrentText(project_state.simulation.solver)
51
+ layout.addRow("Solver:", self.solver_combo)
52
+
53
+ value = self.project_state.simulation.clock
54
+ self.clock_combo = QComboBox()
55
+ self.clock_combo.addItem("internal")
56
+ self.clock_combo.addItem("external")
57
+ if value is not None:
58
+ self.clock_combo.setCurrentText(str(value))
59
+ layout.addRow("Simulation clock:", self.clock_combo)
60
+
61
+ # -------- Logs --------
62
+ self.logs_list = QListWidget()
63
+ self.logs_list.itemChanged.connect(self._on_log_changed)
64
+ self._define_log_list()
65
+ layout.addRow("Signals logged:", self.logs_list)
66
+
67
+ def apply(self):
68
+
69
+ params = {}
70
+ try:
71
+ params["dt"] = float(self.dt_edit.text())
72
+ except ValueError:
73
+ params["dt"] = self.dt_edit.text()
74
+ try:
75
+ params["T"] = float(self.T_edit.text())
76
+ except ValueError:
77
+ params["T"] = self.T_edit.text()
78
+ params["solver"] = self.solver_combo.currentText()
79
+ params["clock"] = self.clock_combo.currentText()
80
+
81
+ selected_signals = [
82
+ self.logs_list.item(i).text()
83
+ for i in range(self.logs_list.count())
84
+ if self.logs_list.item(i).checkState() == Qt.Checked
85
+ ]
86
+
87
+ self.project_controller.update_simulation_params(params)
88
+ if self._logs_dirty:
89
+ self.project_controller.set_logged_signals(selected_signals)
90
+ self._logs_dirty = False
91
+
92
+ def refresh_from_project(self):
93
+ """
94
+ Synchronize the log checkbox list with project_state.logging.
95
+ Called when the Simulation tab becomes active.
96
+ """
97
+ self._define_log_list()
98
+ selected = set(self.project_state.logging)
99
+ self.logs_list.blockSignals(True)
100
+
101
+ for i in range(self.logs_list.count()):
102
+ item = self.logs_list.item(i)
103
+ should_be_checked = item.text() in selected
104
+ item.setCheckState(
105
+ Qt.Checked if should_be_checked else Qt.Unchecked
106
+ )
107
+
108
+ self.logs_list.blockSignals(False)
109
+ self._logs_dirty = False
110
+
111
+ def _define_log_list(self):
112
+ self.logs_list.blockSignals(True)
113
+ self.logs_list.clear()
114
+ available = self.project_state.get_output_signals()
115
+ selected = set(self.project_state.logging)
116
+ for sig in available:
117
+ item = QListWidgetItem(sig)
118
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
119
+ item.setCheckState(Qt.Checked if sig in selected else Qt.Unchecked)
120
+ self.logs_list.addItem(item)
121
+ self.logs_list.blockSignals(False)
122
+ self._logs_dirty = False
123
+
124
+ def _on_log_changed(self, item: QListWidgetItem):
125
+ self._logs_dirty = True
126
+ if item.checkState() == Qt.Unchecked:
127
+ used = any(
128
+ item.text() in plot["signals"]
129
+ for plot in self.project_state.plots
130
+ )
131
+ if used:
132
+ QMessageBox.warning(
133
+ self,
134
+ "Signal in use",
135
+ f"The signal '{item.text()}' is used in a plot."
136
+ )
137
+ item.setCheckState(Qt.Checked)