Paralelizando operações GIS no PyQGIS?

15

Um requisito comum no GIS é aplicar uma ferramenta de processamento a vários arquivos ou aplicar um processo a vários recursos de um arquivo a outro arquivo.

Muitas dessas operações são embaraçosamente paralelas, pois os resultados dos cálculos não influenciam em nenhuma outra operação do loop. Não apenas isso, mas frequentemente os arquivos de entrada são distintos.

Um exemplo clássico é o agrupamento de arquivos de forma contra arquivos que contêm polígonos para prendê-los.

Aqui está um método processual clássico (testado) para conseguir isso em um script python para QGIS. (fyi a saída de arquivos de memória temporários para arquivos reais mais da metade do tempo para processar meus arquivos de teste)

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

Isso seria bom, exceto que meu arquivo de entrada tem 2 GB e o arquivo de recorte de polígono contém mais de 400 polígonos. O processo resultante leva mais de uma semana na minha máquina quad core. Enquanto isso, três núcleos estão ociosos.

A solução que tenho em minha cabeça é exportar o processo para arquivos de script e executá-los de forma assíncrona usando o gnu paralelo, por exemplo. No entanto, parece uma pena ter que abandonar o QGIS em uma solução específica do SO, em vez de usar algo nativo do python do QGIS. Então, minha pergunta é:

Posso paralelizar operações geográficas embaraçosamente paralelas nativamente dentro do python QGIS?

Caso contrário, talvez alguém já tenha o código para enviar esse tipo de trabalho para scripts shell assíncronos?

Mr Purple
fonte
Não está familiarizado com multiprocessamento no QGIS, mas este exemplo específico do ArcGIS podem ser de alguma utilidade: gis.stackexchange.com/a/20352/753
blah238
Parece interessante. Vou ver o que posso fazer com isso.
Mr Purple

Respostas:

11

Se você alterar o seu programa para ler o nome do arquivo na linha de comando e dividir o arquivo de entrada em partes menores, poderá fazer algo parecido com isso usando o GNU Parallel:

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

Isso executará 1 trabalho por núcleo.

Todos os novos computadores possuem múltiplos núcleos, mas a maioria dos programas é de natureza serial e, portanto, não usa os múltiplos núcleos. No entanto, muitas tarefas são extremamente paralelizáveis:

  • Execute o mesmo programa em muitos arquivos
  • Execute o mesmo programa para cada linha em um arquivo
  • Execute o mesmo programa para cada bloco em um arquivo

O GNU Parallel é um paralelizador geral e facilita a execução de trabalhos em paralelo na mesma máquina ou em várias máquinas às quais você tem acesso ssh.

Se você tiver 32 tarefas diferentes que deseja executar em 4 CPUs, uma maneira direta de paralelizar é executar 8 tarefas em cada CPU:

Programação simples

O GNU Parallel gera um novo processo quando se termina - mantendo as CPUs ativas e economizando tempo:

Programação paralela GNU

Instalação

Se o GNU Parallel não estiver empacotado para sua distribuição, você poderá fazer uma instalação pessoal, que não requer acesso root. Isso pode ser feito em 10 segundos, fazendo o seguinte:

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

Para outras opções de instalação, consulte http://git.savannah.gnu.org/cgit/parallel.git/tree/README

Saber mais

Veja mais exemplos: http://www.gnu.org/software/parallel/man.html

Assista aos vídeos de introdução: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Percorra o tutorial: http://www.gnu.org/software/parallel/parallel_tutorial.html

Inscreva-se na lista de e-mails para obter suporte: https://lists.gnu.org/mailman/listinfo/parallel

