pythonpyqgis

Iterative calls of processing algorithm fails


I have written a processing script for QGIS 3.x, which itself calls a native processing function (qgis:shortestpathpointtolayer) in a for loop.

from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                       QgsFeatureRequest,
                       QgsExpression,
                       QgsProcessingContext,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterVectorLayer,
                       QgsProcessingFeatureSourceDefinition)
from qgis import processing


class findDownstreamPathAlgorithm(QgsProcessingAlgorithm):
    """
    This is an algorithm that calculates downstream paths on
    a simplified network.

    """

    INPUT_NETWORK = 'INPUT_NETWORK'
    INPUT_VALVES = 'INPUT_VALVES'

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return findDownstreamPathAlgorithm()

    def name(self):
        return 'iterateclusters'

    def displayName(self):
        return self.tr('Iterate clusters')

    def group(self):
        return self.tr('Thvilum')

    def groupId(self):
        return 'lukkelister'

    def shortHelpString(self):
        return self.tr("Iterate clusters in a simplified network")

    def initAlgorithm(self, config=None):

        self.addParameter(
            QgsProcessingParameterVectorLayer(
                self.INPUT_NETWORK,
                self.tr('Netværkslag'),
                [QgsProcessing.SourceType.TypeVectorLine]
            )
        )

        self.addParameter(
            QgsProcessingParameterVectorLayer(
                self.INPUT_VALVES,
                self.tr('Ventillag'),
                [QgsProcessing.SourceType.TypeVectorPoint]
            )
        )

    def processAlgorithm(self, parameters, context, feedback):

        # Retrieve the feature sources
        network = self.parameterAsVectorLayer(
            parameters,
            self.INPUT_NETWORK,
            context
        )

        valves = self.parameterAsVectorLayer(
            parameters,
            self.INPUT_VALVES,
            context
        )

        if network is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_NETWORK))

        if valves is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_VALVES))

        # Store inlets from valves layer in separate scractch layer
        inlets = valves.materialize(QgsFeatureRequest(QgsExpression("situation = 'IsInlet'")))

        # Collect clusters to evaluate
        clusterList = []
        for feat in network.getFeatures():
            clusterList.append(feat["cluster"])
 
        # Remove duplicates and sort list
        clusterList = sorted(set(clusterList))
        if len(clusterList) > 0:
            total = 100.0 / len(clusterList)
        else:
            total = 0

        isCanceled = False
        for current, cluster in enumerate(clusterList):
            # Update the progress bar
            feedback.setProgress(int(current * total))
            
            valves.selectByExpression("cluster = {} and situation = 'Flow'".format(cluster))
            for feat in valves.selectedFeatures():
                # Stop the algorithm if cancel button has been clicked
                if feedback.isCanceled():
                    isCanceled = True
                    break

                vertex_id = feat["vertexId"]
                valve_geom = feat.geometry()
                network.selectByExpression("cluster != {}".format(cluster))

                parameters = {
                    'DEFAULT_DIRECTION' : 2,
                    'DEFAULT_SPEED' : 50,
                    'DIRECTION_FIELD' : '',
                    'END_POINTS' : inlets,
                    'INPUT' : QgsProcessingFeatureSourceDefinition( network.id(), selectedFeaturesOnly=True),
                    'OUTPUT' : 'TEMPORARY_OUTPUT',
                    #'OUTPUT_NON_ROUTABLE' : 'TEMPORARY_OUTPUT',
                    'SPEED_FIELD' : '',
                    'START_POINT' : valve_geom,
                    'STRATEGY' : 0,
                    'TOLERANCE' : 0,
                    'VALUE_BACKWARD' : '',
                    'VALUE_BOTH' : '',
                    'VALUE_FORWARD' : ''
                    }
            
                result = processing.run("qgis:shortestpathpointtolayer", parameters, context=context, feedback=feedback, is_child_algorithm=True)
                shortest_path = QgsProcessingContext.takeResultLayer(context, result['OUTPUT'])
                
                # Do something with the path
                    
                del shortest_path
                    
            if isCanceled:
                break


        return {self.INPUT_NETWORK: network}

When running this script on QGIS 3.28.4 it runs a couple of iterations and then raises an exception (access violation) with the following description:

**Python Stack Trace** Windows fatal exception: access violation

Thread 0x00005d8c (most recent call first): File "C:\\PROGRA\~1/QGIS32\~2.4/apps/qgis-ltr/./python/plugins\\processing\\gui\\AlgorithmExecutor.py", line 72 in execute results, ok = alg.run(parameters, context, feedback, {}, False) File "C:\\PROGRA\~1/QGIS32\~2.4/apps/qgis-ltr/./python/plugins\\processing\\core\\Processing.py", line 187 in runAlgorithm ret, results = execute(alg, parameters, context, feedback, catch_exceptions=False) File "C:\\PROGRA\~1/QGIS32\~2.4/apps/qgis-ltr/./python/plugins\\processing\\tools\\general.py", line 116 in run return Processing.runAlgorithm(algOrName, parameters, onFinish=post_process, feedback=feedback, context=context) File "C:\\Users\\morte\\AppData\\Roaming\\QGIS\\QGIS3\\profiles\\default\\processing\\scripts\\iterateClusters.py", line 156 in processAlgorithm result = processing.run("qgis:shortestpathpointtolayer", parameters, context=context, feedback=feedback, is_child_algorithm=True)

Current thread 0x000075e0 (most recent call first): File "C:\\PROGRA\~1/QGIS32\~2.4/apps/qgis-ltr/./python/plugins\\processing\\ProcessingPlugin.py", line 394 in executeAlgorithm dlg.exec\_() File "C:\\PROGRA\~1/QGIS32\~2.4/apps/qgis-ltr/./python/plugins\\processing\\gui\\ProcessingToolbox.py", line 234 in executeAlgorithm self.executeWithGui.emit(alg.id(), self, self.in_place_mode, False)

On QGIS 3.38.3 the process also runs for a couple of iteration and then just freezes the whole application.

Running a simple (=non processing) version of this script from the Python console works beautifully on both QGIS versions!

What am I missing in my processing script?


Solution

  • By selecting the network features (network.selectByExpression("cluster != {}".format(cluster))) you access data on the QgisInterface thread from another thread resulting in a crash of QGIS.

    You can either execute your script on the interface thread or avoid selecting features from a loaded layer. When you execute your script from the python console, the interface thread is used. Avoiding a crash.

    Materializing the network layer the same way you did with the valves layer should work fine.

    To execute your script in the same thread as the interface, you can add the following method, but it freezes QGIS during the execution.

    def flags(self):
        """
        Algorithm manipulating project (toggle layer) or using external library are not thread safe
        See :https://api.qgis.org/api/classQgsProcessingAlgorithm.html#a6a8c21fab75e03f50f45e41a9d67dbc3a229dea6cedf8c59132196dee09d4f2f6
        """
        return super().flags() | QgsProcessingAlgorithm.FlagNoThreading 
    

    I hope this resolve your issue.