Partial differential equations » Solving the Navier-Stokes equations

Steady incompressible Navier-Stokes with Picard iteration.

Image

Introduction

The incompressible Navier-Stokes equations describe the motion of viscous fluids. In the steady-state case, they read:

\[ \left\{ \begin{aligned} -\nu \Delta \mathbf{u} + (\mathbf{u} \cdot \nabla)\mathbf{u} + \nabla p &= \mathbf{f} && \mathrm{in} \ \Omega \\ \nabla \cdot \mathbf{u} &= 0 && \mathrm{in} \ \Omega \\ \mathbf{u} &= \mathbf{g} && \mathrm{on} \ \partial \Omega \end{aligned} \right. \]

where:

  • $ \mathbf{u} : \Omega \to \mathbb{R}^d $ is the velocity field
  • $ p : \Omega \to \mathbb{R} $ is the pressure field
  • $ \nu > 0 $ is the kinematic viscosity
  • $ \mathbf{f} $ is a body force
  • $ \mathbf{g} $ is the prescribed velocity on the boundary

The Navier-Stokes system is a mixed problem: we solve simultaneously for the velocity $ \mathbf{u} $ and pressure $ p $ . The incompressibility constraint $ \nabla \cdot \mathbf{u} = 0 $ acts as a coupling condition.

The Stokes Problem

When the nonlinear convective term $ (\mathbf{u} \cdot \nabla)\mathbf{u} $ is dropped, we obtain the Stokes equations, which are linear:

\[ \left\{ \begin{aligned} -\nu \Delta \mathbf{u} + \nabla p &= \mathbf{f} && \mathrm{in} \ \Omega \\ \nabla \cdot \mathbf{u} &= 0 && \mathrm{in} \ \Omega \end{aligned} \right. \]

The Stokes problem serves as the foundation for the Navier-Stokes solver and is also an important model in its own right (for very viscous or slow flows).

Weak Formulation

The weak formulation seeks $ (\mathbf{u}, p) \in \mathbf{V} \times Q $ such that for all $ (\mathbf{v}, q) \in \mathbf{V}_0 \times Q $ :

\[ \nu \int_\Omega \nabla \mathbf{u} : \nabla \mathbf{v} \, dx + \int_\Omega (\mathbf{w} \cdot \nabla)\mathbf{u} \cdot \mathbf{v} \, dx - \int_\Omega p \, \nabla \cdot \mathbf{v} \, dx + \int_\Omega q \, \nabla \cdot \mathbf{u} \, dx = \int_\Omega \mathbf{f} \cdot \mathbf{v} \, dx \]

where $ \mathbf{w} $ is the convective velocity (equal to $ \mathbf{u} $ at convergence). The mixed finite element spaces must satisfy the inf-sup (LBB) condition**. A classical choice is the Taylor-Hood pair:

  • Velocity: $ \mathbf{V} = [H^1_h(\Omega)]^d $ with polynomial order 2 (quadratic)
  • Pressure: $ Q = H^1_h(\Omega) $ with polynomial order 1 (linear)

Pressure Uniqueness

The pressure is determined only up to a constant (only its gradient appears in the momentum equation). To fix the solution uniquely, we add a mean-pressure constraint** $ \int_\Omega p \, dx = 0 $ using a Lagrange multiplier $ \lambda $ from a P0 space.

Picard Iteration (Oseen Linearization)

The nonlinear term $ (\mathbf{u} \cdot \nabla)\mathbf{u} $ is handled by Picard iteration (also known as Oseen linearization). At each iteration $ k $ :

  1. Fix the convective velocity $ \mathbf{w} = \mathbf{u}^{k-1} $ from the previous iteration
  2. Solve the linearized (Oseen) problem for $ (\mathbf{u}^k, p^k) $ :

    \[ \nu \int_\Omega \nabla \mathbf{u}^k : \nabla \mathbf{v} \, dx + \int_\Omega (\mathbf{w} \cdot \nabla)\mathbf{u}^k \cdot \mathbf{v} \, dx - \int_\Omega p^k \, \nabla \cdot \mathbf{v} \, dx + \int_\Omega q \, \nabla \cdot \mathbf{u}^k \, dx = \int_\Omega \mathbf{f} \cdot \mathbf{v} \, dx \]
  3. Repeat until convergence: $ \|\mathbf{u}^k - \mathbf{u}^{k-1}\| < \varepsilon $

Implementation in Rodin

This example uses the Taylor-Green vortex as a manufactured solution. The code uses the H1 finite element space for higher-order elements.

Setup

#include <Rodin/Assembly.h>
#include <Rodin/Variational.h>
#include <Rodin/Variational/H1.h>
#include <Rodin/Solver/SparseLU.h>
#include <Rodin/IO/XDMF.h>

using namespace Rodin;
using namespace Rodin::Geometry;
using namespace Rodin::Variational;
using namespace Rodin::Solver;

Mesh and Spaces

We create a mesh on $ [0,1]^2 $ and define the Taylor-Hood finite element spaces. The H1 class supports arbitrary polynomial order via an std::integral_constant template parameter:

constexpr Real nu = 1.0;      // Kinematic viscosity
constexpr size_t maxIts = 12; // Max Picard iterations
constexpr Real tol = 1e-10;   // Convergence tolerance

const auto pi = Math::Constants::pi();

// Create mesh on unit square
Mesh mesh;
mesh = mesh.UniformGrid(Polytope::Type::Triangle, { 16, 16 });
mesh.scale(1.0 / 15.0);
mesh.getConnectivity().compute(1, 2);

// Taylor-Hood spaces: P2 for velocity, P1 for pressure
H1 uh(std::integral_constant<size_t, 2>{}, mesh, mesh.getSpaceDimension());
H1 ph(std::integral_constant<size_t, 1>{}, mesh);

// Lagrange multiplier for mean-pressure constraint
P0g p0g(mesh);

Manufactured Solution

The Taylor-Green vortex provides an exact solution for verification:

VectorFunction u_exact{
  sin(pi * F::x) * cos(pi * F::y),
  -cos(pi * F::x) * sin(pi * F::y)
};

auto p_exact = -0.25 * (cos(2 * pi * F::x) + cos(2 * pi * F::y));

VectorFunction f{
  2 * nu * pi * pi * sin(pi * F::x) * cos(pi * F::y)
    + pi * sin(2 * pi * F::x),
  -2 * nu * pi * pi * cos(pi * F::x) * sin(pi * F::y)
    + pi * sin(2 * pi * F::y)
};

Here F::x and F::y are symbolic coordinate functions that allow building expressions.

Trial and Test Functions

The Problem class supports multiple trial/test function pairs for mixed formulations. We define pairs for velocity, pressure, and the Lagrange multiplier:

TrialFunction u(uh);       // Velocity (vector)
TrialFunction p(ph);       // Pressure (scalar)
TrialFunction lambda(p0g); // Mean-pressure Lagrange multiplier

TestFunction  v(uh);       // Velocity test function
TestFunction  q(ph);       // Pressure test function
TestFunction  mu(p0g);     // Lagrange multiplier test function

Picard Iteration Loop

The convective velocity $ \mathbf{w} $ is stored as a GridFunction and updated after each solve. The convective term is computed using Mult (matrix-vector product) and Dot:

GridFunction w(uh);
w = VectorFunction{ Zero(), Zero() };  // Initial guess: zero velocity

GridFunction u_prev(uh);
u_prev = VectorFunction{ Zero(), Zero() };

for (size_t k = 0; k < maxIts; ++k)
{
  // Build the linearized (Oseen) problem
  Problem ns(u, p, lambda, v, q, mu);
  const auto conv_u = Mult(Jacobian(u), w);  // (w · ∇)u

  ns = nu * Integral(Jacobian(u), Jacobian(v))  // Viscous term
     + Integral(Dot(conv_u, v))                  // Convective term
     - Integral(p, Div(v))                       // Pressure-velocity coupling
     + Integral(Div(u), q)                       // Incompressibility
     + Integral(lambda, q)                       // Mean-pressure constraint
     + Integral(p, mu)                           // (symmetric counterpart)
     - Integral(f, v)                            // Body force
     + DirichletBC(u, u_exact);                  // Boundary conditions

  SparseLU solver(ns);
  solver.solve();

  // Check convergence
  P1 sh(mesh);
  GridFunction du(sh);
  du = Pow(Frobenius(u.getSolution() - u_prev), 2);
  Real updateNorm = std::sqrt(Integral(du).compute());

  // Update convective velocity for next iteration
  w.setData(u.getSolution().getData());
  u_prev.setData(u.getSolution().getData());

  if (updateNorm < tol)
    break;
}

Key Rodin expressions used:

  • Jacobian(u) — the gradient matrix $ \nabla \mathbf{u} $
  • Mult(Jacobian(u), w) — the product $ (\nabla \mathbf{u}) \mathbf{w} $ , representing the convective term
  • Div(v) — the divergence $ \nabla \cdot \mathbf{v} $
  • Frobenius(...) — the Frobenius norm for measuring convergence

XDMF Output

After convergence, export the velocity and pressure fields using the IO::XDMF class:

IO::XDMF xdmf("NavierStokes");
xdmf.grid().setMesh(mesh)
  .add("velocity", u.getSolution())
  .add("pressure", p.getSolution());
xdmf.write();

Open NavierStokes.xdmf in ParaView to visualize the velocity vectors and pressure contours.

Theoretical Notes

Inf-Sup Stability

Not every combination of velocity and pressure spaces is stable. The spaces must satisfy the inf-sup (LBB) condition:

\[ \inf_{q \in Q_h \setminus \{0\}} \sup_{\mathbf{v} \in \mathbf{V}_h \setminus \{0\}} \frac{\int_\Omega q \, \nabla \cdot \mathbf{v} \, dx} {\|\mathbf{v}\|_{H^1} \|q\|_{L^2}} \geq \beta > 0 \]

The Taylor-Hood pair (P2 velocity / P1 pressure) satisfies this condition and is one of the most widely used choices.

Picard Convergence

Picard iteration converges for sufficiently small Reynolds numbers $ \mathrm{Re} = U L / \nu $ . For high Reynolds number flows, Newton's method provides faster convergence but requires computing the Jacobian of the nonlinear system.

See Also