F2Py com matrizes de forma alocáveis ​​e assumidas

18

Eu gostaria de usar f2pycom o Fortran moderno. Em particular, estou tentando fazer o exemplo básico a seguir funcionar. Este é o menor exemplo útil que eu poderia gerar.

! alloc_test.f90
subroutine f(x, z)
  implicit none

! Argument Declarations !
  real*8, intent(in) ::  x(:)
  real*8, intent(out) :: z(:)

! Variable Declarations !
  real*8, allocatable :: y(:)
  integer :: n

! Variable Initializations !
  n = size(x)
  allocate(y(n))

! Statements !
  y(:) = 1.0
  z = x + y

  deallocate(y)
  return
end subroutine f

Observe que né deduzido da forma do parâmetro de entrada x. Observe que yé alocado e desalocado dentro do corpo da sub-rotina.

Quando eu compilar isso com f2py

f2py -c alloc_test.f90 -m alloc

E então execute em Python

from alloc import f
from numpy import ones
x = ones(5)
print f(x)

Estou tendo o erro a seguir

ValueError: failed to create intent(cache|hide)|optional array-- must have defined dimensions but got (-1,)

Então, eu vou criar e editar o pyfarquivo manualmente

f2py -h alloc_test.pyf -m alloc alloc_test.f90

Original

python module alloc ! in 
    interface  ! in :alloc
        subroutine f(x,z) ! in :alloc:alloc_test.f90
            real*8 dimension(:),intent(in) :: x
            real*8 dimension(:),intent(out) :: z
        end subroutine f
    end interface 
end python module alloc

Modificado

python module alloc ! in 
    interface  ! in :alloc
        subroutine f(x,z,n) ! in :alloc:alloc_test.f90
            integer, intent(in) :: n
            real*8 dimension(n),intent(in) :: x
            real*8 dimension(n),intent(out) :: z
        end subroutine f
    end interface 
end python module alloc

Agora ele roda, mas os valores da saída zsão sempre 0. Algumas impressões de depuração revelam que ntem o valor 0dentro da sub-rotina f. Suponho que estou perdendo alguma f2pymágica de cabeçalho para gerenciar essa situação corretamente.

De maneira geral, qual é a melhor maneira de vincular a sub-rotina acima ao Python? Eu preferiria fortemente não ter que modificar a própria sub-rotina.

MRocklin
fonte
Matt, você está familiarizado com o guia de melhores práticas do Ondrej Certik, especificamente, na seção Interface com o Python ? Temos vindo a debater uma questão de interface semelhante para PyClaw e não ter resolvido ainda neste ponto, quer :)
Aron Ahmadia

Respostas:

23

