Pesquisa

só um divisor

Alimentando e executando

um programa externo desde Python

A forma mais simples de resolver este tipo de problemas é utilizando BASH (ou similares), mas devido às limitações, principalmente matemáticas é melhor utilizar algo um pouco mais sofisticado como PERL ou PYTHON.

Vamos supor que eu quero rodar o programa com nome executavel, este programa recebe 3 parâmetros externos os quais podem variar. Suponha que os parâmetros são i1, i2 e i3. O primeiro dos parâmetros vai variar desde 0.1 até 1.0 com incremento de 0.1. A segunda vai desde 0.30 até 0.70 com 0.05 de incremento. A última vai desde 1 até 10, de 1 em 1.

Com a escolha de parâmetros, a quantidade de dados a serem geradas é enorme, no exemplo que trataremos a seguir teremos algo como N=10*9*10=900 dados. Se executavel devolve arquivo com dados resulta interessante renomear os dados de saída, sejam eles o1.dat, o2.dat e o3.dat de forma a que expressem os dados de entrada, algo como: o1_i1_val1_i2_val2_i3_val3_.dat, aqui vali diz respeito ao valor utilizado para a entrada i. Nos últimos anos eu mudei essa estrategia, no lugar de nudar o nome de cada variável eu crio uma pasta com o nome dos dados e crio um link simbólico do executavel para dentro dessa pasta e mando a rodar desde ae dentro, assim quando o programa devolver os dados eles ficam na pasta certa. Qual dos métodos é melhor, cada um tem suas vantagens e desvantagens, eu vou mostrar aqui o II caso.

