Overview
The numerical method
The DNS solver uses the IB method presented by Luchini et al. (2025); its simplicity and performance are key in achieving the computational speed AMPHIBIOUS is capable of. The spatial discretization is based on second-order finite differences. Equations are written in primitive variables; time integration is fully explicit with a fractional-step approach; pressure correction is computed iteratively. Temporal discretization uses a Runge-Kutta three-stage temporal scheme. Implementing additional time-discretization schemes is straightforward. The method is second-order accurate in space, regardless of the complexity of the boundary; the temporal accuracy is preserved. The algorithm is simple, fast and extremely robust, thanks to the favorable stability properties inherited from the IB correction, applied implicitly both in space and time. The computational domain (i.e. where the Navier--Stokes equations are solved) is enclosed in a parallelepiped of dimensions referred to as the bounding box. Its dimensions are either assigned from input whenever a simple geometry is described analytically (as in the channel flow example), or computed automatically from the geometry itself, whenever STL files are used to describe the computational domain (as in the nose flow example).
The key property of the IB method is that only the central point of the Laplacian stencil is corrected, which brings in substantial code simplifications. The grid is Cartesian; the three velocity components are defined at the cell faces, and the pressure is defined at the cell center, as shown in figure below. Arbitrary grid spacing is allowed.
Boundary conditions (BC) are applied on the body or on planar surfaces which must be parallel to the faces of the bounding box. The set of available BC can be extended with little more than defining the new BC itself. More details are provided in the dedicated Section.
Parallelism, domain decomposition and cube pruning
All programs in AMPHIBIOUS run in parallel with MPI; the number of ranks and domain decomposition in every direction can be chosen with maximum flexibility. GPU computing is only available for solver, the only compute-intensive part.
The entire domain is discretized by preproc on a grid made by Nx x Ny x Nz mesh points. It is also decomposed into nDecompCubes = nCubesX x nCubesY x nCubesZ cubic subdomains. Each cube possesses halo points used to exchange data with neighboring cubes. Decomposing the domain into cubes is useful for large cases involving complex geometries; the procedure is inspired by Nakahasi & Kim (2004) and Jansson et al (2019).
A global file general-info.dat and several cube-specific files are written by preproc in the subfolder cubes/.
The file general-info.dat contains the value of nTotCubes, the number of non-empty cubes, as well as the list of their IDs.
Note that nTotCubes may be less than the total number nDecompCubes, depending on the complexity of geometry and the input parameters nCubesX, nCubesY, nCubesZ.
These parameters also determine the MPI preprocessor ranks. Multiple cubes may then be loaded at once by each MPI rank of the solver.
Therefore:
nDecompCubesis the total number of cubes after the initial domain decomposition;nTotCubes <= nDecompCubesis the total number of cubes left after the pruning process;nCubes <= nTotCubesis the total number of cubes loaded by one MPI rank.
The solver is built such that each MPI rank can be assigned one or more cubes. Therefore, the number of possible MPI ranks ranges from 1 to nTotCubes.
When using just one MPI rank, all non-pruned cubes are loaded by the rank; instead, when nTotCubes ranks are used, each rank loads one cube.
The two figures below provide examples where the optimal value for nDecompCubes is chosen by following a different rationale.

