Parallelisierung von GIS-Operationen in PyQGIS?

15

Eine häufige Anforderung in GIS besteht darin, ein Verarbeitungswerkzeug auf eine Reihe von Dateien anzuwenden oder einen Prozess für eine Reihe von Features in einer Datei auf eine andere Datei anzuwenden.

Viele dieser Operationen sind insofern peinlich parallel, als die Ergebnisse der Berechnungen keinen Einfluss auf andere Operationen in der Schleife haben. Nicht nur das, sondern auch die Eingabedateien sind oft unterschiedlich.

Ein klassisches Beispiel hierfür ist das Kacheln von Formdateien mit Dateien, die Polygone enthalten, um sie zu kürzen.

Hier ist eine (getestete) klassische prozedurale Methode, um dies in einem Python-Skript für QGIS zu erreichen. (Für die Ausgabe von temporären Speicherdateien in echte Dateien wurde die Zeit für die Verarbeitung meiner Testdateien mehr als halbiert.)

import processing
import os
input_file="/path/to/input_file.shp"
clip_polygons_file="/path/to/polygon_file.shp"
output_folder="/tmp/test/"
input_layer = QgsVectorLayer(input_file, "input file", "ogr")
QgsMapLayerRegistry.instance().addMapLayer(input_layer)
tile_layer  = QgsVectorLayer(clip_polygons_file, "clip_polys", "ogr")
QgsMapLayerRegistry.instance().addMapLayer(tile_layer)
tile_layer_dp=input_layer.dataProvider()
EPSG_code=int(tile_layer_dp.crs().authid().split(":")[1])
tile_no=0
clipping_polygons = tile_layer.getFeatures()
for clipping_polygon in clipping_polygons:
    print "Tile no: "+str(tile_no)
    tile_no+=1
    geom = clipping_polygon.geometry()
    clip_layer=QgsVectorLayer("Polygon?crs=epsg:"+str(EPSG_code)+\
    "&field=id:integer&index=yes","clip_polygon", "memory")
    clip_layer_dp = clip_layer.dataProvider()
    clip_layer.startEditing()
    clip_layer_feature = QgsFeature()
    clip_layer_feature.setGeometry(geom)
    (res, outFeats) = clip_layer_dp.addFeatures([clip_layer_feature])
    clip_layer.commitChanges()
    clip_file = os.path.join(output_folder,"tile_"+str(tile_no)+".shp")
    write_error = QgsVectorFileWriter.writeAsVectorFormat(clip_layer, \
    clip_file, "system", \
    QgsCoordinateReferenceSystem(EPSG_code), "ESRI Shapefile")
    QgsMapLayerRegistry.instance().addMapLayer(clip_layer)
    output_file = os.path.join(output_folder,str(tile_no)+".shp")
    processing.runalg("qgis:clip", input_file, clip_file, output_file)
    QgsMapLayerRegistry.instance().removeMapLayer(clip_layer.id())

Dies wäre in Ordnung, außer dass meine Eingabedatei 2 GB groß ist und die Polygon-Clipping-Datei mehr als 400 Polygone enthält. Der resultierende Prozess dauert auf meinem Quad-Core-Rechner über eine Woche. Währenddessen laufen drei Kerne nur im Leerlauf.

Die Lösung, die ich in meinem Kopf habe, besteht darin, den Prozess in Skriptdateien zu exportieren und sie asynchron auszuführen, zum Beispiel mit gnu parallel. Es scheint jedoch eine Schande zu sein, QGIS in einer betriebssystemspezifischen Lösung beenden zu müssen, anstatt etwas zu verwenden, das ursprünglich für QGIS-Python gedacht ist. Meine Frage lautet also:

Kann ich peinlich parallele geografische Operationen nativ in Python QGIS parallelisieren?

Wenn nicht, hat vielleicht schon jemand den Code, um diese Art von Arbeit an asynchrone Shell-Skripte zu senden?

Mr Purple
quelle
Nicht vertraut mit Multiprocessing in QGIS, aber dieses ArcGIS-spezifische Beispiel kann von Nutzen sein: gis.stackexchange.com/a/20352/753
blah238 28.10.14
Sieht interessant aus. Ich werde sehen, was ich damit anfangen kann.
Mr Purple

Antworten:

11

Wenn Sie Ihr Programm so ändern, dass es den Dateinamen von der Befehlszeile liest und Ihre Eingabedatei in kleinere Teile aufteilt, können Sie mit GNU Parallel Folgendes tun:

parallel my_processing.py {} /path/to/polygon_file.shp ::: input_files*.shp

Dadurch wird 1 Job pro Kern ausgeführt.

