Introduction
OpenFOAM is an incredibly versatile open-source CFD toolbox that allows you to modify existing solvers or create entirely new ones. In this tutorial, I’ll walk you through the steps of taking an existing solver (foamRun introduced in OpenFOAM 12), duplicating it, renaming it, and adding a scalar transport equation. This process will help you understand the solver structure, the compilation process, and how to integrate new fields into your own custom solver. Prerequisites are:
- OpenFOAM 12 installed and sourced properly.
- A basic understanding of the OpenFOAM directory structure and C++ code organization.
- Familiarity with the standard OpenFOAM solvers and how they operate.
1. Setting Up Your Working Environment
Starting from the OpenFOAM 12 root directory, navigate to the solver directory:
cd $WM_PROJECT_DIR/applications/solvers
Here, you’ll find various solver families. We’ll work with a solver called foamRun. First, copy the foamRun solver into a directory of your choice where you can make modifications:
cp -r foamRun $FOAM_USER_APPBIN/myFoamRunSolver
This creates a copy that you can customize without affecting the system-level installations.
2. Understanding the Make Folder
Inside your newly copied solver folder, you’ll see a Make directory containing two files: files and options.
“files” file:
This file lists which source files should be compiled into the final executable and what the resulting executable name should be. It typically includes .C files and may contain directives that specify linking rules.
Currently, it looks something like this (from the original solver):
EXE = $(FOAM_APPBIN)/foamRun
We need to rename the solver to myFoamRun to avoid conflicts with any existing bash commands or executables. Inside files, change:
EXE = $(FOAM_APPBIN)/foamRun
to
EXE = $(FOAM_USER_APPBIN)/myFoamRun
The $(FOAM_USER_APPBIN) variable points to the user’s local application binary directory, ensuring that your custom solver’s executable myFoamRun is placed in a user-level directory rather than the system-level directory. This keeps your custom developments separate, organized, and avoids overwriting system executables.
Also, ensure that the main source file in files is renamed from foamRun.C to myFoamRun.C. This step ensures consistency in naming and uniqueness.
options file:
The options file specifies additional compile and link directives, such as including libraries and setting up include paths. It often remains unchanged for simple modifications, as it already references the necessary OpenFOAM libraries required by the solver. For now, no changes are needed in the options file.
3. Reviewing Supporting Files: setDeltaT.C and setDeltaT.H
Before diving into the main solver code, let’s look at two files that handle time-step adjustments: setDeltaT.C and setDeltaT.H.
setDeltaT.C and setDeltaT.H:
These files provide functions (setDeltaT and adjustDeltaT) that dynamically control the time-step size. They look at conditions like whether the solver is transient and what maximum time-step (maxDeltaT) is allowed. Initially, they ensure that the solver doesn’t use an unstable or excessively large time-step. Think of these as small utility functions that your solver calls before and during the simulation loop to maintain stability and optimal runtime conditions. We do not need to modify these files for our scalar transport addition. They serve as good examples of how OpenFOAM neatly separates solver logic from utility functions.
4. Renaming and Inspecting the Main Solver File
Now, rename the main solver file from foamRun.C to myFoamRun.C:
mv foamRun.C myFoamRun.C
In this step, we’ll examine the original solver file foamRun.C closely. We will explain its contents line by line or in logical groups. Then, we’ll show the modified version and highlight where and how the scalar transport equation is integrated.
Original foamRun.C Breakdown:
Header and License Block:
*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration | Website: https://openfoam.org
\\ / A nd | Copyright ...
\\/ M anipulation |
-------------------------------------------------------------------------------
...
\*---------------------------------------------------------------------------*/
This is the standard OpenFOAM license and header information. It states the distribution and license terms. Every official OpenFOAM file starts with a similar block.
Application and Description Sections:
Application
foamRun
Description
Loads and executes an OpenFOAM solver module...
Explanation:
- Application: Declares the executable’s name (
foamRun). - Description: Explains what this solver or application does. In this case, it dynamically loads and runs a chosen solver. The solver name can be set in the
controlDictor via command-line arguments.
Usage Examples and Additional Notes:
Usage
foamRun [OPTION]
-solver <name>
Solver name
-libs '("lib1.so" ... "libN.so")'
Specify additional libraries
Example usage:
foamRun -solver fluid
Shows how to run this application, what arguments it accepts, and how you might specify a solver or libraries from the command line. Also gives sample configurations for controlDict.
Include Statements:
#include "argList.H"
#include "solver.H"
#include "pimpleSingleRegionControl.H"
#include "setDeltaT.H"
argList.H: Handles command-line arguments and parsing.solver.H: Provides the base class and functionality for loading and using various solver modules.pimpleSingleRegionControl.H: Implements the PIMPLE loop control structure (a hybrid of PISO and SIMPLE algorithms).setDeltaT.H: Functions to set and adjust the time-step according to solver constraints.
using namespace Foam;
Brings the main OpenFOAM namespace into scope for convenience.
Main Function and Argument Handling:
int main(int argc, char *argv[])
{
argList::addOption
(
"solver",
"name",
"Solver name"
);
#include "setRootCase.H"
#include "createTime.H"
- argList::addOption(“solver”, …): Adds a
-solvercommand-line option. - “setRootCase.H”: Sets up paths to the case directory, reading from environment variables.
- “createTime.H”: Initializes the
runTimeobject, which manages simulation time steps, controlDict, and data I/O.
Reading and Setting the Solver Name:
word solverName
(
runTime.controlDict().lookupOrDefault("solver", word::null)
);
args.optionReadIfPresent("solver", solverName);
if (solverName == word::null)
{
args.printUsage();
FatalErrorIn(args.executable())
<< "solver not specified ..."
<< exit(FatalError);
}
else
{
solver::load(solverName);
}
Retrieves the solver keyword from the controlDict.
- If
-solverwas specified on the command line, it overrides thecontrolDictsetting. - If no solver is found, it prints usage and exits with a fatal error. Otherwise, it dynamically loads the specified solver library.
Mesh Creation and Solver Instantiation:
#include "createMesh.H"
autoPtr<solver> solverPtr(solver::New(solverName, mesh));
solver& solver = solverPtr();
- “createMesh.H”: Builds the computational mesh from the provided case setup.
- solver::New(…): Uses the chosen solver’s factory method to create a solver instance, passing in the mesh.
solverPtris anautoPtr, a smart pointer managing memory.solveris a reference to the solver object.
PIMPLE Control Setup and Time-Step Adjustment:
pimpleSingleRegionControl pimple(solver.pimple);
setDeltaT(runTime, solver);
- Constructs the PIMPLE control object that manages the outer iteration loop for steady/transient simulations.
- Calls
setDeltaTto initialize the time-step size.
Main Time Loop:
Info<< nl << "Starting time loop\n" << endl;
while (pimple.run(runTime))
{
pimple.read();
solver.preSolve();
adjustDeltaT(runTime, solver);
runTime++;
Info<< "Time = " << runTime.userTimeName() << nl << endl;
// PIMPLE corrector loop
while (pimple.loop())
{
solver.moveMesh();
solver.motionCorrector();
solver.fvModels().correct();
solver.prePredictor();
solver.momentumPredictor();
solver.thermophysicalPredictor();
solver.pressureCorrector();
solver.postCorrector();
}
solver.postSolve();
runTime.write();
Info<< "ExecutionTime = " << runTime.elapsedCpuTime() << " s"
<< " ClockTime = " << runTime.elapsedClockTime() << " s"
<< nl << endl;
}
Info<< "End\n" << endl;
return 0;
}
- pimple.run(runTime): Determines if the simulation should continue.
- pimple.read(): Reads updated PIMPLE settings if they’ve changed.
- solver.preSolve()/postSolve(): Hooks for solver-specific operations before and after each iteration.
- adjustDeltaT(runTime, solver): Adjusts time-step if needed.
- runTime++: Increments time and moves to the next time-step.
- PIMPLE corrector loop: Executes multiple correction steps per time-step if required, improving solution accuracy and stability.
- After completing the loops, it writes out results, prints execution times, and repeats until
pimple.run(runTime)returns false (end of simulation).
This original file does not include a scalar transport equation. It focuses on loading a solver and running a simulation loop with the PIMPLE algorithm, leaving the actual physics (like momentum, pressure, energy) to the dynamically loaded solver module.
Modified Version with Scalar Transport Equation:
In the modified version, we add a separate header file (e.g., tracerEqn.H) and include it at an appropriate point in the time loop. The modifications would look like this:
// After setting and adjusting the time-step and before writing results, we include the scalar transport equation:
#include "tracerEqn.H"
What Does tracerEqn.H Contain?tracerEqn.H defines and solves the scalar transport equation. It might look like this:
5. Introducing the Scalar Transport Equation
To integrate a scalar transport equation (such as a tracer concentration) into your solver, you need to modify the solver loop to include an equation that represents the scalar’s evolution over time.

