Julia調用PyFR

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_commonprocess_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)

應該能看到進度條。恭喜你完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章