nCubesX=nCubesY=nCubesZ=5 cubes (center); no cubes are pruned (right) as the fluid domain and the bounding box are coincident: nDecompCubes=nTotCubes=125.
Bottom: cube-rank assignment, colors identify cubes loaded by each rank. At run time, different MPI ranks can be used, as for example 1 (left), 3 (center) or 5 (right).
nCubesX=nCubesY=nCubesZ=5 cubes (center); empty cubes are pruned (right), so that nDecompCubes=125 but nTotCubes=91.
Bottom: cube-rank assignment, colors identify cubes loaded by each rank. At run time, different MPI ranks can be used, as for example 1 (left), 3 (center) or 5 (right).
Since each cube may perform up to 26 communications, an excessive number of cubes may lead to a substantial overhead. A large number of cubes is therefore recommended only whenever a load-balancing advantage or significant memory savings are achieved via pruning.
Since solver does not alter files written by preproc, experimenting to find the optimal number of ranks for solver is convenient.
Running on GPUs
The compute-intensive solver supports both CPUs and GPUs. GPU programming is implemented using OpenACC directives, which appear in the source code as special comment lines.
When running on GPUs, all data structures are copied to GPU at startup, and then permanently reside there. The slow CPU-GPU communications are kept to a minimum: during a time step, such communications only involve single global variables (like e.g. the flow rate).
When running on multiple GPUs, GPU-aware MPI communications are used, and explicit CPU-GPU copying is once again avoided.
Since the performance degradation caused by multiple MPI ranks working on the same GPU can be substantial (up to 2x slowdown measured), in the multi-GPU case the number of MPI ranks must equal the number of GPUs (a warning is issued otherwise). Load balancing can be performed by properly assigning cubes to MPI ranks, therefore balancing GPU load is not required provided MPI ranks are balanced.
To enable GPU execution, one defines the macro TARGET_IS_GPU in the file headers.cpl. At compile time, the Makefile uses that macro to compile for the intended target.
How to set up a simulation
The simulation process is controlled by a single configuration file, settings.in. Data for one computational experiment are contained within a single directory, the case directory caseDir. The information required for a simulation is:
- a properly filled
settings.in, located in<caseDir>; - the geometrical description of the problem, that can be provided analytically, or through a set of STL files (which, for simple geometries, can be optionally created by hand).
The input file settings.in is organized as a list of independent sections, which can appear in arbitrary sequence. Variables inside a section, instead, must follow the same order as they are READ FROM by the program. An explanatory comment on each line of settings.in helps understanding the meaning of each variable.
The sections are as follows:
-
PREPROC:
sets the variablesNx,Ny,Nz(number of discretization points in each direction) andnCubesX,nCubesY,nCubesZ(number of cubes in each direction); -
NONUNIFORM_GRID:
whenever the Cartesian grid has non-uniform spacing, sets the law of variation of the grid step, independently for each of the three directions; -
ANALYTIC_GEOMETRY:
contains the description of a geometry given in analytic form (as in the channel flow example), in terms of box size, number of boundary planes and the corresponding boundary conditions. Alternatively, the sectionSTL_GEOMETRYcontains the number and names of the STL files which define the computational domain, each accompanied by the type of boundary condition; -
SOLVER:
sets fluid properties (e.g. the fluid viscositynu), as well as parameters for the temporal discretization (e.g. the time stepdt), and for the management of the simulation (e.g. the maximum simulation timeTmax); time step can be constant or computed after a CFL condition. The variablenItPressureCorrectionsets the number of iterations for the solution of the pressure equation. Physical quantities are expressed in dimensional SI units; -
HT:
sets properties relative to the heat transfer part of the problem, namelythermalDiffusivityandthermalConductivityRatio. The latter is only used when homogenized BCs are employed, and represent the ratio between the fluid and solid thermal conductivity where the interface problem is solved; -
LES:
switches between available LES models and allows to tune the models' constantCmand the Prandtl numberprandtlTurbfor the turbulence model; -
INPUT_OUTPUT:
controls whether and how a restart file is saved and used, as well as the creation of a database of flow fields. A set of first and second moments for velocity and pressure can also be computed. Since the full statistics can lead to large file sizes, a subset of statistics can be selected by filling the variablestatsNameswith the required fields: e.g.statsNames=um,p2only accumulates the mean of the velocity component and the variance of pressure fluctuations; -
PROBES:
controls writing to file time histories of virtual (0d, 1d or 2d) probes placed in the domain; the temporal and spatial frequencies can be regulated withwriteProbesEveryTstepsandspaceSampling; -
POSTPROC:
converts binary data accumulated during the simulation into three-dimensional VTK files for flow field(s) and (velocity and pressure) statistics. The latter can be computed and saved in double or simple precision to save disk space; averaging over homogeneous directions (when present) is also possible to get statistics in an ASCII file; -
a flow-specific section, e.g.
CHANNEL_FLOW:
contains information for the specific flow case; for the channel flow, for example, it contains the enforced value of the flow rate / pressure gradient in two of the three Cartesian directions.
Boundary conditions
The boundary conditions are specified in the *_GEOMETRY section of settings.in.
The infrastructure for boundary conditions is such that extensions and refinements are relatively easy. Five BC types are available: no-slip solid wall (w), periodic (p), in-plane moving wall (m), total pressure inlet (i) and total pressure outlet (o).
In settings.in, the number of boundaries must be first specified (either as nFaces or nStlFiles for the analytic and STL descriptions, respectively); then, each boundary condition must be prescribed with one line of the type:
type=<bcType> <bcSpec1>=value <bcSpec2>=value ...
where the descriptors <bcType> and <bcSpecs> are described in the following table:
| Boundary condition | bcType | bcSpecs | Notes |
|---|---|---|---|
| No-slip solid wall (isothermal, fixed flux or conjugate heat transfer) |
w |
bcT,bcTn,bcTthickness,bcTbase |
Velocity and Dirichlet-type for temperature are managed via the IB coefficients; Neumann-type temperature BC are handled with an optional NeumannData infrastructure |
| Periodic | p |
None | Must be prescribed in pairs |
| Plane wall with prescribed wall-parallel velocity | m |
vel1, vel2,strDistr1, a1, k1, omega1, strDistr2, a2, k2, omega2 |
None |
| Total pressure inlet | i |
bcP,bcT |
None |
| Total pressure outlet | o |
bcP |
Homogeneous Neumann is assumed for temperature if needed |
With STL geometry, one boundary condition applies to each STL. With analytical geometry, the faces of the bounding box are enumerated starting from , then , and then again for the and directions.
-
No-slip solid wall This is the standard solid wall, where no-penetration and no-slip boundary conditions are applied via the IB coefficients. The same applies to Dirichlet-type BC for temperature, set by
bcT. Constant heat flux is specified throughbcTn, whereasbcTthicknessandbcTbasespecify an approximate solution of the conjugate heat transfer problem. Note that, besides requesting the heat-transfer equation to be solved with the macroUSE_HT, whenever the normal derivative of temperature is involved the additionalWALLT_IS_NEUMANNmust be enabled. -
Periodic Periodic BC must be applied on one (or more) pairs of whole faces of the bounding box. The correct pairing is not explicitly checked.
-
Plane wall with prescribed wall-parallel velocity A plane wall can be given a non-zero velocity in the wall-parallel directions. The wall velocity can be spatially non-uniform, and time-dependent as well. The two wall-parallel directions in the Cartesian reference frame are referred to by the indices 1 and 2. The descriptors
vel1andvel2are constants that can be for example used to prescribe a Couette flow. Time- and space-dependency is limited to sinusoidal variations, with amplitudea1, a2, temporal frequencyomega1, omega2and spatial frequency (wavenumber)k1, k2. The booleanstrDistrprescribes whether the sinusoid is streamwise or spanwise. Hence, for a plane wall with normal withstrDistr2=YES, the expression: enforces for the spanwise2component a travelling wave along the streamwise direction. -
Inlet: total pressure At the inlet boundaries of the computational domain, total pressure is prescribed by assigning a value for the total pressure through the descriptor
bcP. Pressure, the velocity component normal to the boundary and the two components and parallel to the boundary are prescribed as follows:- : zero gradient;
Temperature, when present, inherits the same BC as velocity. Hence, a constant inlet temperature is specified via bcT
- Outlet: total pressure
At the outlet boundaries of the computational domain, total pressure is prescribed by assigning a value for the total pressure through the descriptor
bcP. Pressure and velocity components are prescribed in a way that depends on whether, locally, a backflow is present. At points with no backflow, the entire velocity has a zero-gradient condition, and for pressure . With backflow, pressure is instead set as . Temperature has a zero-gradient condition along the direction normal to the BC plane.
Compilation and dependencies
AMPHIBIOUS is built to be as self-contained as possible, yet some dependencies exist. The current stable version has been tested with the following software:
- the CPL compiler and its (few) dependencies
- a C compiler (current version tested with
gccv.12.2.0 and v.14.2.0) and thenvccompiler for Nvidia GPUs (current version tested withnvcv.23.11 and v.25.1) - MPI for parallel CPU computing (current version tested with
openmpiv.4.1.6 and v.5.0.7 andhpcx-mpiv.2.19); GPU-aware MPI is required for multiGPU computing - OpenACC >= v.2.6 for parallel (multi)GPU computing
Programs and utilities are compiled with the provided Makefile. The default compiler is gcc for the CPU target and nvc for the GPU target.
Setting the compilation MACROs
The program contains compile-time customization options:
-
several macros, which can be optionally set in the file
headers.cplby uncommenting the related#definelines; -
a flow-specific file, placed in the
modules-solver/folder, to be used whenever flow-specific operations insolverare required. To do so, the user mustUSEit insolver. In the two tutorial examples, the filespecifics-channel.cplmanages the screen output at runtime and the forcing term, whereasspecifics-nose.cplonly manages the screen output.
The following macros can be set:
| Name | Used by | Description |
|---|---|---|
GRID_IS_NONUNIFORM |
preproc |
Enables non-uniform Cartesian grid in the three directions (default is uniform) |
GEOMETRY_IS_ANALYTIC |
preproc |
Allows preprocessing of analytically-described geometries |
WALLT_IS_NEUMANN |
preproc |
Computes IB coefficients for a Neumann-type temperature BC on the wall |
TARGET_IS_GPU |
solver |
Sets GPU compile target (requires a C compiler supporting OpenACC directives) |
STATSIZE_IS_DOUBLE |
solver postproc |
Computes statistics in double precision (default is single precision) |
USE_LES |
solver postproc |
Toggles use of LES model, either at run time or to compute the field of in post-processing |
USE_HT |
solver postproc |
Switches on the equation for the passive scalar (heat transfer) |
Flow-specific files can be used by solver to customize its behavior. In the tutorials, the file specifics-channel.cpl manages the runtime screen output and the forcing term, and includes initial conditions for laminar Poiseuille and Couette flows, as well as a laminar Stokes layer profile on one wall, with optional deterministic and/or random perturbations. In specifics-nose.cpl, instead, only the screen output is specified.
When needed, flow-specific input can be assigned in a dedicated section in the settings.in file.
Any ad hoc routine defined in the flow-specific files is INLINEd.
By doing so, solver variables can be accessed even when still undefined (of course, they must be defined when the function is called).
The CPL deferred assignment operator == must be used. An example can be found in specifics-channel.cpl.
The case directory and its structure
preproc, solver and postproc all read and write files to/from the case folder <caseDir>, where they are organized in subfolders. Subfolders have constant names. The file names, instead, are as follows:
- a fixed part, which informs of the content of the file (e.g.
restart); - a variable part, which is unique for each cube, based on cube ID and the total number of cubes
nDecompCubes; - the extension of the file (e.g.
.vtkor.bin).
For example, by executing the program serially, the restart file is called restart.1of1.bin as only one cube is generated.
After the execution of postproc, the case directory is fully populated and presents the following structure:

