libMesh
vector_fe_ex7.C
Go to the documentation of this file.
1 // The libMesh Finite Element Library.
2 // Copyright (C) 2002-2025 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 
19 // <h1>Vector Finite Elements Example 7 - Hybridized Raviart-Thomas elements</h1>
20 // \author Alexander Lindsay
21 // \date 2023
22 //
23 // This example hybridizes Raviart-Thomas elements to solve a model div-grad
24 // problem in both 2d and 3d. Before hybridization, the mixed problem is simply
25 // a div-grad formulation, \vec{u} = -\nabla p, and \nabla \cdot \vec{u} = f, of
26 // the Poisson problem in Introduction Example 3, \nabla^2 p = -f. A standard
27 // (non-hybridized) Raviart-Thomas discretization of the same problem can be
28 // found in Vector Example 6.
29 //
30 // One of the first references for the hybridized Raviart-Thomas (RT)
31 // formulation is Arnold's and Brezzi's "Mixed and nonconforming finite element
32 // methods: implementation, postprocessing and error estimates". The key piece
33 // to the hybridized formulation is breaking the continuity of the RT space,
34 // e.g. we no longer require that the normal component of the RT space be
35 // continuous across interelement boundaries. In the notation of Arnold and
36 // Brezzi this means a change from RT_0^k -> RT_{-1}^k where k is the polynomial
37 // order of the basis (note that here k denotes the polynomial order of the
38 // *divergence* of the vector shape functions, so for k = 0, the RT basis in
39 // libMesh is FIRST). This breakage in continuity is accomplished by changing
40 // from an FEFamily of RAVIART_THOMAS to L2_RAVIART_THOMAS. Instead of being
41 // enforced by the finite element space itself, continuity of the normal
42 // component of the vector field is enforced through Lagrange multipliers that
43 // live on the sides of the elements. Let's introduce a subspace of L2, M_{-1}^k,
44 // where the functions in M_{-1}^k are polynomials of degree of k or less. These
45 // polynomials can live on our elements (I_h) or on our internal element faces
46 // (E0_h); we will denote these polynomial subspaces respectively as M_{-1}^k (I_h)
47 // and M_{-1}^k (E0_h). We will denote boundary faces by E. The hybridized
48 // problem can be summarized as follows:
49 //
50 // find the approximate solutions (u_h, p_h, lambda_h) in
51 // RT_{-1}^k x M_{-1}^k (I_h) x M^{-1}^k (E0_h)
52 // such that
53 //
54 // (u, tau) - (p, div(tau)) + <lambda, tau*n>_E0 = -<g, tau*n>_E for all tau in RT_{-1}^k (I_h)
55 // -(v, div(u)) = -(f, v) for all v in M_{-1}^k (I_h)
56 // <mu, u*n> = 0 for all mu in M_{-1}^k (E0_h)
57 //
58 // with p = g on the boundary (Dirichlet problem). In the above, n denotes the
59 // normal vector facing outward from the element for which we are doing local
60 // assembly
61 //
62 // We can write this in a matrix form:
63 // (A B C)(u) (G)
64 // (Bt 0 0)(p) = (F)
65 // (Ct 0 0)(lambda) (0)
66 //
67 // The matrix A is block diagonal, e.g. it is entirely localized within an
68 // element due to the breakage of the continuity requirements of the RT
69 // space. This allows us to write:
70 //
71 // u = A^{-1} (G - Bp - C lambda)
72 //
73 // We can further eliminate p to end up with a global system that depends only on lambda:
74 //
75 // E lambda = H
76 //
77 // where
78 //
79 // E = Ct * A^{-1} * (A - B * S^{-1} * Bt) * A^{-1} * C
80 // S = Bt * A^{-1} * B
81 // H = Hg + Hf
82 // Hg = Ct * A^{-1} * (A - B * S^{-1} * Bt) * A^{-1} * G
83 // Hf = Ct * A^{-1} * B * S^{-1} * F
84 //
85 // Here in our example we compose local element matrices A, B, C, Bt, Ct, G, and
86 // F using finite element assembly. Then due to the small size of the local
87 // element matrices, we actually compute the required inverses and build E and H
88 // which is then fed into the global matrix and vector respectively. The
89 // resulting global matrix is symmetric positive definite which is a nice change
90 // over the saddle point standard RT discretization. Moreover, the global system
91 // size of the hybridized problem is less than standard RT.
92 //
93 // Once the global system is solved. We go through finite element assembly a
94 // second time to locally construct the u and p solutions from lambda. Finally,
95 // lambda (and p for non-simplices) are used to reconstruct a higher-order
96 // approximation of p, e.g. a solution using a basis of polynomials of degree k + 1.
97 // Lambda is used in this reconstruction because it represents a
98 // projection of the true solution for p onto M_{-1}^k (E0_h) ; e.g. as is so
99 // often the case, the Lagrange multipliers have a physical meaning
100 
101 // Basic utilities.
102 #include "libmesh/string_to_enum.h"
103 
104 // The solver packages supported by libMesh.
105 #include "libmesh/enum_solver_package.h"
106 
107 // The mesh object and mesh generation and modification utilities.
108 #include "libmesh/mesh.h"
109 #include "libmesh/mesh_generation.h"
110 #include "libmesh/mesh_modification.h"
111 
112 // Matrix and vector types.
113 #include "libmesh/dense_matrix.h"
114 #include "libmesh/sparse_matrix.h"
115 #include "libmesh/dense_vector.h"
116 #include "libmesh/numeric_vector.h"
117 
118 // The finite element object and the geometric element type.
119 #include "libmesh/fe.h"
120 #include "libmesh/elem.h"
121 
122 // Gauss quadrature rules.
123 #include "libmesh/quadrature_gauss.h"
124 
125 // The dof map, which handles degree of freedom indexing.
126 #include "libmesh/dof_map.h"
127 
128 // The system of equations.
129 #include "libmesh/equation_systems.h"
130 #include "libmesh/linear_implicit_system.h"
131 
132 // The exact solution and error computation.
133 #include "libmesh/exact_solution.h"
134 #include "libmesh/enum_norm_type.h"
135 #include "solution_function.h"
136 
137 // I/O utilities.
138 #include "libmesh/getpot.h"
139 #include "libmesh/exodusII_io.h"
140 
141 #ifdef LIBMESH_HAVE_EIGEN_DENSE
142 
143 #include <Eigen/Dense>
144 
145 using namespace libMesh;
146 using namespace Eigen;
147 
148 #ifdef LIBMESH_USE_COMPLEX_NUMBERS
149 typedef MatrixXcd EigenMatrix;
150 typedef VectorXcd EigenVector;
151 #else
152 typedef MatrixXd EigenMatrix;
153 typedef VectorXd EigenVector;
154 #endif
155 
156 void fe_assembly(EquationSystems & es, bool global_solve);
157 void assemble_divgrad(EquationSystems & es, const std::string & system_name);
158 
159 int
160 main(int argc, char ** argv)
161 {
162  // Initialize libMesh.
163  LibMeshInit init(argc, argv);
164 
165  // This example requires a linear solver package.
166  libmesh_example_requires(libMesh::default_solver_package() != INVALID_SOLVER_PACKAGE,
167  "--enable-petsc, --enable-trilinos, or --enable-eigen");
168 
169  // Parse the input file.
170  GetPot infile("vector_fe_ex7.in");
171 
172  // But allow the command line to override it.
173  infile.parse_command_line(argc, argv);
174 
175  // Read in parameters from the command line and the input file.
176  const unsigned int dimension = infile("dim", 2);
177  const unsigned int grid_size = infile("grid_size", 15);
178 
179  // Skip higher-dimensional examples on a lower-dimensional libMesh build.
180  libmesh_example_requires(dimension <= LIBMESH_DIM, dimension << "D support");
181 
182  // Create a mesh, with dimension to be overridden later, distributed
183  // across the default MPI communicator.
184  Mesh mesh(init.comm());
185 
186  // Use the MeshTools::Generation mesh generator to create a uniform
187  // grid on the cube [-1,1]^D. To accomodate Raviart-Thomas elements, we must
188  // use TRI6/7 or QUAD8/9 elements in 2d, or TET14 or HEX27 in 3d.
189  const std::string elem_str = infile("element_type", std::string("TRI6"));
190 
191  libmesh_error_msg_if((dimension == 2 && elem_str != "TRI6" && elem_str != "TRI7" &&
192  elem_str != "QUAD8" && elem_str != "QUAD9") ||
193  (dimension == 3 && elem_str != "TET14" && elem_str != "HEX27"),
194  "You selected "
195  << elem_str
196  << " but this example must be run with TRI6, TRI7, QUAD8, or QUAD9 in 2d"
197  << " or with TET14, or HEX27 in 3d.");
198 
199  if (dimension == 2)
201  mesh, grid_size, grid_size, -1., 1., -1., 1., Utility::string_to_enum<ElemType>(elem_str));
202  else if (dimension == 3)
204  grid_size,
205  grid_size,
206  grid_size,
207  -1.,
208  1.,
209  -1.,
210  1.,
211  -1.,
212  1.,
213  Utility::string_to_enum<ElemType>(elem_str));
214 
215  // Make sure the code is robust against nodal reorderings.
217 
218  // Create an equation systems object.
219  EquationSystems equation_systems(mesh);
220  const bool simplicial = (elem_str == "TRI6") || (elem_str == "TRI7") || (elem_str == "TET14");
221  equation_systems.parameters.set<bool>("simplicial") = simplicial;
222 
223 
224  // Declare the system "DivGrad" and its variables.
225  auto & system = equation_systems.add_system<System>("DivGrad");
226 
227  // Add the LM system
228  auto & lm_system = equation_systems.add_system<LinearImplicitSystem>("Lambda");
229 
230  // Adds the variable "u" and "p" to "DivGrad". "u" will be our vector field
231  // whereas "p" will be the scalar field.
232  system.add_variable("u", FIRST, L2_RAVIART_THOMAS);
233  system.add_variable("p", CONSTANT, MONOMIAL);
234  // We also add a higher order version of our 'p' variable whose solution we
235  // will compute using the Lagrange multiplier field and, for non-simplexes,
236  // the low order 'p' solution
237  system.add_variable("p_enriched", FIRST, MONOMIAL);
238 
239  // Add our Lagrange multiplier to the implicit system
240  lm_system.add_variable("lambda", CONSTANT, SIDE_HIERARCHIC);
241 
242  // Give the system a pointer to the matrix assembly
243  // function. This will be called when needed by the library.
245 
246  // Initialize the data structures for the equation system.
247  equation_systems.init();
248 
249  // Solve the implicit system for the Lagrange multiplier
250  lm_system.solve();
251 
252  // Armed with our Lagrange multiplier solution, we can now compute the vector and scalar solutions
253  fe_assembly(equation_systems, /*global_solve=*/false);
254 
255  //
256  // Now we will compute our solution approximation errors
257  //
258 
259  ExactSolution exact_sol(equation_systems);
260 
261  if (dimension == 2)
262  {
263  SolutionFunction<2> soln_func;
264  SolutionGradient<2> soln_grad;
265 
266  // Build FunctionBase* containers to attach to the ExactSolution object.
267  std::vector<FunctionBase<Number> *> sols(1, &soln_func);
268  std::vector<FunctionBase<Gradient> *> grads(1, &soln_grad);
269 
270  exact_sol.attach_exact_values(sols);
271  exact_sol.attach_exact_derivs(grads);
272  }
273  else if (dimension == 3)
274  {
275  SolutionFunction<3> soln_func;
276  SolutionGradient<3> soln_grad;
277 
278  // Build FunctionBase* containers to attach to the ExactSolution object.
279  std::vector<FunctionBase<Number> *> sols(1, &soln_func);
280  std::vector<FunctionBase<Gradient> *> grads(1, &soln_grad);
281 
282  exact_sol.attach_exact_values(sols);
283  exact_sol.attach_exact_derivs(grads);
284  }
285 
286  // Use higher quadrature order for more accurate error results.
287  int extra_error_quadrature = infile("extra_error_quadrature", 2);
288  if (extra_error_quadrature)
289  exact_sol.extra_quadrature_order(extra_error_quadrature);
290 
291  // Compute the error.
292  exact_sol.compute_error("DivGrad", "u");
293  exact_sol.compute_error("DivGrad", "p");
294 #if !defined(LIBMESH_HAVE_PETSC) || !defined(LIBMESH_USE_REAL_NUMBERS)
295  if (simplicial)
296 #endif
297  exact_sol.compute_error("DivGrad", "p_enriched");
298 
299  // Print out the error values.
300  libMesh::out << "L2 error is: " << exact_sol.l2_error("DivGrad", "u") << std::endl;
301  libMesh::out << "HDiv semi-norm error is: " << exact_sol.error_norm("DivGrad", "u", HDIV_SEMINORM)
302  << std::endl;
303  libMesh::out << "HDiv error is: " << exact_sol.hdiv_error("DivGrad", "u") << std::endl;
304  libMesh::out << "L2 error for p is: " << exact_sol.l2_error("DivGrad", "p") << std::endl;
305 #if !defined(LIBMESH_HAVE_PETSC) || !defined(LIBMESH_USE_REAL_NUMBERS)
306  if (simplicial)
307 #endif
308  libMesh::out << "L2 error p_enriched is: " << exact_sol.l2_error("DivGrad", "p_enriched")
309  << std::endl;
310 
311 #ifdef LIBMESH_HAVE_EXODUS_API
312 
313  // We write the file in the ExodusII format.
314  ExodusII_IO(mesh).write_equation_systems("out.e", equation_systems);
315 
316 #endif // #ifdef LIBMESH_HAVE_EXODUS_API
317 
318  // All done.
319  return 0;
320 }
321 
322 // We will perform finite element assembly twice. The first time to setup the global implicit system
323 // for the Lagrange multiplier degrees of freedom. And then the second time to compute the vector
324 // and scalar field solutions using the already-compute Lagrange multiplier solution
325 void
326 fe_assembly(EquationSystems & es, const bool global_solve)
327 {
328  const MeshBase & mesh = es.get_mesh();
329  const unsigned int dim = mesh.mesh_dimension();
330  // Are our elements simplicial?
331  const bool simplicial = es.parameters.get<bool>("simplicial");
332 
333  // The div-grad, e.g. vector-scalar system
334  auto & system = es.get_system<System>("DivGrad");
335  // Our implicit Lagrange multiplier system
336  auto & lambda_system = es.get_system<LinearImplicitSystem>("Lambda");
337 
338  const auto & dof_map = system.get_dof_map();
339  const auto & lambda_dof_map = lambda_system.get_dof_map();
340 
341  const FEType vector_fe_type = dof_map.variable_type(system.variable_number("u"));
342  const FEType scalar_fe_type = dof_map.variable_type(system.variable_number("p"));
343  const FEType enriched_scalar_fe_type =
344  dof_map.variable_type(system.variable_number("p_enriched"));
345  const FEType lambda_fe_type =
346  lambda_dof_map.variable_type(lambda_system.variable_number("lambda"));
347 
348  // Volumetric FE objects
349  std::unique_ptr<FEVectorBase> vector_fe(FEVectorBase::build(dim, vector_fe_type));
350  std::unique_ptr<FEBase> scalar_fe(FEBase::build(dim, scalar_fe_type));
351  std::unique_ptr<FEBase> enriched_scalar_fe(FEBase::build(dim, enriched_scalar_fe_type));
352 
353  // Volumetric quadrature rule
354  QGauss qrule(dim, FIFTH);
355 
356  // Attach quadrature rules for the FE objects that we will reinit within the element "volume"
357  vector_fe->attach_quadrature_rule(&qrule);
358  scalar_fe->attach_quadrature_rule(&qrule);
359  enriched_scalar_fe->attach_quadrature_rule(&qrule);
360 
361  // Declare finite element objects for boundary integration
362  std::unique_ptr<FEVectorBase> vector_fe_face(FEVectorBase::build(dim, vector_fe_type));
363  std::unique_ptr<FEBase> enriched_scalar_fe_face(FEBase::build(dim, enriched_scalar_fe_type));
364  std::unique_ptr<FEBase> lambda_fe_face(FEBase::build(dim, lambda_fe_type));
365 
366  // Boundary integration requires one quadrature rule with dimensionality one
367  // less than the dimensionality of the element.
368  QGauss qface(dim - 1, FIFTH);
369 
370  // Attach quadrature rules for the FE objects that we will reinit on the element faces
371  vector_fe_face->attach_quadrature_rule(&qface);
372  enriched_scalar_fe_face->attach_quadrature_rule(&qface);
373  lambda_fe_face->attach_quadrature_rule(&qface);
374 
375  // pre-request our required volumetric data
376  const auto & JxW = vector_fe->get_JxW();
377  const auto & q_point = vector_fe->get_xyz();
378  const auto & vector_phi = vector_fe->get_phi();
379  const auto & scalar_phi = scalar_fe->get_phi();
380  const auto & enriched_scalar_phi = enriched_scalar_fe->get_phi();
381  const auto & div_vector_phi = vector_fe->get_div_phi();
382 
383  // pre-request our required element face data
384  const auto & vector_phi_face = vector_fe_face->get_phi();
385  const auto & enriched_scalar_phi_face = enriched_scalar_fe_face->get_phi();
386  const auto & lambda_phi_face = lambda_fe_face->get_phi();
387  const auto & JxW_face = vector_fe_face->get_JxW();
388  const auto & qface_point = vector_fe_face->get_xyz();
389  const auto & normals = vector_fe_face->get_normals();
390 
391  //
392  // We follow the notation of Cockburn in "A Charactrization of Hybridized
393  // Mixed methods for Second Order Elliptic problems"for the element
394  // matrices/vectors. We will need "Eigen" versions of many of the
395  // matrices/vectors because the libMesh DenseMatrix doesn't have an inverse
396  // API
397  //
398 
399  // LM matrix and RHS after eliminating vector and scalar dofs
400  DenseMatrix<Number> E_libmesh;
401  DenseVector<Number> H_libmesh;
402  EigenMatrix E;
403  EigenMatrix H;
404  // Auxiliary matrices and RHS. A is vector-vector. B is vector-scalar. C is
405  // vector-LM. Sinv is the inverse of the Schur complement S which is given by
406  // S = Bt * A^{-1} * B. G is the RHS of the vector equation (e.g. the
407  // Dirichlet condition). F is the RHS of the scalar equation (resulting from
408  // the body force or MMS force in this case). Hg is the post-elimination
409  // version of G, and Hf is the post-elimination version of F
410  EigenMatrix A, Ainv, B, Bt, C, Ct, Sinv;
411  EigenVector G, F, Hg, Hf;
412  // element matrix for boundary LM dofs. We have to have this because our
413  // SIDE_HIERARCHIC variables live on *all* element faces, not just interior
414  // ones.
415  EigenMatrix L;
416 
417  // Lambda eigen vector for constructing vector and scalar solutions
418  EigenVector Lambda;
419  // The lambda solution at the quadrature points
420  std::vector<Number> lambda_qps;
422  std::vector<Number> scalar_qps;
423 
425  DenseMatrix<Number> K_enriched_scalar;
426  DenseVector<Number> F_enriched_scalar, U_enriched_scalar;
427 
428  // Containers for dof indices
429  std::vector<dof_id_type> vector_dof_indices;
430  std::vector<dof_id_type> scalar_dof_indices;
431  std::vector<dof_id_type> enriched_scalar_dof_indices;
432  std::vector<dof_id_type> lambda_dof_indices;
433  std::vector<Number> lambda_solution_std_vec;
434 
435  // The global system matrix
436  SparseMatrix<Number> & matrix = lambda_system.get_system_matrix();
437 
438  for (const auto & elem : mesh.active_local_element_ptr_range())
439  {
440  // Retrive our dof indices for all fields
441  dof_map.dof_indices(elem, vector_dof_indices, system.variable_number("u"));
442  dof_map.dof_indices(elem, scalar_dof_indices, system.variable_number("p"));
443  lambda_dof_map.dof_indices(elem, lambda_dof_indices, lambda_system.variable_number("lambda"));
444 
445  const auto vector_n_dofs = vector_dof_indices.size();
446  const auto scalar_n_dofs = scalar_dof_indices.size();
447  const auto lambda_n_dofs = lambda_dof_indices.size();
448 
449  // Reinit our volume FE objects
450  vector_fe->reinit(elem);
451  scalar_fe->reinit(elem);
452 
453  libmesh_assert_equal_to(vector_n_dofs, vector_phi.size());
454  libmesh_assert_equal_to(scalar_n_dofs, scalar_phi.size());
455 
456  // prepare our matrix/vector data structures for the vector equation
457  A.setZero(vector_n_dofs, vector_n_dofs);
458  B.setZero(vector_n_dofs, scalar_n_dofs);
459  C.setZero(vector_n_dofs, lambda_n_dofs);
460  G.setZero(vector_n_dofs);
461  // and for the scalar equation
462  Bt.setZero(scalar_n_dofs, vector_n_dofs);
463  F.setZero(scalar_n_dofs);
464  // and for the LM equation
465  Ct.setZero(lambda_n_dofs, vector_n_dofs);
466  L.setZero(lambda_n_dofs, lambda_n_dofs);
467 
468  for (const auto qp : make_range(qrule.n_points()))
469  {
470  // Vector equation dependence on vector dofs
471  for (const auto i : make_range(vector_n_dofs))
472  for (const auto j : make_range(vector_n_dofs))
473  A(i, j) += JxW[qp] * (vector_phi[i][qp] * vector_phi[j][qp]);
474 
475  // Vector equation dependence on scalar dofs
476  for (const auto i : make_range(vector_n_dofs))
477  for (const auto j : make_range(scalar_n_dofs))
478  B(i, j) -= JxW[qp] * (div_vector_phi[i][qp] * scalar_phi[j][qp]);
479 
480  // Scalar equation dependence on vector dofs
481  for (const auto i : make_range(scalar_n_dofs))
482  for (const auto j : make_range(vector_n_dofs))
483  Bt(i, j) -= JxW[qp] * (scalar_phi[i][qp] * div_vector_phi[j][qp]);
484 
485  // This is the end of the matrix summation loop
486  // Now we build the element right-hand-side contribution.
487  // This involves a single loop in which we integrate the "forcing
488  // function" in the PDE against the scalar test functions (k).
489  {
490  const Real x = q_point[qp](0);
491  const Real y = q_point[qp](1);
492  const Real z = q_point[qp](2);
493 
494  // "f" is the forcing function for the Poisson equation, which is
495  // just the divergence of the exact solution for the vector field.
496  // This is the well-known "method of manufactured solutions".
497  Real f = 0;
498  if (dim == 2)
499  f = DivGradExactSolution().forcing(x, y);
500  else if (dim == 3)
501  f = DivGradExactSolution().forcing(x, y, z);
502 
503  // Scalar equation RHS
504  for (const auto i : make_range(scalar_n_dofs))
505  F(i) -= JxW[qp] * scalar_phi[i][qp] * f;
506  }
507  }
508 
509  {
510  for (auto side : elem->side_index_range())
511  {
512  // Reinit our face FE objects
513  vector_fe_face->reinit(elem, side);
514  libmesh_assert_equal_to(vector_n_dofs, vector_phi_face.size());
515  lambda_fe_face->reinit(elem, side);
516 
517  if (elem->neighbor_ptr(side) == nullptr)
518  for (const auto qp : make_range(qface.n_points()))
519  {
520  const Real xf = qface_point[qp](0);
521  const Real yf = qface_point[qp](1);
522  const Real zf = qface_point[qp](2);
523 
524  // The boundary value for scalar field.
525  Real scalar_value = 0;
526  if (dim == 2)
527  scalar_value = DivGradExactSolution().scalar(xf, yf);
528  else if (dim == 3)
529  scalar_value = DivGradExactSolution().scalar(xf, yf, zf);
530 
531  // External boundary -> Dirichlet faces -> Vector equaton RHS
532  for (const auto i : make_range(vector_n_dofs))
533  G(i) -= JxW_face[qp] * (vector_phi_face[i][qp] * normals[qp]) * scalar_value;
534 
535  // Need to do something with the external boundary LM dofs to prevent the matrix from
536  // being singular
537  for (const auto i : make_range(lambda_n_dofs))
538  for (const auto j : make_range(lambda_n_dofs))
539  L(i, j) += JxW_face[qp] * lambda_phi_face[i][qp] * lambda_phi_face[j][qp];
540  }
541  else
542  for (const auto qp : make_range(qface.n_points()))
543  {
544  // Vector equation dependence on LM dofs
545  for (const auto i : make_range(vector_n_dofs))
546  for (const auto j : make_range(lambda_n_dofs))
547  C(i, j) +=
548  JxW_face[qp] * (vector_phi_face[i][qp] * normals[qp]) * lambda_phi_face[j][qp];
549 
550  // LM equation dependence on vector dofs
551  for (const auto i : make_range(lambda_n_dofs))
552  for (const auto j : make_range(vector_n_dofs))
553  Ct(i, j) +=
554  JxW_face[qp] * lambda_phi_face[i][qp] * (vector_phi_face[j][qp] * normals[qp]);
555  }
556  }
557  }
558 
559  Ainv = A.inverse();
560  // Compute the Schur complement inverse
561  Sinv = (Bt * Ainv * B).inverse();
562  // These equations can be derived at by eliminating the u and p dofs from the system
563  // (A B C)(u) (G)
564  // (Bt 0 0)(p) = (F)
565  // (Ct 0 0)(lambda) (0)
566  E = Ct * Ainv * (A - B * Sinv * Bt) * Ainv * C;
567  E = E + L;
568  Hg = Ct * Ainv * (A - B * Sinv * Bt) * Ainv * G;
569  Hf = Ct * Ainv * B * Sinv * F;
570  H = Hg + Hf;
571 
572  // Build our libMesh data structures from the Eigen ones
573  E_libmesh.resize(E.rows(), E.cols());
574  H_libmesh.resize(H.size());
575  libmesh_assert((E.rows() == E.cols()) && (E.rows() == H.size()));
576  for (const auto i : make_range(E.rows()))
577  {
578  H_libmesh(i) = H(i);
579  for (const auto j : make_range(E.cols()))
580  E_libmesh(i, j) = E(i, j);
581  }
582 
583  if (global_solve)
584  {
585  // We were performing our finite element assembly for the implicit solve step of our
586  // example. Add our local element vectors/matrices into the global system
587  dof_map.constrain_element_matrix_and_vector(E_libmesh, H_libmesh, lambda_dof_indices);
588  matrix.add_matrix(E_libmesh, lambda_dof_indices);
589  lambda_system.rhs->add_vector(H_libmesh, lambda_dof_indices);
590  }
591  else
592  {
593  //
594  // We are doing our finite element assembly for the second time. We now know the Lagrange
595  // multiplier solution. With that and the local element matrices and vectors we can compute
596  // the vector and scalar solutions
597  //
598 
599  Lambda.resize(lambda_n_dofs);
600  lambda_system.current_local_solution->get(lambda_dof_indices, lambda_solution_std_vec);
601  for (const auto i : make_range(lambda_n_dofs))
602  Lambda(i) = lambda_solution_std_vec[i];
603  const auto scalar_soln = Sinv * Bt * Ainv * G - Sinv * F - Sinv * Bt * Ainv * C * Lambda;
604  const auto vector_soln = Ainv * (G - B * scalar_soln - C * Lambda);
605  for (const auto i : make_range(vector_n_dofs))
606  system.solution->set(vector_dof_indices[i], vector_soln(i));
607  for (const auto i : make_range(scalar_n_dofs))
608  system.solution->set(scalar_dof_indices[i], scalar_soln(i));
609 
610 #if !defined(LIBMESH_HAVE_PETSC) || !defined(LIBMESH_USE_REAL_NUMBERS)
611  if (!simplicial)
612  // We don't support SVD solves in this configuration
613  continue;
614 #endif
615 
616  //
617  // Now solve for the enriched scalar solution using our Lagrange multiplier solution and, for
618  // non-simplexes, the lower-order scalar solution. Note that the Lagrange multiplier
619  // represents the trace of p so it is a logical choice to leverage in this postprocessing
620  // stage!
621  //
622 
623  dof_map.dof_indices(elem, enriched_scalar_dof_indices, system.variable_number("p_enriched"));
624  const auto enriched_scalar_n_dofs = enriched_scalar_dof_indices.size();
625  const auto m = simplicial ? lambda_n_dofs : lambda_n_dofs + 1;
626  const auto n = enriched_scalar_n_dofs;
627 
628  K_enriched_scalar.resize(m, n);
629  F_enriched_scalar.resize(m);
630  U_enriched_scalar.resize(n);
631 
632  // L2 projection of the enriched scalar into the LM space
633  for (const auto side : elem->side_index_range())
634  {
635  vector_fe_face->reinit(elem, side); // for JxW_face and qface_point
636  enriched_scalar_fe_face->reinit(elem, side);
637  lambda_fe_face->reinit(elem, side);
638  if (elem->neighbor_ptr(side) == nullptr)
639  for (const auto qp : make_range(qface.n_points()))
640  {
641  const Real xf = qface_point[qp](0);
642  const Real yf = qface_point[qp](1);
643  const Real zf = qface_point[qp](2);
644 
645  // The boundary value for scalar field.
646  Real scalar_value = 0;
647  if (dim == 2)
648  scalar_value = DivGradExactSolution().scalar(xf, yf);
649  else if (dim == 3)
650  scalar_value = DivGradExactSolution().scalar(xf, yf, zf);
651 
652  for (const auto i : make_range(lambda_n_dofs))
653  {
654  F_enriched_scalar(i) += JxW_face[qp] * lambda_phi_face[i][qp] * scalar_value;
655  for (const auto j : make_range(enriched_scalar_n_dofs))
656  K_enriched_scalar(i, j) +=
657  JxW_face[qp] * lambda_phi_face[i][qp] * enriched_scalar_phi_face[j][qp];
658  }
659  }
660  else
661  {
662  // compute local face lambda solution
663  lambda_qps.resize(qface.n_points());
664  for (auto & lambda_qp : lambda_qps)
665  lambda_qp = 0;
666  for (const auto qp : make_range(qface.n_points()))
667  for (const auto i : index_range(lambda_solution_std_vec))
668  lambda_qps[qp] += lambda_solution_std_vec[i] * lambda_phi_face[i][qp];
669 
670  for (const auto qp : make_range(qface.n_points()))
671  for (const auto i : make_range(lambda_n_dofs))
672  {
673  F_enriched_scalar(i) += JxW_face[qp] * lambda_phi_face[i][qp] * lambda_qps[qp];
674  for (const auto j : make_range(enriched_scalar_n_dofs))
675  K_enriched_scalar(i, j) +=
676  JxW_face[qp] * lambda_phi_face[i][qp] * enriched_scalar_phi_face[j][qp];
677  }
678  }
679  }
680 
681  if (simplicial)
682  K_enriched_scalar.lu_solve(F_enriched_scalar, U_enriched_scalar);
683  else
684  {
685  //
686  // For tensor product elements, the system of equations coming from the L2 projection into
687  // the Lagrange multiplier space is singular. To make the system nonsingular, we add an L2
688  // projection into the low-order scalar solution space.
689  //
690 
691  enriched_scalar_fe->reinit(elem);
692  libmesh_assert(scalar_n_dofs == 1);
693  libmesh_assert(scalar_soln.size() == 1);
694  libmesh_assert(scalar_phi.size() == 1);
695 
696  scalar_qps.resize(qrule.n_points());
697  for (auto & scalar_qp : scalar_qps)
698  scalar_qp = 0;
699  for (const auto qp : make_range(qrule.n_points()))
700  scalar_qps[qp] += scalar_soln(0) * scalar_phi[0][qp];
701  for (const auto qp : make_range(qrule.n_points()))
702  {
703  F_enriched_scalar(n) += JxW[qp] * scalar_phi[0][qp] * scalar_qps[qp];
704  for (const auto j : make_range(n))
705  K_enriched_scalar(n, j) += JxW[qp] * scalar_phi[0][qp] * enriched_scalar_phi[j][qp];
706  }
707  // We have more rows than columns so an LU solve isn't an option
708  K_enriched_scalar.svd_solve(F_enriched_scalar, U_enriched_scalar);
709  }
710 
711  // Our solution for the local enriched scalar dofs is complete. Insert into the global vector
712  system.solution->insert(U_enriched_scalar, enriched_scalar_dof_indices);
713  }
714  }
715 
716  if (!global_solve)
717  {
718  system.solution->close();
719  // Scatter solution into the current_solution which is used in error computation
720  system.update();
721  }
722 }
723 
724 // Call this assembly function when assembling the global implicit system
725 void
726 assemble_divgrad(EquationSystems & es, const std::string & libmesh_dbg_var(system_name))
727 {
728  libmesh_assert_equal_to(system_name, "Lambda");
729  fe_assembly(es, /*global_solve=*/true);
730 }
731 
732 #else
733 
734 int
736 {
737  return 0;
738 }
739 
740 #endif
int main(int argc, char **argv)
class FEType hides (possibly multiple) FEFamily and approximation orders, thereby enabling specialize...
Definition: fe_type.h:196
Eigen::Matrix< Number, Eigen::Dynamic, Eigen::Dynamic > EigenMatrix
This class handles the computation of the L2 and/or H1 error for the Systems in the EquationSystems o...
This is the EquationSystems class.
Real hdiv_error(std::string_view sys_name, std::string_view unknown_name)
unsigned int dim
The ExodusII_IO class implements reading meshes in the ExodusII file format from Sandia National Labs...
Definition: exodusII_io.h:52
void attach_exact_values(const std::vector< FunctionBase< Number > *> &f)
Clone and attach arbitrary functors which compute the exact values of the EquationSystems&#39; solutions ...
Manages consistently variables, degrees of freedom, coefficient vectors, matrices and linear solvers ...
Eigen::Matrix< Number, Eigen::Dynamic, 1 > EigenVector
void resize(const unsigned int n)
Resize the vector.
Definition: dense_vector.h:396
MeshBase & mesh
void extra_quadrature_order(const int extraorder)
Increases or decreases the order of the quadrature rule used for numerical integration.
NumericVector< Number > & add_vector(std::string_view vec_name, const bool projections=true, const ParallelType type=PARALLEL)
Adds the additional vector vec_name to this system.
Definition: system.C:768
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.
virtual void reinit() override
Reinitializes the member data fields associated with the system, so that, e.g., assemble() may be use...
The LibMeshInit class, when constructed, initializes the dependent libraries (e.g.
Definition: libmesh.h:90
VectorXcd EigenVector
The libMesh namespace provides an interface to certain functionality in the library.
void attach_exact_derivs(const std::vector< FunctionBase< Gradient > *> &g)
Clone and attach arbitrary functors which compute the exact gradients of the EquationSystems&#39; solutio...
const T_sys & get_system(std::string_view name) const
This is the MeshBase class.
Definition: mesh_base.h:75
unsigned int variable_number(std::string_view var) const
Definition: system.C:1609
void permute_elements(MeshBase &mesh)
Randomly permute the nodal ordering of each element (without twisting the element mapping)...
SolverPackage default_solver_package()
Definition: libmesh.C:1117
Definition: assembly.h:38
virtual void add_matrix(const DenseMatrix< T > &dm, const std::vector< numeric_index_type > &rows, const std::vector< numeric_index_type > &cols)=0
Add the full matrix dm to the SparseMatrix.
const T & get(std::string_view) const
Definition: parameters.h:426
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.
Definition: exodusII_io.C:2033
Manages consistently variables, degrees of freedom, and coefficient vectors.
Definition: system.h:96
void compute_error(std::string_view sys_name, std::string_view unknown_name)
Computes and stores the error in the solution value e = u-u_h, the gradient grad(e) = grad(u) - grad(...
void init(triangulateio &t)
Initializes the fields of t to nullptr/0 as necessary.
static std::unique_ptr< FEGenericBase > build(const unsigned int dim, const FEType &type)
Builds a specific finite element type.
libmesh_assert(ctx)
MatrixXcd EigenMatrix
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:1357
void fe_assembly(EquationSystems &es, bool global_solve)
unsigned int n_points() const
Definition: quadrature.h:131
void attach_assemble_function(void fptr(EquationSystems &es, const std::string &name))
Register a user function to use in assembling the system matrix and RHS.
Definition: system.C:2161
Real l2_error(std::string_view sys_name, std::string_view unknown_name)
virtual void solve()
Solves the system.
Definition: system.h:347
void lu_solve(const DenseVector< T > &b, DenseVector< T > &x)
Solve the system Ax=b given the input vector b.
const FEType & variable_type(const unsigned int i) const
Definition: system.h:2508
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
T & set(const std::string &)
Definition: parameters.h:469
Real forcing(Real x, Real y)
OStreamProxy out
const MeshBase & get_mesh() const
void resize(const unsigned int new_m, const unsigned int new_n)
Resizes the matrix to the specified size and calls zero().
Definition: dense_matrix.h:895
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:140
This class implements specific orders of Gauss quadrature.
unsigned int mesh_dimension() const
Definition: mesh_base.C:372
Parameters parameters
Data structure holding arbitrary parameters.
std::unique_ptr< NumericVector< Number > > current_local_solution
All the values I need to compute my contribution to the simulation at hand.
Definition: system.h:1605
virtual void init()
Initialize all the systems.
void assemble_divgrad(EquationSystems &es, const std::string &system_name)
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.
The Mesh class is a thin wrapper, around the ReplicatedMesh class by default.
Definition: mesh.h:50
const DofMap & get_dof_map() const
Definition: system.h:2374
void svd_solve(const DenseVector< T > &rhs, DenseVector< T > &x, Real rcond=std::numeric_limits< Real >::epsilon()) const
Solve the system of equations for in the least-squares sense.
auto index_range(const T &sizable)
Helper function that returns an IntRange<std::size_t> representing all the indices of the passed-in v...
Definition: int_range.h:117
void build_cube(UnstructuredMesh &mesh, const unsigned int nx=0, const unsigned int ny=0, const unsigned int nz=0, const Real xmin=0., const Real xmax=1., const Real ymin=0., const Real ymax=1., const Real zmin=0., const Real zmax=1., const ElemType type=INVALID_ELEM, const bool gauss_lobatto_grid=false)
Builds a (elements) cube.
Real scalar(Real x, Real y)
Real error_norm(std::string_view sys_name, std::string_view unknown_name, const FEMNormType &norm)