Paralléliser les opérations SIG dans PyQGIS?

15

Une exigence courante dans le SIG est d'appliquer un outil de traitement à un certain nombre de fichiers ou d'appliquer un processus pour un certain nombre de fonctionnalités d'un fichier à un autre fichier.

La plupart de ces opérations sont parallèlement d'une manière embarrassante en ce sens que les résultats des calculs n'influencent en aucune manière aucune autre opération dans la boucle. Non seulement cela, mais souvent les fichiers d'entrée sont chacun distincts.

Un exemple classique est la mise en mosaïque de fichiers de forme contre des fichiers contenant des polygones pour les couper.

Voici une méthode procédurale classique (testée) pour y parvenir dans un script python pour QGIS. (pour la sortie des fichiers de mémoire temporaire en fichiers réels, le temps de traitement de mes fichiers de test a été réduit de plus de moitié)

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())

Ce serait bien, sauf que mon fichier d'entrée est de 2 Go et que le fichier d'écrêtage de polygone contient plus de 400 polygones. Le processus résultant prend plus d'une semaine sur ma machine quadricœur. Pendant ce temps, trois cœurs tournent au ralenti.

La solution que j'ai dans ma tête est d'exporter le processus vers des fichiers de script et de les exécuter de manière asynchrone en utilisant gnu parallel par exemple. Cependant, il semble dommage de devoir abandonner QGIS dans une solution spécifique au système d'exploitation plutôt que d'utiliser quelque chose de natif pour QGIS python. Ma question est donc:

Puis-je paralléliser des opérations géographiques parallèlement embarrassantes en natif dans python QGIS?

Sinon, alors peut-être que quelqu'un a déjà le code pour envoyer ce genre de travail aux scripts shell asynchrones?

Mr Purple
la source
Pas familier avec le multitraitement dans QGIS, mais cet exemple spécifique à ArcGIS peut être d'une certaine utilité: gis.stackexchange.com/a/20352/753
blah238
Semble intéressant. Je vais voir ce que je peux en faire.
M. Purple

Réponses:

11

Si vous changez votre programme pour lire le nom de fichier à partir de la ligne de commande et divisez votre fichier d'entrée en petits morceaux, vous pouvez faire quelque chose comme ça en utilisant GNU Parallel:

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

Cela exécutera 1 travail par cœur.

Tous les nouveaux ordinateurs ont plusieurs cœurs, mais la plupart des programmes sont de nature série et n'utiliseront donc pas les cœurs multiples. Cependant, de nombreuses tâches sont extrêmement parallélisables:

  • Exécutez le même programme sur de nombreux fichiers
  • Exécutez le même programme pour chaque ligne d'un fichier
  • Exécutez le même programme pour chaque bloc d'un fichier

GNU Parallel est un paralléliseur général et permet d'exécuter facilement des travaux en parallèle sur la même machine ou sur plusieurs machines auxquelles vous avez accès ssh.

Si vous souhaitez exécuter 32 tâches différentes sur 4 processeurs, une manière simple de paralléliser consiste à exécuter 8 tâches sur chaque processeur:

Planification simple

Au lieu de cela, GNU Parallel engendre un nouveau processus une fois terminé - en gardant les CPU actifs et en économisant ainsi du temps:

Planification parallèle GNU

Installation

Si GNU Parallel n'est pas empaqueté pour votre distribution, vous pouvez faire une installation personnelle, qui ne nécessite pas d'accès root. Cela peut être fait en 10 secondes en procédant comme suit:

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

Pour d'autres options d'installation, voir http://git.savannah.gnu.org/cgit/parallel.git/tree/README

Apprendre encore plus

Voir plus d'exemples: http://www.gnu.org/software/parallel/man.html

Regardez les vidéos d'introduction: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Parcourez le didacticiel: http://www.gnu.org/software/parallel/parallel_tutorial.html

Inscrivez-vous à la liste de diffusion pour obtenir de l'aide: https://lists.gnu.org/mailman/listinfo/parallel