reference-output.dat is only present in the tutorials).
The cube-specific files are made by two parts:
- an ASCII header, whose length of 8kB is set in
headers.cpl, which contains cube data and metadata, e.g. cube connectivity for thecube-datafiles, or the snapshot timestamp in therestartfiles - a binary chunk, which contains the saved data (e.g. IB coefficients, accumulated statistics, restart field, snapshots).
Binary data are written on file in the way they are organized in memory at run time, i.e. with SoA ordering (Structure of Arrays) in row-major order (as in C language: arrays are indexed with the most rapidly changing index last). The data structure is written to disk without discriminating between internal or external points. Not all data structures share the same size: IB coefficients, restart fields and snapshots data structures are defined also in the halo points of the cube, while statistics are not.
Performance
Performance analysis is still preliminary, and certain parts of the code do require substantial optimization. Nevertheless, the starting point in terms of raw computational performance is encouraging. The figure below illustrates the measured time, normalized for point and time (sub)step, for CPU and GPU execution, on a problems sized 10243 (strong scaling). The test hardware is the Leonardo Booster partition at CINECA. Each of the 3456 nodes has a single socket Intel Xeon Platinum 8358 with 32 cores, and four NVIDIA A100 GPUs with 64 GB RAM. The node has 512 GB DDR4 RAM; inter-node communications are serviced through NVIDIA Mellanox Infiniband with 200 Gbps and DragonFly+ topology. The GPUs in a node are interconnected through NVLink 3.0 for peer-to-peer memory transfers.