Where:
- Temporal term:
- This represents the rate of change of the scalar T over time.
- Represented in the code as
fvm::ddt(T). - In steady-state, this term is typically zero.
- Convection term:
- Describes the transport of scalar T due to the flow field velocity.
- Represented in the code as
fvm::div(phi, T), wherephiis the volumetric flux.
- Diffusion term:
- Accounts for the diffusion of scalar T within the medium.
- DT the thermal diffusivity (or a similar diffusion coefficient).
- Represented in the code as
fvm::laplacian(DT, T).
Create a new file called tracerEqn.H and place it in the same directory as myFoamRun.C. This file will define the scalar equation. For example:
fvScalarMatrix tracerEqn
(
fvm::ddt(tracer) // Time derivative of tracer field
+ fvm::div(phi, tracer) // Convective transport of tracer by the flow field
- fvm::laplacian(DT, tracer) // Diffusive spreading of tracer
);
tracerEqn.solve();
Info << "Min/Max Tracer: " << min(tracer).value() << "/ " << max(tracer).value() << endl;
The scalar transport equation typically has three main parts:
- Temporal derivative (
fvm::ddt(tracer)): This represents how the scalar changes over time. - Convection term (
fvm::div(phi, tracer)): This represents how the scalar is carried by the flow field. - Diffusion term (
fvm::laplacian(DT, tracer)): This accounts for the scalar spreading out due to diffusion.
DT is the diffusivity of the scalar, and phi is the volumetric flux field. Together, these terms form a partial differential equation that OpenFOAM discretizes and solves at each time-step.]
To incorporate tracerEqn.H into your solver, include it in myFoamRun.C at an appropriate location in the time loop, typically after your other primary equations are solved but before writing the results.
6. Compiling Your New Solver
Once you’ve completed all modifications (renamed files, updated EXE path in Make/files, introduced tracerEqn.H and included it in myFoamRun.C), it’s time to compile.
First, ensure your OpenFOAM environment is loaded:
source /opt/openfoam12/etc/bashrc
This command sets up all the required environment variables, paths, and aliases to compile and run OpenFOAM utilities and solvers. If you’ve defined a custom alias for sourcing OpenFOAM, use that instead.
Before compiling, clean any stale build files:
wclean
The wclean command removes previously compiled objects and ensures that you start the compilation process from a clean state. This is helpful when switching code branches, changing compilers, or modifying solver source files to avoid leftover binaries causing unexpected behavior.
Now, compile with:
wmake
The wmake tool is a wrapper around the underlying compiler commands, making it straightforward to rebuild OpenFOAM applications and libraries. After running wmake, you should see your new myFoamRun solver in the directory specified by FOAM_USER_APPBIN.
7. Setting Up Your Case
For testing the newly developed solver, let’s consider a prepared pitzDaily case. Before running the simulation, ensure the following steps are completed:
- Adjusting the Diffusivity:
In theconstant/transportPropertiesfile, add or modify theDT(diffusivity) entry for the tracer field. This specifies how quickly the tracer diffuses through the flow field. - Initializing the Tracer Field:
In the0directory, create an initial conditions file fortracer—similar to howU(velocity) orp(pressure) fields are defined. Set its internal field and boundary conditions according to your problem setup. For example, you may start with a uniform tracer concentration or a specified gradient.
Once these preparations are complete, run the solver from the case directory:
myFoamRun
If everything is set correctly, you will observe your tracer field evolving alongside the primary flow variables (e.g., velocity and pressure) as the simulation progresses.
Conclusion
Congratulations! You’ve successfully taken an existing solver in OpenFOAM, customized it, and introduced a scalar transport equation. This journey gave you insight into the solver’s internal structure, file organization, and the flexibility of OpenFOAM’s framework. With your new solver running the pitzDaily case, you can now visualize how the tracer diffuses and interacts with the flow field.
For even easier access and collaboration, both the solver and the test case are available on GitHub, ready for you to download, experiment with, and improve.
Happy coding and happy simulating! 🎉🚀