Dica Quente: como diz Forrest Gump: Shit happens, pode ser que você apague sem querer uma pasta lotada de dados e no Linux, como todo é arquivo, essa pode ser seu home inteiro. O melhor para evitar o transtorno futuro é ter backup dos dados, mas se isso não for possível porque você tem Teras e Teras de dados procure colocar na primeira fila de todos seus dados um comentário (que significa que a linha inicia com #) informando os parâmetros utilizados e outras informações que ache relevantes. Assim se tiver que utilizar algum desses programa de recuperação de dados (quando você percebe que fez caca - 2 seg depois do enter-, tire da tomada o computado - puxe o cabo mesmo, já que os dados não são apagados, na verdade a tabela de nome é que é apagada, por tanto deve evitar regravar qualquer coisa sobre o disco, isso implica o processos de desligar que grava the last state do sistema) saberá a que file esse arquivo faz referencia.

Primeiro vamos criar os dados com que o programa principal vai ser alimentado:


      #!/usr/bin/env python
      
      import os
      
      mv1 = []
      mv2 = []
      mv3 = []
      
      job = []
      chavesJob = ['v1', 'v2', 'v3', 'dirJob']
      
      v1i = 0.1
      v1f = 1.0
      dv1 = 0.1
      
      v2i = 0.3
      v2f = 0.7
      dv2 = 0.05
      
      v3i = 1.0
      v3f = 10.0
      dv3 = 1.0
      
      v1 = v1i
      while (v1 <= v1f):
        sv1 = str('%.2f' % v1)
        mv1.append(sv1)
        v1 = v1 + dv1
      
      v2 = v2i
      while (v2 <= v2f):
        sv2 = str('%.2f' % v2)
        mv2.append(sv2)
        v2 = v2 + dv2
      
      v3 = v3i
      while (v3 <= v3f):
        sv3 = str('%.1f' % v3)
        mv3.append(sv3)
        v3 = v3 + dv3
        
      print mv3
      

Vamos analisar está primeira parte. A linha 2 importa o módulo que trata com chamada ao sistema operacional, por isso o nome os. Nas linhas 5 - 7 são criadas os vetores (chamado de listas no Python) vazios, neles vamos armazenar os diferentes valores que cada uma das variáveis irá tomar. Os limites e incrementos das variáveis $v_1$, $V_2$ e $v_3$ são definidas entre as linhas 12 - 22. Entre as linhas 24 - 28 são calculados os valores que a variável $v_1$ adquire dada as condições ($v_{1i}$, $v_{1i}$ e $dv_{1}$), esse valores todos são armazenados na lista $v1$. Igualmente acontece para as variáveis $v_2$ (linhas 30 - 34) e a variável $v_3$ (linhas 36 - 40).


      #!/usr/bin/env python
      
      import os
      
      dirPai = os.getcwd()
      
      mv1 = []
      mv2 = []
      mv3 = []
      
      job = []
      chavesJob = ['v1', 'v2', 'v3', 'dirJob']
      
      v1i = 0.1
      v1f = 1.0
      dv1 = 0.1
      
      v2i = 0.3
      v2f = 0.7
      dv2 = 0.05
      
      v3i = 1.0
      v3f = 10.0
      dv3 = 1.0
      
      v1 = v1i
      while (v1 <= v1f):
        sv1 = str('%.2f' % v1)
        mv1.append(sv1)
        v1 = v1 + dv1
      
      v2 = v2i
      while (v2 <= v2f):
        sv2 = str('%.2f' % v2)
        mv2.append(sv2)
        v2 = v2 + dv2
      
      v3 = v3i
      while (v3 <= v3f):
        sv3 = str('%.1f' % v3)
        mv3.append(sv3)
        v3 = v3 + dv3
        
      
      
      for v1 in mv1:
        for v2 in mv2:
          for v3 in mv3:
      
            dirJob = 'resul_v1_' + v1 + '_v2_' + v2 + '_v3_' + v3
            dirJob = os.path.join(dirPai, dirJob)
        
            testFile = os.path.join(dirJob, 'terminoRodar.out')
            if (not(os.path.isfile(testFile))):
              val  = [v1, v2, v3, dirJob]
              job.append( dict( zip(chavesJob, val) ) )
      
      for i in range(len(job)):
        pastaOndeVaiRodar = job[i]['dirJob']
        v1                = job[i]['v1']
        v2                = job[i]['v2']
        v3                = job[i]['v3']
        print i, job[i]
        
        os.mkdir(pastaOndeVaiRodar)
        OriExecutavel = os.path.join(dirPai, 'nomeExecutavel')
        NewExecutavel = os.path.join(pastaOndeVaiRodar, 'nomeExecutavel')
        if not os.path.lexists(NewExecutavel):
          os.symlink(OriExecutavel, NewExecutavel)
        else
          os.remove(NewExecutavel)
          os.symlink(OriExecutavel, NewExecutavel)
      

que rodado até aqui dá:

[usuario@pclabfis: ]# python roda01.py
0 {'v1': '0.10', 'v2': '0.30', 'v3': '1.0', 'dirJob': 'resul_v1_0.10_v2_0.30_v3_1.0_.dat'}
1 {'v1': '0.10', 'v2': '0.30', 'v3': '2.0', 'dirJob': 'resul_v1_0.10_v2_0.30_v3_2.0_.dat'}
2 {'v1': '0.10', 'v2': '0.30', 'v3': '3.0', 'dirJob': 'resul_v1_0.10_v2_0.30_v3_3.0_.dat'}
.
.
.
797 {'v1': '1.00', 'v2': '0.65', 'v3': '8.0', 'dirJob': 'resul_v1_1.00_v2_0.65_v3_8.0_.dat'}
798 {'v1': '1.00', 'v2': '0.65', 'v3': '9.0', 'dirJob': 'resul_v1_1.00_v2_0.65_v3_9.0_.dat'}
799 {'v1': '1.00', 'v2': '0.65', 'v3': '10.0', 'dirJob': 'resul_v1_1.00_v2_0.65_v3_10.0_.dat'}

Note que na lina 5 foi colocado uma variável extra que guarda o diretório desde onde você mando rodar tudo. Entre as linhas 46 e 48 são definidos 3 loop onde se varre cada uma das listas em que estão definidos os valores assumidos pelas 3 variáveis utilizadas, com os diversos dados se monta o nome da pasta onde será rodado o programa com as variáveis utilizadas para definir o nome. O método os.path.join se utiliza para juntar de forma inteligente um certo caminho, por exemplo os.path.join("home", "usuario", "teste.dat") resulta em /home/usuario/teste.dat, no caso em análise estou pressupondo que quando o programa terminar de rodar ele vai criar um arquivo chamado terminoRodar.out, por isso na linha 53 é testada a existência deste arquivo, caso não exista então os valores das 3 variáveis e o nome da pasta são colocados todos em um dicionario, para isso primeiro definimos uma lista com os valores das variáveis e o nome da pasta a que juntamos a outra lista previamente definida (linha 10) mediante a utilização da função zip, dessa forma criamos uma lista de dicionários onde cada elemento da lista vai ter os diversos dados e o nome da pasta onde será rodado o programa.

Observe que o resultado de imprimir cada elemento da lista é um dicionario que possui todos as variáveis e o nome da pasta onde será colocado os resultados, por exemplo o item 2 da lista job (job[2]) é o dicionario {'v1': '0.10', 'v2': '0.30', 'v3': '3.0', 'dirJob': 'resul_v1_0.10_v2_0.30_v3_3.0_.dat'} o que significa que ao rodarmos esse job os dados serão armazenados na pasta resul_v1_0.10_v2_0.30_v3_3.0_.dat, e as variáveis v1, v2, v3 tomarão os valores: 0.10, 0.30, 3.0, respectivamente. Note que nas linhas 56-59 esses valores são atribuídos às variáveis com o nome adequado para não ter que digitar coisas como job[i]['v1'] e sim v1.

Das linhas 65 - 72 é colocado o programa na pasta certa. A linha 65 cria a pasta onde será rodado o programa. A linha 66 e 67 são os caminhos até a origem do programa e sua nova localização na pasta recém criada. Na linha 68 - 72 é criado um link simbólico (não é uma copia, simplesmente aponta para o arquivo original), note que se o link já existe ele apaga e faz de novo.

Neste ponto você está pronto para mandar rodar seu programa e o fim do script vai depender de onde você quer rodar isto é, se você quer rodar utilizando o sistema de filas do cluster que é o Torque ou se você quer rodar na sua maquina.

Rodando em uma maquina local vários programas

O problema aqui é complexo já que você quer rodar $n$ programas por vez e o script tem que ficar esperando a ver se algum desses $n$ programa termino para mandar o próximo. $n$ é o número de núcleos que possui o computador, tipicamente são 1, 2, 4, 6, 8, dependendo do modelo de processador, para saber quantos núcleos tem o computador digitamos no terminal:

[usuario@pclabfis: ]# lscpu | grep On-line
On-line CPU(s) list:   0-3

lscpu lista informações relativas ao processador que você possui, grep é um comando de linux que casa padrão, no caso o padrão é On-line, quando encontrado o padrão o grep permite a impressão daquela linha. Dessa forma em nosso script $n=4$.

Como o script não tem como saber quanto tempo dura cada um dos programas é necessário checar a ver se alguns dos $n$ programas terminou e assim mandar rodar outro, para isso acontecer devemos definir um período de checagem. Quanto tempo entre checagens deixamos, vai depender do nosso programa, o que você tem que saber é que esse tempo deve ser dado em segundos. No script abaixo o tempo foi colocado em 15 minutos (linha 7), supondo que o programa roda algo como uma hora, então seria razoável.

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-
      
      import os, shutil, os.path, time, subprocess

      nomeExecutavel = 'NomeDoExecutavel'
      tempoEntreProg = 900  #<---- Tempo entre checagens da situação
      numCores       = 4    #<---- Número de núcleos no processador
      
      dirPai = os.getcwd()
      chaves         = ['run', 'nome']
      
      mv1 = []
      mv2 = []
      mv3 = []
      
      job = []
      chavesJob = ['v1', 'v2', 'v3', 'dirJob']
      
      v1i = 0.1
      v1f = 1.0
      dv1 = 0.1
      
      v2i = 0.3
      v2f = 0.7
      dv2 = 0.05
      
      v3i = 1.0
      v3f = 10.0
      dv3 = 1.0
      
      v1 = v1i
      while (v1 <= v1f):
        sv1 = str('%.2f' % v1)
        mv1.append(sv1)
        v1 = v1 + dv1
      
      v2 = v2i
      while (v2 <= v2f):
        sv2 = str('%.2f' % v2)
        mv2.append(sv2)
        v2 = v2 + dv2
      
      v3 = v3i
      while (v3 <= v3f):
        sv3 = str('%.1f' % v3)
        mv3.append(sv3)
        v3 = v3 + dv3
        
      print mv3
      
      for v1 in mv1:
        for v2 in mv2:
          for v3 in mv3:
      
            dirJob = 'resul_v1_' + v1 + '_v2_' + v2 + '_v3_' + v3 
            dirJob = os.path.join(dirPai, dirJob)
        
            testFile = os.path.join(dirJob, 'terminoRodar')
            if (not(os.path.isfile(testFile))):
              val  = [v1, v2, v3, dirJob]
              job.append( dict( zip(chavesJob, val) ) )

      totalRodando = 0
      executando   = open('IniciouExecucao.txt', 'w')
      executado    = open('TerminouExecucao.txt', 'w')
      processo     =  []
            
      for i in range(len(job)):
        pastaOndeVaiRodar = job[i]['dirJob']
        v1                = job[i]['v1']
        v2                = job[i]['v2']
        v3                = job[i]['v3']
        
        if (totalRodando < numCores):
          if os.path.isdir(pastaOndeVaiRodar):
            shutil.rmtree(pastaOndeVaiRodar)
          os.mkdir(pastaOndeVaiRodar)
          OriExecutavel = os.path.join(dirPai, nomeExecutavel)
          NewExecutavel = os.path.join(pastaOndeVaiRodar, nomeExecutavel)
          if not os.path.lexists(NewExecutavel):
            os.symlink(OriExecutavel, NewExecutavel)
          else:
            os.remove(NewExecutavel)
            os.symlink(OriExecutavel, NewExecutavel)
            
          os.chdir(pastaOndeVaiRodar)
          
          dadosEntrada = open('dados.in', 'w')
          print >>dadosEntrada, v1, v2, v3
          dadosEntrada.close()
          
          comandoExterno = './' + nomeExecutavel
          rodada   = subprocess.Popen( [ comandoExterno ],\
             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          
          valores  = [rodada, job[i]['dirJob']]
          juntos   = dict(zip(chaves, valores))
          processo.append(juntos)
          
          print >>executando, job[i]['dirJob'],\
             time.asctime( time.localtime(time.time()))
          executando.flush()
          
          totalRodando = totalRodando + 1
                                          
          os.chdir(dirPai)                                    
          
        else:
          SaiDoLoop = False
          checa = 0
          while True:
            time.sleep(tempoEntreProg)
            for j in range(totalRodando):
              if processo[j]['run'].poll() is not None:
                SaiDoLoop    = True
                totalRodando = totalRodando - 1
                processo.pop(j)
                executado.flush()
                print >>executado, processo[j]['nome'],/
                  time.asctime( time.localtime(time.time()))
                executado.flush()
                break
            if (SaiDoLoop):
              break          
      

Este script não foi testado, o único que fiz foi ver se ele faz o que a gente deseja, rodar $n$ processos e se algum terminar colocar outro, a este tipo de script se lhes conhece como queue (fila) scripts, pois coloca em fila os processos que serão rodados. O coração do script será descrito a continuação:

O processo se repete para todos os job da lista.

Alimentando um programa com dados utilizando Python

Com bastante frequência criamos programas que perguntam no terminal pelos dados de entrada; a forma como esses dados são encaminhados para o programa depende de se desejamos ler como parâmetros do programa ou via a leitura do terminal, a seguir veremos como proceder em cada caso

Dados como parâmetros do programa

      Program teste
        implicit None

        Integer            :: i, ndados
        Real (kind = 8)    :: a
        Character (len=32) :: arg
        
        ndados = command_argument_count()
        print"('temos ', 1I0.2, ' dados de entrada')", ndados
        Do i = 1, ndados
          CALL get_command_argument(i, arg)
          Read(arg, *) a                   !converte de character para double
          print"('dado ', I0.2, ' = ', 1F9.6)", i, a
        End Do

      end Program teste
      
[usuario@pclabfis: ]# gfortran teste.f90 -o teste
[usuario@pclabfis: ]# ./teste 1.2 -1.3 1.4
temos 03 dados de entrada
dado 01 =  1.200000
dado 02 = -1.300000
dado 03 =  1.400000

Ou em C

      #include <stdlib.h>
      #include <stdio.h>
      #include <math.h>
      
      int main(int argc, char** argv){

        int i;
        double a;

        printf("temos %d dados de entrada\n", argc-1);

        for (i=1; i < argc; i++){
          //sscanf(argv[i], "%lf", &a);  //pode ser utilizado este
          //a = atof(argv[i]);           //ou este
          a = strtod(argv[i], NULL);     //as tres formas sao equivalentes
          printf("dado %d = %f\n", i, a);
        }

        return 0;

      }    
      
[usuario@pclabfis: ]# gcc teste.f90 -o teste
[usuario@pclabfis: ]# ./teste 1.2 -1.3 1.4
temos 3 dados de entrada
dado 1 = 1.200000
dado 2 = -1.300000
dado 3 = 1.400000

Em ambos dos caso poderiamos ter utilizado read para ler os dados, mas preferimos utilizar diretamente os dados como argumento do executável a fim de exemplificar este tipo de opção tanto de C como de fortran 2003. Em ambos dos casos os programas são muito simples, mas ilustra de forma magistral a situação acima definida. Nosso desejo é poder utilizar o python como alimentador desses programas

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-

      import numpy as np
      from subprocess import Popen, PIPE, STDOUT

      print 'Primeiro do jeito simples'

      a = '%f'%1.25324
      b = '%f'%-3.141516
      c = '%f'%np.exp(1.0)
      print 'dados de entrada = ', a, b, c

      programa = Popen(['./teste', a, b, c], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
      result = programa.communicate()[0]
      print result

      print 'Com um outro jeito eh utilizando split()'

      a = 1.25324
      b = -3.141516
      c = np.exp(1.0)

      executavel = './teste %f %f %f'%(a, b, c)
      print 'programa + dados de entrada = ', executavel.split()

      programa = Popen(executavel.split(), stdout=PIPE, stdin=PIPE, stderr=STDOUT)
      result = programa.communicate()[0]
      print result

      print 'Ou utilizando a opção shell=True'
      print 'programa + dados de entrada = ', executavel

      programa = Popen(executavel, stdout=PIPE, stdin=PIPE, stderr=STDOUT, shell=True)
      result = programa.communicate()[0]
      print result      
      
[usuario@pclabfis: ]# python teste.py
Primeiro do jeito simples
dados de entrada = 1.253240 -3.141516 2.718282
temos 03 dados
dado 01 =  1.253240
dado 02 = -3.141516
dado 03 =  2.718282

Com um outro jeito eh utilizando split()
programa + dados de entrada = ['./teste', '1.253240', '-3.141516', '2.718282']
temos 03 dados
dado 01 =  1.253240
dado 02 = -3.141516
dado 03 =  2.718282

Ou utilizando a opção shell=True
programa + dados de entrada = ./teste 1.253240 -3.141516 2.718282
temos 03 dados
dado 01 =  1.253240
dado 02 = -3.141516
dado 03 =  2.718282

Dados lidos desde o terminar (ou entrada padrão)

Nesse caso o programa em Fortran muda para

      Program teste
        implicit None

        Real (kind = 8) :: a, b, c
        
        print"('Digite os dados de entrada')"
        Read(*,*) a, b, c
        print"('Os dado lidos pelo fortran são', 3F10.6)", a, b, c

      end Program teste
      
[usuario@pclabfis: ]# gfortran teste2.f90 -o teste
[usuario@pclabfis: ]# ./teste
Digite os dados de entrada: 1.253240 -3.141516 2.718282
de entrada são: 1.253240 -3.141516 2.718282
Os dado lidos pelo fortran são 1.253240 -3.141516 2.718282

O script utilizado para alimentar os dados muda um pouco em relação ao anterior

      #!/usr/bin/env python
      #-*- coding: utf-8 -*-

      import numpy as np
      from subprocess import Popen, PIPE, STDOUT

      a = 1.25324
      b = -3.141516
      c = np.exp(1.0)

      dados_entrada = '%f %f %f'%(a, b, c)

      print 'os dados que o python envia são:', dados_entrada

      programa = Popen(['./teste'], stdout=PIPE, stdin=PIPE, stderr=STDOUT, shell=True)

      result = programa.communicate(input=dados_entrada)[0]

      print result      
      
[usuario@pclabfis: ]# pyton teste.py
os dados que o python envia são: 1.253240 -3.141516 2.718282

Digite os dados de entrada: Os dado lidos pelo fortran foram: 1.253240 -3.141516 2.718282