Parallelizzare le operazioni GIS in PyQGIS?


15

Un requisito comune in GIS è applicare uno strumento di elaborazione a un numero di file o applicare un processo per una serie di funzioni in un file a un altro file.

Molte di queste operazioni sono imbarazzantemente parallele in quanto i risultati dei calcoli non influenzano in alcun modo qualsiasi altra operazione nel ciclo. Non solo, ma spesso i file di input sono distinti.

Un classico esempio è la piastrellatura dei file di forma rispetto ai file contenenti poligoni per agganciarli.

Ecco un metodo procedurale classico (testato) per raggiungere questo obiettivo in uno script Python per QGIS. (grazie all'output di file di memoria temporanei su file reali più che dimezzato il tempo di elaborazione dei miei file di test)

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

Questo andrebbe bene, tranne per il fatto che il mio file di input è 2 GB e il file di ritaglio poligonale contiene oltre 400 poligoni. Il processo risultante richiede più di una settimana sulla mia macchina quad core. Nel frattempo tre core sono solo inattivi.

La soluzione che ho in testa è esportare il processo in file di script ed eseguirli in modo asincrono usando ad esempio gnu parallel. Tuttavia, sembra un peccato dover abbandonare QGIS in una soluzione specifica del sistema operativo anziché utilizzare qualcosa di nativo di QGIS Python. Quindi la mia domanda è:

Posso parallelizzare in modo nativo operazioni geografiche imbarazzanti all'interno di Python QGIS?

In caso contrario, forse qualcuno ha già il codice per inviare questo tipo di lavoro agli script shell asincroni?


Non ha familiarità con il multiprocessing in QGIS, ma questo esempio specifico di ArcGIS potrebbe essere di qualche utilità: gis.stackexchange.com/a/20352/753
blah238

Sembra interessante. Vedrò cosa posso farci.
Mr Purple,

Risposte:


11

Se cambi programma per leggere il nome del file dalla riga di comando e suddividere il file di input in blocchi più piccoli, puoi fare qualcosa del genere usando GNU Parallel:

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

Questo eseguirà 1 lavoro per core.

Tutti i nuovi computer dispongono di più core, ma la maggior parte dei programmi è di natura seriale e pertanto non utilizzerà più core. Tuttavia, molte attività sono estremamente parallelizzabili:

  • Esegui lo stesso programma su molti file
  • Esegui lo stesso programma per ogni riga di un file
  • Esegui lo stesso programma per ogni blocco in un file

GNU Parallel è un parallelizzatore generale e semplifica l'esecuzione di lavori in parallelo sulla stessa macchina o su più macchine a cui si ha accesso ssh.

Se hai 32 lavori diversi che vuoi eseguire su 4 CPU, un modo semplice per parallelizzare è quello di eseguire 8 lavori su ogni CPU:

Pianificazione semplice

GNU Parallel invece genera un nuovo processo al termine - mantenendo attive le CPU e risparmiando tempo:

GNU Programmazione parallela

Installazione

Se GNU Parallel non è impacchettato per la tua distribuzione, puoi eseguire un'installazione personale, che non richiede l'accesso come root. Puoi farlo in 10 secondi facendo questo:

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

Per altre opzioni di installazione consultare http://git.savannah.gnu.org/cgit/parallel.git/tree/README

Per saperne di più

Vedi altri esempi: http://www.gnu.org/software/parallel/man.html

Guarda i video introduttivi: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Segui il tutorial: http://www.gnu.org/software/parallel/parallel_tutorial.html

Iscriviti alla lista e-mail per ottenere supporto: https://lists.gnu.org/mailman/listinfo/parallel


Questo è qualcosa che stavo per provare e provare, ma ho bisogno che rimanga tutto dentro Python. La riga deve essere riscritta per usare diciamo Popen per esempio ... Qualcosa del tipo: dall'importazione di sottoprocessi Popen, PIPE p = Popen (["parallel", "ogr2ogr", "- clipsrc", "clip_file * .shp", "output * .shp "input.shp"], stdin = PIPE, stdout = PIPE, stderr = PIPE) Il problema è che non so ancora come preparare correttamente la sintassi
Mr Purple,

Risposta fantastica. Non avevo mai incontrato operatori di colon triplo (o quadruplo) prima (anche se al momento sto facendo un mooc Haskell su edX, quindi senza dubbio arriverà qualcosa di simile). Sono d'accordo con te su Babbo Natale, fantasmi, fate e dei, ma sicuramente non folletti: D
John Powell,

@MrPurple Penso che quel commento meriti una domanda da solo. La risposta è decisamente troppo lunga per inserire un commento.
Ole Tange,

OK, grazie per i collegamenti. Se formulerò una risposta usando gnu parallel, la pubblicherò qui.
Mr Purple,

Una buona formulazione per te my_processing.pypuò essere trovata su gis.stackexchange.com/a/130337/26897
Mr Purple

4

Anziché utilizzare il metodo GNU Parallel, è possibile utilizzare il modulo mutliprocess python per creare un pool di attività ed eseguirle. Non ho accesso a un'impostazione QGIS per testarla, ma il multiprocesso è stato aggiunto in Python 2.6, quindi a condizione che tu stia utilizzando 2.6 o versione successiva dovrebbe essere disponibile. Ci sono molti esempi online sull'uso di questo modulo.


2
Ho provato multiprocesso, ma devo ancora vederlo impiantato con successo nel pitone incorporato di QGIS. Ho riscontrato una serie di problemi mentre l'ho provato. Potrei postarli come domande separate. Per quanto ne so, non ci sono esempi pubblici accessibili a qualcuno che inizia con questo.
Sig. Purple,

È un vero peccato. Se qualcuno potesse scrivere un esempio del modulo multiprocesso che avvolge una singola funzione pyQGIS come ho fatto con il parallelo gnu, allora potremmo andare tutti in parallelo e parallelizzare qualunque cosa abbiamo scelto.
Sig. Purple,

Sono d'accordo ma, come ho detto, al momento non ho accesso a un QGIS.
Steve Barnes,

Questa domanda e risposta potrebbero essere di aiuto se si esegue Windows, gis.stackexchange.com/questions/35279/…
Steve Barnes,

@MrPurple e questo gis.stackexchange.com/questions/114260/… fornisce un esempio
Steve Barnes,

3

Ecco la soluzione parallela gnu. Con un po 'di attenzione si potrebbero creare algoritmi ogr o saga basati su Linux parallelamente più imbarazzante all'interno dell'installazione di QGIS.

Ovviamente questa soluzione richiede l'installazione di gnu parallel. Per installare gnu parallel in Ubuntu, ad esempio, vai sul tuo terminale e digita

sudo apt-get -y install parallel

NB: Non sono riuscito a far funzionare il comando della shell parallela in Popen o sottoprocesso, cosa che avrei preferito, quindi ho hackerato insieme un'esportazione in uno script bash ed eseguito invece con Popen.

Ecco il comando shell specifico che usa parallel che ho avvolto in Python

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

Ogni {1} viene scambiato con un numero compreso nell'intervallo {1..400} e quindi i quattrocento comandi della shell vengono gestiti da gnu parallel per utilizzare contemporaneamente tutti i core del mio i7 :).

Ecco il vero codice Python che ho scritto per risolvere il problema di esempio che ho pubblicato. Si potrebbe incollarlo direttamente dopo la fine del codice nella domanda.

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

Lascia che ti dica che è davvero qualcosa quando vedi tutti i core accendersi fino al massimo rumore :). Un ringraziamento speciale a Ole e al team che ha creato Gnu Parallel.

Sarebbe bello avere una soluzione multipiattaforma e sarebbe bello se avessi potuto capire il modulo multiprocessore Python per il qgis embedded Python, ma purtroppo non lo sarebbe stato.

Indipendentemente da ciò, questa soluzione mi servirà e forse anche tu.


Ovviamente si dovrebbe commentare la riga "processing.runalg" nel primo pezzo di codice in modo che la clip non venga eseguita in sequenza prima di essere eseguita in parallelo. Oltre a ciò, si tratta semplicemente di copiare e incollare il codice dalla risposta sotto il codice nella domanda.
Mr Purple,

Se vuoi solo eseguire molti comandi di elaborazione come un insieme di "qgis: dissolve" applicato a diversi file in parallelo, puoi vedere il mio processo per questo su purplelinux.co.nz/?p=190
Mr Purple
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.