Alle neuen Computer haben mehrere Kerne, aber die meisten Programme sind serieller Natur und verwenden daher nicht die mehreren Kerne. Viele Aufgaben sind jedoch extrem parallelisierbar:

  • Führen Sie das gleiche Programm für viele Dateien aus
  • Führen Sie für jede Zeile in einer Datei dasselbe Programm aus
  • Führen Sie für jeden Block in einer Datei dasselbe Programm aus

GNU Parallel ist ein allgemeiner Parallelisierer, mit dem Sie auf einfache Weise Jobs auf demselben Computer oder auf mehreren Computern, auf die Sie ssh-Zugriff haben, parallel ausführen können.

Wenn Sie 32 verschiedene Jobs auf 4 CPUs ausführen möchten, können Sie auf einfache Weise 8 Jobs auf jeder CPU parallelisieren:

Einfaches Scheduling

GNU Parallel erzeugt stattdessen einen neuen Prozess, wenn man fertig ist - die CPUs aktiv zu halten und damit Zeit zu sparen:

GNU Parallel Scheduling

Installation

Wenn GNU Parallel nicht für Ihre Distribution gepackt ist, können Sie eine persönliche Installation durchführen, für die kein Root-Zugriff erforderlich ist. Dies kann in 10 Sekunden geschehen:

