PyFR is written in Python3 and does not provide an interface, so we need to do it ourselves and transform it to call its internal functions in Julia. Last time we introduced an example of PyFR operation: two-dimensional Euler vortex. In this example, there is a step to convert the .msh file. How to call PyFR to achieve this step in Julia? We will start from here and finally realize that Julia calls PyFR to run a complete simulation. Before everything starts, it is assumed that you have installed Julia's PyCall package and read the readme document of PyCall's Github project.
Getting Started: How to call PyFR function to convert grid file
First, we have to install PyFR. This is not the Python virtual environment, but a normal environment, installing, executing, directly in the terminal: sudo apt install pyfr
.
After installation, you still need to switch to the euler_vortex_2d directory in the examples in the source code mentioned last time. Try to run: pyfr import euler_vortex_2d.msh euler_vortex_2d.pyfrm
to see if it runs normally.
The source code of the installed PyFR is /usr/local/lib/python3.7/dist-packages/pyfr/
in, open the __main__.py
file, there are several vital functions inside. _process_common
It is the most important solving function, but before you understand it, you need to learn how to call some simple functions. For example, we are going to talk about process_import
it now . It receives a parameter args, and uses the variables in args to perform several operations, and save the .msh file Convert to .pyfrm.
args is actually a Namespace, a special dictionary in python. We noticed that most __main__.py
of the functions inside take args as input parameters, so it is necessary to figure out what args contains. There is a main
function at the beginning of the file , which uses python's argparse package to customize terminal commands. Take a closer look, this code is also an excellent example for learning argparse. One line args = ap.parse_args()
is where args is created. We add a line below:
args = ap.parse_args()
print(args)
And save the file (permission required).
Then rerun in the terminal pyfr import euler_vortex_2d.msh euler_vortex_2d.pyfrm
, you will see the following output:
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)
This is the true face of args. When we run other PyFR commands such as pyfr run
, we will see that the output contained in this Namespace is different.
There seems to be no corresponding Namespace in Julia. Although you can call the argparse package with PyCall, you cannot use custom commands. So we should abandon the Namespace, decompose args into multiple variables, and pass them to the function to achieve the goal.
Specifically, we observe process_import
the statement and find that the args here has three variables: type, inmesh, and outmesh. The type can be []
, inmesh is the io stream created by python reading the .msh file, and outmesh is the file name of the converted file. So we rewrite the function as:
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
save. Open another file __init__.py
. Add two lines:
from pyfr.__main__ import process_import as pimp
__all__ = ['pimp']
This __init__.py
file is the export interface of the python package. Anything in this scope can be referenced externally. The first line we added imported the function in the __main__
file process_import
into the scope and named pimp
it so that it could be referenced externally. __all__
The variable is just a list of names used for prompting, and it can be omitted. save.
Now we are operating in Julia REPL. First open a new terminal in the euler_vortex_2d directory and enter the Julia REPL.
The first step is to call the necessary packages.
using PyCall
pyfr = pyimport("pyfr")
We use python's io package to create inmesh variables:
io = pyimport("io")
inmesh = io.open("euler_vortex_2d.msh","r",encoding="utf-8")
Declare a type and outmesh variable:
type = []
outmesh="euler_vortex_2d.pyfrm"
Call pimp
function (note the file path):
pyfr.pimp(type,inmesh,outmesh)
You can see the newly generated .pyfrm file in the folder, indicating that it has been completed.
Finally, we can check the list we just wrote to see what can be called:
julia> pyfr.__all__
1-element Array{
String,1}:
"pimp"
Tip 1: Note that there are two underscores before and after the __main__.py file. Sometimes there is one underscore, sometimes two, which is easy to misunderstand.
Tip 2: Every time the pyfr source file is modified, it will not be automatically updated in the python environment or Julia REPL environment. It needs to be re-imported. If it does not work, restart the environment.
Advanced: How to call the PyFR function to complete the simulation
With the above foundation, the rest will come naturally. We open the terminal in the euler_vortex_2d directory and run:
pyfr run -b openmp -p euler_vortex_2d.pyfrm euler_vortex_2d.ini
__main__.py
The sentence we wrote earlier print(args)
now makes a new contribution to us, revealing the pyfr run
content of args under the command:
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)
Among them, the three parameters of backend, cfg, and mesh are useful to us. Both backend and mesh are strings, and cfg is an io stream. Continuing the previous idea, we decompose args, and rewrite pyfr run
the two functions _process_common
and the implementation commands process_run
as:
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)
)
Simply put, just change the parameters. Note that there is a progress parameter here, which is generally set to true to display a simulation progress bar.
We call process_run
it externally , so __init__.py
write it in :
# -*- 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']
Finally, we call prun
to execute in Julia , might as well write a script:
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)
You should see a progress bar. Congratulations on your completion.