Ole Tange
fonte
Isso é algo que eu tentaria, mas preciso que todos permaneçam dentro do python. A linha precisa ser reescrita para usar, digamos, Popen, por exemplo ... Algo como: de subprocesso de importação Popen, PIPE p = Popen (["paralelo", "ogr2ogr", "- clipsrc", "clip_file * .shp", "output * .shp "input.shp"], stdin = PIPE, stdout = PIPE, stderr = PIPE) o problema é que eu não ainda sabe como preparar a sintaxe corretamente
Sr. roxo
Resposta incrível. Eu nunca tinha encontrado operadores de cólon triplos (ou quádruplos) antes (embora atualmente esteja fazendo um haskell no edX, então, sem dúvida, algo semelhante virá). Eu concordo com você sobre Papai Noel, fantasmas, fadas e deuses, mas definitivamente não são duendes: D
John Powell
@ MrPurple Acho que esse comentário merece uma pergunta por si só. A resposta é definitivamente longa demais para colocar um comentário.
precisa
OK, obrigado pelos links. Se eu formular uma resposta usando o gnu paralelo, postarei aqui.
Mr Purple
Uma boa formulação para o seu my_processing.pypode ser encontrada em gis.stackexchange.com/a/130337/26897
Mr Purple
4

Em vez de usar o método GNU Parallel, você pode usar o módulo mutliprocess python para criar um conjunto de tarefas e executá-las. Não tenho acesso a uma configuração do QGIS para testá-lo, mas o multiprocesso foi adicionado no Python 2.6, desde que você esteja usando o 2.6 ou posterior, pois ele deve estar disponível. Existem muitos exemplos online sobre o uso deste módulo.

Steve Barnes
fonte
2
Dei uma chance ao multiprocesso, mas ainda não o vi implementado com sucesso no python incorporado do QGIS. Eu encontrei uma série de problemas ao testá-lo. Posso publicá-las como perguntas separadas. Até onde eu sei, não há exemplos públicos acessíveis a alguém que comece com isso.
Sr. Roxo
É uma pena. Se alguém pudesse escrever um exemplo do módulo multiprocesso envolvendo uma única função pyQGIS, como eu fiz com o paralelo do gnu, todos nós poderíamos sair e paralelizar o que quiséssemos.
Sr. Roxo
Eu concordo, mas como eu disse, não tenho acesso a um QGIS no momento.
Steve Barnes
Esta questão & resposta pode ser de alguma ajuda se você estiver executando no Windows, gis.stackexchange.com/questions/35279/...
Steve Barnes
@MrPurple e este gis.stackexchange.com/questions/114260/… dá um exemplo
Steve Barnes
3

Aqui está a solução paralela gnu. Com algum cuidado, a maioria dos algoritmos ogr ou saga baseados em linux, paralelamente surpreendentemente paralelos, podem ser executados com ele dentro da instalação do QGIS.

Obviamente, esta solução requer a instalação do gnu paralelo. Para instalar o gnu paralelo no Ubuntu, por exemplo, vá ao seu terminal e digite

sudo apt-get -y install parallel

NB: Não consegui que o comando shell paralelo funcionasse no Popen ou no subprocesso, o que eu preferiria, então cortei uma exportação para um script bash e executei com o Popen.

Aqui está o comando shell específico usando paralelo que envolvi em python

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

Cada {1} é trocado por um número do intervalo {1..400} e os quatrocentos comandos do shell são gerenciados pelo gnu paralelamente para usar simultaneamente todos os núcleos do meu i7 :).

Aqui está o código python real que escrevi para resolver o problema de exemplo que publiquei. Pode-se colá-lo diretamente após o final do código na pergunta.

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

Deixe-me dizer-lhe que é realmente algo quando você vê todos os núcleos dispararem com ruído total :). Agradecimentos especiais a Ole e à equipe que construiu o Gnu Parallel.

Seria bom ter uma solução de plataforma cruzada e seria bom se eu pudesse descobrir o módulo python de multiprocessamento para o python incorporado qgis, mas, infelizmente, não deveria ser.

Independentemente desta solução vai me servir e talvez você muito bem.

Mr Purple
fonte
Obviamente, deve-se comentar a linha "processing.runalg" no primeiro trecho de código, para que o clipe não seja executado seqüencialmente primeiro antes de ser executado em paralelo. Fora isso, é simplesmente uma questão de copiar e colar o código da resposta abaixo do código da pergunta.
Sr. Roxo
Se você quiser executar vários comandos de processamento, como um conjunto de "qgis: dissolve", aplicado a diferentes arquivos em paralelo, poderá ver meu processo para isso em purplelinux.co.nz/?p=190
Mr Purple,