具有可分配和假定形状数组的 F2Py

计算科学 Python 正则
2021-11-28 21:29:52

我想f2py与现代 Fortran 一起使用。特别是我试图让以下基本示例起作用。这是我能生成的最小的有用示例。

! 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

请注意,这n是从输入参数的形状推断出来的x请注意,y在子例程的主体内分配和释放。

当我编译这个f2py

f2py -c alloc_test.f90 -m alloc

然后在 Python 中运行

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

我收到以下错误

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

pyf所以我去手动创建和编辑文件

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

原来的

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

修改的

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

现在它运行了,但输出的值z总是0. 一些调试打印显示有子程序内n的值我认为我缺少一些标题魔法来正确管理这种情况。 0ff2py

更一般地说,将上述子例程链接到 Python 的最佳方法是什么?我强烈希望不必修改子程序本身。

4个回答

我对 f2py 内部结构不是很熟悉,但我对包装 Fortran 非常熟悉。F2py 只是自动化了下面的部分或全部事情。

  1. 您首先需要使用 iso_c_binding 模块导出到 C,如下所述:

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

    免责声明:我是 fortran90.org 页面的主要作者。这是从 C 调用 Fortran 的唯一平台和编译器独立方式。这是 F2003,所以现在没有理由使用任何其他方式。

  2. 您只能导出/调用指定全长(显式形状)的数组,即:

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

    但不采取形状:

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

    那是因为 C 语言本身不支持这样的数组。有人谈论在 F2008 或更高版本中包含这种支持(我不确定),它的工作方式是通过一些支持的 C 数据结构,因为您需要携带有关数组的形状信息。

    在 Fortran 中,您应该主要使用假设形状,只有在特殊情况下才应该使用显式形状,如下所述:

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

    这意味着,根据我上面的第一个链接,您需要围绕假设形状子例程编写一个简单的包装器,它将将事物包装到显式形状数组中。

  3. 一旦你有了 C 签名,就可以以任何你喜欢的方式从 Python 调用它,我使用 Cython,但你可以手动使用 ctype 或 C/API。

  4. deallocate(y)不需要该语句,Fortran 会自动解除分配。

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

  5. real*8不应该使用,而是real(dp)

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

  6. 该语句y(:) = 1.0以单精度分配 1.0,因此其余数字将是随机的!这是一个常见的陷阱:

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

    你需要使用y(:) = 1.0_dp.

  7. 不用写y(:) = 1.0_dp,你可以写y = 1,就是这样。您可以将整数分配给浮点数而不会失去准确性,并且您不需要将冗余(:)放在那里。简单得多。

  8. 代替

    y = 1
    z = x + y
    

    只需使用

    z = x + 1
    

    并且根本不用y阵列。

  9. 您不需要子例程末尾的“return”语句。

  10. 最后,您可能应该使用模块,并且只需放在implicit none模块级别,您不需要在每个子程序中重复它。

    否则对我来说看起来不错。以下是上述建议 1-10 之后的代码:

    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
    

    它显示了简化的子例程以及 C 包装器。

    就 f2py 而言,它可能会尝试为您编写此包装器并失败。我也不确定它是否正在使用该iso_c_binding模块。因此,出于所有这些原因,我更喜欢用手包装东西。然后很清楚发生了什么。

您所要做的就是:

!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

尽管数组 x 和 z 的大小现在作为显式参数传递,但 f2py 使参数 n 是可选的。以下是在 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)

从 python 导入和调用它:

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

给出以下输出:

[ 2.  2.  2.  2.  2.]

我在使用假定形状的虚拟阵列时遇到了同样的问题,Jonatan Ostrom (这里)的方法也对我有用。因为 f2py 为带有 的虚拟参数创建了一个新变量intent(out),所以这些参数似乎需要明确的形状信息。这与带有intent(in)intent(inout)或不带有意图的虚拟数组形成对比,后者将 Numpy ndarrays(在 Python 端创建)传递给 Fortran,因此 f2py 可能会为假定形状的数组创建具有足够形状信息的包装器。

我的代码.f90:

module fmod
    implicit none
contains

!! Use assumed-shape dummy args.
subroutine fsub( a_in, a_inout, a_out, a_none )

    real(8), intent(in)    :: a_in(:)
    real(8), intent(inout) :: a_inout(:)

    real(8), intent(out)   :: a_out( size(a_in) )  !! OK
    !real(8), intent(out)  :: a_out(:)             !! fails

    real(8)                :: a_none(:)   !! no intent

    print *
    print *, "fort| setting 777 to a_out(:)"
    a_out(:) = 777

    print *
    print *, "fort| a_in    = ", a_in
    print *, "fort| a_inout = ", a_inout
    print *, "fort| a_out   = ", a_out
    print *, "fort| a_none  = ", a_none

    print *
    print *, "fort| multiplying a_none(:) by 100"
    a_none(:) = a_none(:) * 100
end

end module

主要.py:

import numpy as np

# Load a Python module created by f2py.
import pymod

# Show info.
print( pymod.__doc__ )
print( pymod.fmod.__doc__ )

# Prepare arrays on the Python side.
b_in    = np.ones( 3, dtype='d' ) * 1.0
b_inout = np.ones( 3, dtype='d' ) * 2.0
b_none  = np.ones( 3, dtype='d' ) * 3.0

print( "\nBefore:" )
print( "py| b_in    = ", b_in )
print( "py| b_inout = ", b_inout )
print( "py| b_none  = ", b_none )

# Call Fortran.
b_out = pymod.fmod.fsub( a_in = b_in,
                         a_inout = b_inout,
                         a_none = b_none )

print( "\nAfter:" )
print( "py| b_in    = ", b_in )
print( "py| b_inout = ", b_inout )
print( "py| b_out   = ", b_out )
print( "py| b_none  = ", b_none )

构建(f2py):

python3.8 -m numpy.f2py -c mycode.f90 -m pymod

跑:

$ py main.py

This module 'pymod' is auto-generated with f2py (version:2).
Functions:
Fortran 90/95 modules:
  fmod --- fsub().
a_out = fsub(a_in,a_inout,a_none)

Wrapper for ``fsub``.

Parameters
----------
a_in : input rank-1 array('d') with bounds (f2py_a_in_d0)
a_inout : in/output rank-1 array('d') with bounds (f2py_a_inout_d0)
a_none : input rank-1 array('d') with bounds (f2py_a_none_d0)

Returns
-------
a_out : rank-1 array('d') with bounds (size(a_in))

Before:
py| b_in    =  [1. 1. 1.]
py| b_inout =  [2. 2. 2.]
py| b_none  =  [3. 3. 3.]

 fort| setting 777 to a_out(:)

 fort| a_in    =    1.0000000000000000        1.0000000000000000        1.0000000000000000     
 fort| a_inout =    2.0000000000000000        2.0000000000000000        2.0000000000000000     
 fort| a_out   =    777.00000000000000        777.00000000000000        777.00000000000000     
 fort| a_none  =    3.0000000000000000        3.0000000000000000        3.0000000000000000     

 fort| multiplying a_none(:) by 100

After:
py| b_in    =  [1. 1. 1.]
py| b_inout =  [2. 2. 2.]
py| b_out   =  [777. 777. 777.]
py| b_none  =  [300. 300. 300.]

我不认为接受的答案实际上回答了这个问题。分配很好。唯一的问题是输出被假定为形状。这是不可能的,因为没有从 Python 传递的输出数组,Fortran 例程可以从中获取形状。所以改变

real*8, intent(out) :: z(:)

进入

real*8, intent(out) :: z(size(x))