相关文章推荐
开朗的皮带  ·  FORTRAN语言的字符串处理_fortra ...·  3 月前    · 
才高八斗的橙子  ·  海岬的迷途之家(日本2021年川面真也执导的 ...·  2 年前    · 
坐怀不乱的乌冬面  ·  广东省公安厅幼儿院:适性而育浸润成长_广东省 ...·  2 年前    · 
性感的玉米  ·  西方众神关系为何很乱? ...·  2 年前    · 
鼻子大的红酒  ·  第10话 - 江湖人很忙 - 包子漫画·  2 年前    · 
可爱的红烧肉  ·  凭岚听鬼剑_百度百科·  2 年前    · 
Code  ›  如何在Fortran中调用Python开发者社区
python python函数 函数调用 fortran
https://cloud.tencent.com/developer/article/1618235
迷茫的马克杯
2 年前
作者头像
bugsuse
0 篇文章

如何在Fortran中调用Python

前往专栏
腾讯云
备案 控制台
开发者社区
学习
实践
活动
专区
工具
TVP
文章/答案/技术大牛
写文章
社区首页 > 专栏 > 气象杂货铺 > 正文

如何在Fortran中调用Python

发布 于 2020-04-21 16:30:52
3.6K 0
举报

Python是机器学习领域不断增长的通用语言。拥有一些非常棒的工具包,比如scikit-learn,tensorflow和pytorch。气候模式通常是使用Fortran实现的。那么我们应该将基于Python的机器学习迁移到Fortran模型中吗?数据科学领域可能会利用HTTP API(比如Flask)封装机器学习方法,但是HTTP在紧密耦合的系统(比如气候模式)中效率太低。因此,可以选择直接从Fortran中调用Python,直接通过RAM传递气候模式的状态,而不是通过高延迟的通信层,比如HTTP。

有很多方法可以实现通过Python调用Fortran,但是从Fortran调用Python的方法却很少。从Fortran调用Python,可以看作是将Python代码嵌入到Fortran,但是Python的设计并不是像嵌入式语言Lua。可以通过以下三种方法实现从Fortran调用Python:

•Python的C语言API。这是最常用的方式,但需要实现大量的C封装代码。•基于Cython。Cython用于从Python中调用C语言,但也可以实现从C调用Python。•基于CFFI。CFFI提供了非常方便的方法可以嵌入Python代码。

无论选择哪种方法,用户都需要将可执行Fortran文件链接到系统Python库,比如通过添加-lpython3.6到Fortran模式的Makefile文件。下面通过Hello World示例演示如何通过Fortran调用Python。Fortran代码保存在test.f90文件,如下:

! test.f90
program call_python
  use, intrinsic :: iso_c_binding
  implicit none
  interface
     subroutine hello_world() bind (c)
     end subroutine hello_world
  end interface
  call hello_world()
end program call_python

首先导入Fortran 2003内部定义的和C语言类型互通的模块iso_c_binding。但使用CFFI时,我们不需要写任何C代码,CFFI会生成C类型的打包接口。下一行则定义了一个C函数hello_world接口,这可以在C语言中实现,但是这里我们使用Python和CFFI。最后,调用hello_world。

为了使用hello_world,我们需要构建CFFI标注,并保存在builder.py中,此代码用于创建可以链接Fortran程序的动态库:

import cffi
ffibuilder = cffi.FFI()
header = """
extern void hello_world(void);
module = """
from my_plugin import ffi
import numpy as np
@ffi.def_extern()
def hello_world():
    print("Hello World!")
with open("plugin.h", "w") as f:
    f.write(header)
ffibuilder.embedding_api(header)
ffibuilder.set_source("my_plugin", r'''
    #include "plugin.h"
ffibuilder.embedding_init_code(module)
ffibuilder.compile(target="libplugin.dylib", verbose=True)

‍首先,我们导入cffi包,并且声明了外部函数接口(FFI)对象。这看起来似乎比较奇怪,这只是CFFI实现这种目的的方式。下一步,header字符串中包含了需要调用的函数接口的定义。module字符串中包含了真正需要执行的Python程序。装饰器@ffi.def_extern用于标记hello_world函数。my_plugin用于获取ffi对象。def_extern装饰器用于处理C类型,指针等。然后,ffibuilder.embedding_api(header)定义了API,embedding_init_code定义了Python代码。

看起来比较奇怪的是在字符串中定义Python代码,但CFFI需要以这种方式将Python代码构建为共享库对象。ffibuilder.set_source来设置源代码信息(?)。

然后执行以下语句创建共享库libplugin.dylib:

python builder.py

然后使用下列命令编译Fortran程序:

gfortran -o test -L./ -lplugin test.f90

以上是在Mac OSX上创建的共享库,如果在Linux上,共享库应该以.so结尾。如果一切没有问题,那么就可以执行文件了:

./test
hello world

以上演示了如何使用CFFI从Fortran中调用Python程序,而不需要写任何C程序。

FAQ

必须将所有Python代码写入header字符串吗

不需要这样。你可以直接在不同的Python模块中定义Python代码(比如my_module.py),然后在module字符串的开头导入即可。比如,builder.py可以改为如下形式:

...
module = """
from my_plugin import ffi
import my_module
@ffi.def_extern()
def hello_world():
    my_module.some_function()
...

这将在Python中使用可导入的形式使用Python程序。在添加到Fortran中之前,你也可以通过python -c "import my_module"测试一下。如果失败了,你可能需要将包含my_module模块的路径添加到Python的sys.path变量中。

如何传递Fortran数组给Python

stack overflow page回答了此问题。下面是一个示例,将代码定义在一个模块文件中,比如my_module.py:

# my_module.py
# Create the dictionary mapping ctypes to np dtypes.
ctype2dtype = {}
# Integer types
for prefix in ('int', 'uint'):
    for log_bytes in range(4):
        ctype = '%s%d_t' % (prefix, 8 * (2**log_bytes))
        dtype = '%s%d' % (prefix[0], 2**log_bytes)
        # print( ctype )
        # print( dtype )
        ctype2dtype[ctype] = np.dtype(dtype)
# Floating point types
ctype2dtype['float'] = np.dtype('f4')
ctype2dtype['double'] = np.dtype('f8')
def asarray(ffi, ptr, shape, **kwargs):
    length = np.prod(shape)
    # Get the canonical C type of the elements of ptr as a string.
    T = ffi.getctype(ffi.typeof(ptr).item)
    # print( T )
    # print( ffi.sizeof( T ) )
    if T not in ctype2dtype:
        raise RuntimeError("Cannot create an array for element type: %s" % T)
    a = np.frombuffer(ffi.buffer(ptr, length * ffi.sizeof(T)), ctype2dtype[T])\
          .reshape(shape, **kwargs)
    return a

asarray函数使用CFFI的ffi对象转换指针ptr为给定形状的numpy数组。可以使用如下形式在builder.py中的module字符串中调用:

module = """
import my_module
@ffi.def_extern()
def add_one(a_ptr)
    a = my_module.asarray(a)
    a[:] += 1
"""

add_one也可以定义在my_module.py中。最后,我们需要定义与函数相关的头文件信息,并且添加到builder.py的header字符串中:

header  = """
extern void add_one (double *);
"""

最后,在Fortran中以如下形式调用:

program call_python
  use, intrinsic :: iso_c_binding
  implicit none
  interface
    subroutine add_one(x_c, n) bind (c)
        use iso_c_binding
        integer(c_int) :: n
        real(c_double) :: x_c(n)
    end subroutine add_one
  end interface
  real(c_double) :: x(10)
  print *, x
  call add_one(x, size(x))
  print *, x
end program call_python

这一部分,我们介绍了如何在Fortran中嵌入Python代码块,以及如何传递数组给Fortran或从Fortran传递数组给Python。然后,有些方面还是不太方便。

必须要在三个不同的区域定义python函数签名吗

任何要传递给Fortran的Python函数,都必须要要在三个区域进行定义。

•首先,必须在header.h中进行C头文件声明•然后,执行函数必须要在builder.py的module字符串中,或一个外部模块中•最后,Fortran代码中必须包含定义子程序的interface块(接口块)

这对于改变Python函数来说就显得有些麻烦。比如,我们写了一个Python函数:

def compute_precipitation(T, Q):
    ...

必须要在所有区域进行声明。如果我们想添加一个垂直涡度W作为输入参数,我们必须要修改builder.py以及调用Fortran的程序。显而易见,对于大的工程来说,这就变得极为麻烦。

对于一般通信而言,采用了一小部分fortran/python代码封装器。主要依赖于一个外部python模块:

# module.py
import imp
STATE = {}
def get(key, c_ptr, shape, ffi):
    """Copy the numpy array stored in STATE[key] to a pointer"""
    # wrap pointer in numpy array
    fortran_arr = asarray(ffi, c_ptr, shape)
    # update the numpy array in place
    fortran_arr[:] = STATE[key]
def set(key, c_ptr, shape, ffi):
    """Call python"""
    STATE[key] = asarray(ffi, c_ptr, shape).copy()
def call_function(module_name, function_name):
    # import the python module
    import importlib
    mod = importlib.import_module(module_name)
    # the function we want to call
    fun = getattr(mod, function_name)
    # call the function
    # this function can edit STATE inplace
    fun(STATE)

全局变量STATE是一个包含了函数需要的所有数据的Python字典。get和set函数的功能主要就是将Fortran数组传递给STATA或者从STATE中取出Fortran数组。如果这些函数使用了Fortran/CFFI封装器,那么可以使用如下方式从Fortran中调用Python函数cumulus.compute_precipitation(state_dict):

call set("Q", q)
call set("T", temperature)
 
推荐文章
开朗的皮带  ·  FORTRAN语言的字符串处理_fortran 格式化字符串
3 月前
才高八斗的橙子  ·  海岬的迷途之家(日本2021年川面真也执导的动画电影)_百度百科
2 年前
坐怀不乱的乌冬面  ·  广东省公安厅幼儿院:适性而育浸润成长_广东省教育厅网站
2 年前
性感的玉米  ·  西方众神关系为何很乱? 他们到底是不是救世神? 东方神又如何?
2 年前
鼻子大的红酒  ·  第10话 - 江湖人很忙 - 包子漫画
2 年前
可爱的红烧肉  ·  凭岚听鬼剑_百度百科
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号