libMesh
vector_fe_ex8.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 // <h1>Vector Finite Elements Example 8 - Single Face Hybridizable Discontinuous Galerkin</h1>
19 // \author Alexander Lindsay
20 // \date 2023
21 //
22 // This example is identical to Example 7 with the exception that the local
23 // solvers are built using a Single Face Discontinuous Galerkin
24 // formulation as opposed to a Raviart-Thomas Galerkin formulation. Equal order
25 // polynomials are used to discretize the scalar and vector fields. "Single
26 // Face" (SF) refers to the stabilization term added to the trace of the
27 // flux. Whereas in example 7, the trace of the flux \hat{u} is simply set to
28 // the flux u, in this SF-DG formulation \hat{u} = u + \tau * (p - \lambda)
29 // where \tau is only nonzero on a single element face
30 
31 // Basic utilities.
32 #include "libmesh/string_to_enum.h"
33 
34 // The solver packages supported by libMesh.
35 #include "libmesh/enum_solver_package.h"
36 
37 // The mesh object and mesh generation and modification utilities.
38 #include "libmesh/mesh.h"
39 #include "libmesh/mesh_generation.h"
40 #include "libmesh/mesh_modification.h"
41 
42 // Matrix and vector types.
43 #include "libmesh/dense_matrix.h"
44 #include "libmesh/sparse_matrix.h"
45 #include "libmesh/dense_vector.h"
46 #include "libmesh/numeric_vector.h"
47 
48 // The finite element object and the geometric element type.
49 #include "libmesh/fe.h"
50 #include "libmesh/fe_interface.h"
51 #include "libmesh/elem.h"
52 
53 // Gauss quadrature rules.
54 #include "libmesh/quadrature_gauss.h"
55 
56 // The dof map, which handles degree of freedom indexing.
57 #include "libmesh/dof_map.h"
58 
59 // The system of equations.
60 #include "libmesh/equation_systems.h"
61 #include "libmesh/linear_implicit_system.h"
62 
63 // The exact solution and error computation.
64 #include "libmesh/exact_solution.h"
65 #include "libmesh/enum_norm_type.h"
66 #include "solution_function.h"
67 
68 // I/O utilities.
69 #include "libmesh/getpot.h"
70 #include "libmesh/exodusII_io.h"
71 
72 #ifdef LIBMESH_HAVE_EIGEN_DENSE
73 
74 #include <Eigen/Dense>
75 
76 using namespace libMesh;
77 using namespace Eigen;
78 
79 #ifdef LIBMESH_USE_COMPLEX_NUMBERS
80 typedef MatrixXcd EigenMatrix;
81 typedef VectorXcd EigenVector;
82 #else
83 typedef MatrixXd EigenMatrix;
84 typedef VectorXd EigenVector;
85 #endif
86 
87 void fe_assembly(EquationSystems & es, bool global_solve);
88 void assemble_hdg(EquationSystems & es, const std::string & system_name);
89 void alternative_fe_assembly(EquationSystems & es, bool global_solve);
90 void alternative_assemble_hdg(EquationSystems & es, const std::string & system_name);
91 
92 int
93 main(int argc, char ** argv)
94 {
95  // Initialize libMesh.
96  LibMeshInit init(argc, argv);
97 
98  // This example requires a linear solver package.
99  libmesh_example_requires(libMesh::default_solver_package() != INVALID_SOLVER_PACKAGE,
100  "--enable-petsc, --enable-trilinos, or --enable-eigen");
101 
102  // Parse the input file.
103  GetPot infile("vector_fe_ex8.in");
104 
105  // But allow the command line to override it.
106  infile.parse_command_line(argc, argv);
107 
108  // Read in parameters from the command line and the input file.
109  const unsigned int dimension = infile("dim", 2);
110  const unsigned int grid_size = infile("grid_size", 15);
111  const unsigned int order = infile("order", 1);
112  const std::string family_str = infile("family", "L2_LAGRANGE");
113  const FEFamily family = Utility::string_to_enum<FEFamily>(family_str);
114  const std::string vec_family_str = infile("vecfamily", "L2_LAGRANGE_VEC");
115  const FEFamily vec_family = Utility::string_to_enum<FEFamily>(vec_family_str);
116 
117  // Skip higher-dimensional examples on a lower-dimensional libMesh build.
118  libmesh_example_requires(dimension <= LIBMESH_DIM, dimension << "D support");
119 
120  // Create a mesh, with dimension to be overridden later, distributed
121  // across the default MPI communicator.
122  Mesh mesh(init.comm());
123 
124  // Use the MeshTools::Generation mesh generator to create a uniform
125  // grid on the cube [-1,1]^D. To accomodate first order side hierarchics, we must
126  // use TRI6/7 or QUAD8/9 elements in 2d, or TET14 or HEX27 in 3d.
127  const std::string elem_str = infile("element_type", std::string("TRI6"));
128 
129  libmesh_error_msg_if((dimension == 2 && elem_str != "TRI6" && elem_str != "TRI7" &&
130  elem_str != "QUAD8" && elem_str != "QUAD9") ||
131  (dimension == 3 && elem_str != "TET14" && elem_str != "HEX27"),
132  "You selected "
133  << elem_str
134  << " but this example must be run with TRI6, TRI7, QUAD8, or QUAD9 in 2d"
135  << " or with TET14, or HEX27 in 3d.");
136 
137  if (dimension == 2)
139  mesh, grid_size, grid_size, -1., 1., -1., 1., Utility::string_to_enum<ElemType>(elem_str));
140  else if (dimension == 3)
142  grid_size,
143  grid_size,
144  grid_size,
145  -1.,
146  1.,
147  -1.,
148  1.,
149  -1.,
150  1.,
151  Utility::string_to_enum<ElemType>(elem_str));
152 
153  // Make sure the code is robust against nodal reorderings.
155 
156  // Create an equation systems object.
157  EquationSystems equation_systems(mesh);
158 
159  // Declare the system "Mixed" and its variables.
160  auto & system = equation_systems.add_system<System>("Mixed");
161 
162  // Add the LM system
163  auto & lm_system = equation_systems.add_system<LinearImplicitSystem>("Lambda");
164 
165  // Adds the variable "q" and "u" to "Mixed". "q" will be our vector field
166  // whereas "u" will be the scalar field.
167  system.add_variable("q", static_cast<Order>(order), vec_family);
168  system.add_variable("u", static_cast<Order>(order), family);
169 
170  // We also add a higher order version of our 'p' variable whose solution we
171  // will postprocess using the Lagrange multiplier, 'u', and the low order 'p'
172  // solution
173  system.add_variable("u_enriched", static_cast<Order>(order+1), family);
174 
175  // Add our Lagrange multiplier to the implicit system
176  lm_system.add_variable("lambda", static_cast<Order>(order), SIDE_HIERARCHIC);
177 
178  // Give the system a pointer to the matrix assembly
179  // function. This will be called when needed by the library.
181 
182  // Initialize the data structures for the equation system.
183  equation_systems.init();
184 
185  // Solve the implicit system for the Lagrange multiplier
186  lm_system.solve();
187 
188  // Armed with our Lagrange multiplier solution, we can now compute the vector and scalar solutions
189  fe_assembly(equation_systems, /*global_solve=*/false);
190 
191  //
192  // Now we will compute our solution approximation errors
193  //
194 
195  ExactSolution exact_sol(equation_systems);
196 
197  if (dimension == 2)
198  {
199  SolutionFunction<2> soln_func;
200  SolutionGradient<2> soln_grad;
201 
202  // Build FunctionBase* containers to attach to the ExactSolution object.
203  std::vector<FunctionBase<Number> *> sols(1, &soln_func);
204  std::vector<FunctionBase<Gradient> *> grads(1, &soln_grad);
205 
206  exact_sol.attach_exact_values(sols);
207  exact_sol.attach_exact_derivs(grads);
208  }
209  else if (dimension == 3)
210  {
211  SolutionFunction<3> soln_func;
212  SolutionGradient<3> soln_grad;
213 
214  // Build FunctionBase* containers to attach to the ExactSolution object.
215  std::vector<FunctionBase<Number> *> sols(1, &soln_func);
216  std::vector<FunctionBase<Gradient> *> grads(1, &soln_grad);
217 
218  exact_sol.attach_exact_values(sols);
219  exact_sol.attach_exact_derivs(grads);
220  }
221 
222  // Use higher quadrature order for more accurate error results.
223  int extra_error_quadrature = infile("extra_error_quadrature", 2);
224  if (extra_error_quadrature)
225  exact_sol.extra_quadrature_order(extra_error_quadrature);
226 
227  // Compute the error.
228  exact_sol.compute_error("Mixed", "q");
229  exact_sol.compute_error("Mixed", "u");
230  exact_sol.compute_error("Mixed", "u_enriched");
231 
232  // Print out the error values.
233  libMesh::out << "L2 error is: " << exact_sol.l2_error("Mixed", "q") << std::endl;
234  libMesh::out << "L2 error for u is: " << exact_sol.l2_error("Mixed", "u") << std::endl;
235  libMesh::out << "L2 error u_enriched is: " << exact_sol.l2_error("Mixed", "u_enriched")
236  << std::endl;
237 
238 #ifdef LIBMESH_HAVE_EXODUS_API
239 
240  // We write the file in the ExodusII format.
241  ExodusII_IO(mesh).write_equation_systems("out.e", equation_systems);
242 
243 #endif // #ifdef LIBMESH_HAVE_EXODUS_API
244 
245  // Allright let's dry a different implementation and then ensure we get the same results
246  // auto mixed_soln = system.solution->clone();
247  // auto lm_soln = lm_system.solution->clone();
249  lm_system.solve();
250  // Armed with our Lagrange multiplier solution, we can now compute the vector and scalar solutions
251  alternative_fe_assembly(equation_systems, /*global_solve=*/false);
252 
253  // Compute the error.
254  exact_sol.compute_error("Mixed", "q");
255  exact_sol.compute_error("Mixed", "u");
256  exact_sol.compute_error("Mixed", "u_enriched");
257  // Print out the error values.
258  libMesh::out << "L2 error is: " << exact_sol.l2_error("Mixed", "q") << std::endl;
259  libMesh::out << "L2 error for u is: " << exact_sol.l2_error("Mixed", "u") << std::endl;
260  libMesh::out << "L2 error u_enriched is: " << exact_sol.l2_error("Mixed", "u_enriched")
261  << std::endl;
262 
263  // All done.
264  return 0;
265 }
266 
267 // compute a solution indexable at quadrature points composed from the local degree of freedom
268 // solution vector and associated basis functions
269 template <typename SolnType, typename PhiType, typename LocalSolutionVector>
270 void
271 compute_qp_soln(std::vector<SolnType> & qp_vec,
272  const unsigned int n_qps,
273  const std::vector<std::vector<PhiType>> & phi,
274  const LocalSolutionVector & soln)
275 {
276  libmesh_assert(cast_int<std::size_t>(soln.size()) == phi.size());
277  qp_vec.resize(n_qps);
278  for (auto & val : qp_vec)
279  val = {};
280  for (const auto i : index_range(phi))
281  {
282  const auto & qp_phis = phi[i];
283  libmesh_assert(qp_phis.size() == n_qps);
284  const auto sol = soln(i);
285  for (const auto qp : make_range(n_qps))
286  qp_vec[qp] += qp_phis[qp] * sol;
287  }
288 }
289 
290 void
292  const DofMap & dof_map,
293  System & system,
294  const Elem * const elem,
295  const EigenVector & vector_soln,
296  const EigenVector & scalar_soln,
297  const EigenVector & Lambda,
298  FEVectorBase & vector_fe,
299  FEVectorBase & vector_fe_face,
300  FEBase & scalar_fe,
301  FEBase & scalar_fe_face,
302  FEBase & lambda_fe_face,
303  QBase & qrule,
304  QBase & qface)
305 {
306  const unsigned int dim = mesh.mesh_dimension();
307 
308  // Create FE objects
309  const FEType enriched_scalar_fe_type =
310  dof_map.variable_type(system.variable_number("u_enriched"));
311  std::unique_ptr<FEBase> enriched_scalar_fe(FEBase::build(dim, enriched_scalar_fe_type));
312  std::unique_ptr<FEBase> enriched_scalar_fe_face(FEBase::build(dim, enriched_scalar_fe_type));
313 
314  // Attach quadrature rules
315  enriched_scalar_fe->attach_quadrature_rule(&qrule);
316  enriched_scalar_fe_face->attach_quadrature_rule(&qface);
317 
318  // pre-request our required volumetric data
319  const auto & JxW = vector_fe.get_JxW();
320  const auto & q_point = vector_fe.get_xyz();
321  const auto & scalar_phi = scalar_fe.get_phi();
322  const auto & enriched_scalar_phi = enriched_scalar_fe->get_phi();
323  const auto & enriched_scalar_dphi = enriched_scalar_fe->get_dphi();
324 
325  // pre-request our required element face data
326  const auto & vector_phi_face = vector_fe_face.get_phi();
327  const auto & scalar_phi_face = scalar_fe_face.get_phi();
328  const auto & enriched_scalar_phi_face = enriched_scalar_fe_face->get_phi();
329  const auto & lambda_phi_face = lambda_fe_face.get_phi();
330  const auto & JxW_face = scalar_fe_face.get_JxW();
331  const auto & normals = vector_fe_face.get_normals();
332 
333  // Data structures for computing the enriched scalar solution
334  DenseMatrix<Number> K_enriched_scalar;
335  DenseVector<Number> F_enriched_scalar, U_enriched_scalar;
336  std::vector<dof_id_type> enriched_scalar_dof_indices;
337 
338  // The lambda solution at the quadrature points, used for computing the enriched scalar solution
339  std::vector<Number> lambda_qps;
340  // The scalar solution at the quadrature points, used for computing the enriched scalar solution
341  std::vector<Number> scalar_qps;
342  // The vector solution at the quadrature points, used for computing the enriched scalar solution
343  std::vector<Gradient> vector_qps;
344 
345  dof_map.dof_indices(elem, enriched_scalar_dof_indices, system.variable_number("u_enriched"));
346  const auto enriched_scalar_n_dofs = enriched_scalar_dof_indices.size();
347  // We have to add one for the mean value constraint
348  const auto m = enriched_scalar_n_dofs + 1;
349  const auto n = enriched_scalar_n_dofs + 1;
350 
351  K_enriched_scalar.resize(m, n);
352  F_enriched_scalar.resize(m);
353  U_enriched_scalar.resize(n);
354 
355  enriched_scalar_fe->reinit(elem);
356 
357  // We need the u solution for getting the correct mean value
358  compute_qp_soln(scalar_qps, qrule.n_points(), scalar_phi, scalar_soln);
359 
360  //
361  // We solve a modified diffusion problem
362  //
363 
364  for (const auto qp : make_range(qrule.n_points()))
365  {
366  // Diffusion kernel
367  for (const auto i : make_range(enriched_scalar_n_dofs))
368  for (const auto j : make_range(enriched_scalar_n_dofs))
369  K_enriched_scalar(i, j) +=
370  JxW[qp] * (enriched_scalar_dphi[i][qp] * enriched_scalar_dphi[j][qp]);
371 
372  // Forcing function kernel
373  {
374  const Real x = q_point[qp](0);
375  const Real y = q_point[qp](1);
376  const Real z = q_point[qp](2);
377 
378  // "f" is the forcing function for the Poisson equation, which is
379  // just the divergence of the exact solution for the vector field.
380  // This is the well-known "method of manufactured solutions".
381  Real f = 0;
382  if (dim == 2)
383  f = MixedExactSolution().forcing(x, y);
384  else if (dim == 3)
385  f = MixedExactSolution().forcing(x, y, z);
386 
387  // Scalar equation RHS
388  for (const auto i : make_range(enriched_scalar_n_dofs))
389  F_enriched_scalar(i) += JxW[qp] * enriched_scalar_phi[i][qp] * f;
390  }
391 
392  // Mean value part
393  {
394  // u dependence on LM
395  for (const auto i : make_range(enriched_scalar_n_dofs))
396  K_enriched_scalar(i, enriched_scalar_n_dofs) += JxW[qp] * enriched_scalar_phi[i][qp];
397 
398  // LM dependence on u
399  for (const auto j : make_range(enriched_scalar_n_dofs))
400  K_enriched_scalar(enriched_scalar_n_dofs, j) += JxW[qp] * enriched_scalar_phi[j][qp];
401 
402  // And RHS of LM equation
403  F_enriched_scalar(enriched_scalar_n_dofs) += JxW[qp] * scalar_qps[qp];
404  }
405  }
406 
407  bool tau_found = false;
408  for (auto side : elem->side_index_range())
409  {
410  // Reinit our face FE objects
411  vector_fe_face.reinit(elem, side);
412  compute_qp_soln(vector_qps, qface.n_points(), vector_phi_face, vector_soln);
413 
414  enriched_scalar_fe_face->reinit(elem, side);
415 
416  if (elem->neighbor_ptr(side) == nullptr)
417  for (const auto qp : make_range(qface.n_points()))
418  // Now do the external boundary term for <\hat{q} \cdot \vec{n}, \omega> ->
419  // <q + \tau (u - g), \omega> ->
420  // <q, \omega> + <\tau u, \omega> - <\tau g, \omega>
421  // BUT u = g on the boundary so we can drop those terms and just end up with
422  // <q, \omega>
423  for (const auto i : make_range(enriched_scalar_n_dofs))
424  F_enriched_scalar(i) -=
425  JxW_face[qp] * enriched_scalar_phi_face[i][qp] * vector_qps[qp] * normals[qp];
426  else
427  {
428  scalar_fe_face.reinit(elem, side);
429  compute_qp_soln(scalar_qps, qface.n_points(), scalar_phi_face, scalar_soln);
430  lambda_fe_face.reinit(elem, side);
431  compute_qp_soln(lambda_qps, qface.n_points(), lambda_phi_face, Lambda);
432 
433  // Stabilization parameter. In the single face discretization, only a single face has a
434  // non-zero value of tau
435  const Real tau = tau_found ? 0 : 1 / elem->hmin();
436  tau_found = true;
437 
438  for (const auto qp : make_range(qface.n_points()))
439  {
440  const auto normal = normals[qp];
441  const auto qhat = vector_qps[qp] + tau * (scalar_qps[qp] - lambda_qps[qp]) * normal;
442 
443  // Now do the internal boundary term for <\hat{q} \cdot \vec{n}, \omega> ->
444  for (const auto i : make_range(enriched_scalar_n_dofs))
445  F_enriched_scalar(i) -= JxW_face[qp] * enriched_scalar_phi_face[i][qp] * qhat * normal;
446  }
447  }
448  }
449 
450  K_enriched_scalar.lu_solve(F_enriched_scalar, U_enriched_scalar);
451 
452  // Our solution for the local enriched scalar dofs is complete. Insert into the global vector
453  // after eliminating the mean value constraint dof
454  DenseVector<Number> U_insertion(enriched_scalar_n_dofs);
455  for (const auto i : make_range(enriched_scalar_n_dofs))
456  U_insertion(i) = U_enriched_scalar(i);
457  system.solution->insert(U_insertion, enriched_scalar_dof_indices);
458 }
459 
460 // We will perform finite element assembly twice. The first time to setup the global implicit system
461 // for the Lagrange multiplier degrees of freedom. And then the second time to compute the vector
462 // and scalar field solutions using the already-compute Lagrange multiplier solution
463 void
464 fe_assembly(EquationSystems & es, const bool global_solve)
465 {
466  const MeshBase & mesh = es.get_mesh();
467  const unsigned int dim = mesh.mesh_dimension();
468 
469  // The mixed, e.g. vector-scalar system
470  auto & system = es.get_system<System>("Mixed");
471  // Our implicit Lagrange multiplier system
472  auto & lambda_system = es.get_system<LinearImplicitSystem>("Lambda");
473 
474  const auto & dof_map = system.get_dof_map();
475  const auto & lambda_dof_map = lambda_system.get_dof_map();
476 
477  const FEType vector_fe_type = dof_map.variable_type(system.variable_number("q"));
478  const FEType scalar_fe_type = dof_map.variable_type(system.variable_number("u"));
479  const FEType lambda_fe_type =
480  lambda_dof_map.variable_type(lambda_system.variable_number("lambda"));
481 
482  // Volumetric FE objects
483  std::unique_ptr<FEVectorBase> vector_fe(FEVectorBase::build(dim, vector_fe_type));
484  std::unique_ptr<FEBase> scalar_fe(FEBase::build(dim, scalar_fe_type));
485 
486  // Volumetric quadrature rule
487  QGauss qrule(dim, scalar_fe_type.default_quadrature_order());
488 
489  // Attach quadrature rules for the FE objects that we will reinit within the element "volume"
490  vector_fe->attach_quadrature_rule(&qrule);
491  scalar_fe->attach_quadrature_rule(&qrule);
492 
493  // Declare finite element objects for boundary integration
494  std::unique_ptr<FEVectorBase> vector_fe_face(FEVectorBase::build(dim, vector_fe_type));
495  std::unique_ptr<FEBase> scalar_fe_face(FEBase::build(dim, scalar_fe_type));
496  std::unique_ptr<FEBase> lambda_fe_face(FEBase::build(dim, lambda_fe_type));
497 
498  // Boundary integration requires one quadrature rule with dimensionality one
499  // less than the dimensionality of the element.
500  QGauss qface(dim - 1, scalar_fe_type.default_quadrature_order());
501 
502  // Attach quadrature rules for the FE objects that we will reinit on the element faces
503  vector_fe_face->attach_quadrature_rule(&qface);
504  scalar_fe_face->attach_quadrature_rule(&qface);
505  lambda_fe_face->attach_quadrature_rule(&qface);
506 
507  // pre-request our required volumetric data
508  const auto & JxW = vector_fe->get_JxW();
509  const auto & q_point = vector_fe->get_xyz();
510  const auto & vector_phi = vector_fe->get_phi();
511  const auto & scalar_phi = scalar_fe->get_phi();
512  const auto & grad_scalar_phi = scalar_fe->get_dphi();
513  const auto & div_vector_phi = vector_fe->get_div_phi();
514 
515  // pre-request our required element face data
516  const auto & vector_phi_face = vector_fe_face->get_phi();
517  const auto & scalar_phi_face = scalar_fe_face->get_phi();
518  const auto & lambda_phi_face = lambda_fe_face->get_phi();
519  const auto & JxW_face = scalar_fe_face->get_JxW();
520  const auto & qface_point = vector_fe_face->get_xyz();
521  const auto & normals = vector_fe_face->get_normals();
522 
523  //
524  // We will need "Eigen" versions of many of the matrices/vectors because the
525  // libMesh DenseMatrix doesn't have an inverse API
526  //
527 
528  // LM matrix and RHS after eliminating vector and scalar dofs
529  DenseMatrix<Number> K_libmesh;
530  DenseVector<Number> P_libmesh;
531  EigenMatrix K;
532  EigenVector P;
533  // (A B C)(q) = (G)
534  // (Bt D E)(u) = (F)
535  // (Ct Et H)(l) = (0)
536  // Sinv is the inverse of a Schur complement S which is given by
537  // S = Bt * A^{-1} * B.
538  EigenMatrix A, Ainv, B, Bt, C, Ct, Sinv, D, E, Et, H;
539  EigenVector G, F;
540 
541  // Lambda eigen vector for constructing vector and scalar solutions
542  EigenVector Lambda;
543 
544  // Containers for dof indices
545  std::vector<dof_id_type> vector_dof_indices;
546  std::vector<dof_id_type> scalar_dof_indices;
547  std::vector<dof_id_type> lambda_dof_indices;
548  std::vector<Number> lambda_solution_std_vec;
549 
550  // The global system matrix
551  SparseMatrix<Number> & matrix = lambda_system.get_system_matrix();
552 
553  for (const auto & elem : mesh.active_local_element_ptr_range())
554  {
555  // Retrive our dof indices for all fields
556  dof_map.dof_indices(elem, vector_dof_indices, system.variable_number("q"));
557  dof_map.dof_indices(elem, scalar_dof_indices, system.variable_number("u"));
558  lambda_dof_map.dof_indices(elem, lambda_dof_indices, lambda_system.variable_number("lambda"));
559 
560  const auto vector_n_dofs = vector_dof_indices.size();
561  const auto scalar_n_dofs = scalar_dof_indices.size();
562  const auto lambda_n_dofs = lambda_dof_indices.size();
563 
564  // Reinit our volume FE objects
565  vector_fe->reinit(elem);
566  scalar_fe->reinit(elem);
567 
568  libmesh_assert_equal_to(vector_n_dofs, vector_phi.size());
569  libmesh_assert_equal_to(scalar_n_dofs, scalar_phi.size());
570 
571  // prepare our matrix/vector data structures for the vector equation
572  A.setZero(vector_n_dofs, vector_n_dofs);
573  B.setZero(vector_n_dofs, scalar_n_dofs);
574  C.setZero(vector_n_dofs, lambda_n_dofs);
575  G.setZero(vector_n_dofs);
576  // and for the scalar equation
577  Bt.setZero(scalar_n_dofs, vector_n_dofs);
578  D.setZero(scalar_n_dofs, scalar_n_dofs);
579  E.setZero(scalar_n_dofs, lambda_n_dofs);
580  F.setZero(scalar_n_dofs);
581  // and for the LM equation
582  Ct.setZero(lambda_n_dofs, vector_n_dofs);
583  Et.setZero(lambda_n_dofs, scalar_n_dofs);
584  H.setZero(lambda_n_dofs, lambda_n_dofs);
585 
586  for (const auto qp : make_range(qrule.n_points()))
587  {
588  // Vector equation dependence on vector dofs
589  for (const auto i : make_range(vector_n_dofs))
590  for (const auto j : make_range(vector_n_dofs))
591  A(i, j) -= JxW[qp] * (vector_phi[i][qp] * vector_phi[j][qp]);
592 
593  // Vector equation dependence on scalar dofs
594  for (const auto i : make_range(vector_n_dofs))
595  for (const auto j : make_range(scalar_n_dofs))
596  B(i, j) += JxW[qp] * (div_vector_phi[i][qp] * scalar_phi[j][qp]);
597 
598  // Scalar equation dependence on vector dofs
599  for (const auto i : make_range(scalar_n_dofs))
600  for (const auto j : make_range(vector_n_dofs))
601  Bt(i, j) += JxW[qp] * (grad_scalar_phi[i][qp] * vector_phi[j][qp]);
602 
603  // This is the end of the matrix summation loop
604  // Now we build the element right-hand-side contribution.
605  // This involves a single loop in which we integrate the "forcing
606  // function" in the PDE against the scalar test functions (k).
607  {
608  const Real x = q_point[qp](0);
609  const Real y = q_point[qp](1);
610  const Real z = q_point[qp](2);
611 
612  // "f" is the forcing function for the Poisson equation, which is
613  // just the divergence of the exact solution for the vector field.
614  // This is the well-known "method of manufactured solutions".
615  Real f = 0;
616  if (dim == 2)
617  f = MixedExactSolution().forcing(x, y);
618  else if (dim == 3)
619  f = MixedExactSolution().forcing(x, y, z);
620 
621  // Scalar equation RHS
622  for (const auto i : make_range(scalar_n_dofs))
623  F(i) -= JxW[qp] * scalar_phi[i][qp] * f;
624  }
625  }
626 
627  // At the beginning of the loop, we mark that we haven't found our "Single-Face" yet
628  bool tau_found = false;
629  for (auto side : elem->side_index_range())
630  {
631  // Reinit our face FE objects
632  vector_fe_face->reinit(elem, side);
633  scalar_fe_face->reinit(elem, side);
634  lambda_fe_face->reinit(elem, side);
635 
636  if (elem->neighbor_ptr(side) == nullptr)
637  for (const auto qp : make_range(qface.n_points()))
638  {
639  const Real xf = qface_point[qp](0);
640  const Real yf = qface_point[qp](1);
641  const Real zf = qface_point[qp](2);
642 
643  // The boundary value for scalar field.
644  Real scalar_value = 0;
645  if (dim == 2)
646  scalar_value = MixedExactSolution().scalar(xf, yf);
647  else if (dim == 3)
648  scalar_value = MixedExactSolution().scalar(xf, yf, zf);
649 
650  // External boundary -> Dirichlet faces -> Vector equation RHS
651  for (const auto i : make_range(vector_n_dofs))
652  G(i) += JxW_face[qp] * (vector_phi_face[i][qp] * normals[qp]) * scalar_value;
653 
654  // Now do the external boundary term for <\hat{q} \cdot \vec{n}, \omega> ->
655  // <q + \tau (u - g), \omega> ->
656  // <q, \omega> + <\tau u, \omega> - <\tau g, \omega>
657  // BUT u = g on the boundary so we can drop those terms and just end up with
658  // <q, \omega>
659  for (const auto i : make_range(scalar_n_dofs))
660  for (const auto j : make_range(vector_n_dofs))
661  Bt(i, j) -=
662  JxW_face[qp] * scalar_phi_face[i][qp] * (vector_phi_face[j][qp] * normals[qp]);
663 
664  // Need to do something with the external boundary LM dofs to prevent
665  // the matrix from being singular. Use a minus sign because the
666  // formulation by Cockburn results in negative diagonals for the LM
667  for (const auto i : make_range(lambda_n_dofs))
668  for (const auto j : make_range(lambda_n_dofs))
669  H(i, j) += JxW_face[qp] * lambda_phi_face[i][qp] * lambda_phi_face[j][qp];
670  }
671  else
672  {
673  // If we haven't found our "Single-Face" yet, then we assign tau to
674  // something non-zero. Else we have previously designated our nonzero
675  // "Single-Face" and so we mark tau as zero
676  const Real tau = tau_found ? 0 : 1 / elem->hmin();
677  tau_found = true;
678 
679  for (const auto qp : make_range(qface.n_points()))
680  {
681  const auto normal = normals[qp];
682  const auto normal_sq = normal * normal;
683 
684  // Vector equation dependence on LM dofs
685  for (const auto i : make_range(vector_n_dofs))
686  for (const auto j : make_range(lambda_n_dofs))
687  C(i, j) -=
688  JxW_face[qp] * (vector_phi_face[i][qp] * normals[qp]) * lambda_phi_face[j][qp];
689 
690  // Now do the internal boundary term for <\hat{q} \cdot \vec{n}, \omega> ->
691  // <q + \tau (u - \lambda), \omega> ->
692  // <q, \omega> + <\tau u, \omega> - <\tau \lambda, \omega>
693  for (const auto i : make_range(scalar_n_dofs))
694  {
695  for (const auto j : make_range(vector_n_dofs))
696  Bt(i, j) -= JxW_face[qp] * scalar_phi_face[i][qp] * (vector_phi_face[j][qp] * normal);
697 
698  for (const auto j : make_range(scalar_n_dofs))
699  D(i, j) -=
700  JxW_face[qp] * scalar_phi_face[i][qp] * tau * scalar_phi_face[j][qp] * normal_sq;
701 
702  for (const auto j : make_range(lambda_n_dofs))
703  E(i, j) +=
704  JxW_face[qp] * scalar_phi_face[i][qp] * tau * lambda_phi_face[j][qp] * normal_sq;
705  }
706 
707  // Now do the internal boundary term for <\hat{q} \cdot \vec{n}, \mu> ->
708  // <q + \tau (u - \lambda), \mu> ->
709  // <q, \mu> + <\tau u, \mu> - <\tau \lambda, \mu>
710  for (const auto i : make_range(lambda_n_dofs))
711  {
712  for (const auto j : make_range(vector_n_dofs))
713  Ct(i, j) -= JxW_face[qp] * lambda_phi_face[i][qp] * (vector_phi_face[j][qp] * normal);
714 
715  for (const auto j : make_range(scalar_n_dofs))
716  Et(i, j) -=
717  JxW_face[qp] * lambda_phi_face[i][qp] * tau * scalar_phi_face[j][qp] * normal_sq;
718 
719  for (const auto j : make_range(lambda_n_dofs))
720  H(i, j) +=
721  JxW_face[qp] * lambda_phi_face[i][qp] * tau * lambda_phi_face[j][qp] * normal_sq;
722  }
723  }
724  }
725  }
726 
727  Ainv = A.inverse();
728  // Compute the Schur complement inverse
729  Sinv = (D - Bt * Ainv * B).inverse();
730  const auto BtAinvCMinusE = Bt * Ainv * C - E;
731  const auto BtAinvGMinusF = Bt * Ainv * G - F;
732  const auto CtAinv = Ct * Ainv;
733  const auto BSinv = B * Sinv;
734  const auto EtSinv = Et * Sinv;
735  K = -CtAinv * (BSinv * BtAinvCMinusE + C) + EtSinv * BtAinvCMinusE + H;
736  P = -CtAinv * (BSinv * BtAinvGMinusF + G) + EtSinv * BtAinvGMinusF;
737 
738  // Build our libMesh data structures from the Eigen ones
739  K_libmesh.resize(K.rows(), K.cols());
740  P_libmesh.resize(P.size());
741  libmesh_assert((K.rows() == K.cols()) && (K.rows() == P.size()));
742  for (const auto i : make_range(K.rows()))
743  {
744  P_libmesh(i) = P(i);
745  for (const auto j : make_range(K.cols()))
746  K_libmesh(i, j) = K(i, j);
747  }
748 
749  if (global_solve)
750  {
751  // We were performing our finite element assembly for the implicit solve step of our
752  // example. Add our local element vectors/matrices into the global system
753  dof_map.constrain_element_matrix_and_vector(K_libmesh, P_libmesh, lambda_dof_indices);
754  matrix.add_matrix(K_libmesh, lambda_dof_indices);
755  lambda_system.rhs->add_vector(P_libmesh, lambda_dof_indices);
756  }
757  else
758  {
759  //
760  // We are doing our finite element assembly for the second time. We now know the Lagrange
761  // multiplier solution. With that and the local element matrices and vectors we can compute
762  // the vector and scalar solutions
763  //
764 
765  Lambda.resize(lambda_n_dofs);
766  lambda_system.current_local_solution->get(lambda_dof_indices, lambda_solution_std_vec);
767  for (const auto i : make_range(lambda_n_dofs))
768  Lambda(i) = lambda_solution_std_vec[i];
769  const auto scalar_soln = Sinv * (BtAinvCMinusE * Lambda - BtAinvGMinusF);
770  const auto vector_soln = Ainv * (G - B * scalar_soln - C * Lambda);
771  for (const auto i : make_range(vector_n_dofs))
772  system.solution->set(vector_dof_indices[i], vector_soln(i));
773  for (const auto i : make_range(scalar_n_dofs))
774  system.solution->set(scalar_dof_indices[i], scalar_soln(i));
775 
776  // Now solve for the enriched scalar solution using our Lagrange
777  // multiplier solution, q, and our low-order u
779  dof_map,
780  system,
781  elem,
782  vector_soln,
783  scalar_soln,
784  Lambda,
785  *vector_fe,
786  *vector_fe_face,
787  *scalar_fe,
788  *scalar_fe_face,
789  *lambda_fe_face,
790  qrule,
791  qface);
792  }
793  }
794 
795  if (!global_solve)
796  {
797  system.solution->close();
798  // Scatter solution into the current_solution which is used in error computation
799  system.update();
800  }
801 }
802 
803 // Call this assembly function when assembling the global implicit system
804 void
805 assemble_hdg(EquationSystems & es, const std::string & libmesh_dbg_var(system_name))
806 {
807  libmesh_assert_equal_to(system_name, "Lambda");
808  fe_assembly(es, /*global_solve=*/true);
809 }
810 
811 // Compute the stabilization parameter
812 Real
813 compute_tau(const bool internal_face, bool & tau_found, const Elem * const elem)
814 {
815  if (!internal_face)
816  // if we're an external face then tau is 0
817  return 0;
818  else if (tau_found)
819  // if we've already applied a non-zero tau on another face, then we also return 0
820  return 0;
821  else
822  {
823  // This is our first internal face. We will apply our non-zero tau (and mark that we have)
824  tau_found = true;
825  return 1 / elem->hmin();
826  }
827 }
828 
829 // We will perform finite element assembly twice. The first time to setup the global implicit system
830 // for the Lagrange multiplier degrees of freedom. And then the second time to compute the vector
831 // and scalar field solutions using the already-compute Lagrange multiplier solution
832 void
833 alternative_fe_assembly(EquationSystems & es, const bool global_solve)
834 {
835  const MeshBase & mesh = es.get_mesh();
836  const unsigned int dim = mesh.mesh_dimension();
837 
838  // The mixed, e.g. vector-scalar system
839  auto & system = es.get_system<System>("Mixed");
840  // Our implicit Lagrange multiplier system
841  auto & lambda_system = es.get_system<LinearImplicitSystem>("Lambda");
842 
843  const auto & dof_map = system.get_dof_map();
844  const auto & lambda_dof_map = lambda_system.get_dof_map();
845 
846  const FEType vector_fe_type = dof_map.variable_type(system.variable_number("q"));
847  const FEType scalar_fe_type = dof_map.variable_type(system.variable_number("u"));
848  const FEType lambda_fe_type =
849  lambda_dof_map.variable_type(lambda_system.variable_number("lambda"));
850 
851  // Volumetric FE objects
852  std::unique_ptr<FEVectorBase> vector_fe(FEVectorBase::build(dim, vector_fe_type));
853  std::unique_ptr<FEBase> scalar_fe(FEBase::build(dim, scalar_fe_type));
854 
855  // Volumetric quadrature rule
856  QGauss qrule(dim, scalar_fe_type.default_quadrature_order());
857 
858  // Attach quadrature rules for the FE objects that we will reinit within the element "volume"
859  vector_fe->attach_quadrature_rule(&qrule);
860  scalar_fe->attach_quadrature_rule(&qrule);
861 
862  // Declare finite element objects for boundary integration
863  std::unique_ptr<FEVectorBase> vector_fe_face(FEVectorBase::build(dim, vector_fe_type));
864  std::unique_ptr<FEBase> scalar_fe_face(FEBase::build(dim, scalar_fe_type));
865  std::unique_ptr<FEBase> lambda_fe_face(FEBase::build(dim, lambda_fe_type));
866 
867  // Boundary integration requires one quadrature rule with dimensionality one
868  // less than the dimensionality of the element.
869  QGauss qface(dim - 1, scalar_fe_type.default_quadrature_order());
870 
871  // Attach quadrature rules for the FE objects that we will reinit on the element faces
872  vector_fe_face->attach_quadrature_rule(&qface);
873  scalar_fe_face->attach_quadrature_rule(&qface);
874  lambda_fe_face->attach_quadrature_rule(&qface);
875 
876  // pre-request our required volumetric data
877  const auto & JxW = vector_fe->get_JxW();
878  const auto & q_point = vector_fe->get_xyz();
879  const auto & vector_phi = vector_fe->get_phi();
880  const auto & scalar_phi = scalar_fe->get_phi();
881  const auto & grad_scalar_phi = scalar_fe->get_dphi();
882  const auto & div_vector_phi = vector_fe->get_div_phi();
883 
884  // pre-request our required element face data
885  const auto & vector_phi_face = vector_fe_face->get_phi();
886  const auto & scalar_phi_face = scalar_fe_face->get_phi();
887  const auto & lambda_phi_face = lambda_fe_face->get_phi();
888  const auto & JxW_face = scalar_fe_face->get_JxW();
889  const auto & qface_point = vector_fe_face->get_xyz();
890  const auto & normals = vector_fe_face->get_normals();
891 
892  //
893  // We will need "Eigen" versions of many of the matrices/vectors because the
894  // libMesh DenseMatrix doesn't have an inverse API
895  //
896 
897  // LM matrix and RHS after eliminating vector and scalar dofs
898  DenseMatrix<Number> K_lm_libmesh;
899  DenseVector<Number> F_lm_libmesh;
900  EigenMatrix K_mixed;
901  EigenMatrix Kinv_mixed;
902  EigenVector F_mixed;
903 
904  // Lambda eigen vector for constructing vector and scalar solutions
905  EigenVector Lambda;
906 
907  // Containers for dof indices
908  std::vector<dof_id_type> vector_dof_indices;
909  std::vector<dof_id_type> scalar_dof_indices;
910  std::vector<dof_id_type> lambda_dof_indices;
911  std::vector<Number> lambda_solution_std_vec;
912 
913  // Helper container for storing a given "mu" (function in the Langrange multiplier space) with
914  // quadrature point evaluations
915  std::vector<Number> mu;
916 
917  // The global system matrix
918  auto & matrix = lambda_system.get_system_matrix();
919 
920  auto compute_and_invert_K =
921  [&](const auto vector_n_dofs_in, const auto scalar_n_dofs_in, const Elem * const elem_in)
922  {
923  const auto mixed_size = vector_n_dofs_in + scalar_n_dofs_in;
924 
925  K_mixed.setZero(mixed_size, mixed_size);
926 
927  for (const auto qp : make_range(qrule.n_points()))
928  {
929  // Vector equation dependence on vector dofs
930  for (const auto i : make_range(vector_n_dofs_in))
931  for (const auto j : make_range(vector_n_dofs_in))
932  K_mixed(i, j) += JxW[qp] * (vector_phi[i][qp] * vector_phi[j][qp]);
933 
934  // Vector equation dependence on scalar dofs
935  for (const auto i : make_range(vector_n_dofs_in))
936  for (const auto j : make_range(scalar_n_dofs_in))
937  K_mixed(i, j + vector_n_dofs_in) -= JxW[qp] * (div_vector_phi[i][qp] * scalar_phi[j][qp]);
938 
939  // Scalar equation dependence on vector dofs
940  for (const auto i : make_range(scalar_n_dofs_in))
941  for (const auto j : make_range(vector_n_dofs_in))
942  K_mixed(i + vector_n_dofs_in, j) -= JxW[qp] * (grad_scalar_phi[i][qp] * vector_phi[j][qp]);
943  }
944 
945  // At the beginning of the loop, we mark that we haven't found our "Single-Face" yet
946  bool tau_found = false;
947  for (auto side : elem_in->side_index_range())
948  {
949  // Reinit our face FE objects
950  vector_fe_face->reinit(elem_in, side);
951  scalar_fe_face->reinit(elem_in, side);
952  const bool internal_face = elem_in->neighbor_ptr(side);
953  const auto tau = compute_tau(internal_face, tau_found, elem_in);
954 
955  for (const auto qp : make_range(qface.n_points()))
956  {
957  const auto normal = normals[qp];
958  const auto normal_sq = normal * normal;
959 
960  // Now do the internal boundary term for <\hat{q} \cdot \vec{n}, \omega> ->
961  // <q + \tau (u - \lambda), \omega> ->
962  // <q, \omega> + <\tau u, \omega> - <\tau \lambda, \omega>
963  for (const auto i : make_range(scalar_n_dofs_in))
964  {
965  for (const auto j : make_range(vector_n_dofs_in))
966  K_mixed(i + vector_n_dofs_in, j) +=
967  JxW_face[qp] * scalar_phi_face[i][qp] * (vector_phi_face[j][qp] * normal);
968 
969  if (tau) // Don't do unnecessary math ops if tau is 0
970  for (const auto j : make_range(scalar_n_dofs_in))
971  K_mixed(i + vector_n_dofs_in, j + vector_n_dofs_in) +=
972  JxW_face[qp] * scalar_phi_face[i][qp] * tau * scalar_phi_face[j][qp] * normal_sq;
973  }
974  }
975  }
976 
977  Kinv_mixed = K_mixed.inverse();
978  };
979 
980  auto compute_rhs = [&](const auto vector_n_dofs_in,
981  const auto scalar_n_dofs_in,
982  const Elem * const elem_in,
983  const unsigned int shape_function)
984  {
985  const auto mixed_size = vector_n_dofs_in + scalar_n_dofs_in;
986  F_mixed.setZero(mixed_size);
987 
988  // If the approximate LM solution was passed in, then we are solving for the elemental solution
989  // of q and u, not the mappings of individual LM shape functions. Consequently, we include the
990  // contribution of the forcing function
991  if (shape_function == libMesh::invalid_uint)
992  for (const auto qp : make_range(qrule.n_points()))
993  {
994  const Real x = q_point[qp](0);
995  const Real y = q_point[qp](1);
996  const Real z = q_point[qp](2);
997 
998  Real f = 0;
999  if (dim == 2)
1000  f = MixedExactSolution().forcing(x, y);
1001  else if (dim == 3)
1002  f = MixedExactSolution().forcing(x, y, z);
1003  for (const auto ii : make_range(scalar_n_dofs_in))
1004  F_mixed(ii + vector_n_dofs_in) += JxW[qp] * f * scalar_phi[ii][qp];
1005  }
1006 
1007  // At the beginning of the loop, we mark that we haven't found our "Single-Face" yet
1008  bool tau_found = false;
1009  std::vector<Number> g;
1010 
1011  for (auto side : elem_in->side_index_range())
1012  {
1013  // Reinit our face FE objects
1014  vector_fe_face->reinit(elem_in, side);
1015  scalar_fe_face->reinit(elem_in, side);
1016  lambda_fe_face->reinit(elem_in, side);
1017 
1018  const auto & qp_mu = [&]()
1019  {
1020  if (shape_function == libMesh::invalid_uint)
1021  {
1022  if (elem_in->neighbor_ptr(side))
1023  {
1024  compute_qp_soln(lambda_solution_std_vec, qface.n_points(), lambda_phi_face, Lambda);
1025  return lambda_solution_std_vec;
1026  }
1027  else
1028  {
1029  g.resize(qface.n_points());
1030  for (const auto qp : make_range(qface.n_points()))
1031  {
1032  auto & g_qp = g[qp];
1033  const Real xf = qface_point[qp](0);
1034  const Real yf = qface_point[qp](1);
1035  const Real zf = qface_point[qp](2);
1036 
1037  // The boundary value for scalar field.
1038  if (dim == 2)
1039  g_qp = MixedExactSolution().scalar(xf, yf);
1040  else if (dim == 3)
1041  g_qp = MixedExactSolution().scalar(xf, yf, zf);
1042  }
1043  return g;
1044  }
1045  }
1046  else
1047  {
1048  auto & real_mu = lambda_phi_face[shape_function];
1049  mu.resize(qface.n_points());
1050  for (const auto qp : make_range(qface.n_points()))
1051  mu[qp] = real_mu[qp];
1052  return mu;
1053  }
1054  }();
1055 
1056  const bool internal_face = elem_in->neighbor_ptr(side);
1057  const auto tau = compute_tau(internal_face, tau_found, elem_in);
1058 
1059  for (const auto qp : make_range(qface.n_points()))
1060  {
1061  const auto normal = normals[qp];
1062  const auto normal_sq = normal * normal;
1063 
1064  // Vector equation dependence on LM/mu
1065  for (const auto ii : make_range(vector_n_dofs_in))
1066  F_mixed(ii) -= JxW_face[qp] * (vector_phi_face[ii][qp] * normal) * qp_mu[qp];
1067 
1068  // Now do the boundary term for <\hat{q} \cdot \vec{n}, \omega> ->
1069  // <q + \tau (u - \lambda), \omega> ->
1070  // <q, \omega> + <\tau u, \omega> - <\tau \lambda, \omega>
1071  for (const auto ii : make_range(scalar_n_dofs_in))
1072  F_mixed(ii + vector_n_dofs_in) +=
1073  JxW_face[qp] * scalar_phi_face[ii][qp] * tau * qp_mu[qp] * normal_sq;
1074  }
1075  }
1076  };
1077 
1078  for (const auto & elem : mesh.active_local_element_ptr_range())
1079  {
1080  std::vector<EigenVector> local_solns;
1081  std::vector<unsigned int> dofs_on_side;
1082  std::unordered_set<unsigned int> external_boundary_indices;
1083  std::vector<std::vector<Gradient>> volumetric_q;
1084  std::vector<std::vector<Number>> volumetric_u;
1085  std::vector<std::vector<Gradient>> face_q;
1086 
1087  // Retrive our dof indices for all fields
1088  dof_map.dof_indices(elem, vector_dof_indices, system.variable_number("q"));
1089  dof_map.dof_indices(elem, scalar_dof_indices, system.variable_number("u"));
1090  lambda_dof_map.dof_indices(elem, lambda_dof_indices, lambda_system.variable_number("lambda"));
1091 
1092  const auto vector_n_dofs = vector_dof_indices.size();
1093  const auto scalar_n_dofs = scalar_dof_indices.size();
1094  const auto lambda_n_dofs = lambda_dof_indices.size();
1095  const std::size_t n_mu_funcs = global_solve ? lambda_n_dofs : 1;
1096 
1097  if (global_solve)
1098  {
1099  K_lm_libmesh.resize(lambda_n_dofs, lambda_n_dofs);
1100  F_lm_libmesh.resize(lambda_n_dofs);
1101 
1102  for (const auto s : elem->side_index_range())
1103  if (!elem->neighbor_ptr(s))
1104  {
1105  FEInterface::dofs_on_side(elem, dim, lambda_fe_type, s, dofs_on_side);
1106  external_boundary_indices.insert(dofs_on_side.begin(), dofs_on_side.end());
1107  }
1108  }
1109 
1110  // Reinit our volume FE objects
1111  vector_fe->reinit(elem);
1112  scalar_fe->reinit(elem);
1113 
1114  libmesh_assert_equal_to(vector_n_dofs, vector_phi.size());
1115  libmesh_assert_equal_to(scalar_n_dofs, scalar_phi.size());
1116 
1117  compute_and_invert_K(vector_n_dofs, scalar_n_dofs, elem);
1118  local_solns.resize(n_mu_funcs);
1119 
1120  if (global_solve)
1121  {
1122  volumetric_q.resize(lambda_n_dofs);
1123  volumetric_u.resize(lambda_n_dofs);
1124  face_q.resize(lambda_n_dofs);
1125 
1126  for (const auto i : make_range(lambda_n_dofs))
1127  {
1128  if (external_boundary_indices.count(i))
1129  continue;
1130 
1131  compute_rhs(vector_n_dofs, scalar_n_dofs, elem, i);
1132  auto & local_soln = local_solns[i];
1133  local_soln = Kinv_mixed * F_mixed;
1134  const auto local_q_soln = local_soln.head(vector_n_dofs);
1135  const auto local_u_soln = local_soln.tail(scalar_n_dofs);
1136  compute_qp_soln(volumetric_q[i], qrule.n_points(), vector_phi, local_q_soln);
1137  compute_qp_soln(volumetric_u[i], qrule.n_points(), scalar_phi, local_u_soln);
1138  }
1139 
1140  // Create the bilinear form for lambda
1141  for (const auto i : make_range(lambda_n_dofs))
1142  if (!external_boundary_indices.count(i))
1143  for (const auto j : make_range(lambda_n_dofs))
1144  if (!external_boundary_indices.count(j))
1145  for (const auto qp : make_range(qrule.n_points()))
1146  K_lm_libmesh(i, j) += JxW[qp] * volumetric_q[i][qp] * volumetric_q[j][qp];
1147 
1148  // Now for the volumetric portion of the RHS
1149  for (const auto qp : make_range(qrule.n_points()))
1150  {
1151  const Real x = q_point[qp](0);
1152  const Real y = q_point[qp](1);
1153  const Real z = q_point[qp](2);
1154 
1155  // "f" is the forcing function for the Poisson equation, which is
1156  // just the divergence of the exact solution for the vector field.
1157  // This is the well-known "method of manufactured solutions".
1158  Real f = 0;
1159  if (dim == 2)
1160  f = MixedExactSolution().forcing(x, y);
1161  else if (dim == 3)
1162  f = MixedExactSolution().forcing(x, y, z);
1163  for (const auto i : make_range(lambda_n_dofs))
1164  if (!external_boundary_indices.count(i))
1165  F_lm_libmesh(i) += JxW[qp] * f * volumetric_u[i][qp];
1166  }
1167 
1168  // Now for the Dirichlet boundary portion of our RHS
1169  for (const auto s : elem->side_index_range())
1170  {
1171  lambda_fe_face->reinit(elem, s);
1172  for (const auto i : make_range(lambda_n_dofs))
1173  if (external_boundary_indices.count(i))
1174  for (const auto j : make_range(lambda_n_dofs))
1175  if (external_boundary_indices.count(j))
1176  for (const auto qp : make_range(qface.n_points()))
1177  K_lm_libmesh(i, j) +=
1178  JxW_face[qp] * lambda_phi_face[i][qp] * lambda_phi_face[j][qp];
1179 
1180  if (!elem->neighbor_ptr(s))
1181  {
1182  vector_fe_face->reinit(elem, s);
1183  for (const auto i : make_range(lambda_n_dofs))
1184  {
1185  if (external_boundary_indices.count(i))
1186  continue;
1187 
1188  const auto local_q_soln = local_solns[i].head(vector_n_dofs);
1189  compute_qp_soln(face_q[i], qface.n_points(), vector_phi_face, local_q_soln);
1190  }
1191 
1192  for (const auto qp : make_range(qface.n_points()))
1193  {
1194  const Real xf = qface_point[qp](0);
1195  const Real yf = qface_point[qp](1);
1196  const Real zf = qface_point[qp](2);
1197 
1198  // The boundary value for scalar field.
1199  Real scalar_value = 0;
1200  if (dim == 2)
1201  scalar_value = MixedExactSolution().scalar(xf, yf);
1202  else if (dim == 3)
1203  scalar_value = MixedExactSolution().scalar(xf, yf, zf);
1204  for (const auto i : make_range(lambda_n_dofs))
1205  if (!external_boundary_indices.count(i))
1206  F_lm_libmesh(i) += JxW_face[qp] * scalar_value * face_q[i][qp] * normals[qp];
1207  }
1208  }
1209  }
1210 
1211  // We were performing our finite element assembly for the implicit solve step of our
1212  // example. Add our local element vectors/matrices into the global system
1213  dof_map.constrain_element_matrix_and_vector(K_lm_libmesh, F_lm_libmesh, lambda_dof_indices);
1214  matrix.add_matrix(K_lm_libmesh, lambda_dof_indices);
1215  lambda_system.rhs->add_vector(F_lm_libmesh, lambda_dof_indices);
1216  }
1217  else
1218  {
1219  // Must populate Lambda before calling compute_rhs
1220  Lambda.resize(lambda_n_dofs);
1221  lambda_system.current_local_solution->get(lambda_dof_indices, lambda_solution_std_vec);
1222  for (const auto i : make_range(lambda_n_dofs))
1223  Lambda(i) = lambda_solution_std_vec[i];
1224 
1225  compute_rhs(vector_n_dofs, scalar_n_dofs, elem, libMesh::invalid_uint);
1226  auto & local_soln = local_solns[0];
1227  local_soln = Kinv_mixed * F_mixed;
1228  const auto vector_soln = local_soln.head(vector_n_dofs);
1229  const auto scalar_soln = local_soln.tail(scalar_n_dofs);
1230  for (const auto i : make_range(vector_n_dofs))
1231  system.solution->set(vector_dof_indices[i], vector_soln(i));
1232  for (const auto i : make_range(scalar_n_dofs))
1233  system.solution->set(scalar_dof_indices[i], scalar_soln(i));
1234 
1235  // Now solve for the enriched scalar solution using our Lagrange
1236  // multiplier solution, q, and our low-order u
1238  dof_map,
1239  system,
1240  elem,
1241  vector_soln,
1242  scalar_soln,
1243  Lambda,
1244  *vector_fe,
1245  *vector_fe_face,
1246  *scalar_fe,
1247  *scalar_fe_face,
1248  *lambda_fe_face,
1249  qrule,
1250  qface);
1251  }
1252  }
1253 
1254  if (!global_solve)
1255  {
1256  system.solution->close();
1257  // Scatter solution into the current_solution which is used in error computation
1258  system.update();
1259  }
1260 }
1261 
1262 // Call this assembly function when assembling the global implicit system
1263 void
1264 alternative_assemble_hdg(EquationSystems & es, const std::string & libmesh_dbg_var(system_name))
1265 {
1266  libmesh_assert_equal_to(system_name, "Lambda");
1267  alternative_fe_assembly(es, /*global_solve=*/true);
1268 }
1269 
1270 #else
1271 
1272 int
1274 {
1275  return 0;
1276 }
1277 
1278 #endif
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.
const unsigned int invalid_uint
A number which is used quite often to represent an invalid or uninitialized value for an unsigned int...
Definition: libmesh.h:310
IntRange< unsigned short > side_index_range() const
Definition: elem.h:2710
void dof_indices(const Elem *const elem, std::vector< dof_id_type > &di) const
Definition: dof_map.C:2164
virtual void reinit()
Reinitializes degrees of freedom and other required data on the current mesh.
Definition: system.C:454
unsigned int dim
Real compute_tau(const bool internal_face, bool &tau_found, const Elem *const elem)
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
const FEType & variable_type(const unsigned int c) const
Definition: dof_map.h:2220
void fe_assembly(EquationSystems &es, bool global_solve)
This is the base class from which all geometric element types are derived.
Definition: elem.h:94
MeshBase & mesh
void extra_quadrature_order(const int extraorder)
Increases or decreases the order of the quadrature rule used for numerical integration.
Real scalar(Real x, Real y)
Order default_quadrature_order() const
Definition: fe_type.h:371
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
This class handles the numbering of degrees of freedom on a mesh.
Definition: dof_map.h:179
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.
virtual Real hmin() const
Definition: elem.C:702
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(...
std::unique_ptr< NumericVector< Number > > solution
Data structure to hold solution values.
Definition: system.h:1593
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)
void compute_enriched_soln(const MeshBase &mesh, const DofMap &dof_map, System &system, const Elem *const elem, const EigenVector &vector_soln, const EigenVector &scalar_soln, const EigenVector &Lambda, FEVectorBase &vector_fe, FEVectorBase &vector_fe_face, FEBase &scalar_fe, FEBase &scalar_fe_face, FEBase &lambda_fe_face, QBase &qrule, QBase &qface)
MatrixXcd EigenMatrix
virtual_for_inffe const std::vector< Real > & get_JxW() const
Definition: fe_abstract.h:303
virtual void reinit(const Elem *elem, const std::vector< Point > *const pts=nullptr, const std::vector< Real > *const weights=nullptr)=0
This is at the core of this class.
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
unsigned int n_points() const
Definition: quadrature.h:131
virtual_for_inffe const std::vector< Point > & get_xyz() const
Definition: fe_abstract.h:280
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
int main(int argc, char **argv)
Definition: vector_fe_ex8.C:93
const Elem * neighbor_ptr(unsigned int i) const
Definition: elem.h:2598
void lu_solve(const DenseVector< T > &b, DenseVector< T > &x)
Solve the system Ax=b given the input vector b.
Real forcing(Real x, Real y)
const FEType & variable_type(const unsigned int i) const
Definition: system.h:2508
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
void assemble_hdg(EquationSystems &es, const std::string &system_name)
virtual_for_inffe const std::vector< Point > & get_normals() const
Definition: fe_abstract.h:459
OStreamProxy out
void alternative_assemble_hdg(EquationSystems &es, const std::string &system_name)
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
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
void compute_qp_soln(std::vector< SolnType > &qp_vec, const unsigned int n_qps, const std::vector< std::vector< PhiType >> &phi, const std::vector< Number > &dof_values)
Definition: hdg_problem.C:36
virtual void init()
Initialize all the systems.
FEFamily
defines an enum for finite element families.
void alternative_fe_assembly(EquationSystems &es, bool global_solve)
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
SparseMatrix< Number > & add_matrix(std::string_view mat_name, ParallelType type=PARALLEL, MatrixBuildType mat_build_type=MatrixBuildType::AUTOMATIC)
Adds the additional matrix mat_name to this system.
Definition: system.C:1010
const DofMap & get_dof_map() const
Definition: system.h:2374
static void dofs_on_side(const Elem *const elem, const unsigned int dim, const FEType &fe_t, unsigned int s, std::vector< unsigned int > &di, const bool add_p_level=true)
Fills the vector di with the local degree of freedom indices associated with side s of element elem A...
Definition: fe_interface.C:597
The QBase class provides the basic functionality from which various quadrature rules can be derived...
Definition: quadrature.h:61
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.
This class forms the foundation from which generic finite elements may be derived.
const std::vector< std::vector< OutputShape > > & get_phi() const
Definition: fe_base.h:207