libMesh
Loading...
Searching...
No Matches
vector_fe_ex8.C
Go to the documentation of this file.
1// The libMesh Finite Element Library.
2// Copyright (C) 2002-2026 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
3
4// This library is free software; you can redistribute it and/or
5// modify it under the terms of the GNU Lesser General Public
6// License as published by the Free Software Foundation; either
7// version 2.1 of the License, or (at your option) any later version.
8
9// This library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12// Lesser General Public License for more details.
13
14// You should have received a copy of the GNU Lesser General Public
15// License along with this library; if not, write to the Free Software
16// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
18// <h1>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
76using namespace libMesh;
77using namespace Eigen;
78
79#ifdef LIBMESH_USE_COMPLEX_NUMBERS
80typedef MatrixXcd EigenMatrix;
81typedef VectorXcd EigenVector;
82#else
83typedef MatrixXd EigenMatrix;
84typedef VectorXd EigenVector;
85#endif
86
87void fe_assembly(EquationSystems & es, bool global_solve);
88void assemble_hdg(EquationSystems & es, const std::string & system_name);
89void alternative_fe_assembly(EquationSystems & es, bool global_solve);
90void alternative_assemble_hdg(EquationSystems & es, const std::string & system_name);
91
92int
93main(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
269template <typename SolnType, typename PhiType, typename LocalSolutionVector>
270void
271compute_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
290void
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
463void
464fe_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
804void
805assemble_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
812Real
813compute_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
832void
833alternative_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
1263void
1264alternative_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
1272int
1274{
1275 return 0;
1276}
1277
1278#endif
unsigned int dim
Real scalar(Real x, Real y)
Real forcing(Real x, Real y)
Defines a dense matrix for use in Finite Element-type computations.
void resize(const unsigned int new_m, const unsigned int new_n)
Resizes the matrix to the specified size and calls zero().
void lu_solve(const DenseVector< T > &b, DenseVector< T > &x)
Solve the system Ax=b given the input vector b.
Defines a dense vector for use in Finite Element-type computations.
void resize(const unsigned int n)
Resize the vector.
This class handles the numbering of degrees of freedom on a mesh.
Definition dof_map.h:181
void dof_indices(const Elem *const elem, std::vector< dof_id_type > &di) const
Definition dof_map.C:2201
const FEType & variable_type(const unsigned int i) const
Definition dof_map.h:2388
This is the base class from which all geometric element types are derived.
Definition elem.h:96
virtual Real hmin() const
Definition elem.C:683
const Elem * neighbor_ptr(unsigned int i) const
Definition elem.h:2615
IntRange< unsigned short > side_index_range() const
Definition elem.h:2727
This is the EquationSystems class.
const MeshBase & get_mesh() const
virtual void init()
Initialize all the systems.
virtual System & add_system(std::string_view system_type, std::string_view name)
Add the system of type system_type named name to the systems array.
const T_sys & get_system(std::string_view name) const
This class handles the computation of the L2 and/or H1 error for the Systems in the EquationSystems o...
Real l2_error(std::string_view sys_name, std::string_view unknown_name)
void attach_exact_values(const std::vector< FunctionBase< Number > * > &f)
Clone and attach arbitrary functors which compute the exact values of the EquationSystems' solutions ...
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 extra_quadrature_order(const int extraorder)
Increases or decreases the order of the quadrature rule used for numerical integration.
void attach_exact_derivs(const std::vector< FunctionBase< Gradient > * > &g)
Clone and attach arbitrary functors which compute the exact gradients of the EquationSystems' solutio...
The ExodusII_IO class implements reading meshes in the ExodusII file format from Sandia National Labs...
Definition exodusII_io.h:53
virtual void write_equation_systems(const std::string &fname, const EquationSystems &es, const std::set< std::string > *system_names=nullptr) override
Writes out the solution for no specific time or timestep.
virtual_for_inffe const std::vector< Point > & get_normals() const
virtual_for_inffe const std::vector< Real > & get_JxW() const
virtual_for_inffe const std::vector< Point > & get_xyz() const
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.
This class forms the foundation from which generic finite elements may be derived.
Definition fe_base.h:86
static std::unique_ptr< FEGenericBase > build(const unsigned int dim, const FEType &type)
Builds a specific finite element type.
const std::vector< std::vector< OutputShape > > & get_phi() const
Definition fe_base.h:207
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...
class FEType hides (possibly multiple) FEFamily and approximation orders, thereby enabling specialize...
Definition fe_type.h:197
Order default_quadrature_order() const
Definition fe_type.h:415
The LibMeshInit class, when constructed, initializes the dependent libraries (e.g.
Definition libmesh.h:92
Manages consistently variables, degrees of freedom, coefficient vectors, matrices and linear solvers ...
virtual void reinit() override
Reinitializes the member data fields associated with the system, so that, e.g., assemble() may be use...
This is the MeshBase class.
Definition mesh_base.h:81
unsigned int mesh_dimension() const
Definition mesh_base.C:430
The Mesh class is a thin wrapper, around the ReplicatedMesh class by default.
Definition mesh.h:51
The QBase class provides the basic functionality from which various quadrature rules can be derived.
Definition quadrature.h:62
unsigned int n_points() const
Definition quadrature.h:131
This class implements specific orders of Gauss quadrature.
Generic sparse matrix.
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.
Manages consistently variables, degrees of freedom, and coefficient vectors.
Definition system.h:100
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:998
virtual void reinit()
Reinitializes degrees of freedom and other required data on the current mesh.
Definition system.C:442
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:1667
const FEType & variable_type(const unsigned int i) const
Definition system.C:2721
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:1959
unsigned int add_variable(std::string_view var, const FEType &type, const std::set< subdomain_id_type > *const active_subdomains=nullptr)
Adds the variable var to the list of variables for this system.
Definition system.C:1344
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:756
std::unique_ptr< NumericVector< Number > > solution
Data structure to hold solution values.
Definition system.h:1655
virtual void solve()
Solves the system.
Definition system.h:349
unsigned int variable_number(std::string_view var) const
Definition system.C:1398
const DofMap & get_dof_map() const
Definition system.h:2417
MeshBase & mesh
void build_square(UnstructuredMesh &mesh, const unsigned int nx, const unsigned int ny, const Real xmin=0., const Real xmax=1., const Real ymin=0., const Real ymax=1., const ElemType type=INVALID_ELEM, const bool gauss_lobatto_grid=false)
A specialized build_cube() for 2D meshes.
void 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.
void permute_elements(MeshBase &mesh)
Randomly permute the nodal ordering of each element (without twisting the element mapping).
The libMesh namespace provides an interface to certain functionality in the library.
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:153
SolverPackage default_solver_package()
Definition libmesh.C:1064
libmesh_assert(ctx)
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:303
OStreamProxy out
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
IntRange< T > make_range(T beg, T end)
The 2-parameter make_range() helper function returns an IntRange<T> when both input parameters are of...
Definition int_range.h:176
Definition assembly.h:39
VectorXcd EigenVector
MatrixXcd EigenMatrix
void alternative_fe_assembly(EquationSystems &es, bool global_solve)
VectorXcd EigenVector
void fe_assembly(EquationSystems &es, bool global_solve)
void compute_qp_soln(std::vector< SolnType > &qp_vec, const unsigned int n_qps, const std::vector< std::vector< PhiType > > &phi, const LocalSolutionVector &soln)
void alternative_assemble_hdg(EquationSystems &es, const std::string &system_name)
MatrixXcd EigenMatrix
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)
Real compute_tau(const bool internal_face, bool &tau_found, const Elem *const elem)
void assemble_hdg(EquationSystems &es, const std::string &system_name)
int main()