PyFR由Python3寫成,並且不提供接口,所以我們需要自己動手,改造一番,才能在Julia裏調用它的內部函數。上回我們介紹了PyFR的運行示例:二維歐拉渦。在該示例中,有一個轉換.msh文件的步驟。如何在Julia調用PyFR實現這個步驟呢?我們將從這裏開始,最終實現Julia調用PyFR運行完整仿真。一切開始前,假定你已經安裝了Julia的PyCall包,並且讀過PyCall的Github項目的readme文檔。
入門:如何調用PyFR函數轉換網格文件
首先,我們要安裝PyFR。這次不是在Python虛擬環境裏,而是正常環境裏安裝,直接在終端執行:sudo apt install pyfr
。
安裝後,仍然需要上回提到的源代碼中的examples,切換到其中的euler_vortex_2d目錄下。嘗試運行:pyfr import euler_vortex_2d.msh euler_vortex_2d.pyfrm
,看看是否正常運行。
被安裝的PyFR的源代碼在/usr/local/lib/python3.7/dist-packages/pyfr/
裏,打開__main__.py
文件,裏面有幾個至關重要的函數。_process_common
是最重要的求解函數,但在瞭解它之前,需要先學習怎麼調用一些簡單的函數,比如我們現在要講的process_import
,它接收一個參數args,並使用args中的變量執行若干操作,將.msh文件轉換爲.pyfrm。
args實際上是一個Namespace,是python中的一種特殊的字典。我們注意到,__main__.py
裏的函數多以args爲輸入參數,所以很有必要搞清楚args到底包含了什麼內容。文件開頭有個main
函數,利用python的argparse包來自定義終端命令。仔細看看,這段代碼也是學習argparse的優秀樣例。其中有一行args = ap.parse_args()
,正是創建args的地方。我們在下方添加一行:
args = ap.parse_args()
print(args)
並保存文件(需要權限)。
然後在終端裏重新運行pyfr import euler_vortex_2d.msh euler_vortex_2d.pyfrm
,會看到如下輸出:
Namespace(cmd='import', inmesh=<_io.TextIOWrapper name='euler_vortex_2d.msh' mode='r' encoding='UTF-8'>, outmesh='euler_vortex_2d.pyfrm', process=<function process_import at 0x7f4ab21be7a0>, type=None, verbose=None)
這就是args的真面目。當我們運行PyFR的其他命令如pyfr run
時,會看到輸出的這個Namespace裏包含的內容是不同的。
Julia裏貌似沒有對應的Namespace。用PyCall雖然可以調用argparse包,但無法使用自定義命令。所以我們應當捨棄Namespace,把args分解爲多個變量,傳給函數以達成目的。
具體來說,我們觀察process_import
裏的語句,發現此處的args有三個變量:type, inmesh, outmesh。其中type可以爲[]
,inmesh是python讀取.msh文件創建的io流,outmesh是轉換文件的文件名。於是我們把函數重新寫成:
def process_import(type,inmesh,outmesh):
# Get a suitable mesh reader instance
if type:
reader = get_reader_by_name(type, inmesh)
else:
extn = os.path.splitext(inmesh.name)[1]
reader = get_reader_by_extn(extn, inmesh)
# Get the mesh in the PyFR format
mesh = reader.to_pyfrm()
# Save to disk
with h5py.File(outmesh, 'w') as f:
for k, v in mesh.items():
f[k] = v
保存。打開另一個文件__init__.py
。添加兩行:
from pyfr.__main__ import process_import as pimp
__all__ = ['pimp']
這個__init__.py
文件就是python package的導出接口,凡是出現這個作用域裏的東西都可以被外部引用。我們添加的第一行把__main__
文件裏的process_import
函數導入該作用域並命名爲pimp
,這樣便可以在外部引用它了。__all__
變量只是一個提示用的名稱列表,可以不寫。保存。
現在我們在Julia REPL裏操作。首先在euler_vortex_2d目錄裏打開新的終端並進入Julia REPL。
第一步是調用必要的包。
using PyCall
pyfr = pyimport("pyfr")
我們用python的io包來創建inmesh變量:
io = pyimport("io")
inmesh = io.open("euler_vortex_2d.msh","r",encoding="utf-8")
聲明一個type和outmesh變量:
type = []
outmesh="euler_vortex_2d.pyfrm"
調用pimp
函數(注意文件路徑):
pyfr.pimp(type,inmesh,outmesh)
可以看到文件夾裏新生成的.pyfrm文件,表示已完成。
最後我們可以查看一下剛纔寫的那個列表,看看有哪些東西可調用:
julia> pyfr.__all__
1-element Array{String,1}:
"pimp"
小貼士一:注意__main__.py 文件前後都是兩條下劃線。有時下劃線是一條,有時是兩條,容易看錯。
小貼士二:每次修改pyfr源文件後,不會自動在python環境或Julia REPL環境裏更新,需要重新import,實在不行就重啓環境。
進階:如何調用PyFR函數完成仿真
有了以上基礎後,剩下的皆水到渠成。我們在euler_vortex_2d目錄裏打開終端,運行:
pyfr run -b openmp -p euler_vortex_2d.pyfrm euler_vortex_2d.ini
之前我們在__main__.py
裏寫的那句print(args)
此刻再立新功,爲我們揭示了pyfr run
命令下的args內容:
Namespace(backend='openmp', cfg=<_io.TextIOWrapper name='euler_vortex_2d.ini' mode='r' encoding='UTF-8'>, cmd='run', mesh='euler_vortex_2d.pyfrm', process=<function process_run at 0x7fafd342f680>, progress=True, verbose=None)
其中對我們有用的是backend、cfg、mesh三個參數。backend和mesh都是字符串,cfg是io流。延續之前的思路,我們把args分解,將實現pyfr run
命令的兩個函數_process_common
和process_run
改寫爲:
def _process_common(backend, progress, mesh, soln, cfg):
# Prefork to allow us to exec processes after MPI is initialised
if hasattr(os, 'fork'):
from pytools.prefork import enable_prefork
enable_prefork()
# Import but do not initialise MPI
from mpi4py import MPI
# Manually initialise MPI
MPI.Init()
# Ensure MPI is suitably cleaned up
register_finalize_handler()
# Create a backend
backend = get_backend(backend, cfg)
# Get the mapping from physical ranks to MPI ranks
rallocs = get_rank_allocation(mesh, cfg)
# Construct the solver
solver = get_solver(backend, rallocs, mesh, soln, cfg)
# If we are running interactively then create a progress bar
if progress and MPI.COMM_WORLD.rank == 0:
pb = ProgressBar(solver.tstart, solver.tcurr, solver.tend)
# Register a callback to update the bar after each step
callb = lambda intg: pb.advance_to(intg.tcurr)
solver.completed_step_handlers.append(callb)
# Execute!
solver.run()
# Finalise MPI
MPI.Finalize()
def process_run(backend, progress, mesh, cfg):
_process_common(
backend, progress, NativeReader(mesh), None, Inifile.load(cfg)
)
簡單地說,就是把參數改一改。注意到這裏有個progress參數,一般設爲true即可,用於顯示一個仿真的進度條。
我們在外部調用的是process_run
,所以在__init__.py
裏寫成:
# -*- coding: utf-8 -*-
from pyfr._version import __version__
from pyfr.__main__ import process_import as pimp
from pyfr.__main__ import process_run as prun
__all__ = ['pimp','prun']
最後,我們在Julia裏調用prun
來執行,不妨寫個腳本:
using PyCall
io = pyimport("io")
pyfr=pyimport("pyfr")
mesh = "euler_vortex_2d.pyfrm"
cfg = io.open("euler_vortex_2d.ini","r",encoding="utf-8")
# 運行仿真
pyfr.prun("openmp",true,mesh,cfg)
應該能看到進度條。恭喜你完成了。