The OpenFOAM learning curve (and why it is worth climbing)
OpenFOAM is intimidating. There is no graphical interface, every simulation is configured through plain text files, and the documentation assumes you already know fluid mechanics. But behind that learning curve lies the most powerful open-source simulation tool available. In this tutorial we will walk through the most classic case in CFD: the lid-driven cavity. By the end, you will have run your first simulation and will have a clear mental map of how OpenFOAM works under the hood. No black magic, no shortcuts — just physics and code.
What you need
To follow this tutorial you need three things: an OpenFOAM installation (either the OpenCFD or Foundation version, both work), ParaView to visualize results, and a terminal. If you are on Windows, the simplest path is WSL2 with OpenFOAM installed via Docker or the Ubuntu package manager. On Linux and macOS, installation is straightforward through your system package manager. No previous CFD experience is required — this tutorial starts from zero.
Step 1: The case directory structure
The three building blocks of an OpenFOAM case
Every OpenFOAM simulation lives inside a folder with three mandatory subdirectories that reflect the solver architecture. The 0/ directory holds the initial and boundary conditions for each flow variable: velocity, pressure, and turbulence quantities if used. The constant/ directory stores physical properties (viscosity, turbulence model) and optionally the mesh geometry in constant/polyMesh/. The system/ directory contains the control files: numerical schemes, linear solvers, and runtime parameters.
You can copy the base structure from the tutorial that ships with OpenFOAM. The cavity case lives at $FOAM_TUTORIALS/incompressible/simpleFoam/cavity/. Navigate to your working directory, copy the folder, and rename it. You now have the skeleton ready to modify.
Step 2: blockMesh and the computational mesh
Defining the geometry in blockMeshDict
The file system/blockMeshDict defines the computational mesh parametrically. For our 1 m × 1 m 2D cavity, we define 8 vertices (a unit cube), one hexahedral block, and four named patches: movingWall (the top lid), fixedWalls (the other three walls), and frontAndBack (the front and back faces, declared as empty to indicate a 2D simulation).
/* Excerpt from system/blockMeshDict — 2D cavity */
vertices
(
(0 0 0) (1 0 0) (1 1 0) (0 1 0)
(0 0 1) (1 0 1) (1 1 1) (0 1 1)
);
blocks
(
hex (0 1 2 3 4 5 6 7) (20 20 1) simpleGrading (1 1 1)
);
boundary
(
movingWall { type wall; faces ( (3 7 6 2) ); }
fixedWalls { type wall; faces ( (0 4 7 3) (2 6 5 1) (1 5 4 0) ); }
frontAndBack { type empty; faces ( (0 3 2 1) (4 5 6 7) ); }
);
The parameter (20 20 1) specifies 20 cells in X, 20 in Y, and 1 in Z (a single cell thickness for 2D flow). simpleGrading (1 1 1) means the mesh is uniform in all directions, without near-wall refinement.
Run blockMesh in the terminal. If everything is correct, you will see "Mesh OK" and the folder constant/polyMesh/ will be populated with points, faces, owner, neighbour and boundary files. Your mesh is ready.
Step 3: Boundary conditions
Velocity and pressure in the 0/ directory
For the lid-driven cavity, the top wall must move at a constant velocity U = 1 m/s in the X direction. The remaining walls have zero velocity (no-slip condition). Pressure has zero gradient on all walls (Neumann boundary condition), which is physically correct for incompressible flow — the normal pressure gradient at a solid wall is zero. The front and back faces use the empty type to enforce a 2D simulation.
/* Excerpt from 0/U — velocity field */
dimensions [0 1 -1 0 0 0 0];
internalField uniform (0 0 0);
boundaryField
{
movingWall
{
type fixedValue;
value uniform (1 0 0);
}
fixedWalls
{
type noSlip;
}
frontAndBack
{
type empty;
}
}
In 0/p, the condition for all walls is zeroGradient, and the frontAndBack faces are empty. The pressure field does not need a fixed reference value because the solver handles the singularity in the linear system internally through the pressure equation.
Step 4: Physical properties
In constant/transportProperties we define the kinematic viscosity nu of the fluid. For the classic laminar case at Re = 100 (with U = 1 m/s and L = 1 m), we need nu = 0.01 m²/s. In constant/turbulenceProperties we select simulationType laminar — no turbulence model is needed for this first case. The simulation will solve the Navier-Stokes equations directly, without Reynolds averaging.
Step 5: System control files
controlDict, fvSchemes, and fvSolution
system/controlDict defines the simulation duration. For Re = 100, the flow reaches steady state within a few iterations. We set application simpleFoam, startFrom startTime, startTime 0, stopAt endTime, endTime 0.5, deltaT 0.005, and writeInterval 10 (write results every 10 time steps).
/* Excerpt from system/controlDict */
application simpleFoam;
startFrom startTime;
startTime 0;
stopAt endTime;
endTime 0.5;
deltaT 0.005;
writeControl timeStep;
writeInterval 10;
purgeWrite 0;
writeFormat ascii;
writePrecision 6;
runTimeModifiable true;
system/fvSchemes defines spatial discretization. We start with first-order schemes for stability: Euler for the time term, Gauss linear for the gradient, Gauss linearUpwind for the divergence term. system/fvSolution configures the linear solvers: GAMG for pressure with tolerance 1e-6, and smoothSolver for velocity with tolerance 1e-7. The pressure-velocity coupling is handled by the SIMPLE (Semi-Implicit Method for Pressure Linked Equations) algorithm.
Step 6: Running simpleFoam and monitoring residuals
Launch simpleFoam from the terminal inside the case directory. You will see the residuals output on screen: Ux, Uy, and pressure. A residual below 1e-4 indicates good convergence. For this simple case, the entire simulation completes in seconds. If you see "Solution converged in X iterations", you have succeeded.
Residuals measure the solution error at each iteration. They do not need to reach zero — dropping three or four orders of magnitude from the initial value yields a solution that is perfectly usable for engineering. If residuals stall, check that your boundary conditions are consistent and that deltaT is not too large.
Step 7: Visualizing results in ParaView
Open ParaView and load the case by creating an empty file with a .foam extension in the case root directory (run touch cavity.foam). Open this file in ParaView and click Apply to load the data. Create a slice on the Z = 0.5 plane using the Slice filter. Add velocity vectors with the Glyph filter — you will see the characteristic primary vortex filling most of the cavity. For pressure, use the Contour filter on the p field.
The classic result shows a large primary vortex occupying most of the cavity, rotating clockwise driven by the moving lid. If your mesh is fine enough, you will see smaller secondary vortices in the bottom corners — a hallmark of a well-resolved simulation. If your visualization matches this description, you have successfully completed your first CFD simulation.
What comes next
This case is only the entry point. The natural next step is to increase the Reynolds number: change nu to 0.001 in transportProperties for Re = 1000, activate a turbulence model such as k-omega SST in turbulenceProperties, and refine the mesh near the walls with a non-uniform simpleGrading. From there you can explore complex geometries with snappyHexMesh, OpenFOAM's mesh generator that creates meshes from STL files, or move on to multiphase cases with interFoam.
OpenFOAM is learned one case at a time, and each new case teaches you a new piece of the system. The community is vast, the code is open, and the possibilities are limitless. The hardest part is done: getting started. The rest is practice.