Ole Tange
la source
C'est quelque chose comme j'allais essayer, mais j'en ai besoin pour rester à l'intérieur de python. La ligne doit être réécrite pour utiliser, par exemple, Popen ... Quelque chose comme: depuis le sous-processus importation Popen, PIPE p = Popen (["parallèle", "ogr2ogr", "- clipsrc", "clip_file * .shp", "output * .shp "input.shp"], stdin = PIPE, stdout = PIPE, stderr = PIPE) Le problème est que je ne sais pas encore comment préparer correctement la syntaxe
Mr Purple
Réponse géniale. Je n'avais jamais rencontré d'opérateurs de côlon triple (ou quadruple) auparavant (bien que je fasse actuellement un mooc Haskell sur edX, donc sans aucun doute quelque chose de similaire arrivera). Je suis d'accord avec vous sur le père Noël, les fantômes, les fées et les dieux, mais certainement pas les gobelins: D
John Powell
@MrPurple Je pense que ce commentaire mérite à lui seul une question. La réponse est définitivement trop longue pour faire un commentaire.
Ole Tange
OK, merci pour les liens. Si je formule une réponse en utilisant gnu parallel, je la posterai ici.
Mr Purple
Une bonne formulation pour vous my_processing.pypeut être trouvée sur gis.stackexchange.com/a/130337/26897
Mr Purple
4

Plutôt que d'utiliser la méthode GNU Parallel, vous pouvez utiliser le module python mutliprocess pour créer un pool de tâches et les exécuter. Je n'ai pas accès à une configuration QGIS pour le tester, mais le multiprocess a été ajouté en Python 2.6, à condition que vous utilisiez 2.6 ou une version ultérieure, il devrait être disponible. Il existe de nombreux exemples en ligne sur l'utilisation de ce module.

Steve Barnes
la source
2
J'ai essayé le multiprocessus mais je ne l'ai pas encore vu avec succès implémenté dans le python intégré de QGIS. J'ai rencontré un certain nombre de problèmes en l'essayant. Je peux les poster en tant que questions séparées. Pour autant que je sache, il n'y a pas d'exemples publics accessibles à quelqu'un qui commence avec ça.
Mr Purple
C'est vraiment dommage. Si quelqu'un pouvait écrire un exemple du module multiprocessus enveloppant une seule fonction pyQGIS comme je l'ai fait avec le parallèle gnu, alors nous pourrions tous partir et paralléliser tout ce que nous avons choisi.
Mr Purple
Je suis d'accord, mais comme je l'ai dit, je n'ai pas accès à un QGIS pour le moment.
Steve Barnes
Cette question et réponse peut être utile si vous utilisez Windows, gis.stackexchange.com/questions/35279/…
Steve Barnes
@MrPurple et celui-ci gis.stackexchange.com/questions/114260/… donne un exemple
Steve Barnes
3

Voici la solution parallèle GNU. Avec un peu de soin, les algorithmes ogr ou saga basés sur Linux les plus parallèles peuvent être exécutés avec lui dans votre installation QGIS.

Evidemment cette solution nécessite l'installation de gnu parallel. Pour installer gnu parallel dans Ubuntu, par exemple, allez sur votre terminal et tapez

sudo apt-get -y install parallel

NB: Je n'ai pas pu faire fonctionner la commande shell parallèle dans Popen ou sous-processus, ce que j'aurais préféré, j'ai donc piraté une exportation vers un script bash et l'ai exécutée avec Popen à la place.

Voici la commande shell spécifique utilisant le parallèle que j'ai enveloppé dans python

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

Chaque {1} est échangé contre un nombre de la plage {1..400}, puis les quatre cents commandes shell sont gérées par gnu parallèlement pour utiliser simultanément tous les cœurs de mon i7 :).

Voici le code python réel que j'ai écrit pour résoudre l'exemple de problème que j'ai publié. On pourrait le coller directement après la fin du code dans la question.

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

Permettez-moi de vous dire que c'est vraiment quelque chose quand vous voyez tous les cœurs se déclencher à plein bruit :). Un merci spécial à Ole et à l'équipe qui a construit Gnu Parallel.

Ce serait bien d'avoir une solution multiplateforme et ce serait bien si j'avais pu comprendre le module python multiprocesseur pour le python intégré qgis mais hélas ça ne devait pas l'être.

Peu importe cette solution me servira et peut-être bien.

Mr Purple
la source
Évidemment, il faut commenter la ligne "processing.runalg" dans le premier morceau de code pour que le clip ne soit pas exécuté en séquence d'abord avant d'être exécuté en parallèle. En dehors de cela, il s'agit simplement de copier et coller le code de la réponse sous le code de la question.
Mr Purple
Si vous voulez simplement exécuter de nombreuses commandes de traitement comme un ensemble de "qgis: dissolve" appliqués à différents fichiers en parallèle, vous pouvez voir mon processus pour cela sur purplelinux.co.nz/?p=190
Mr Purple