(wget -O - pi.dk/3 || curl pi.dk/3/ || fetch -o - http://pi.dk/3) | bash

Weitere Installationsoptionen finden Sie unter http://git.savannah.gnu.org/cgit/parallel.git/tree/README

Erfahren Sie mehr

Weitere Beispiele finden Sie unter: http://www.gnu.org/software/parallel/man.html

Sehen Sie sich die Intro-Videos an: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Gehen Sie durch das Tutorial: http://www.gnu.org/software/parallel/parallel_tutorial.html

Melden Sie sich für die E-Mail-Liste an, um Unterstützung zu erhalten: https://lists.gnu.org/mailman/listinfo/parallel

Ole Tange
quelle
Dies ist so etwas wie ich wollte versuchen und versuchen, aber ich brauche es, um alles in Python zu bleiben. Die Zeile muss umgeschrieben werden, um beispielsweise "say Popen" zu verwenden. So etwas wie: aus dem Unterprozess-Import "Popen", "PIPE p = Popen" (["parallel", "ogr2ogr", "- clipsrc", "clip_file * .shp", "output * .shp "input.shp"], stdin = PIPE, stdout = PIPE, stderr = PIPE) Das Problem ist, dass ich noch nicht weiß, wie ich die Syntax richtig vorbereiten soll
Mr Purple
Geniale Antwort. Ich hatte vorher noch nie Dreifach- (oder Vierfach-) Doppelpunktoperatoren gesehen (obwohl ich gerade ein Haskell-Mooc auf edX mache, also wird zweifellos etwas Ähnliches kommen). Ich stimme Ihnen in Bezug auf Santa, Geister, Feen und Götter zu, aber definitiv nicht Goblins: D
John Powell
@ MrPurple Ich denke, dieser Kommentar rechtfertigt eine Frage für sich. Die Antwort ist definitiv zu lang, um einen Kommentar abzugeben.
Ole Tange
OK, danke für die Links. Wenn ich mit gnu parallel eine Antwort formuliere, poste ich sie hier.
Mr Purple
Eine gute Formulierung für Ihre my_processing.pyfinden Sie unter gis.stackexchange.com/a/130337/26897
Mr Purple
4

Anstatt die GNU Parallel-Methode zu verwenden, können Sie das Python- Multiprozess- Modul verwenden, um einen Pool von Tasks zu erstellen und diese auszuführen. Ich habe keinen Zugriff auf ein QGIS-Setup, um es zu testen, aber in Python 2.6 wurde Multiprocess hinzugefügt, vorausgesetzt, dass Sie 2.6 oder höher verwenden, sollte es verfügbar sein. Es gibt viele Online-Beispiele zur Verwendung dieses Moduls.

Steve Barnes
quelle
2
Ich habe multiprocess ausprobiert, aber ich habe noch nicht gesehen, dass es erfolgreich in das eingebettete Python von QGIS implementiert wurde. Ich habe beim Ausprobieren eine Reihe von Problemen festgestellt. Ich kann sie als separate Fragen posten. Soweit ich das beurteilen kann, gibt es keine öffentlichen Beispiele, die jemandem zugänglich sind, der damit anfängt.
Mr Purple
Es ist eine echte Schande. Wenn jemand ein Beispiel für ein Multiprozess-Modul schreiben könnte, das eine einzelne pyQGIS-Funktion umschließt, wie ich es mit der Gnu-Parallele getan habe, könnten wir alle loslegen und parallelisieren, was immer wir wollten.
Mr Purple
Ich stimme zu, aber wie gesagt, ich habe im Moment keinen Zugang zu einem QGIS.
Steve Barnes
Diese Frage und Antwort kann hilfreich sein, wenn Sie unter Windows arbeiten ( gis.stackexchange.com/questions/35279/…
Steve Barnes,
@ MrPurple und dieser gis.stackexchange.com/questions/114260/… gibt ein Beispiel
Steve Barnes
3

Hier ist die Gnu-Parallellösung. Mit einiger Sorgfalt könnten die meisten auf Linux basierenden Parallel-Ogr- oder -Saga-Algorithmen so erstellt werden, dass sie in Ihrer QGIS-Installation ausgeführt werden.

Offensichtlich erfordert diese Lösung die Installation von gnu parallel. Um gnu parallel in Ubuntu zu installieren, gehen Sie zum Beispiel zu Ihrem Terminal und geben Sie Folgendes ein

sudo apt-get -y install parallel

NB: Ich konnte den parallelen Shell-Befehl nicht in Popen oder Subprozessen ausführen, was mir lieber gewesen wäre. Deshalb habe ich einen Export in ein Bash-Skript gehackt und diesen stattdessen mit Popen ausgeführt.

Hier ist der spezielle Shell-Befehl, der parallele Befehle verwendet, die ich in Python eingebunden habe

parallel ogr2ogr -skipfailures -clipsrc tile_{1}.shp output_{1}.shp input.shp ::: {1..400}

Jede {1} wird gegen eine Zahl aus dem Bereich {1..400} ausgetauscht, und dann werden die vierhundert Shell-Befehle von gnu parallel verwaltet, um alle Kerne meines i7 :) gleichzeitig zu verwenden.

Hier ist der eigentliche Python-Code, den ich geschrieben habe, um das Beispielproblem zu lösen, das ich veröffentlicht habe. Man könnte es direkt nach dem Ende des Codes in die Frage einfügen.

import stat
from subprocess import Popen
from subprocess import PIPE
feature_count=tile_layer.dataProvider().featureCount()
subprocess_args=["parallel", \
"ogr2ogr","-skipfailures","-clipsrc",\
os.path.join(output_folder,"tile_"+"{1}"+".shp"),\
os.path.join(output_folder,"output_"+"{1}"+".shp"),\
input_file,\
" ::: ","{1.."+str(feature_count)+"}"]
#Hacky part where I write the shell command to a script file
temp_script=os.path.join(output_folder,"parallelclip.sh")
f = open(temp_script,'w')
f.write("#!/bin/bash\n")
f.write(" ".join(subprocess_args)+'\n')
f.close()
st = os.stat(temp_script)
os.chmod(temp_script, st.st_mode | stat.S_IEXEC)
#End of hacky bash script export
p = Popen([os.path.join(output_folder,"parallelclip.sh")],\
stdin=PIPE, stdout=PIPE, stderr=PIPE)
#Below is the commented out Popen line I couldn't get to work
#p = Popen(subprocess_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
output, err = p.communicate(b"input data that is passed to subprocess' stdin")
rc = p.returncode
print output
print err

#Delete script and old clip files
os.remove(os.path.join(output_folder,"parallelclip.sh"))
for i in range(feature_count):
    delete_file = os.path.join(output_folder,"tile_"+str(i+1)+".shp")
    nosuff=os.path.splitext(delete_file)[0]
    suffix_list=[]
    suffix_list.append('.shx')
    suffix_list.append('.dbf')
    suffix_list.append('.qpj')
    suffix_list.append('.prj')
    suffix_list.append('.shp')
    suffix_list.append('.cpg')
    for suffix in suffix_list:
        try:
            os.remove(nosuff+suffix)
        except:
            pass

Lass mich dir sagen, dass es wirklich etwas ist, wenn du siehst, wie alle Kerne auf vollen Touren laufen :). Besonderer Dank geht an Ole und das Team, das Gnu Parallel gebaut hat.

Es wäre schön, eine plattformübergreifende Lösung zu haben, und es wäre schön, wenn ich das Multiprozessor-Python-Modul für das in qgis eingebettete Python hätte herausfinden können, aber leider sollte es nicht so sein.

Unabhängig davon wird diese Lösung mir und vielleicht auch Ihnen guttun.

Mr Purple
quelle
Natürlich sollte man die Zeile "processing.runalg" im ersten Teil des Codes auskommentieren, damit der Clip nicht zuerst nacheinander ausgeführt wird, bevor er parallel ausgeführt wird. Ansonsten geht es einfach darum, den Code aus der Antwort unter dem Code in der Frage zu kopieren und einzufügen.
Mr Purple
Wenn Sie nur viele Verarbeitungsbefehle ausführen möchten, wie z. B. eine Reihe von "qgis: dissolve", die parallel auf verschiedene Dateien angewendet werden, können Sie meinen Vorgang unter purplelinux.co.nz/?p=190
Mr Purple