Weak Formulations
Understanding and implementing weak formulations in Rodin.
Introduction
The weak formulation (also called variational formulation) is the foundation of the finite element method. Instead of solving a PDE in its classical (strong) form, we reformulate it in a weaker sense that is more amenable to numerical approximation.
The general idea is to multiply the PDE by a test function, integrate over the domain, and apply integration by parts. This process:
- Reduces the regularity requirements on the solution
- Naturally incorporates certain boundary conditions
- Leads to a well-posed problem suitable for discretization
Derivation Procedure
Consider a general second-order PDE on a domain :
To derive the weak formulation:
- Multiply by a test function from an appropriate space
- Integrate over :
- Integrate by parts to transfer derivatives from to
- Apply boundary conditions (essential or natural)
Example: The Poisson Equation
Strong Form
The Poisson equation with homogeneous Dirichlet boundary conditions:
Derivation
Multiply by and integrate:
Apply Green's formula (integration by parts):
Weak Formulation
Find such that for all :
Implementation in Rodin
The implementation in Rodin closely mirrors the mathematical notation:
#include <Rodin/Solver.h> #include <Rodin/Geometry.h> #include <Rodin/Variational.h> using namespace Rodin; using namespace Rodin::Geometry; using namespace Rodin::Variational; int main() { Mesh mesh; mesh = mesh.UniformGrid(Polytope::Type::Triangle, {16, 16}); mesh.getConnectivity().compute(1, 2); P1 Vh(mesh); TrialFunction u(Vh); TestFunction v(Vh); RealFunction f = 1; Problem poisson(u, v); poisson = Integral(Grad(u), Grad(v)) // ∫ ∇u · ∇v dx - Integral(f, v) // ∫ f v dx + DirichletBC(u, Zero()); // u = 0 on ∂Ω Solver::CG(poisson).solve(); return 0; }
Example: Linearized Elasticity
Strong Form
The linearized elasticity system governs the displacement of an elastic body:
where is the Cauchy stress tensor (Hooke's law) and is the linearized (infinitesimal) strain tensor.
Lamé Parameters
The Lamé parameters and characterize the material. They are related to the more common Young's modulus and Poisson's ratio by:
For example, with and :
const Real E = 1.0, nu = 0.3; const Real lambda = E * nu / ((1.0 + nu) * (1.0 - 2.0 * nu)); // ≈ 0.5769 const Real mu = E / (2.0 * (1.0 + nu)); // ≈ 0.3846
Derivation
Multiply by , integrate, and use the divergence theorem. The boundary integral on produces the traction term:
Expanding Hooke's law, the bilinear form becomes:
Implementation
Rodin provides LinearElasticityIntegral (in <Rodin/) which assembles the bilinear form for the Hooke tensor. The helper class LinearElasticityIntegral(u, v) returns a callable that accepts the Lamé parameters — either as constants or spatially-varying functions:
#include <Rodin/Solver.h> #include <Rodin/Geometry.h> #include <Rodin/Variational.h> #include <Rodin/Solid.h> using namespace Rodin; using namespace Rodin::Geometry; using namespace Rodin::Variational; int main() { Mesh mesh; mesh.load("beam.mesh", IO::FileFormat::MFEM); mesh.getConnectivity().compute(1, 2); const size_t d = mesh.getSpaceDimension(); const Real E = 1.0, nu = 0.3; const Real lambda = E * nu / ((1.0 + nu) * (1.0 - 2.0 * nu)); const Real mu = E / (2.0 * (1.0 + nu)); P1 Vh(mesh, d); // Vector-valued P1 space TrialFunction u(Vh); TestFunction v(Vh); VectorFunction traction{0, -1}; // Traction force (stress vector) on Neumann boundary Problem elasticity(u, v); elasticity = LinearElasticityIntegral(u, v)(lambda, mu) - BoundaryIntegral(traction, v).over(GammaN) + DirichletBC(u, VectorFunction{0, 0}).on(GammaD); Solver::CG(elasticity).solve(); return 0; }
Beyond Small Deformations
For large deformations (finite strain), use the Solid module's hyperelastic framework with MaterialTangent and InternalForce integrators coupled with a NewtonSolver. Available constitutive laws include NeoHookean, SaintVenantKirchhoff, and MooneyRivlin.
The Newton linearization at each iterate solves:
where is the material tangent and is the internal force vector.
#include <Rodin/Solid.h> #include <Rodin/Solver/NewtonSolver.h> #include <Rodin/Solver/SparseLU.h> Solid::NeoHookean law(lambda, mu); GridFunction u(Vh); // accumulated displacement Solid::MaterialTangent tangent(law, du, v); tangent.setDisplacement(u); Solid::InternalForce residual(law, v); residual.setDisplacement(u); Problem newton(du, v); newton = tangent + residual - Integral(bodyForce, v) + DirichletBC(du, VectorFunction{Zero(), Zero()}).on(fixedBC); SparseLU linearSolver(newton); NewtonSolver solver(linearSolver); solver.solve(u); // updates u in place
See Solid module for the full API.
General Structure
Most weak formulations in Rodin follow this pattern:
where:
- is a bilinear form (depends on both and )
- is a linear form (depends only on )
In Rodin, the problem is defined by moving everything to one side:
Problem problem(u, v); problem = [bilinear terms] - [linear terms] + [boundary conditions];
Bilinear Forms
A bilinear form is a mapping that is linear in both arguments. It involves both the trial function and the test function , and is assembled into the system matrix .
Common examples:
| Mathematical expression | Rodin code | Name |
|---|---|---|
Integral(Grad(u), Grad(v)) | Stiffness | |
Integral(u, v) | Mass | |
Integral(lambda * Grad(u), Grad(v)) | Weighted stiffness | |
Integral(lambda * Div(u), Div(v)) | Divergence penalization |
Linear Forms
A linear form is a mapping that is linear in only (it does not depend on the unknown ). It is assembled into the right-hand side vector** .
Common examples:
| Mathematical expression | Rodin code | Name |
|---|---|---|
Integral(f, v) | Volume source | |
BoundaryIntegral(g, v).over(GammaN) | Neumann load | |
Integral(f, v) | Vector body force |
Boundary Conditions in Weak Form
Dirichlet (Essential) Conditions
Dirichlet conditions are imposed directly on the function space:
// u = 0 on entire boundary DirichletBC(u, Zero()) // u = g on specific boundary DirichletBC(u, g).on(boundaryAttribute)
Neumann (Natural) Conditions
Neumann conditions appear as boundary integrals in the weak formulation:
// ∂u/∂n = g on boundary with attribute 2 BoundaryIntegral(g, v).over(2)
Periodic Boundary Conditions
Periodic conditions tie DOFs on opposite boundary segments using an IndexMap. See the periodic Poisson example for a complete walkthrough.
// Build a DOF correspondence map: left edge ↔ right edge IndexMap<IndexSet> dofs; for (Index i = 0; i < Vh.getSize(); i += n) dofs[i].insert(i + n - 1); Problem problem(u, v); problem = Integral(Grad(u), Grad(v)) - Integral(f, v) + PeriodicBC(u, dofs);
Mixed Formulations
Some PDEs involve multiple unknowns from different spaces. The Navier–Stokes example uses Taylor–Hood elements with separate trial and test functions for velocity and pressure:
H1 Vh(std::integral_constant<size_t, 2>{}, mesh, d); // Velocity: vector H1, order 2 H1 Qh(std::integral_constant<size_t, 1>{}, mesh); // Pressure: scalar H1, order 1 TrialFunction u(Vh); TestFunction v(Vh); // Velocity pair TrialFunction p(Qh); TestFunction q(Qh); // Pressure pair Problem stokes(u, p, v, q); stokes = Integral(Grad(u), Grad(v)) // viscous term - Integral(p, Div(v)) // pressure–velocity coupling + Integral(Div(u), q) // incompressibility constraint - Integral(f, v) // body force + DirichletBC(u, uD); // velocity BC
Well-Posedness
The Lax-Milgram theorem guarantees existence and uniqueness of the solution when:
- The bilinear form is continuous and coercive** on the function space
- The linear form is continuous on
For the Poisson equation, the bilinear form is coercive on by the Poincaré inequality.
For mixed formulations (saddle-point problems), the Brezzi conditions (inf-sup stability) replace coercivity. Taylor–Hood elements satisfy the inf-sup condition, making them a standard choice for Stokes-type problems.