fortran语言和python_如何在Fortran中调用Python
Python是機器學(xué)習(xí)領(lǐng)域不斷增長的通用語言。擁有一些非常棒的工具包,比如scikit-learn,tensorflow和pytorch。氣候模式通常是使用Fortran實現(xiàn)的。那么我們應(yīng)該將基于Python的機器學(xué)習(xí)遷移到Fortran模型中嗎?數(shù)據(jù)科學(xué)領(lǐng)域可能會利用HTTP API(比如Flask)封裝機器學(xué)習(xí)方法,但是HTTP在緊密耦合的系統(tǒng)(比如氣候模式)中效率太低。因此,可以選擇直接從Fortran中調(diào)用Python,直接通過RAM傳遞氣候模式的狀態(tài),而不是通過高延遲的通信層,比如HTTP。
有很多方法可以實現(xiàn)通過Python調(diào)用Fortran,但是從Fortran調(diào)用Python的方法卻很少。從Fortran調(diào)用Python,可以看作是將Python代碼嵌入到Fortran,但是Python的設(shè)計并不是像嵌入式語言Lua。可以通過以下三種方法實現(xiàn)從Fortran調(diào)用Python:
?Python的C語言API。這是最常用的方式,但需要實現(xiàn)大量的C封裝代碼。?基于Cython。Cython用于從Python中調(diào)用C語言,但也可以實現(xiàn)從C調(diào)用Python。?基于CFFI。CFFI提供了非常方便的方法可以嵌入Python代碼。
無論選擇哪種方法,用戶都需要將可執(zhí)行Fortran文件鏈接到系統(tǒng)Python庫,比如通過添加-lpython3.6到Fortran模式的Makefile文件。下面通過Hello World示例演示如何通過Fortran調(diào)用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
首先導(dǎo)入Fortran 2003內(nèi)部定義的和C語言類型互通的模塊iso_c_binding。但使用CFFI時,我們不需要寫任何C代碼,CFFI會生成C類型的打包接口。下一行則定義了一個C函數(shù)hello_world接口,這可以在C語言中實現(xiàn),但是這里我們使用Python和CFFI。最后,調(diào)用hello_world。
為了使用hello_world,我們需要構(gòu)建CFFI標(biāo)注,并保存在builder.py中,此代碼用于創(chuàng)建可以鏈接Fortran程序的動態(tài)庫: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)
?首先,我們導(dǎo)入cffi包,并且聲明了外部函數(shù)接口(FFI)對象。這看起來似乎比較奇怪,這只是CFFI實現(xiàn)這種目的的方式。下一步,header字符串中包含了需要調(diào)用的函數(shù)接口的定義。module字符串中包含了真正需要執(zhí)行的Python程序。裝飾器@ffi.def_extern用于標(biāo)記hello_world函數(shù)。my_plugin用于獲取ffi對象。def_extern裝飾器用于處理C類型,指針等。然后,ffibuilder.embedding_api(header)定義了API,embedding_init_code定義了Python代碼。
看起來比較奇怪的是在字符串中定義Python代碼,但CFFI需要以這種方式將Python代碼構(gòu)建為共享庫對象。ffibuilder.set_source來設(shè)置源代碼信息(?)。
然后執(zhí)行以下語句創(chuàng)建共享庫libplugin.dylib:python builder.py
然后使用下列命令編譯Fortran程序:gfortran -o test -L./ -lplugin test.f90
以上是在Mac OSX上創(chuàng)建的共享庫,如果在Linux上,共享庫應(yīng)該以.so結(jié)尾。如果一切沒有問題,那么就可以執(zhí)行文件了:./test
hello world
以上演示了如何使用CFFI從Fortran中調(diào)用Python程序,而不需要寫任何C程序。
FAQ
必須將所有Python代碼寫入header字符串嗎
不需要這樣。你可以直接在不同的Python模塊中定義Python代碼(比如my_module.py),然后在module字符串的開頭導(dǎo)入即可。比如,builder.py可以改為如下形式:...
module = """
from my_plugin import ffi
import my_module
@ffi.def_extern()
def hello_world():
my_module.some_function()
"""
...
這將在Python中使用可導(dǎo)入的形式使用Python程序。在添加到Fortran中之前,你也可以通過python -c "import my_module"測試一下。如果失敗了,你可能需要將包含my_module模塊的路徑添加到Python的sys.path變量中。
如何傳遞Fortran數(shù)組給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函數(shù)使用CFFI的ffi對象轉(zhuǎn)換指針ptr為給定形狀的numpy數(shù)組。可以使用如下形式在builder.py中的module字符串中調(diào)用:module = """
import my_module
@ffi.def_extern()
def add_one(a_ptr)
a = my_module.asarray(a)
a[:] += 1
"""
add_one也可以定義在my_module.py中。最后,我們需要定義與函數(shù)相關(guān)的頭文件信息,并且添加到builder.py的header字符串中:header = """
extern void add_one (double *);
"""
最后,在Fortran中以如下形式調(diào)用: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代碼塊,以及如何傳遞數(shù)組給Fortran或從Fortran傳遞數(shù)組給Python。然后,有些方面還是不太方便。
必須要在三個不同的區(qū)域定義python函數(shù)簽名嗎
任何要傳遞給Fortran的Python函數(shù),都必須要要在三個區(qū)域進行定義。
?首先,必須在header.h中進行C頭文件聲明?然后,執(zhí)行函數(shù)必須要在builder.py的module字符串中,或一個外部模塊中?最后,Fortran代碼中必須包含定義子程序的interface塊(接口塊)
這對于改變Python函數(shù)來說就顯得有些麻煩。比如,我們寫了一個Python函數(shù):def compute_precipitation(T, Q):
...
必須要在所有區(qū)域進行聲明。如果我們想添加一個垂直渦度W作為輸入?yún)?shù),我們必須要修改builder.py以及調(diào)用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是一個包含了函數(shù)需要的所有數(shù)據(jù)的Python字典。get和set函數(shù)的功能主要就是將Fortran數(shù)組傳遞給STATA或者從STATE中取出Fortran數(shù)組。如果這些函數(shù)使用了Fortran/CFFI封裝器,那么可以使用如下方式從Fortran中調(diào)用Python函數(shù)cumulus.compute_precipitation(state_dict):call set("Q", q)
call set("T", temperature)
call set("ahother_arg", 1)
call call_function("cumulus", "compute_precipitation")
call get("prec", prec)
如果需要傳遞更多的數(shù)據(jù)給compute_precipitation,那么需要添加更多的call set命令,這可能會改變Python函數(shù)的執(zhí)行。我們就不需要改變builder.py中的任何代碼。
結(jié)論
上面描述了如何傳遞Fortran數(shù)據(jù)給Python函數(shù),然后再獲取計算輸出。為了解決頻繁更改接口的問題,我們將fortran數(shù)據(jù)放到了Python模塊的字典中。通過調(diào)用給定的名稱來獲取數(shù)據(jù),并且將計算結(jié)果也存儲到相同的字段中,然后,Fortran代碼通過索引字典中正確的關(guān)鍵詞來獲取結(jié)果。Cython中使用了類似的架構(gòu),但CFFI更為方便。最重要的是,從C語言中調(diào)用Cython需要導(dǎo)入Python.h頭文件,還要運行Py_initialize和init_my_cython_module函數(shù)。然而,CFFI會在后臺完成這些操作。
這篇文章只是起到一個簡單的指示性作用,有很多問題都沒有討論,比如如何傳遞Fortran字符給Python。更多的代碼信息,見Github。
感興趣的也可以看一下Forpy[2]這個包。
References
[1]: https://www.noahbrenowitz.com/post/calling-fortran-from-python/
[2]: https://github.com/ylikx/forpy#using-forpy-with-anaconda
總結(jié)
以上是生活随笔為你收集整理的fortran语言和python_如何在Fortran中调用Python的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 类的知识点整理_Pytho
- 下一篇: python测试网络连通性_python