Não estou familiarizado com os elementos internos do f2py, mas estou familiarizado com o agrupamento do Fortran. O F2py apenas automatiza algumas ou todas as coisas abaixo.

  1. Você primeiro precisa exportar para C usando o módulo iso_c_binding, conforme descrito, por exemplo, aqui:

    http://fortran90.org/src/best-practices.html#interfacing-with-c

    Isenção de responsabilidade: Eu sou o principal autor das páginas de fortran90.org. Essa é a única maneira independente de plataforma e compilador de chamar o Fortran do C. Este é o F2003, portanto, atualmente, não há razão para usar qualquer outro meio.

  2. Você só pode exportar / chamar matrizes com o comprimento total especificado (forma explícita), ou seja:

    integer(c_int), intent(in) :: N
    real(c_double), intent(out) :: mesh(N)

    mas não assume forma:

    real(c_double), intent(out) :: mesh(:)

    Isso ocorre porque a linguagem C não suporta essas matrizes em si. Fala-se em incluir esse suporte no F2008 ou posterior (não tenho certeza), e a maneira como funcionaria é através de algumas estruturas de dados C de suporte, pois é necessário transportar informações de forma sobre a matriz.

    No Fortran, você deve usar principalmente a forma assumida, apenas em casos especiais você deve usar a forma explícita, conforme descrito aqui:

    http://fortran90.org/src/best-practices.html#arrays

    Isso significa que você precisa escrever um invólucro simples em torno da subrotina de forma assumida, que envolverá as coisas em matrizes de forma explícitas, conforme meu primeiro link acima.

  3. Depois de ter uma assinatura C, basta chamá-la de Python da maneira que quiser, eu uso o Cython, mas você pode usar ctype ou C / API manualmente.

  4. A deallocate(y)declaração não é necessária, o Fortran desaloca automaticamente.

    http://fortran90.org/src/best-practices.html#allocatable-arrays

  5. real*8não deve ser usado, mas real(dp):

    http://fortran90.org/src/best-practices.html#floating-point-numbers

  6. A instrução y(:) = 1.0está atribuindo 1.0 com precisão única, portanto o restante dos dígitos será aleatório! Esta é uma armadilha comum:

    http://fortran90.org/src/gotchas.html#floating-point-numbers

    Você precisa usar y(:) = 1.0_dp.

  7. Em vez de escrever y(:) = 1.0_dp, você pode simplesmente escrever y = 1, é isso. Você pode atribuir um número inteiro a um número de ponto flutuante sem perder a precisão e não precisa colocar o redundante (:)nele. Muito mais simples.

  8. Ao invés de

    y = 1
    z = x + y

    Apenas use

    z = x + 1

    e não se incomode com a ymatriz.

  9. Você não precisa da declaração "return" no final da sub-rotina.

  10. Finalmente, você provavelmente deve estar usando módulos, basta colocar implicit noneno nível do módulo e não precisará repeti-lo em cada sub-rotina.

    Caso contrário, parece bom para mim. Aqui está o código que segue as sugestões de 1 a 10 acima:

    module test
    use iso_c_binding, only: c_double, c_int
    implicit none
    integer, parameter :: dp=kind(0.d0)
    
    contains
    
    subroutine f(x, z)
    real(dp), intent(in) ::  x(:)
    real(dp), intent(out) :: z(:)
    z = x + 1
    end subroutine
    
    subroutine c_f(n, x, z) bind(c)
    integer(c_int), intent(in) :: n
    real(c_double), intent(in) ::  x(n)
    real(c_double), intent(out) :: z(n)
    call f(x, z)
    end subroutine
    
    end module

    Ele mostra a sub-rotina simplificada, bem como um wrapper C.

    No que diz respeito ao f2py, ele provavelmente tenta gravar esse wrapper para você e falha. Também não tenho certeza se ele está usando o iso_c_bindingmódulo. Por todas essas razões, prefiro embrulhar as coisas à mão. Então fica exatamente claro o que está acontecendo.

Ondřej Čertík
fonte
Tanto quanto sei, o f2py não depende de ligações ISO C (seu principal objetivo é o código Fortran 77 e Fortran 90).
Aron Ahmadia
Eu sabia que estava sendo um pouco burro, ymas queria que algo fosse alocado (meu código real tem alocações não triviais). Eu não sabia sobre muitos dos outros pontos. Parece que eu deveria procurar algum tipo de guia de práticas recomendadas do Fortran90 ... Obrigado pela resposta completa!
MRocklin
Observe que, usando os compiladores Fortran de hoje, você agrupa o F77 exatamente da mesma maneira --- escrevendo um wrapper iso_c_binding simples e chama a sub-rotina legada do f77.
Ondřej Čertík
6

Tudo o que você precisa fazer é o seguinte:

!alloc_test.f90
subroutine f(x, z, n)
  implicit none

! Argument Declarations !
  integer :: n
  real*8, intent(in) ::  x(n)
  real*8, intent(out) :: z(n)

! Variable Declarations !
  real*8, allocatable :: y(:)

! Variable Initializations !
  allocate(y(n))

! Statements !
  y(:) = 1.0
  z = x + y

  deallocate(y)
  return
end subroutine f

Embora o tamanho da matriz x e z agora seja passado como um argumento explícito, f2py torna o argumento n opcional. A seguir, é apresentada a docstring da função como ela aparece no python:

Type:       fortran
String Form:<fortran object>
Docstring:
f - Function signature:
  z = f(x,[n])
Required arguments:
  x : input rank-1 array('d') with bounds (n)
Optional arguments:
  n := len(x) input int
Return objects:
  z : rank-1 array('d') with bounds (n)

Importando e chamando-o de python:

from alloc import f
from numpy import ones
x = ones(5)
print f(x)

fornece a seguinte saída:

[ 2.  2.  2.  2.  2.]
Prometheous
fonte
Existe uma maneira de usar alguma expressão não trivial como tamanho? Por exemplo, eu passo ne quero obter uma matriz de tamanho 2 ** n. Até agora eu tenho que passar também 2 ** n como um argumento separado.
Alleo