¿Paralelizar las operaciones SIG en PyQGIS?

15

Un requisito común en los SIG es aplicar una herramienta de procesamiento a varios archivos o aplicar un proceso para una serie de características de un archivo a otro.

Muchas de estas operaciones son vergonzosamente paralelas en el sentido de que los resultados de los cálculos de ninguna manera influyen en ninguna otra operación en el ciclo. No solo eso, sino que a menudo los archivos de entrada son distintos.

Un ejemplo clásico es la disposición en mosaico de archivos de formas contra archivos que contienen polígonos para recortarlos.

Aquí hay un método de procedimiento clásico (probado) para lograr esto en un script de Python para QGIS. (Para su información, la salida de archivos de memoria temporal a archivos reales más de la mitad del tiempo para procesar mis archivos de prueba)

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

Esto estaría bien, excepto que mi archivo de entrada es de 2GB y el archivo de recorte de polígonos contiene más de 400 polígonos. El proceso resultante lleva más de una semana en mi máquina de cuatro núcleos. Mientras tanto, tres núcleos están inactivos.

La solución que tengo en mi cabeza es exportar el proceso a archivos de script y ejecutarlos de forma asíncrona usando gnu paralelo, por ejemplo. Sin embargo, parece una pena tener que abandonar QGIS en una solución específica del sistema operativo en lugar de usar algo nativo de QGIS python. Entonces mi pregunta es:

¿Puedo paralelizar operaciones geográficas embarazosamente paralelas de forma nativa dentro de Python QGIS?

Si no es así, ¿tal vez alguien ya tenga el código para enviar este tipo de trabajo a scripts de shell asíncronos?

Señor púrpura
fuente
No estoy familiarizado con el multiprocesamiento en QGIS, pero este ejemplo específico de ArcGIS puede ser útil: gis.stackexchange.com/a/20352/753
blah238
Parece interesante. Veré qué puedo hacer con él.
Mr Purple

Respuestas:

11

Si cambia su programa para leer el nombre del archivo desde la línea de comandos y divide su archivo de entrada en fragmentos más pequeños, puede hacer algo como esto usando GNU Parallel:

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

Esto ejecutará 1 trabajo por núcleo.

Todas las computadoras nuevas tienen múltiples núcleos, pero la mayoría de los programas son de naturaleza serial y, por lo tanto, no utilizarán los múltiples núcleos. Sin embargo, muchas tareas son extremadamente paralelizables:

  • Ejecute el mismo programa en muchos archivos
  • Ejecute el mismo programa para cada línea en un archivo
  • Ejecute el mismo programa para cada bloque en un archivo

GNU Parallel es un paralelizador general y hace que sea fácil ejecutar trabajos en paralelo en la misma máquina o en varias máquinas a las que tiene acceso ssh.

Si tiene 32 trabajos diferentes que desea ejecutar en 4 CPU, una forma directa de paralelizar es ejecutar 8 trabajos en cada CPU:

Programación simple

GNU Parallel genera un nuevo proceso cuando uno termina, manteniendo las CPU activas y ahorrando así tiempo:

Programación paralela de GNU

Instalación

Si GNU Parallel no está empaquetado para su distribución, puede hacer una instalación personal, que no requiere acceso de root. Se puede hacer en 10 segundos haciendo esto:

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

Para otras opciones de instalación, consulte http://git.savannah.gnu.org/cgit/parallel.git/tree/README

Aprende más

Ver más ejemplos: http://www.gnu.org/software/parallel/man.html

Mira los videos de introducción: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Recorre el tutorial: http://www.gnu.org/software/parallel/parallel_tutorial.html

Regístrese en la lista de correo electrónico para obtener asistencia: https://lists.gnu.org/mailman/listinfo/parallel

