pythonasynchronouspyqtsimpyevent-simulation

SimPy and PyQt Interaction


I've been looking to make PyQt and SimPy "talk" to one another. For instance, in the code below, I have a PyQt widget with a single label that displays the SimPy environment time. I'd like for this label to be updated as the simulation progresses (which I've tried to show with the simpy_generator function -- the label in the widget is updated, SimPy times out for a unit of time, and this pattern repeats).

import sys
import simpy
from PyQt5 import QtWidgets

class Window(QtWidgets.QWidget):
    """ A generic Qt window that can interact with SimPy.
    """

    def __init__(self, env, parent=None):
        """ Class constructor.

        Args:
            env: SimPy environment.
            parent: Optional parent of this widget.
        """
        super(Window, self).__init__()
        self.env = env
        self.init()

    def init(self) -> None:
        """ Initialise the layout of the widget. Just a label that displays the
        SimPy Environment time.
        """
        layout = QtWidgets.QVBoxLayout()
        self.label = QtWidgets.QLabel('SimPy Time: {}'.format(self.env.now))
        layout.addWidget(self.label)
        self.setLayout(layout)
        self.show()

    def update(self) -> None:
        """ Update method for the window; retrieve the current SimPy environment time
        and update the label.
        """
        self.label.setText("SimPy Time: {}".format(self.env.now))

def simpy_generator(env, window):
    """ Generator for SimPy simulation; just tick the environment's clock and update
    the QtWidget's fields.

    Args:
        env: SimPy environment.
        window: QtWidget to update with SimPy data.
    """
    while True:
        window.update()
        yield env.timeout(1)

if __name__ == "__main__":

    env = simpy.Environment()
    app = QtWidgets.QApplication(sys.argv)
    window = Window(env=env)

    ### These need to be incorporated into the Qt event queue somehow?
    # simpy_process = env.process(simpy_generator(env, window))
    # env.run(until=25)

    app.exec_()

However, I've well and truly confused myself as to how this can be made to work. I understand that Qt needs to manage its event queue, and updating would typically be done with some QTimer that emits a signal to trigger the running of some slot that it's linked to (the Window's update method in this case). But this seems to be incompatible with SimPy's own event queue (or rather, I'm too ignorant to understand how they should interact); the generator needs to be added to the environment as a process, then the environment set to run until completion (see the commented-out portion of the code).

Could anyone please advise on how I might be able to accomplish this?


Solution

  • As per Michael's answer, the SimPy simulation is launched on an independent thread. The above code is modified to contain the following:

    def simpy_thread(env, window):
        """ Function to be run on a dedicated thread where the SimPy simulation will live.
    
        Args:
            env: SimPy environment.
            window: QtWidget to update with SimPy data.
        """
        simpy_process = env.process(simpy_generator(env, window))
        env.run(until=25)
    
    if __name__ == "__main__":
    
        env = simpy.rt.RealtimeEnvironment(factor=1)
    
        # Create the Qt window
        app = QtWidgets.QApplication(sys.argv)
        window = Window(env=env)
    
        # Launch a dedicated SimPy thread that has a reference to the window for updating
        thread = threading.Thread(target=simpy_thread, args=(env,window,))
        thread.start()
    
        # Finally, start the Qt event queue; the code will block at this point until the window
        # object is destroyed
        app.exec_()
    

    Would still be very interested to know whether there are alternative solutions that people have :)