{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%matplotlib inline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Observe parallel functions\n\nThis demo shows how to use a customized progressObserver to observer and\npossibly cancel multiple 'complex' function calls, that are executed in \nparallel in four different background threads.\n\nEach single function call has its own progressObserver (member observer of class\nFunctionCall). Additionally, there is one major progressObserver, that tracks\nthe total state of all coomplex functions call (each lasting for 10 seconds).\n\nIn this demo script, we do not only use the pre-defined possibilities of\nthe class ``itom.progressObserver`` to show the current progress value and\ntext in a given ``itom.uiItem`` of the GUI, but we use the \n``itom.progressObserver.connect`` method to connect different callable python\nmethods to signals of the progressObserver such that customized actions\ncan be done.\n\nThe overall observer is set to a range between 0 and 100 (percent). Every of the four\nparallel function calls can add up to 25% of the total progress. Once the\noverall progress reaches 100%, the call is considered to be finished.\n\nUsing the cancellation features of each progressObserver, this demo further\nshows, that it is both possible to only cancel single function calls or the\nentire call to all (currently running) sub-functions.\n\nThis demo uses the Thread class of python. Since python does not allow\nreal concurrent calls within python itself, we do not have to handle race conditions.\n\nHint: Algorithm calls of itom algorithms will release the Python GIL during the\nentire time of the function call, therefore other Python thread can work in the\nmeantime.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from itomUi import ItomUi\nfrom itom import progressObserver\nfrom threading import Thread\nfrom time import sleep\n\n\nclass FunctionCall:\n \"\"\"This class wraps GUI items, the thread and observer of one function call.\n \"\"\"\n\n def __init__(self, label, progress, abortButton, overallObserver):\n \"\"\"Constructor.\"\"\"\n self.label = label\n self.progress = progress\n self.abortBtn = abortButton\n self.thread = None\n\n # create the local observer for one function call\n self.observer = progressObserver(\n progressBar=self.progress, progressMinimum=0, progressMaximum=100\n )\n\n # pass the overall observer\n self.overallObserver = overallObserver\n\n # make several connections to signals of some objects with methods of this class\n self.abortBtn.connect(\"clicked()\", self.on_abortButton_clicked)\n self.observer.connect(\n \"progressValueChanged(int)\", self.on_progressValue_changed\n )\n self.observer.connect(\n \"progressTextChanged(QString)\", self.on_progressText_changed\n )\n self.observer.connect(\n \"cancellationRequested()\", self.on_observer_cancellationRequested\n )\n\n # True if the function is currently executed\n self.inProgress = False\n\n # Last reported progress value to the overallObserver [0, 25] in percent.\n self._lastReportedProgressValue = 0\n\n def reset(self):\n \"\"\"Resets the relevant GUI items to an idle state.\"\"\"\n self.label[\"text\"] = \"-\"\n self.progress[\"value\"] = 0\n self.progress[\"enabled\"] = False\n self.abortBtn[\"enabled\"] = False\n\n def on_abortButton_clicked(self):\n \"\"\"Callback if the abort button of this function is clicked.\"\"\"\n if self.observer:\n self.abortBtn[\"enabled\"] = False\n # force the observer to cancel the running algorithm. The algorithm\n # has to regularily check for this request and terminate the algorithm\n # (with an exception set) as soon as possible.\n self.observer.requestCancellation()\n\n def start(self):\n \"\"\"Start the complex function in a Python thread by executing self.run.\"\"\"\n self.thread = Thread(target=self.run)\n self.thread.start()\n\n def cancel(self):\n \"\"\"Method to request a cancellation of the algorithm call as public interface.\n \n This method is usually called if the global abort button is clicked.\"\"\"\n self.observer.requestCancellation()\n\n def run(self):\n \"\"\"Run method, executed in a thread.\n \n This method mainly starts the itom algorithm ``demoCancellationFunction``\n and passes the local observer to this function. If this observer should\n be requested to cancel, the algorithm will return with a RuntimeError.\n \n This exception is handled. At the end, the contribution to the global\n progress of this function is set to the maximum of 25% (Even in the case\n of a cancellation).\n \"\"\"\n self.observer.reset()\n\n self.progress[\"value\"] = 0\n self.progress[\"enabled\"] = True\n self.abortBtn[\"enabled\"] = True\n\n try:\n self.inProgress = True\n filter(\"demoCancellationFunction\", _observer=self.observer)\n # self.on_progressValue_changed(100)\n except RuntimeError:\n # cancellation\n pass\n finally:\n # done or cancelled: report a full progress of 25%\n self.overallObserver.progressValue += max(\n 0, 25 - self._lastReportedProgressValue\n )\n self._lastReportedProgressValue = 25\n self.abortBtn[\"enabled\"] = False\n self.inProgress = False\n sleep(1)\n self.reset()\n\n def on_progressValue_changed(self, value):\n \"\"\"Callback if the local observer reports a new progress value.\"\"\"\n if self.inProgress:\n if value < 100:\n self.label[\"text\"] = \"%i/100\" % value\n else:\n self.label[\"text\"] = \"done\"\n\n self.overallObserver.progressValue += max(\n 0, (value // 4) - self._lastReportedProgressValue\n )\n self._lastReportedProgressValue = value // 4\n\n def on_progressText_changed(self, text):\n \"\"\"Callback if the local observer reports a new progress text.\n \n Hint: it makes no real sense to change the toolTip. It is just an example.\"\"\"\n self.label[\"toolTip\"] = text\n\n def on_observer_cancellationRequested(self):\n \"\"\"Callback if a cancellation has been requested to the local observer.\"\"\"\n if self.inProgress:\n self.label[\"text\"] = \"cancelled\"\n self.progress[\"value\"] = 0\n\n\nclass DemoObserver(ItomUi):\n \"\"\"Main GUI class that provides functionality to run for complex algorithms in parallel.\"\"\"\n\n def __init__(self):\n \"\"\"Constructor.\"\"\"\n ItomUi.__init__(self, \"observedParallelFunctions.ui\")\n\n self.sets = (\n []\n ) # sets of widgets in the GUI for each parallel function execution\n\n # this overallObserver gives 25% to each of the 4 parallel function calls.\n self.overallObserver = progressObserver(\n progressBar=self.gui.progressAll,\n progressMinimum=0,\n progressMaximum=100,\n )\n self.overallObserver.connect(\n \"progressValueChanged(int)\", self.overallProgressChanged\n )\n\n # Initialization of four FunctionCall objects for four parallel complex\n # function calls.\n for idx in range(1, 5):\n self.sets.append(\n FunctionCall(\n self.gui.getChild(\"lblRun%i\" % idx),\n self.gui.getChild(\"progressRun%i\" % idx),\n self.gui.getChild(\"btnAbortRun%i\" % idx),\n self.overallObserver,\n )\n )\n\n # reset and hide all besides the start button\n for s in self.sets:\n s.reset()\n\n self.gui.btnAbort[\"enabled\"] = False\n self.gui.progressAll[\"visible\"] = False\n\n @ItomUi.autoslot(\"\")\n def on_btnStart_clicked(self):\n \"\"\"Auto-connected slot, called if the start button is clicked.\"\"\"\n self.gui.btnAbort[\"enabled\"] = True\n self.gui.btnStart[\"enabled\"] = False\n self.gui.progressAll[\"visible\"] = True\n\n # resets the overall\n self.overallObserver.reset()\n\n # start the 4 threads with a short delay\n for s in self.sets:\n s.start()\n sleep(0.1)\n\n @ItomUi.autoslot(\"\")\n def on_btnAbort_clicked(self):\n \"\"\"Informs the algorithm call to interrupt as soon as possible.\"\"\"\n\n # cancels all four function calls\n for s in self.sets:\n s.cancel()\n\n self.gui.btnAbort[\"enabled\"] = False\n self.gui.btnStart[\"enabled\"] = True\n self.gui.progressAll[\"visible\"] = False\n\n def overallProgressChanged(self, value):\n \"\"\"Callback if the progressValueChanged signal of the overall progressObserver is emitted.\"\"\"\n if value >= 100:\n # all done\n self.gui.btnAbort[\"enabled\"] = False\n self.gui.btnStart[\"enabled\"] = True\n self.gui.progressAll[\"visible\"] = False\n\n\nif __name__ == \"__main__\":\n demoObserverGui = DemoObserver()\n demoObserverGui.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" } }, "nbformat": 4, "nbformat_minor": 0 }