This guide explains how to create and use user-defined functions (UDFs) to extend the capabilities of the user interface beyond its default options.
In some cases, the built-in tools provided by Notus may not offer enough flexibility to compute or initialize fields using custom logic or to perform advanced post-processing. User-defined functions (UDFs) offer a powerful and flexible mechanism for implementing such custom behavior within your CFD simulations.
Notus allows the creation of UDFs for various purposes such as post-processing, field initialization (e.g., initial conditions, source terms, linear terms), and defining custom equations. You can create multiple UDFs grouped into a UDF library.
This tutorial walks through the process of creating and integrating UDFs into your project.
To illustrate UDF usage, we will develop a post-processing utility to compute the flow rate through a cross-section in a 2D periodic Poiseuille flow (see Figure 1). This feature is not currently supported by the post_processing
block of the user interface.
Follow these steps to set up the simulation and demonstrate UDF functionality:
Create a tutorial
directory at the root of the Notus project and add a poiseuille
subdirectory:
Create a file named poiseuille.nts
in the poiseuille
directory with the following content:
Ensure that Notus is compiled with MUMPS support (refer to How to Build Notus) and run the simulation:
If successful, the simulation should converge by the 29th iteration, and the log will end with lines indicating convergence.
UDFs must be written in Fortran and stored in an out-of-tree directory (i.e., outside the src
directory). To generate a UDF library, use the notus.py
script with the add-udf
tool. Run ./notus.py add-udf --help
to view full documentation:
This utility requires two arguments:
UDF_NAME
: the name of the UDF library. Note: The resulting directory name will be prefixed with udf_
, so specifying flow_rate
will create a directory named udf_flow_rate
.PATH
: the directory where the UDF library will be created.For post-processing, execute the following command from the Notus root directory:
If successful, you will see output confirming that the UDF files were created and instructions to compile and use the library (last two lines):
The generated directory structure will be:
Edit main.f90
only if you need to add a UDF type that wasn't initially enabled. You can safely ignore this file most of the time. Your main implementation will go into post_process.f90
.
To compile the library:
You should see output confirming that the build script have found the UDF library:
The resulting shared object file libflow_rate.so
will be placed in the udf_flow_rate
directory. To use it, add the following line to the poiseuille.nts
file before the domain
block:
Rerun the simulation:
You should see output confirming that Notus loaded the UDF library:
Before proceeding, note that since the post_process.f90
file is still empty, there is no other visible effect yet. We now need to implement the logic to compute the flow rate.
Edit the post_process()
subroutine in post_process.f90
as follows:
Recompile the UDF (no need to specify --udf
again, unless you have used the clean option -c
):
Run the simulation again and look for Flow rate:
messages in the log. At iteration 29, the flow rate should be approximately:
The expected flow rate (per unit of depth) for a Poiseuille flow is:
\[ Q = \frac{2}{3}\frac{f h^3}{\nu} \]
Where:
Height = 0.05
.Substituting the values gives \( Q = 10^{-3}\mathrm{m^2}\mathrm{s}^{-1}\).
This result matches the numerical flow rate obtained from Notus.
Currently, the utility works for a single MPI process and uses a hard-coded x-position. In future sections, we will demonstrate how to make it parallel and more flexible.
To enhance flexibility and parallel execution, we will replace the hard-coded x-index of the cross-section with a user-defined variable.
First, define a variable named iCrossSection
in the system
block of your .nts
file. This variable represents the x-index of the cross-section and will be exported so it can be accessed from the UDF:
You can declare and export variables anywhere in the .nts
file, but placing them near the UDF loading directive helps with readability and organization.
In Fortran, exported UI variables can be accessed using the variables_ui
module. Depending on the variable type, use one of the following functions:
ui_get_integer(name)
ui_get_double(name)
ui_get_boolean(name)
ui_get_string(name)
For our example, we use ui_get_integer("iCrossSection")
to retrieve the x-index.
To ensure the utility works in parallel:
variables_mpi
to check whether the cross-section lies within the local domain. is_global_physical_domain
and ie_global_physical_domain
store the bounds of the local domain.global_sum()
to gather results from all MPI processes.Update the post_process()
subroutine as follows:
You can now run the simulation with multiple MPI processes. If you increase the number of processes, you may also need to increase the number of grid cells in the x-direction to ensure load balancing.
Modify the utility so that the cross-section is defined by a physical x-coordinate rather than a cell index.
double
variable named xCrossSection
instead of iCrossSection
.ui_get_double("xCrossSection")
to retrieve the value.variables_cell_coordinates
module.variables_mpi
.Yes, as long as you provide the full path with the --udf
option when building.
Yes, simply load multiple libraries in the system
block of your .nts
file:
If both libraries implement the same UDF (e.g., post_process()
), they will be called in the order in which they are loaded.
The build system (via CMake) automatically detects UDFs placed in certain subdirectories of test_cases
. This helps streamline the build process for non-regression tests without requiring manual specification of each UDF path.