jonstakvik10545 | 2021-05-06 11:57:09 UTC | #1
This is my first question asked in this forum
My goal is to create a proof-of-concept code to run a simple dynamic simulator in a Python pyqt5 HMI. I've created a very simple simulator with some setters and getters and I'm using a model-view-controller design pattern. The HMI is fairly simple with a just a few inputs. I'm also using pyqtgraph to plot the simulated results in real-time.
My issue is that my HMI freezes at start up unless I add at least one partial function when I connect my signals to the slots:
def _connectSignals(self):
self._view.buttons['u'].clicked.connect(self._on_u_update)
self._view.buttons['k'].clicked.connect(self._on_k_update)
self._view.buttons['dt'].clicked.connect(partial(self._on_dt_update, 'dummy')) # Program freeze without at least one partial function. No idea why.
def _on_k_update(self):
self._model.set_k(float(self._view.lineEdits['k'].text()))
def _on_dt_update(self, dt): # Need to have an input here to avoid program freezing. See connect of dt above
dt_ = float(self._view.lineEdits['dt'].text())
dt_ = float(dt_)
self._model.set_dt(dt_ / 1000.0)
self.u_line.set_n_samples(seconds=60, dt=dt_ / 1000.0)
self.x_line.set_n_samples(seconds=60, dt=dt_ / 1000.0)
self.dt = int(dt_)
self.timer.setInterval(self.dt)
def _on_u_update(self):
self._model.set_u(float(self._view.lineEdits['u'].text()))
This even happen when I avoid calling my _connectSignals
function. I suspect that it might have something to do with me using QTimer to control the stepping of the simulator.
There are no error messages, and I'm not able to pause/debug the application. I would greatly appreciate any help on solving this. The entire code is located below:
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QWidget
from PyQt5 import QtCore
from functools import partial
from scipy.integrate import odeint
import pyqtgraph as pg
class POC_Simulator():
def __init__(self):
self.u = 0.0
self.k = 1.0
self.x = 0.0
self.dt = 0.050
self.t = 0.0
def set_k(self, k):
self.k = float(k)
def get_k(self):
return self.k
def set_u(self, u):
self.u = float(u)
def get_u(self):
return self.u
def set_dt(self, dt):
self.dt = float(dt)
def get_dt(self):
return self.dt
def get_x(self):
return self.x
def get_t(self):
return self.t
def model(self, x, t):
dxdt = 1/self.k*(-x + self.u)
return dxdt
def step(self):
t_span = [0, self.dt]
x = odeint(self.model, self.x, t_span)
self.x = float(x[1])
self.t += self.dt
return self.x, self.t
class GraphLine():
def __init__(self, graph: pg.PlotWidget, n_samples=100):
self.graph = graph
self.x = []
self.y = []
self.n_samples = n_samples
def createPlot(self, color, linetype, name, width):
if linetype == '-':
style = QtCore.Qt.SolidLine
elif linetype == ':':
style = QtCore.Qt.DotLine
elif linetype == '--':
style = QtCore.Qt.DashLine
else:
style = QtCore.Qt.SolidLine
pen = pg.mkPen(color=color)
self.plt = self.graph.plot(self.x, self.y, pen=pen, style=style, name=name, width=width)
def updatePlot(self, x, y):
if len(self.x) >= self.n_samples:
self.x = self.x[1:]
self.x.append(x)
if len(self.y) >= self.n_samples:
self.y = self.y[1:]
self.y.append(y)
self.plt.setData(self.x, self.y)
def set_n_samples(self, seconds, dt):
self.n_samples = seconds / dt
class SimulatorUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('POC Simulator UI')
self.setFixedSize(800, 500)
self.generalLayout = QHBoxLayout()
self._centralWidget = QWidget()
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
self._createInputs()
self._createPlot()
self.n_samples = 100
def _createInputs(self):
layout = QVBoxLayout()
lines = ('u', 'k', 'dt')
self.lineEdits = {}
self.labelValues = {}
self.buttons = {}
for name in lines:
lineLayout = QHBoxLayout()
lineEdit = QLineEdit('')
lineLayout.addWidget(QLabel(name))
lineLayout.addWidget(lineEdit)
self.lineEdits[name] = lineEdit
btn = QPushButton(f'Update {name}')
lineLayout.addWidget(btn)
self.buttons[name] = btn
value = QLabel('')
lineLayout.addWidget(value)
self.labelValues[name] = value
layout.addLayout(lineLayout)
self.generalLayout.addLayout(layout)
def _createPlot(self):
self.graphWidget = pg.PlotWidget()
self.graphWidget.setBackground('w')
self.graphWidget.addLegend()
self.graphWidget.showGrid(x=True, y=True)
self.generalLayout.addWidget(self.graphWidget)
def addPlot(self, color, name, width=2, linetype='-'):
line = GraphLine(self.graphWidget)
line.createPlot(color=color, linetype=linetype, width=width, name=name)
return line
class POC_Ctrl():
def __init__(self, model: POC_Simulator, view: SimulatorUI):
self._model = model
self._view = view
self._connectSignals()
self.timer = QtCore.QTimer()
self.u_line = self._view.addPlot(color='k', linetype=':', name='u')
self.x_line = self._view.addPlot(color='r', name='x')
u_0 = self._model.get_u()
k_0 = self._model.get_k()
dt_0 = self._model.get_dt()
self.dt = int(dt_0 * 1000)
self.timer.setInterval(self.dt)
self.timer.timeout.connect(self._onTimerTimeout)
self._view.labelValues['u'].setText(f'u value: {u_0}')
self._view.labelValues['k'].setText(f'k value: {k_0}')
self._view.labelValues['dt'].setText(f'dt value: {self.dt}')
self._view.lineEdits['u'].setText(f'{u_0}')
self._view.lineEdits['k'].setText(f'{k_0}')
self._view.lineEdits['dt'].setText(f'{self.dt}')
self.timer.start()
def _connectSignals(self):
self._view.buttons['u'].clicked.connect(self._on_u_update)
self._view.buttons['k'].clicked.connect(self._on_k_update)
self._view.buttons['dt'].clicked.connect(partial(self._on_dt_update, 'dummy')) # Program freeze without at least one partial function. No idea why.
def _on_k_update(self):
self._model.set_k(float(self._view.lineEdits['k'].text()))
def _on_dt_update(self, dt): # Need to have an input here to avoid program freezing. See connect of dt above
dt_ = float(self._view.lineEdits['dt'].text())
dt_ = float(dt_)
self._model.set_dt(dt_ / 1000.0)
self.u_line.set_n_samples(seconds=60, dt=dt_ / 1000.0)
self.x_line.set_n_samples(seconds=60, dt=dt_ / 1000.0)
self.dt = int(dt_)
self.timer.setInterval(self.dt)
def _on_u_update(self):
self._model.set_u(float(self._view.lineEdits['u'].text()))
def _onTimerTimeout(self):
self._model.step()
self._view.labelValues['u'].setText(f'u value: {self._model.get_u()}')
self._view.labelValues['k'].setText(f'k value: {self._model.get_k()}')
self._view.labelValues['dt'].setText(f'dt value: {int(self._model.get_dt()*1000.0)}')
self.u_line.updatePlot(self._model.get_t(), self._model.get_u())
self.x_line.updatePlot(self._model.get_t(), self._model.get_x())
self.timer.start()
def main():
app = QApplication(sys.argv)
view = SimulatorUI()
view.show()
model = POC_Simulator()
POC_Ctrl(model=model, view=view)
sys.exit(app.exec())
if __name__ == '__main__':
main()
martin | 2021-04-02 19:31:49 UTC | #2
PyQt6 Crash Course — a new tutorial in your Inbox every day
Beginner-focused crash course explaining the basics with hands-on examples.
Hi @jonstakvik10545 welcome to the forum. This was a tricky one to work out, but I think I have a solution for you.
The issue is caused by where you create the controller object at the bottom of your application. You create the POC_Ctrl(model=model, view=view)
object, but don't keep a reference to it (by assigning to a variable). The fix is shown below.
def main():
app = QApplication(sys.argv)
view = SimulatorUI()
view.show()
model = POC_Simulator()
ctrl = POC_Ctrl(model=model, view=view)
sys.exit(app.exec())
When I make that change the plot updates as expected.
In Python objects without references are automatically cleared up. So what's happening is that when your application is running the controller is being created and immediately destroyed, and as a result the timer is being destroyed along with it.
As to why the partial
function stops it happening? The partial function holds a reference to the object self
through its reference to self._on_dt_update
-- and that reference is preventing the object from being deleted. Pretty weird eh.
self._view.buttons['dt'].clicked.connect(partial(self._on_dt_update, 'dummy'))
# ^ self here prevents it being deleted, as there will always be a reference to it.
Let me know if anything isn't clear.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.