Ole Tange
fuente
Esto es algo que iba a intentar e intentar, pero necesito que todos permanezcan dentro de Python. La línea necesita ser reescrita para usar decir Popen, por ejemplo ... Algo así como: desde la importación de subprocesos Popen, PIPE p = Popen (["parallel", "ogr2ogr", "- clipsrc", "clip_file * .shp", "output * .shp "input.shp"], stdin = PIPE, stdout = PIPE, stderr = PIPE) El problema es que todavía no sé cómo preparar la sintaxis correctamente
Mr Purple
Impresionante respuesta. No me había encontrado con operadores de colon triples (o cuádruples) antes (aunque actualmente estoy haciendo un mok Haskell en edX, así que sin duda algo similar vendrá). Estoy de acuerdo contigo sobre santa, fantasmas, hadas y dioses, pero definitivamente no sobre duendes: D
John Powell
@ MrPurple Creo que ese comentario justifica una pregunta por sí solo. La respuesta definitivamente es demasiado larga para poner un comentario.
Ole Tange
OK, gracias por los enlaces. Si formulo una respuesta usando GNU paralelo, la publicaré aquí.
Mr Purple
my_processing.pyPuede encontrar una buena formulación para usted en gis.stackexchange.com/a/130337/26897
Mr Purple
4

En lugar de utilizar el método paralelo GNU, puede utilizar el módulo de multiprocesamiento de Python para crear un grupo de tareas y ejecutarlas. No tengo acceso a una configuración de QGIS para probarlo, pero se agregó multiproceso en Python 2.6, por lo que siempre que esté usando 2.6 o posterior, debería estar disponible. Hay muchos ejemplos en línea sobre el uso de este módulo.

Steve Barnes
fuente
2
Le di una oportunidad al multiproceso, pero aún no lo he visto con éxito en el python incrustado de QGIS. Llegué a varios problemas cuando lo probé. Puedo publicarlos como preguntas separadas. Por lo que puedo decir, no hay ejemplos públicos accesibles para alguien que comienza con esto.
Mr Purple el
Es una verdadera pena. Si alguien pudiera escribir un ejemplo del módulo multiproceso que envolviera una sola función pyQGIS como hice con el paralelo gnu, entonces todos podríamos salir y paralelizar lo que elijamos.
Mr Purple el
Estoy de acuerdo, pero como dije no tengo acceso a un QGIS en este momento.
Steve Barnes
Esta pregunta y respuesta pueden ser de ayuda si está ejecutando bajo Windows, gis.stackexchange.com/questions/35279/…
Steve Barnes
@MrPurple y este gis.stackexchange.com/questions/114260/… da un ejemplo
Steve Barnes,
3

Aquí está la solución paralela GNU. Con un poco de cuidado, la mayoría de los algoritmos ogr o saga basados ​​en linux de forma paralela podrían ejecutarse dentro de su instalación QGIS.

Obviamente esta solución requiere la instalación de GNU en paralelo. Para instalar gnu paralelo en Ubuntu, por ejemplo, vaya a su terminal y escriba

sudo apt-get -y install parallel

Nota: no pude hacer que el comando de shell paralelo funcionara en Popen o en un subproceso, lo que hubiera preferido, así que pirateé una exportación a un script bash y ejecuté eso con Popen.

Aquí está el comando de shell específico usando paralelo que envolví en python

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

Cada {1} se intercambia por un número del rango {1..400} y luego los cuatrocientos comandos de shell se gestionan mediante gnu paralelo para utilizar simultáneamente todos los núcleos de mi i7 :).

Aquí está el código real de Python que escribí para resolver el problema de ejemplo que publiqué. Uno podría pegarlo directamente después del final del código en la pregunta.

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

Déjame decirte que es realmente algo cuando ves que todos los núcleos se disparan al máximo ruido :). Un agradecimiento especial a Ole y al equipo que construyó Gnu Parallel.

Sería bueno tener una solución multiplataforma y sería bueno si hubiera podido descifrar el módulo de multiprocesamiento de Python para el qgis Python incrustado, pero desafortunadamente no fue así.

Independientemente, esta solución me servirá y quizás a usted muy bien.

Señor púrpura
fuente
Obviamente, uno debería comentar la línea "processing.runalg" en el primer fragmento de código para que el clip no se ejecute secuencialmente antes de que se ejecute en paralelo. Aparte de eso, es simplemente una cuestión de copiar y pegar el código de la respuesta debajo del código en la pregunta.
Mr Purple
Si solo desea ejecutar muchos comandos de procesamiento como un conjunto de "qgis: disolver" aplicado a diferentes archivos en paralelo, puede ver mi proceso para esto en purplelinux.co.nz/?p=190
Mr Purple