libMesh
Loading...
Searching...
No Matches
finite_volume_ex1.C
Go to the documentation of this file.
1// The libMesh Finite Element Library.
2// Copyright (C) 2002-2026 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
3
4// This library is free software; you can redistribute it and/or
5// modify it under the terms of the GNU Lesser General Public
6// License as published by the Free Software Foundation; either
7// version 2.1 of the License, or (at your option) any later version.
8
9// This library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12// Lesser General Public License for more details.
13
14// You should have received a copy of the GNU Lesser General Public
15// License along with this library; if not, write to the Free Software
16// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
18// <h1>Finite Volume Example 1 - Cell-Centered Advection-Diffusion on Polygonal Meshes</h1>
19// \author Alexander Lindsay
20// \date 2026
21//
22// This example solves the steady advection-diffusion equation
23//
24// -div(D grad u) + div(v u) = f
25//
26// on the unit square with a cell-centered finite volume method on a
27// mesh of C0Polygon elements. The solution is a CONSTANT MONOMIAL
28// variable, so each cell carries a single degree of freedom,
29// interpreted as the solution value at the cell centroid.
30//
31// Integrating the equation over a cell and applying the divergence
32// theorem gives a balance of fluxes through the cell faces. Cell
33// gradients, reconstructed by least squares from the face neighbors
34// and the boundary data, enter these fluxes in two places:
35//
36// * The diffusive flux uses a two-point difference along the vector
37// joining adjacent cell centroids, plus a correction for the angle
38// between that vector and the face normal. On this mesh the two
39// are not parallel across the slanted hexagon faces, and without
40// the correction the scheme would not converge to the exact
41// solution.
42//
43// * The advective flux is upwinded, with the upwind cell value
44// linearly extrapolated to the face centroid, giving second-order
45// accuracy.
46//
47// A face flux therefore depends on the two cells sharing the face and
48// on their face neighbors, so the matrix row of a cell couples it to
49// two layers of side neighbors. The DofMap's DefaultCoupling functor
50// is configured for two layers before the mesh is built, so that the
51// required ghost elements are retained when a distributed mesh deletes
52// remote elements.
53//
54// The example solves a manufactured-solution problem on a sequence of
55// uniformly refined meshes and reports the centroid-sampled L2 error.
56// Refinement continues until a least-squares fit over the last three
57// meshes gives a convergence rate close to 2, and the example fails if
58// that does not happen within the allowed number of refinements.
59
60// libMesh includes
61#include "libmesh/libmesh.h"
62#include "libmesh/mesh.h"
63#include "libmesh/mesh_generation.h"
64#include "libmesh/equation_systems.h"
65#include "libmesh/linear_implicit_system.h"
66#include "libmesh/dof_map.h"
67#include "libmesh/default_coupling.h"
68#include "libmesh/elem.h"
69#include "libmesh/sparse_matrix.h"
70#include "libmesh/numeric_vector.h"
71#include "libmesh/dense_matrix.h"
72#include "libmesh/dense_vector.h"
73#include "libmesh/getpot.h"
74#include "libmesh/enum_solver_package.h"
75#include "libmesh/int_range.h"
76#include "libmesh/exodusII_io.h"
77
78// C++ includes
79#include <cmath>
80#include <map>
81#include <memory>
82#include <vector>
83
84using namespace libMesh;
85
86namespace
87{
88
89// The manufactured solution, which also supplies the Dirichlet data
90// on the whole boundary.
91Real
92exact_solution(const Point & p)
93{
94 return std::sin(libMesh::pi * p(0)) * std::cos(libMesh::pi * p(1));
95}
96
97// The source term -D lap(u) + v.grad(u) for the manufactured solution
98// and a constant, hence divergence-free, velocity v.
99Real
100forcing(const Point & p, const Real diffusivity, const RealVectorValue & velocity)
101{
102 const Real pi = libMesh::pi;
103 const Real x = p(0);
104 const Real y = p(1);
105 return 2 * pi * pi * diffusivity * exact_solution(p) +
106 velocity(0) * pi * std::cos(pi * x) * std::cos(pi * y) -
107 velocity(1) * pi * std::sin(pi * x) * std::sin(pi * y);
108}
109
110// An affine function of the cell solution values,
111// sum_i coefficients[i] * u_i + constant,
112// whose value type T is Real for scalar quantities such as face fluxes
113// and RealVectorValue for reconstructed gradients.
114template <typename T>
115struct AffineStencil
116{
117 std::map<dof_id_type, T> coefficients;
118 T constant = T();
119};
120
121typedef AffineStencil<Real> ScalarStencil;
122typedef AffineStencil<RealVectorValue> GradientStencil;
123
124// Adds scale * (g . a) to out.
125void
126add_dot(ScalarStencil & out, const GradientStencil & g, const RealVectorValue & a, const Real scale)
127{
128 for (const auto & [dof, coef] : g.coefficients)
129 out.coefficients[dof] += scale * (coef * a);
130 out.constant += scale * (g.constant * a);
131}
132
133// Returns 0.5 * (a + b).
134GradientStencil
135average(const GradientStencil & a, const GradientStencil & b)
136{
137 GradientStencil avg;
138 for (const auto & [dof, coef] : a.coefficients)
139 avg.coefficients[dof] += 0.5 * coef;
140 for (const auto & [dof, coef] : b.coefficients)
141 avg.coefficients[dof] += 0.5 * coef;
142 avg.constant = 0.5 * (a.constant + b.constant);
143 return avg;
144}
145
146struct FaceGeometry
147{
148 Point centroid;
149 Real area;
150 Point normal;
151};
152
153// Computes the centroid, area, and outward unit normal of a planar
154// element side. Polygons and polyhedra have no reference element, so
155// the geometry is taken from the physical side directly.
156FaceGeometry
157face_geometry(const Elem & elem, const unsigned int side)
158{
159 const std::unique_ptr<const Elem> side_elem = elem.build_side_ptr(side);
160 return {side_elem->true_centroid(), side_elem->volume(), elem.side_vertex_average_normal(side)};
161}
162
163// Returns the least-squares gradient of u in elem as an affine function
164// of cell values. Each face contributes one sample: the neighbor's
165// centroid value for interior faces and the Dirichlet value at the
166// face centroid for boundary faces. The gradient g minimizes
167// sum_k ((x_k - x_elem) . g - (u_k - u_elem))^2,
168// which reproduces linear functions exactly.
169GradientStencil
170gradient_stencil(const Elem & elem, const unsigned int sys_num)
171{
172 struct Sample
173 {
174 RealVectorValue offset;
175 dof_id_type dof;
176 Real value;
177 };
178
179 const unsigned int dim = elem.dim();
180 const Point x_elem = elem.true_centroid();
181
182 std::vector<Sample> samples;
183 DenseMatrix<Real> normal_matrix(dim, dim);
184 for (const auto s : elem.side_index_range())
185 {
186 Sample sample;
187 if (const Elem * neighbor = elem.neighbor_ptr(s))
188 sample = {neighbor->true_centroid() - x_elem, neighbor->dof_number(sys_num, 0, 0), 0};
189 else
190 {
191 const FaceGeometry face = face_geometry(elem, s);
192 sample = {face.centroid - x_elem, DofObject::invalid_id, exact_solution(face.centroid)};
193 }
194
195 for (const auto i : make_range(dim))
196 for (const auto j : make_range(dim))
197 normal_matrix(i, j) += sample.offset(i) * sample.offset(j);
198 samples.push_back(sample);
199 }
200
201 // g = sum_k w_k (u_k - u_elem), with weights w_k solving
202 // normal_matrix w_k = x_k - x_elem.
203 GradientStencil g;
204 const dof_id_type elem_dof = elem.dof_number(sys_num, 0, 0);
205 for (const auto & sample : samples)
206 {
207 DenseVector<Real> rhs(dim), w;
208 for (const auto i : make_range(dim))
209 rhs(i) = sample.offset(i);
210 normal_matrix.lu_solve(rhs, w);
211
213 for (const auto i : make_range(dim))
214 weight(i) = w(i);
215
216 g.coefficients[elem_dof] -= weight;
217 if (sample.dof == DofObject::invalid_id)
218 g.constant += sample.value * weight;
219 else
220 g.coefficients[sample.dof] += weight;
221 }
222
223 return g;
224}
225
226} // anonymous namespace
227
228// Assembles one matrix row per local cell from the balance
229// sum_faces (-D grad(u) + v u) . n |face| = f(x_elem) |elem|.
230void
231assemble_advection_diffusion(EquationSystems & es, const std::string & system_name)
232{
233 const MeshBase & mesh = es.get_mesh();
234 auto & system = es.get_system<LinearImplicitSystem>(system_name);
235 const unsigned int sys_num = system.number();
236
237 const Real diffusivity = es.parameters.get<Real>("diffusivity");
238 const RealVectorValue velocity = es.parameters.get<RealVectorValue>("velocity");
239
240 for (const Elem * elem : mesh.active_local_element_ptr_range())
241 {
242 const dof_id_type elem_dof = elem->dof_number(sys_num, 0, 0);
243 const Point x_elem = elem->true_centroid();
244 const GradientStencil grad_elem = gradient_stencil(*elem, sys_num);
245
246 // The net outward flux through the cell boundary
247 ScalarStencil net_flux;
248
249 for (const auto s : elem->side_index_range())
250 {
251 const FaceGeometry face = face_geometry(*elem, s);
252 const Elem * neighbor = elem->neighbor_ptr(s);
253
254 // On interior faces, the neighbor centroid and gradient; on
255 // boundary faces, the face centroid, where the Dirichlet
256 // value is known, and the gradient of this cell.
257 const Point x_across = neighbor ? neighbor->true_centroid() : face.centroid;
258 const GradientStencil grad_neighbor =
259 neighbor ? gradient_stencil(*neighbor, sys_num) : GradientStencil();
260 const GradientStencil grad_face = neighbor ? average(grad_elem, grad_neighbor) : grad_elem;
261
262 // Diffusion: with d the offset from x_elem to x_across,
263 // n.grad(u) ~= (u_across - u_elem) / (d.n) + (n - d / (d.n)).grad(u)_face,
264 // which is exact for linear u. The second term vanishes when d
265 // is parallel to n.
266 const RealVectorValue d = x_across - x_elem;
267 const Real d_dot_n = d * face.normal;
268 const Real diffusive_scale = -diffusivity * face.area;
269
270 net_flux.coefficients[elem_dof] -= diffusive_scale / d_dot_n;
271 if (neighbor)
272 net_flux.coefficients[neighbor->dof_number(sys_num, 0, 0)] += diffusive_scale / d_dot_n;
273 else
274 net_flux.constant += diffusive_scale / d_dot_n * exact_solution(face.centroid);
275 add_dot(net_flux, grad_face, face.normal - d / d_dot_n, diffusive_scale);
276
277 // Advection: the upwind cell value, extrapolated to the face
278 // centroid with that cell's gradient. Inflow boundary faces
279 // take the Dirichlet value.
280 const Real mass_flux = (velocity * face.normal) * face.area;
281 if (mass_flux >= 0)
282 {
283 net_flux.coefficients[elem_dof] += mass_flux;
284 add_dot(net_flux, grad_elem, face.centroid - x_elem, mass_flux);
285 }
286 else if (neighbor)
287 {
288 net_flux.coefficients[neighbor->dof_number(sys_num, 0, 0)] += mass_flux;
289 add_dot(net_flux, grad_neighbor, face.centroid - x_across, mass_flux);
290 }
291 else
292 net_flux.constant += mass_flux * exact_solution(face.centroid);
293 }
294
295 std::vector<dof_id_type> row_dofs = {elem_dof};
296 std::vector<dof_id_type> column_dofs;
297 DenseMatrix<Number> Ke(1, net_flux.coefficients.size());
298 for (const auto & [dof, coef] : net_flux.coefficients)
299 {
300 Ke(0, column_dofs.size()) = coef;
301 column_dofs.push_back(dof);
302 }
303
305 Fe(0) = forcing(x_elem, diffusivity, velocity) * elem->volume() - net_flux.constant;
306
307 system.matrix->add_matrix(Ke, row_dofs, column_dofs);
308 system.rhs->add_vector(Fe, row_dofs);
309 }
310}
311
312// Returns the L2 norm of the difference between the cell values and
313// the exact solution sampled at cell centroids.
314Real
316{
317 const unsigned int sys_num = system.number();
318
319 Real error_sq = 0;
320 for (const Elem * elem : system.get_mesh().active_local_element_ptr_range())
321 {
322 const Real u_h = libmesh_real(system.current_solution(elem->dof_number(sys_num, 0, 0)));
323 const Real diff = u_h - exact_solution(elem->true_centroid());
324 error_sq += elem->volume() * diff * diff;
325 }
326 system.comm().sum(error_sq);
327
328 return std::sqrt(error_sq);
329}
330
331int
332main(int argc, char ** argv)
333{
334 // Initialize libMesh.
335 LibMeshInit init(argc, argv);
336
337 // This example requires a linear solver package.
338 libmesh_example_requires(libMesh::default_solver_package() != INVALID_SOLVER_PACKAGE,
339 "--enable-petsc, --enable-trilinos, or --enable-eigen");
340
341 // The mesh is two-dimensional.
342 libmesh_example_requires(2 <= LIBMESH_DIM, "2D support");
343
344 // Parse the input file, allowing the command line to override it.
345 GetPot infile("finite_volume_ex1.in");
346 infile.parse_command_line(argc, argv);
347
348 const unsigned int coarse_grid_size = infile("coarse_grid_size", 4);
349 const unsigned int max_refinements = infile("max_refinements", 6);
350 const Real diffusivity = infile("diffusivity", 1.);
351 const RealVectorValue velocity(infile("velocity_x", 1.), infile("velocity_y", 0.5));
352
353 // The scheme is second order, and a measured rate within this
354 // distance of 2 is taken as confirmation of that order. The bound is
355 // loose enough to absorb the pre-asymptotic behavior of the error on
356 // the coarsest grids and the round-off of any supported Real
357 // precision, and tight enough to catch a loss of second-order
358 // accuracy. Dropping the advection reconstruction reduces the scheme
359 // to first order, and dropping the non-orthogonal diffusion
360 // correction stops the error from converging at all.
361 const Real expected_rate = 2;
362 const Real rate_tolerance = 0.1;
363
364 std::vector<Real> errors;
365 bool converged = false;
366 for (const auto level : make_range(max_refinements + 1))
367 {
368 const unsigned int grid_size = coarse_grid_size << level;
369
370 Mesh mesh(init.comm());
371 EquationSystems equation_systems(mesh);
372 auto & system = equation_systems.add_system<LinearImplicitSystem>("AdvectionDiffusion");
373 system.add_variable("u", CONSTANT, MONOMIAL);
374 system.attach_assemble_function(assemble_advection_diffusion);
375
376 // Couple each cell to two layers of side neighbors. The DofMap
377 // registers its coupling functor with the mesh, so building the
378 // mesh after this call also retains the corresponding ghost
379 // elements on a distributed mesh.
380 system.get_dof_map().default_coupling().set_n_levels(2);
381
382 MeshTools::Generation::build_square(mesh, grid_size, grid_size, 0., 1., 0., 1., C0POLYGON);
383
384 equation_systems.parameters.set<Real>("diffusivity") = diffusivity;
385 equation_systems.parameters.set<RealVectorValue>("velocity") = velocity;
386 equation_systems.init();
387
388 system.solve();
389
390 const Real error = centroid_l2_error(system);
391 errors.push_back(error);
392 libMesh::out << "Grid size " << grid_size << ": " << mesh.n_active_elem()
393 << " cells, centroid L2 error " << error;
394 if (level)
395 libMesh::out << ", convergence rate " << std::log2(errors[level - 1] / error);
396
397 // Fit a line to log2(error) against log2(h) over the last three
398 // grids. The grid spacing h halves at each level, so the sample
399 // points are equally spaced and the least-squares slope reduces
400 // to the difference between the first and last points divided by
401 // their separation of two levels.
402 if (level >= 2)
403 {
404 const Real fit_rate = std::log2(errors[level - 2] / error) / 2;
405 libMesh::out << ", three-grid fit rate " << fit_rate;
406 converged = std::abs(fit_rate - expected_rate) < rate_tolerance;
407 }
408 libMesh::out << std::endl;
409
410 if (converged)
411 {
412#ifdef LIBMESH_HAVE_EXODUS_API
413 ExodusII_IO(mesh).write_equation_systems("out.e", equation_systems);
414#endif
415 break;
416 }
417 }
418
419 libmesh_error_msg_if(!converged,
420 "The three-grid fit of the L2 error convergence rate did not reach "
421 << expected_rate << " +/- " << rate_tolerance << " within "
422 << max_refinements << " refinements");
423
424 return 0;
425}
unsigned int dim
Number(* exact_solution)(const Point &p, const Parameters &, const std::string &, const std::string &)
Defines a dense matrix for use in Finite Element-type computations.
Defines a dense vector for use in Finite Element-type computations.
dof_id_type dof_number(const unsigned int s, const unsigned int var, const unsigned int comp) const
static constexpr dof_id_type invalid_id
An invalid id to distinguish an uninitialized DofObject.
Definition dof_object.h:473
This is the base class from which all geometric element types are derived.
Definition elem.h:96
virtual Point true_centroid() const
Definition elem.C:515
virtual unsigned short dim() const =0
virtual Point side_vertex_average_normal(const unsigned int s) const
Definition elem.C:3922
virtual Real volume() const
Definition elem.C:3865
const Elem * neighbor_ptr(unsigned int i) const
Definition elem.h:2736
IntRange< unsigned short > side_index_range() const
Definition elem.h:2848
virtual std::unique_ptr< Elem > build_side_ptr(const unsigned int i)=0
This is the EquationSystems class.
const MeshBase & get_mesh() const
Parameters parameters
Data structure holding arbitrary parameters.
virtual void init()
Initialize all the systems.
virtual System & add_system(std::string_view system_type, std::string_view name)
Add the system of type system_type named name to the systems array.
const T_sys & get_system(std::string_view name) const
The ExodusII_IO class implements reading meshes in the ExodusII file format from Sandia National Labs...
Definition exodusII_io.h:53
virtual void write_equation_systems(const std::string &fname, const EquationSystems &es, const std::set< std::string > *system_names=nullptr) override
Writes out the solution for no specific time or timestep.
The LibMeshInit class, when constructed, initializes the dependent libraries (e.g.
Definition libmesh.h:92
Manages consistently variables, degrees of freedom, coefficient vectors, matrices and linear solvers ...
This is the MeshBase class.
Definition mesh_base.h:81
virtual dof_id_type n_active_elem() const =0
The Mesh class is a thin wrapper, around the ReplicatedMesh class by default.
Definition mesh.h:51
const Parallel::Communicator & comm() const
T & set(const std::string &)
Definition parameters.h:494
const T & get(std::string_view) const
Definition parameters.h:451
A Point defines a location in LIBMESH_DIM dimensional Real space.
Definition point.h:41
Manages consistently variables, degrees of freedom, and coefficient vectors.
Definition system.h:100
Number current_solution(const dof_id_type global_dof_number) const
Definition system.C:162
unsigned int add_variable(std::string_view var, const FEType &type, const std::set< subdomain_id_type > *const active_subdomains=nullptr)
Adds the variable var to the list of variables for this system.
Definition system.C:1344
unsigned int number() const
Definition system.h:2393
const MeshBase & get_mesh() const
Definition system.h:2401
static const Real b
Number forcing(const Point &p)
Real centroid_l2_error(const System &system)
void assemble_advection_diffusion(EquationSystems &es, const std::string &system_name)
MeshBase & mesh
void build_square(UnstructuredMesh &mesh, const unsigned int nx, const unsigned int ny, const Real xmin=0., const Real xmax=1., const Real ymin=0., const Real ymax=1., const ElemType type=INVALID_ELEM, const bool gauss_lobatto_grid=false)
A specialized build_cube() for 2D meshes.
void scale(MeshBase &mesh, const Real xs, const Real ys=0., const Real zs=0.)
Scales the mesh.
dof_id_type weight(const MeshBase &mesh, const processor_id_type pid)
Definition mesh_tools.C:444
The libMesh namespace provides an interface to certain functionality in the library.
SolverPackage default_solver_package()
Definition libmesh.C:1064
OStreamProxy out
const Real pi
.
Definition libmesh.h:292
LIBMESH_DEVICE_INLINE T libmesh_real(T a)
uint8_t dof_id_type
Definition id_types.h:67
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
IntRange< T > make_range(T beg, T end)
The 2-parameter make_range() helper function returns an IntRange<T> when both input parameters are of...
Definition int_range.h:176
int main()
static const bool value
Definition xdr_io.C:55