The CPU execution has a scalar speed of 55 x 10-9 seconds per point/step on one core; efficiency is gradually affected until the cores of the first node get fully used. After that, linear scaling is observed up to the maximum tested number of 1024 cores. The GPU execution has a single-GPU speed of 7.5 10-10 seconds per point/step: a case of size 10243 advances by one time step in 0.75 seconds. The multi-GPU scaling is approximately linear up to 32 GPUs, and then starts to drift gradually. Note how the memory efficiency allows us to run a relatively large case with over a billion points on a single core. In fact AMPHIBIOUS needs about 120 bytes per (internal) point, plus 4/8 additional bytes for each quantity for which statistics are accumulated.
Running a simulation
In the Tutorials section, simple cases are presented with comments and a step-by-step guide. Instructional videos are also available.
The typical workflow involves the sequential run of preproc, solver and possibly postproc, once the simulation is set up by preparing the settings.in file (and by providing STL files, when required).
Data are read from/written to files residing in a single user-defined directory, the case directory caseDir. Each program is executed as:
mpirun -n <N> <program> <caseDir>
where <program> is ./preproc, ./solver or ./postproc, N is the number of MPI ranks, and <caseDir> is where the settings.in file is located.
The execution model of AMPHIBIOUS assumes that source, macros and the settings file settings.in are changed wisely amid the execution of a case, i.e. between two steps of the execution pipeline preproc-solver-postproc.