Based on the above instruction, we get the following errors:
In file included from myFoamRun.C:189:
tracerEqn.H: In function ‘int main(int, char**)’:
tracerEqn.H:8:5: error: variable ‘Foam::fvScalarMatrix tracerEqn’ has initializer but incomplete type
8 | fvm::ddt(tracer) // Time derivative of tracer field
| ^~~
tracerEqn.H:8:5: error: ‘fvm’ has not been declared
tracerEqn.H:8:14: error: ‘tracer’ was not declared in this scope
8 | fvm::ddt(tracer) // Time derivative of tracer field
| ^~~~~~
tracerEqn.H:9:5: error: ‘fvm’ has not been declared
9 | + fvm::div(phi, tracer) // Convective transport of tracer by the flow field
| ^~~
tracerEqn.H:9:14: error: ‘phi’ was not declared in this scope
9 | + fvm::div(phi, tracer) // Convective transport of tracer by the flow field
| ^~~
tracerEqn.H:12:5: error: ‘fvm’ has not been declared
12 | – fvm::laplacian(DT, tracer) // Diffusive spreading of tracer
| ^~~
tracerEqn.H:12:20: error: ‘DT’ was not declared in this scope
12 | – fvm::laplacian(DT, tracer) // Diffusive spreading of tracer
| ^~
make: *** [/home/andy/OpenFOAM-dev/wmake/rules/General/transform:26: Make/linux64GccDPInt32Opt/myFoamRun.o] Error 1
Hey! Thanks for sharing the error log — a few concrete fixes should get this building.
TL;DR causes:
You’re compiling on OpenFOAM-dev, while the instructions (and the repo) target OpenFOAM-12. API and headers are mostly compatible, but small differences plus missing includes will trigger exactly these errors.
In your tracerEqn.H, the finite-volume operators aren’t in scope and the fields aren’t declared yet. That’s why you see “fvm has not been declared”, “tracer/phi/DT not declared”, and “incomplete type”.
Quick checklist (apply all of these):
Includes / namespace
(With fvCFD.H in place, you don’t need to include fvm.H separately.)
Define your fields before including tracerEqn.H:
tracer (a volScalarField)
phi (a surfaceScalarField—usually created via #include “createPhi.H” after U exists)
DT (either a dimensionedScalar for constant diffusivity, or a volScalarField for spatially varying)
Minimal examples:
And make sure your constant/transportProperties has:
The equation itself (watch out for a sneaky Unicode dash):
Make sure you use a normal minus -, not an en-dash – (copy/paste often swaps these).
fvSchemes/fvSolution entries
Add (or check) in system/fvSchemes:
And a standard scalar solver in system/fvSolution:
Include order
In myFoamRun.C, the usual flow is: create mesh → create U/p → #include “createPhi.H” → define/read tracer and DT → then #include “tracerEqn.H” inside the time loop.
OpenFOAM-dev specifics
Dev sometimes tightens headers and namespaces. If you still see missing symbols, ensure your Make/options has:
Thanks for the custom Solver. I have modified the code to add the source uisng fvModel and it work. Here is the Code
tracerEqn.H
fvScalarMatrix tracerEqn
(
fvm::ddt(tracer) //Temporal term
+ fvm::div(phi, tracer) //Convection term
– fvm::laplacian(DT, tracer) //Diffusion term
==
fvModels.source(tracer) // if you want to define a source term
);
fvConstraints.constrain(tracerEqn);
tracerEqn.solve();
fvConstraints.constrain(tracer);
Info << "Min/Max Tracer: " << min(tracer).value() << "/ " << max(tracer).value() << endl;
myFoamRun.C
#include "argList.H"
#include "solver.H"
#include "pimpleSingleRegionControl.H"
#include "setDeltaT.H"
#include "fvModels.H"
#include "fvConstraints.H"
#include "fvcDdt.H"
#include "fvcGrad.H"
#include "fvcFlux.H"
#include "fvmDdt.H"
#include "fvmDiv.H"
#include "fvmLaplacian.H"
using namespace Foam;
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
//Constructor function
int main(int argc, char *argv[])
{
argList::addOption
(
"solver",
"name",
"Solver name"
);
#include "setRootCase.H" //setting root folder
#include "createTime.H"
// Read the solverName from the optional solver entry in controlDict
word solverName
(
runTime.controlDict().lookupOrDefault("solver", word::null)
);
// Optionally reset the solver name from the -solver command-line argument
args.optionReadIfPresent("solver", solverName);
// Check the solverName has been set
if (solverName == word::null)
{
args.printUsage();
FatalErrorIn(args.executable())
<< "solver not specified in the controlDict or on the command-line"
<< exit(FatalError);
}
else
{
// Load the solver library
solver::load(solverName);
}
// Create the default single region mesh
#include "createMesh.H" //read created mesh
// Instantiate the selected solver
autoPtr solverPtr(solver::New(solverName, mesh));
solver& solver = solverPtr();
const fvModels& fvModels = fvModels::New(mesh);
const fvConstraints& fvConstraints = fvConstraints::New(mesh);
// Create the outer PIMPLE loop and control structure
pimpleSingleRegionControl pimple(solver.pimple);
// Set the initial time-step
setDeltaT(runTime, solver);
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
Info<< nl << "Starting time loop\n" << endl; //Info print information into the terminal
Info<< "Reading scalar field Tracer\n" << endl; //Info print information into the terminal
// it should be after mesh read before the loop
//this is the scalar variable that is going to track
// here only define the variable
volScalarField tracer
(
IOobject
(
"Tracer",
runTime.name(),
mesh,
IOobject::MUST_READ,
IOobject::AUTO_WRITE
),
mesh
);
Info<< "Reading Vector field U\n" << endl; //Info print information into the terminal
volVectorField U
(
IOobject
(
"U",
runTime.name(),
mesh,
IOobject::MUST_READ,
IOobject::AUTO_WRITE
),
mesh
);
Info<< "Creating scalar field phi\n" << endl; //Info print information into the terminal
surfaceScalarField phi
(
IOobject
(
"phi",
runTime.name(),
mesh,
IOobject::NO_READ,
IOobject::AUTO_WRITE
),
fvc::flux(U)
);
Info<< "Reading physicalProperties\n" << endl;
IOdictionary physicalProperties
(
IOobject
(
"physicalProperties",
runTime.constant(),
mesh,
IOobject::MUST_READ_IF_MODIFIED,
IOobject::NO_WRITE
)
);
Info<< "Reading diffusivity DT\n" << endl;
dimensionedScalar DT
(
physicalProperties.lookup("DT")
);
while (pimple.run(runTime))
{
// Update PIMPLE outer-loop parameters if changed
pimple.read();
solver.preSolve();
// Adjust the time-step according to the solver maxDeltaT
adjustDeltaT(runTime, solver); //related to adjustable runtime
runTime++; //next timestep
Info<< "Time = " << runTime.userTimeName() << nl << endl;
// PIMPLE corrector loop
while (pimple.loop())
{
solver.moveMesh(); //if you have moving mesh first it try to reform mesh
solver.motionCorrector(); // make correction for the mesh movement
solver.fvModels().correct(); //finite volume model correction
solver.prePredictor(); //
solver.momentumPredictor(); //solve Navier-Stokes equation
solver.thermophysicalPredictor(); //predict thermophysical property
solver.pressureCorrector(); //correct the mesh
solver.postCorrector();
}
#include "tracerEqn.H"
solver.postSolve(); //update fields
runTime.write(); //check if data should be written
Info<< "ExecutionTime = " << runTime.elapsedCpuTime() << " s"
<< " ClockTime = " << runTime.elapsedClockTime() << " s"
<< nl << endl;
}
Info<< "End\n" << endl;
return 0; //dummy value return if the run is going on
}
// ************************************************************************* //