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