Python调用Fortran复杂模型:实现内存驻留式优化器交互问询
问题
我有一个独立的Fortran复杂模型,可读取输入文件、分配内存、初始化、求解非线性方程、输出结果并释放内存。目前已通过以下Python代码实现单次调用该模型:
import sys import ctypes as ct lib = ct.CDLL('prog.dll') f = getattr(lib,'M_EXFUN_mp_EXFUN') f.argtypes = [ct.c_char_p,ct.c_int] f.restype = None x = b'x ' + sys.argv[1].encode('UTF-8') + b' debug=yes' f(x,len(x))
现在需要用Python优化器将该模型作为目标函数反复调用,预期Python代码逻辑如下:
def fun(x): return f(x) x0 = [2.0, 0.0] res = minimize(fun, x0)
其中minimize为Python库提供的优化函数。由于每次调用完整模型会重复初始化并释放内存,效率极低,我希望先运行Fortran模型完成内存分配与参数初始化后暂停,返回控制权给Python,再由Python反复调用Fortran内的目标函数计算逻辑。
附简化示例代码:
Fortran主模块main2_mod(负责内存分配与参数初始化):
module main2_mod contains !dec$ attributes dllexport :: main2 subroutine main2(cc) use objfun_mod implicit none character(*), intent(in) :: cc integer :: i, n real(8) :: f real(8), allocatable :: x(:), par(:) n = 5 allocate(x(n),par(n)) x(:) = (/(i,i=1,n)/) par(:) = (/(i**0.5,i=1,n)/) call objfun(n,x,par,f) write(6,'(<n>f6.1,f6.1)') (x(i),i=1,n), f deallocate(x) deallocate(par) return end end module
目标函数模块objfun_mod:
module objfun_mod contains !dec$ attributes dllexport :: objfun subroutine objfun(n,x,par,f) implicit none integer, intent(in) :: n real(8), intent(in) :: x(n) real(8), intent(in) :: par(n) real(8), intent(out) :: f integer :: i real(8) :: ff ff = 0.0 do i = 1, n ff = ff + x(i)*par(i) enddo f = ff return end end module
当前Python调用main2的代码:
# for ctypes import ctypes as ct # load the DLL lib = ct.CDLL('main2.dll') # find entry point for function to be called f = getattr(lib,'MAIN2_MOD_mp_MAIN2') # specify its argument types f.argtypes = [ct.c_char_p,ct.c_int] # specify return type f.restype = None # set up the argument to be sent to the function x = b'x ' # run the function with commandline (x) as an argument f(x,len(x))
现需调整实现:先调用main2完成初始化但不释放内存,再让Python优化器反复调用objfun计算目标函数值,询问该方案是否可行,是否需要回调机制及具体实现方式。
解决方案
方案可行性
完全可行,不需要复杂的回调机制。核心是利用Fortran模块变量的持久化特性——模块内定义的变量在DLL加载后会一直存在,直到DLL卸载或显式释放,刚好可以存储初始化后的内存和参数。
具体实现步骤
1. 修改Fortran代码
调整模块结构,把需要保留的初始化变量放到模块全局作用域,新增三个导出接口:
initialize: 完成内存分配、参数初始化(替代原main2的初始化逻辑,不释放内存)compute_objfun: 接收Python传入的x,利用已初始化的par计算目标函数值cleanup: 显式释放内存,避免泄漏
修改后的main2_mod:
module main2_mod ! 持久化变量:模块级变量会在DLL生命周期内保留 integer, save :: n real(8), allocatable, save :: par(:) contains !dec$ attributes dllexport :: initialize subroutine initialize(cc) implicit none character(*), intent(in) :: cc integer :: i n = 5 allocate(par(n)) par(:) = (/(i**0.5,i=1,n)/) ! 这里可保留原有的输入文件读取、其他初始化逻辑 return end subroutine initialize !dec$ attributes dllexport :: compute_objfun subroutine compute_objfun(x, f) use objfun_mod implicit none real(8), intent(in) :: x(n) ! 直接使用模块全局的n real(8), intent(out) :: f call objfun(n, x, par, f) return end subroutine compute_objfun !dec$ attributes dllexport :: cleanup subroutine cleanup() implicit none if(allocated(par)) deallocate(par) n = 0 return end subroutine cleanup end module main2_mod
objfun_mod可保持不变,也可以合并到main2_mod中。
2. 修改Python调用代码
先调用initialize完成初始化,在优化器目标函数里反复调用compute_objfun,最后调用cleanup释放内存:
import ctypes as ct from scipy.optimize import minimize # 加载DLL lib = ct.CDLL('main2.dll') # 初始化接口配置 initialize = getattr(lib, 'MAIN2_MOD_mp_INITIALIZE') initialize.argtypes = [ct.c_char_p, ct.c_int] initialize.restype = None # 计算目标函数接口配置 compute_objfun = getattr(lib, 'MAIN2_MOD_mp_COMPUTE_OBJFUN') compute_objfun.argtypes = [ct.POINTER(ct.c_double), ct.POINTER(ct.c_double)] compute_objfun.restype = None # 清理接口配置 cleanup = getattr(lib, 'MAIN2_MOD_mp_CLEANUP') cleanup.argtypes = [] cleanup.restype = None # 第一步:执行初始化 init_args = b'x ' # 原有的输入参数 initialize(init_args, len(init_args)) # 定义优化器的目标函数 def fun(x): x_arr = (ct.c_double * len(x))(*x) f = ct.c_double(0.0) compute_objfun(x_arr, ct.byref(f)) return f.value # 第二步:执行优化(x0维度需和Fortran的n一致) x0 = [2.0, 0.0, 3.0, 4.0, 5.0] res = minimize(fun, x0, method='nelder-mead') print(res) # 第三步:显式清理内存 cleanup()
关键注意事项
- Fortran模块变量线程不安全,多线程优化场景需额外处理
- 必须显式调用
cleanup,否则DLL卸载时可能导致内存泄漏 - 确保Python传入的
x维度与Fortran初始化的n完全一致,避免越界访问 - 若需动态调整
n,可在initialize接口中接收参数并设置全局n
内容的提问来源于stack exchange,提问作者Adrian
相关产品推荐
相关产品推荐

