1

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. 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(SimulatorUI, self).__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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Does [this question](https://stackoverflow.com/questions/47941743/lifetime-of-object-in-lambda-connected-to-pyqtsignal) help? In your specific program, without the partial function, the `POC_Ctrl` object in `main()` will be immediately garbage-collected by lack of references. With a partial function (or lambda function for that matter), the partial function keeps a reference to the object which prevents it from being deleted. To get around this you could assign the `POC_Ctrl` object to a local variable in `main()`, i.e. use `poc = POC_Ctrl(...)`. – Heike Mar 29 '21 at 10:03
  • Thanks a lot @Heike! That solved my issue. I didn't realize that Python would garbage collect the POC_Ctrl object without a reference. With the `poc = POC(...)` trick it worked out perfectly :) – Jon Åge Løvås Mar 29 '21 at 10:32
  • pyqtgraph maintainer here, FWIW we only recently removed references to lambda/partial expressions when connected to signals for this very reason. Makes me wish there was a flake8-qt plugin. – Ogi Moore Apr 05 '21 at 23:45

0 Answers0