656 *
class AnalyticalSolutionPressureImag :
public Function<dim>
659 *
AnalyticalSolutionPressureImag(
const double wavenumber,
const double theta)
661 *
, wavenumber(wavenumber)
665 *
const unsigned int component)
const override;
668 *
const double wavenumber;
669 *
const double theta;
673 *
double AnalyticalSolutionPressureImag<dim>::value(
675 *
const unsigned int )
const
682 *
class AnalyticalSolutionVelocityReal :
public TensorFunction<1, dim>
685 *
AnalyticalSolutionVelocityReal(
const double wavenumber,
const double theta)
687 *
, wavenumber(wavenumber)
693 *
const double wavenumber;
694 *
const double theta;
699 *
AnalyticalSolutionVelocityReal<dim>::value(
const Point<dim> &p)
const
710 *
return return_value;
714 *
class AnalyticalSolutionVelocityImag :
public TensorFunction<1, dim>
717 *
AnalyticalSolutionVelocityImag(
const double wavenumber,
const double theta)
719 *
, wavenumber(wavenumber)
725 *
const double wavenumber;
726 *
const double theta;
731 *
AnalyticalSolutionVelocityImag<dim>::value(
const Point<dim> &p)
const
742 *
return return_value;
748 * applied to constrain the dofs. The
main difference with the above function
749 * declaration is that the number of components will now be 4 because
this
750 * function will be applied to our space of skeleton unknowns via
752 * because the skeleton unknowns on faces
for the velocity field are scalars
753 * from the definition @f$\hat{u}_{n} = \mathbf{u} \cdot n@f$ and there are the
754 * real and imaginary parts of both fields. The returned
value will be based
756 * - <code>
component == 0</code> : real part of velocity skeleton;
757 * - <code>
component == 1</code> : imaginary part of velocity skeleton;
758 * - <code>
component == 2</code> : real part of pressure skeleton;
759 * - <code>
component == 3</code> : imaginary part of pressure skeleton.
766 *
class BoundaryValues :
public Function<dim>
769 *
BoundaryValues(
const double wavenumber,
const double theta)
771 *
, wavenumber(wavenumber)
775 *
const unsigned int component)
const override;
783 *
double BoundaryValues<dim>::value(
const Point<dim> &p,
784 *
const unsigned int component)
const
786 *
if (component == 0)
791 *
else if (component == 1)
795 *
else if (component == 2)
799 *
else if (component == 3)
805 *
AssertThrow(
false, ExcMessage(
"Invalid component for BoundaryValues"));
813 * <a name=
"step_100-ThecodeDPGHelmholtzcodeclassdeclaration"></a>
814 * <h3>The <code>DPGHelmholtz</code>
class declaration</h3>
818 * Next let
's declare the main class of this program. The main difference from
819 * other examples lies in the fact that we rely on multiple DoFHandler and
820 * FESystem objects. The DoFHandler objects that we rely on are the following:
821 * - The <code>dof_handler_trial_interior</code> is for the unknowns in the
822 * interior of the cells;
823 * - The <code>dof_handler_trial_skeleton</code> is for the unknowns in the
825 * - The <code>dof_handler_test</code> is for the test functions. Although we
826 * do not use the unknowns associated with this DoFHandler, it enables us to
827 * evaluate the test functions we will use in DPG.
831 * The same applies for the three FESystem objects:
832 * <code>fe_system_trial_interior</code>,
833 * <code>fe_system_trial_skeleton</code> and <code>fe_system_test</code>. In
834 * each one of these objects, we will store the relevant finite element space
835 * in the same order as for the BoundaryValues function. The first component
836 * will therefore always be related to the real part of the velocity, the
837 * second component to its imaginary part, the third component to the real
838 * part of the pressure and the fourth component to its imaginary part.
842 * The constructor of the class takes four arguments that define the problem.
843 * The first two are related to the finite element spaces degree, i.e.,
844 * <code>degree</code> defines the polynomial degree of the trial space and
845 * <code>delta_degree</code> defines the difference in degree between the
846 * trial and the test space. Since the test space needs to be enriched
847 * compared to the trial space in DPG, <code>delta_degree</code> must be at
848 * least 1 to ensure that the method is well posed. The last two arguments are
849 * related to the plane wave parameters. The parameter <code>wavenumber</code>
850 * defines the wavenumber @f$k@f$ of the plane wave problem while
851 * <code>theta</code> defines the incident angle. That angle must be in the
852 * closed interval @f$[0, \pi/2]@f$ for the boundary conditions to make
853 * sense. All those restrictions are asserted in the constructor.
857 * The class also provides a number of member functions that are responsible
858 * for setting up, solving, and postprocessing the DPG formulation. The
859 * <code>setup_system()</code> function initializes the three DoFHandler
860 * objects, the sparsity pattern, the
861 * system matrix, and the right-hand side vector. It also imposes both
862 * Dirichlet and Neumann boundary conditions using AffineConstraints. The
863 * function <code>assemble_system(bool solve_interior)</code> handles the
864 * assembly of the DPG system. It takes a boolean argument as input to
865 * indicate whether the assembly is being performed for the skeleton solve or
866 * for the interior reconstruction. When <code>solve_interior = false</code>,
867 * the bilinear and linear forms are assembled and the system is locally
868 * condensed so that the resulting global system only involves the skeleton
869 * unknowns. When <code>solve_interior = true</code>, the system is assembled
870 * again and the previously computed skeleton solution is used to reconstruct
871 * the interior solution variables. As mentioned in the introduction, this
872 * two-step approach is interesting to reduce the size of the global system
873 * that needs to be solved which helps for memory consumption and for the
874 * iterative solver convergence, but this requires assembling the system
875 * twice. The boolean flag introduced is interesting since it allows reusing
876 * the same assembly function for both steps and avoid code duplication. The
877 * last functions of the class are pretty standard and include
878 * <code>solve_linear_system_skeleton()</code>, that solves the resulting
879 * linear system, <code>refine_grid()</code>, which applies uniform refinement
880 * to the triangulation, <code>output_results()</code>, that writes both the
881 * skeleton and interior solutions to separate VTU files for visualization,
882 * and finally <code>calculate_L2_error()</code>, which computes the @f$L^2@f$
883 * norm of the error using the known analytical solution.
887 * In addition to these member functions, the class defines a number of member
888 * variables that are used throughout the implementation. These include the
889 * triangulation, finite element systems, DoFHandler objects, solution
890 * vectors, linear system data structures, and a ConvergenceTable used to
891 * store the @f$L^2@f$ error and related quantities. The coefficients defining the
892 * incident plane wave, namely the wavenumber and the angle of incidence, are
893 * also stored as class members. The class defines also several
894 * FEValuesExtractors variables that are reused at multiple points in the
895 * implementation to select the appropriate components of the finite element
896 * spaces for both the trial and test functions. These extractors provide
897 * access to the real and imaginary parts of the velocity and pressure
898 * variables. Since the skeleton space does not have the same number of
899 * components as the interior or test spaces (because the @f$H^{-1/2}@f$ space
900 * associated with the velocity field is scalar) additional extractors are
901 * defined specifically for the skeleton variables.
911 * DPGHelmholtz(const unsigned int degree,
912 * const unsigned int delta_degree,
913 * const double wavenumber,
914 * const double theta);
918 * void setup_system();
919 * void assemble_system(bool solve_interior);
920 * void solve_linear_system_skeleton();
921 * void refine_grid(unsigned int cycle);
922 * void output_results(unsigned int cycle);
923 * void calculate_L2_error();
925 * Triangulation<dim> triangulation;
927 * const FESystem<dim> fe_trial_interior;
928 * DoFHandler<dim> dof_handler_trial_interior;
929 * Vector<double> solution_interior;
931 * const FESystem<dim> fe_trial_skeleton;
932 * DoFHandler<dim> dof_handler_trial_skeleton;
933 * Vector<double> solution_skeleton;
934 * Vector<double> system_rhs;
935 * SparsityPattern sparsity_pattern;
936 * SparseMatrix<double> system_matrix;
937 * AffineConstraints<double> constraints;
939 * const FESystem<dim> fe_test;
940 * DoFHandler<dim> dof_handler_test;
942 * ConvergenceTable error_table;
944 * const double wavenumber;
945 * const double theta;
947 * const FEValuesExtractors::Vector extractor_u_real;
948 * const FEValuesExtractors::Vector extractor_u_imag;
949 * const FEValuesExtractors::Scalar extractor_p_real;
950 * const FEValuesExtractors::Scalar extractor_p_imag;
952 * const FEValuesExtractors::Scalar extractor_u_hat_real;
953 * const FEValuesExtractors::Scalar extractor_u_hat_imag;
954 * const FEValuesExtractors::Scalar extractor_p_hat_real;
955 * const FEValuesExtractors::Scalar extractor_p_hat_imag;
961 * <a name="step_100-codeDPGHelmholtzcodeConstructor"></a>
962 * <h3><code>DPGHelmholtz</code> Constructor</h3>
963 * In the constructor, we assign the relevant finite element to each FESystem
964 * following the nomenclature described above:
965 * - <code>fe_system_trial_interior</code> contains @f$\Re(\mathbf{u})@f$,
966 * @f$\Im(\mathbf{u})@f$, @f$\Re(p^*)@f$, @f$\Im(p^*)@f$ ;
967 * - <code>fe_system_trial_skeleton</code> contains @f$\Re(\hat{u}_n)@f$,
968 * @f$\Im(\hat{u}_n)@f$, @f$\Re(\hat{p}^*)@f$, @f$\Im(\hat{p}^*)@f$ ;
969 * - <code>fe_system_test</code> contains @f$\Re(\mathbf{v})@f$,
970 * @f$\Im(\mathbf{v})@f$, @f$\Re(q)@f$, @f$\Im(q)@f$.
974 * Note that the FE_Q and FE_TraceQ elements have a higher degree than the
975 * others because their numbering starts at 1 instead of 0. This is to ensure
976 * that the spaces chosen follow the exact sequence of energy spaces
977 * @f$\text{Q}_{k+1} \rightarrow \text{Nédélec}_k \rightarrow
978 * \text{Raviart-Thomas}_k \rightarrow \text{DGQ}_k@f$. We also initialize the
979 * FEValuesExtractors that will be used according to our FESystems
980 * nomenclature. The constructor also includes assertions to check that the
981 * provided template parameter <code>dim</code> is equal to 2. The dimension
982 * is 2 because the problem is not implemented in 3D. We also verify that the
983 * <code>delta_degree</code> variable is at least 1 since the degree of the
984 * test space must be at least one degree higher than the trial space.
985 * Finally, we check that the <code>wavenumber</code> is positive since it is
986 * the magnitude of the wave vector and that the angle <code>theta</code> is
988 * @f$[0, \pi/2]@f$ because, as stated above, other angles would not be compatible
989 * with the current boundary definitions.
996 * DPGHelmholtz<dim>::DPGHelmholtz(const unsigned int degree,
997 * const unsigned int delta_degree,
1000 * : fe_trial_interior(FE_DGQ<dim>(degree) ^ dim,
1001 * FE_DGQ<dim>(degree) ^ dim,
1002 * FE_DGQ<dim>(degree),
1003 * FE_DGQ<dim>(degree))
1004 * , dof_handler_trial_interior(triangulation)
1005 * , fe_trial_skeleton(FE_FaceQ<dim>(degree),
1006 * FE_FaceQ<dim>(degree),
1007 * FE_TraceQ<dim>(degree + 1),
1008 * FE_TraceQ<dim>(degree + 1))
1009 * , dof_handler_trial_skeleton(triangulation)
1010 * , fe_test(FE_RaviartThomas<dim>(degree + delta_degree),
1011 * FE_RaviartThomas<dim>(degree + delta_degree),
1012 * FE_Q<dim>(degree + delta_degree + 1),
1013 * FE_Q<dim>(degree + delta_degree + 1))
1014 * , dof_handler_test(triangulation)
1015 * , wavenumber(wavenumber)
1017 * , extractor_u_real(0)
1018 * , extractor_u_imag(dim)
1019 * , extractor_p_real(2 * dim)
1020 * , extractor_p_imag(2 * dim + 1)
1021 * , extractor_u_hat_real(0)
1022 * , extractor_u_hat_imag(1)
1023 * , extractor_p_hat_real(2)
1024 * , extractor_p_hat_imag(3)
1027 * static_assert(dim == 2, "This tutorial example only works for dim==2");
1029 * AssertThrow(delta_degree >= 1,
1030 * ExcMessage("The delta_degree needs to be at least 1."));
1032 * AssertThrow(wavenumber > 0, ExcMessage("The wavenumber must be positive."));
1034 * AssertThrow(theta >= 0 && theta <= pi / 2,
1036 * "The angle theta must be in the interval [0, pi/2]."));
1042 * <a name="step_100-DPGHelmholtzsetup_system"></a>
1043 * <h3>DPGHelmholtz::setup_system</h3>
1044 * This function sets up the multiple DOFHandler objects and records the
1045 * number of DoFs associated with each space in the ConvergenceTable for later
1046 * reference. It also defines the constraints, but since the global linear
1047 * system is posed exclusively in terms of the skeleton unknowns, constraints
1048 * are only built for this corresponding DoFHandler. These include
1049 * hanging-node constraints as well as boundary conditions. In particular,
1050 * Dirichlet and Neumann boundary conditions are enforced on selected
1051 * components of the skeleton variables by interpolating analytical boundary
1052 * data onto the appropriate trace spaces using component masks and
1053 * FEValuesExtractors. A Dirichlet condition is first applied to the pressure
1054 * trace on the left boundary (<code>types::boundary_id(0)</code>), while a
1055 * Neumann condition on the pressure is enforced by prescribing the normal
1056 * component of the velocity trace on the bottom boundary
1057 * (<code>types::boundary_id(2)</code>). Note that the Robin boundary
1058 * conditions are not enforced through constraints and are instead
1059 * incorporated later during the assembly of the bilinear and linear forms.
1063 * Once all constraints have been specified and closed, the vectors and
1064 * matrices associated with the global linear system are initialized. Because
1065 * the system only involves skeleton degrees of freedom, the sparsity pattern,
1066 * system matrix, and right-hand side are constructed accordingly. The
1067 * solution vectors for both the skeleton and interior unknowns are also
1068 * initialized at this stage, preparing the class for the subsequent assembly
1069 * and solution steps.
1072 * template <int dim>
1073 * void DPGHelmholtz<dim>::setup_system()
1075 * dof_handler_trial_skeleton.distribute_dofs(fe_trial_skeleton);
1076 * dof_handler_trial_interior.distribute_dofs(fe_trial_interior);
1077 * dof_handler_test.distribute_dofs(fe_test);
1079 * std::cout << std::endl
1080 * << "Number of dofs for the interior: "
1081 * << dof_handler_trial_interior.n_dofs() << std::endl;
1082 * error_table.add_value("dofs_interior", dof_handler_trial_interior.n_dofs());
1084 * std::cout << "Number of dofs for the skeleton: "
1085 * << dof_handler_trial_skeleton.n_dofs() << std::endl;
1086 * error_table.add_value("dofs_skeleton", dof_handler_trial_skeleton.n_dofs());
1088 * std::cout << "Number of dofs for the test space: "
1089 * << dof_handler_test.n_dofs() << std::endl;
1090 * error_table.add_value("dofs_test", dof_handler_test.n_dofs());
1092 * constraints.clear();
1094 * DoFTools::make_hanging_node_constraints(dof_handler_trial_skeleton,
1097 * const BoundaryValues<dim> boundary_values(wavenumber, theta);
1099 * VectorTools::interpolate_boundary_values(dof_handler_trial_skeleton,
1100 * types::boundary_id(0),
1103 * fe_trial_skeleton.component_mask(
1104 * extractor_p_hat_real));
1105 * VectorTools::interpolate_boundary_values(dof_handler_trial_skeleton,
1106 * types::boundary_id(0),
1109 * fe_trial_skeleton.component_mask(
1110 * extractor_p_hat_imag));
1112 * VectorTools::interpolate_boundary_values(dof_handler_trial_skeleton,
1113 * types::boundary_id(2),
1116 * fe_trial_skeleton.component_mask(
1117 * extractor_u_hat_real));
1118 * VectorTools::interpolate_boundary_values(dof_handler_trial_skeleton,
1119 * types::boundary_id(2),
1122 * fe_trial_skeleton.component_mask(
1123 * extractor_u_hat_imag));
1125 * constraints.close();
1127 * solution_skeleton.reinit(dof_handler_trial_skeleton.n_dofs());
1128 * system_rhs.reinit(dof_handler_trial_skeleton.n_dofs());
1129 * solution_interior.reinit(dof_handler_trial_interior.n_dofs());
1131 * DynamicSparsityPattern dsp(dof_handler_trial_skeleton.n_dofs());
1132 * DoFTools::make_sparsity_pattern(dof_handler_trial_skeleton,
1136 * sparsity_pattern.copy_from(dsp);
1137 * system_matrix.reinit(sparsity_pattern);
1143 * <a name="step_100-DPGHelmholtzassemble_system"></a>
1144 * <h3>DPGHelmholtz::assemble_system</h3>
1148 * This function incorporates the core difference of a DPG solver by
1149 * assembling the local contributions of the bilinear and linear forms. In it,
1150 * we begin by defining volume and face quadrature rules. Since the test space
1151 * has a higher polynomial degree than the trial spaces by construction, the
1152 * quadrature order is chosen based on the test finite element to ensure
1153 * sufficient accuracy for all integrals. The number of quadrature points for
1154 * both cell and face integration is also stored for later use.
1158 * Next, we create FEValues and FEFaceValues objects for the interior trial,
1159 * skeleton trial, and test spaces. In the ultraweak formulation used here,
1160 * gradients are only required for the test functions, while values are needed
1161 * for all spaces. Because all spaces are defined on the same triangulation,
1162 * the update of quadrature points and the transformation jacobian values is
1163 * only required for one of the FEValues objects, which we choose to be the
1164 * interior trial space.
1168 * We then query and store the number of degrees of freedom per cell
1169 * associated with each finite element space. These values determine the sizes
1170 * of all local matrices and vectors used during assembly. Notably, they are
1171 * used to build containers to store shape function values, gradients,
1172 * divergences, and their complex conjugates at each quadrature point to avoid
1173 * repeated queries to FEValues objects. The first group of containers defined
1174 * below corresponds to the test space quantities, including vector-valued
1175 * test functions, their divergence, scalar test functions, and their
1176 * gradients, both in the cell interior and on faces. The second group stores
1177 * the interior trial variables, namely the velocity and pressure fields. The
1178 * third group contains the skeleton trial variables, which represent the
1179 * normal velocity and pressure traces and their complex conjugates.
1183 * Also with the goal of avoiding repeated queries when determining to which
1184 * element a shape function belongs, we define an <code>enum
1185 * ShapeFunctionType</code> that classifies shape functions into four
1186 * categories: velocity real part, velocity imaginary part, pressure real
1187 * part, and pressure imaginary part. It also defines two composite
1188 * categories, one for all velocity shape functions and another for all
1189 * pressure shape functions. This enumeration uses bits as boolean flags
1190 * to facilitate efficient checks during assembly of the DPG matrices and
1191 * vectors. Containers for these classifications are defined for each of the
1192 * three finite element spaces with the size corresponding to the number of
1193 * DoFs per cell in each space.
1197 * Then, the local DPG matrices are allocated. These include the Gram matrix
1198 * @f$G@f$ of the test space, the coupling matrix between test and interior trial
1199 * spaces @f$B@f$, the coupling matrix between test and skeleton trial spaces
1200 * @f$\hat{B}@f$, and the matrix @f$D@f$ associated with skeleton coupling terms
1201 * arising from Robin boundary conditions. In addition, local vectors
1202 * corresponding to the linear functional in the test space @f$l@f$ and to the
1203 * skeleton trial space @f$g@f$ are defined. Together, these matrices and vectors
1204 * define the uncondensed local DPG system.
1208 * To perform the local static condensation, we need to allocate a set of
1209 * auxiliary matrices that represent intermediate block operators arising in
1210 * the elimination of interior degrees of freedom (@f$M_1@f$, @f$M_2@f$, @f$M_3@f$, @f$M_4@f$
1211 * and @f$M_5@f$ defined in the last section of the introduction). Further
1212 * temporary matrices and vectors are also created to store intermediate
1213 * results during matrix–matrix and matrix–vector products. These temporary
1214 * objects are labeled with a "tmp" prefix.
1218 * Two of these local matrices are inverted on every cell: the Gram matrix
1219 * @f$G@f$ and the condensed matrix @f$M_1@f$. The former stems from the inner
1220 * product of the test space, while the latter reads @f$B^\dagger G^{-1} B@f$
1221 * with @f$B@f$ of full column rank. Both are thus symmetric positive definite.
1222 * By default, LAPACKFullMatrix considers a matrix to be general and inverts
1223 * it using an LU factorization with partial pivoting. We assign
1224 * the LAPACKSupport::symmetric property to these two matrices, in which
1225 * case LAPACKFullMatrix::invert() relies on a Cholesky factorization
1226 * instead. Since a Cholesky factorization is used, the matrix must
1227 * be positive definite.
1231 * Finally, we define the local cell matrix and right-hand side vector
1232 * associated with the skeleton degrees of freedom that will be used to solve
1233 * our system, together with a local-to-global DoF index map used for
1234 * distribution into the global system. These are relevant to obtain the
1235 * solution when <code>solve_interior = false </code>, but when
1236 * <code>solve_interior = true</code>, we need to define additional vectors to
1237 * store the interior solution, interior right-hand side, and the skeleton
1238 * solution that will be used for the interior reconstruction step. Note that
1239 * since the Helmholtz problem is complex-valued, we also define the imaginary
1240 * unit and several complex constants that appear in the bilinear and linear
1241 * forms. Although the global linear system that is built is real-valued,
1242 * complex arithmetic from the C++ standard library is used locally to
1243 * simplify the formulation, in the same spirit as in @ref step_81 "step-81".
1249 * template <int dim>
1250 * void DPGHelmholtz<dim>::assemble_system(const bool solve_interior)
1252 * const QGauss<dim> quadrature_formula(fe_test.degree + 1);
1253 * const QGauss<dim - 1> face_quadrature_formula(fe_test.degree + 1);
1254 * const unsigned int n_q_points = quadrature_formula.size();
1255 * const unsigned int n_face_q_points = face_quadrature_formula.size();
1257 * FEValues<dim> fe_values_trial_interior(fe_trial_interior,
1258 * quadrature_formula,
1260 * update_quadrature_points |
1261 * update_JxW_values);
1262 * FEValues<dim> fe_values_test(fe_test,
1263 * quadrature_formula,
1264 * update_values | update_gradients);
1265 * FEFaceValues<dim> fe_values_trial_skeleton(fe_trial_skeleton,
1266 * face_quadrature_formula,
1268 * update_quadrature_points |
1269 * update_normal_vectors |
1270 * update_JxW_values);
1271 * FEFaceValues<dim> fe_face_values_test(fe_test,
1272 * face_quadrature_formula,
1275 * const unsigned int dofs_per_cell_test = fe_test.n_dofs_per_cell();
1276 * const unsigned int dofs_per_cell_trial_interior =
1277 * fe_trial_interior.n_dofs_per_cell();
1278 * const unsigned int dofs_per_cell_trial_skeleton =
1279 * fe_trial_skeleton.n_dofs_per_cell();
1281 * std::vector<Tensor<1, dim, std::complex<double>>> v(dofs_per_cell_test);
1282 * std::vector<Tensor<1, dim, std::complex<double>>> v_conj(
1283 * dofs_per_cell_test);
1284 * std::vector<std::complex<double>> div_v(dofs_per_cell_test);
1285 * std::vector<std::complex<double>> div_v_conj(dofs_per_cell_test);
1286 * std::vector<std::complex<double>> q(dofs_per_cell_test);
1287 * std::vector<std::complex<double>> q_conj(dofs_per_cell_test);
1288 * std::vector<Tensor<1, dim, std::complex<double>>> grad_q(
1289 * dofs_per_cell_test);
1290 * std::vector<Tensor<1, dim, std::complex<double>>> grad_q_conj(
1291 * dofs_per_cell_test);
1292 * std::vector<std::complex<double>> v_face_n(dofs_per_cell_test);
1293 * std::vector<std::complex<double>> v_face_n_conj(dofs_per_cell_test);
1294 * std::vector<std::complex<double>> q_face(dofs_per_cell_test);
1295 * std::vector<std::complex<double>> q_face_conj(dofs_per_cell_test);
1297 * std::vector<Tensor<1, dim, std::complex<double>>> u(
1298 * dofs_per_cell_trial_interior);
1299 * std::vector<std::complex<double>> p(dofs_per_cell_trial_interior);
1301 * std::vector<std::complex<double>> u_hat_n(dofs_per_cell_trial_skeleton);
1302 * std::vector<std::complex<double>> u_hat_n_conj(
1303 * dofs_per_cell_trial_skeleton);
1304 * std::vector<std::complex<double>> p_hat(dofs_per_cell_trial_skeleton);
1305 * std::vector<std::complex<double>> p_hat_conj(dofs_per_cell_trial_skeleton);
1307 * enum ShapeFunctionType : unsigned char
1309 * velocity_real = 1u << 0,
1310 * velocity_imag = 1u << 1,
1311 * pressure_real = 1u << 2,
1312 * pressure_imag = 1u << 3,
1314 * is_velocity = velocity_real | velocity_imag,
1315 * is_pressure = pressure_real | pressure_imag
1317 * std::vector<unsigned char> shape_function_type_test(dofs_per_cell_test);
1318 * std::vector<unsigned char> shape_function_type_trial_interior(
1319 * dofs_per_cell_trial_interior);
1320 * std::vector<unsigned char> shape_function_type_trial_skeleton(
1321 * dofs_per_cell_trial_skeleton);
1323 * LAPACKFullMatrix<double> G_matrix(dofs_per_cell_test, dofs_per_cell_test);
1324 * LAPACKFullMatrix<double> B_matrix(dofs_per_cell_test,
1325 * dofs_per_cell_trial_interior);
1326 * LAPACKFullMatrix<double> B_hat_matrix(dofs_per_cell_test,
1327 * dofs_per_cell_trial_skeleton);
1328 * LAPACKFullMatrix<double> D_matrix(dofs_per_cell_trial_skeleton,
1329 * dofs_per_cell_trial_skeleton);
1331 * Vector<double> g_vector(dofs_per_cell_trial_skeleton);
1332 * Vector<double> l_vector(dofs_per_cell_test);
1334 * LAPACKFullMatrix<double> M1_matrix(dofs_per_cell_trial_interior,
1335 * dofs_per_cell_trial_interior);
1336 * LAPACKFullMatrix<double> M2_matrix(dofs_per_cell_trial_interior,
1337 * dofs_per_cell_trial_skeleton);
1338 * LAPACKFullMatrix<double> M3_matrix(dofs_per_cell_trial_skeleton,
1339 * dofs_per_cell_trial_skeleton);
1340 * LAPACKFullMatrix<double> M4_matrix(dofs_per_cell_trial_interior,
1341 * dofs_per_cell_test);
1342 * LAPACKFullMatrix<double> M5_matrix(dofs_per_cell_trial_skeleton,
1343 * dofs_per_cell_test);
1345 * G_matrix.set_property(LAPACKSupport::symmetric);
1346 * M1_matrix.set_property(LAPACKSupport::symmetric);
1348 * LAPACKFullMatrix<double> tmp_matrix(dofs_per_cell_trial_skeleton,
1349 * dofs_per_cell_trial_interior);
1350 * LAPACKFullMatrix<double> tmp_matrix2(dofs_per_cell_trial_skeleton,
1351 * dofs_per_cell_trial_skeleton);
1352 * LAPACKFullMatrix<double> tmp_matrix3(dofs_per_cell_trial_skeleton,
1353 * dofs_per_cell_test);
1354 * Vector<double> tmp_vector(dofs_per_cell_trial_interior);
1356 * FullMatrix<double> cell_matrix(dofs_per_cell_trial_skeleton,
1357 * dofs_per_cell_trial_skeleton);
1358 * Vector<double> cell_skeleton_rhs(dofs_per_cell_trial_skeleton);
1359 * std::vector<types::global_dof_index> local_dof_indices(
1360 * dofs_per_cell_trial_skeleton);
1362 * Vector<double> cell_interior_rhs(dofs_per_cell_trial_interior);
1363 * Vector<double> cell_interior_solution(dofs_per_cell_trial_interior);
1364 * Vector<double> cell_skeleton_solution(dofs_per_cell_trial_skeleton);
1366 * constexpr std::complex<double> imag(0., 1.);
1367 * const std::complex<double> iomega = imag * wavenumber;
1368 * const std::complex<double> iomega_conj = std::conj(iomega);
1372 * After defining all the variables for our assembly, we now assemble the
1373 * local contributions of the DPG formulation. As usual, we loop over all
1374 * active cells of the triangulation. We choose the DoFHandler associated
1375 * with the interior trial space as the primary iterator, since the
1376 * cell-wise assembly is naturally tied to the interior unknowns. For each
1377 * such cell, we explicitly obtain the corresponding iterators for the test
1378 * space and for the skeleton trial space to ensure that all FEValues
1379 * objects are reinitialized on the same physical cell. The loop on cells is
1380 * used to assemble all the local matrices and vectors entering the DPG
1381 * static condensation procedure (@f$G@f$, @f$B@f$, @f$\hat{B}@f$, @f$D@f$, @f$g@f$, @f$l@f$). It
1382 * follows that all these objects are reinitialized to zero at the beginning
1383 * of each cell loop. In addition, we need to reset the local condensation
1384 * matrix @f$M_1@f$ because LAPACKFullMatrix keeps track of its inverse status
1385 * between iterations and forbids to invert it again if it has the inverted
1386 * status. These resets do not alter the symmetric property of @f$G@f$ and
1387 * @f$M_1@f$, which is why we only need to set it once before the loop.
1391 * At each quadrature point, we evaluate
1392 * and cache the values, gradients, and divergences of the test functions
1393 * (@f$\mathbf{v}@f$ and @f$q@f$), as well as the values of the trial functions
1394 * (@f$\mathbf{u}@f$ and @f$p@f$) in the relevant containers. These quantities
1395 * are stored as complex-valued expressions, together with their complex
1396 * conjugates, in order to directly form the sesquilinear forms appearing in
1397 * the time-harmonic formulation. In addition, we check and store in the
1398 * <code>shape_function_type_test</code>,
1399 * <code>shape_function_type_trial_interior</code>, and
1400 * <code>shape_function_type_trial_skeleton</code> vectors to which field
1401 * each shape function is associated.
1405 * For each quadrature point, we then loop over the test space degrees of
1406 * freedom. In a first nested loop over test indices, we assemble the Gram
1407 * matrix @f$G@f$. Depending on whether the test basis functions correspond to
1408 * the velocity or pressure components, we add the appropriate
1410 * - If both <code>i</code> and <code>j</code> are in test space associated
1411 * to the test functions @f$\mathbf{v}@f$, we build the terms @f$(\mathbf{v},
1412 * \mathbf{v})_{\Omega_h} + (\nabla \cdot \mathbf{v}, \nabla \cdot
1413 * \mathbf{v})_{\Omega_h} + (i\omega\mathbf{v}, i\omega
1414 * \mathbf{v})_{\Omega_h}@f$;
1415 * - If the dof <code>i</code> is in test function @f$\mathbf{v}@f$ and dof
1416 * <code>j</code> in test function @f$q@f$ we build the terms @f$(i\omega
1417 * \mathbf{v}, \nabla q)_{\Omega_h} + (\nabla \cdot \mathbf{v}, i\omega
1419 * - If the dof <code>i</code> is in test function @f$q@f$ and the dof
1420 * <code>j</code> is in the test function @f$\mathbf{v}@f$, we build the terms
1421 * @f$(\nabla q, i\omega \mathbf{v})_{\Omega_h} + (i\omega q, \nabla \cdot
1422 * \mathbf{v})_{\Omega_h}@f$;
1423 * - Finally, <code>i</code> and <code>j</code> are in test space
1424 * associated to @f$q@f$, we build the terms @f$(q, q)_{\Omega_h} + (\nabla
1425 * q,\nabla q)_{\Omega_h} + (i\omega q, i\omega q)_{\Omega_h}@f$.
1429 * In a second nested loop over interior trial space degrees of freedom, we
1430 * assemble the operator matrix @f$B@f$. Here again, the contributions depend on
1431 * the pairing of test and trial components:
1432 * - If dof <code>i</code> in test function @f$\mathbf{v}@f$ and dof
1433 * <code>j</code> in trial function @f$\mathbf{u}@f$ we build the term
1434 * @f$(\mathbf{v}, i\omega \mathbf{u})_{\Omega_h}@f$;
1435 * - If dof <code>i</code> in test function @f$\mathbf{v}@f$ and dof
1436 * <code>j</code> in trial function @f$p@f$ we build the term @f$ -( \nabla \cdot
1437 * \mathbf{v}, p^*)_{\Omega_h}@f$;
1438 * - If dof <code>i</code> in test function @f$q@f$ and dof <code>j</code> in
1439 * trial function @f$\mathbf{u}@f$ we build the term @f$-(\nabla q,
1440 * \mathbf{u})_{\Omega_h}@f$;
1441 * - If dof <code>i</code> in test function @f$q@f$ and dof <code>j</code> in
1442 * trial function @f$p@f$ we build the term @f$(q, i\omega p^*)_{\Omega_h}@f$.
1446 * Finally, we assemble the load vector @f$l@f$. In the present plane wave
1447 * configuration, the volumetric source term is zero, but we nevertheless
1448 * assemble @f$(q, l)_{\Omega_h}@f$ over the cell for completeness.
1451 * for (const auto &cell : dof_handler_trial_interior.active_cell_iterators())
1453 * fe_values_trial_interior.reinit(cell);
1455 * const typename DoFHandler<dim>::active_cell_iterator cell_test =
1456 * cell->as_dof_handler_iterator(dof_handler_test);
1457 * fe_values_test.reinit(cell_test);
1459 * const typename DoFHandler<dim>::active_cell_iterator cell_skeleton =
1460 * cell->as_dof_handler_iterator(dof_handler_trial_skeleton);
1470 * for (unsigned int q_point = 0; q_point < n_q_points; ++q_point)
1472 * const double JxW = fe_values_trial_interior.JxW(q_point);
1474 * for (unsigned int k : fe_values_test.dof_indices())
1477 * fe_values_test[extractor_u_real].value(k, q_point) +
1478 * imag * fe_values_test[extractor_u_imag].value(k, q_point);
1480 * fe_values_test[extractor_u_real].value(k, q_point) -
1481 * imag * fe_values_test[extractor_u_imag].value(k, q_point);
1484 * fe_values_test[extractor_u_real].divergence(k, q_point) +
1486 * fe_values_test[extractor_u_imag].divergence(k, q_point);
1488 * fe_values_test[extractor_u_real].divergence(k, q_point) -
1490 * fe_values_test[extractor_u_imag].divergence(k, q_point);
1493 * fe_values_test[extractor_p_real].value(k, q_point) +
1494 * imag * fe_values_test[extractor_p_imag].value(k, q_point);
1496 * fe_values_test[extractor_p_real].value(k, q_point) -
1497 * imag * fe_values_test[extractor_p_imag].value(k, q_point);
1500 * fe_values_test[extractor_p_real].gradient(k, q_point) +
1501 * imag * fe_values_test[extractor_p_imag].gradient(k, q_point);
1503 * fe_values_test[extractor_p_real].gradient(k, q_point) -
1504 * imag * fe_values_test[extractor_p_imag].gradient(k, q_point);
1506 * if (fe_test.shape_function_belongs_to(k, extractor_u_real))
1507 * shape_function_type_test[k] |= velocity_real;
1508 * if (fe_test.shape_function_belongs_to(k, extractor_u_imag))
1509 * shape_function_type_test[k] |= velocity_imag;
1510 * if (fe_test.shape_function_belongs_to(k, extractor_p_real))
1511 * shape_function_type_test[k] |= pressure_real;
1512 * if (fe_test.shape_function_belongs_to(k, extractor_p_imag))
1513 * shape_function_type_test[k] |= pressure_imag;
1516 * for (unsigned int k : fe_values_trial_interior.dof_indices())
1519 * fe_values_trial_interior[extractor_u_real].value(k, q_point) +
1521 * fe_values_trial_interior[extractor_u_imag].value(k,
1525 * fe_values_trial_interior[extractor_p_real].value(k, q_point) +
1527 * fe_values_trial_interior[extractor_p_imag].value(k,
1530 * if (fe_trial_interior.shape_function_belongs_to(
1531 * k, extractor_u_real))
1532 * shape_function_type_trial_interior[k] |= velocity_real;
1533 * if (fe_trial_interior.shape_function_belongs_to(
1534 * k, extractor_u_imag))
1535 * shape_function_type_trial_interior[k] |= velocity_imag;
1536 * if (fe_trial_interior.shape_function_belongs_to(
1537 * k, extractor_p_real))
1538 * shape_function_type_trial_interior[k] |= pressure_real;
1539 * if (fe_trial_interior.shape_function_belongs_to(
1540 * k, extractor_p_imag))
1541 * shape_function_type_trial_interior[k] |= pressure_imag;
1544 * for (const auto i : fe_values_test.dof_indices())
1546 * const unsigned char test_type_i = shape_function_type_test[i];
1548 * for (const auto j : fe_values_test.dof_indices())
1550 * const unsigned char test_type_j =
1551 * shape_function_type_test[j];
1553 * if ((test_type_i & is_velocity) &&
1554 * (test_type_j & is_velocity))
1557 * (((v_conj[i] * v[j]) + (div_v_conj[i] * div_v[j]) +
1558 * (iomega_conj * v_conj[i] * iomega * v[j])) *
1563 * else if ((test_type_i & is_velocity) &&
1564 * (test_type_j & is_pressure))
1567 * (((iomega_conj * v_conj[i] * grad_q[j]) +
1568 * (div_v_conj[i] * iomega * q[j])) *
1573 * else if ((test_type_i & is_pressure) &&
1574 * (test_type_j & is_velocity))
1577 * (((grad_q_conj[i] * iomega * v[j]) +
1578 * (iomega_conj * q_conj[i] * div_v[j])) *
1583 * else if ((test_type_i & is_pressure) &&
1584 * (test_type_j & is_pressure))
1587 * (((q_conj[i] * q[j]) + (grad_q[j] * grad_q_conj[i]) +
1588 * (iomega_conj * q_conj[i] * iomega * q[j])) *
1594 * for (const auto j : fe_values_trial_interior.dof_indices())
1596 * const unsigned char trial_type_j =
1597 * shape_function_type_trial_interior[j];
1599 * if ((test_type_i & is_velocity) &&
1600 * (trial_type_j & is_velocity))
1603 * ((v_conj[i] * iomega * u[j]) * JxW).real();
1606 * else if ((test_type_i & is_velocity) &&
1607 * (trial_type_j & is_pressure))
1609 * B_matrix(i, j) -= ((div_v_conj[i] * p[j]) * JxW).real();
1612 * else if ((test_type_i & is_pressure) &&
1613 * (trial_type_j & is_velocity))
1616 * ((grad_q_conj[i] * u[j]) * JxW).real();
1619 * else if ((test_type_i & is_pressure) &&
1620 * (trial_type_j & is_pressure))
1623 * ((q_conj[i] * iomega * p[j]) * JxW).real();
1627 * if (test_type_i & is_pressure)
1629 * double source_term = 0.0;
1630 * l_vector(i) += (q_conj[i] * source_term * JxW).real();
1637 * We now need to assemble the skeleton (face) contributions of the DPG
1638 * formulation. For this purpose, we loop over all faces of the current
1639 * cell using the DoFHandler associated with the skeleton trial space.
1640 * On each face, we reinitialize the FEFaceValues objects for both the
1641 * test space and the skeleton trial space, ensuring that all quantities
1642 * are evaluated on the same geometric entity.
1646 * In addition to the standard face integrals, this loop also accounts
1647 * for Robin boundary conditions, which in the present plane wave
1648 * configuration are imposed on two boundaries of the domain
1649 * (<code>types::boundary_id(1)</code> and
1650 * <code>types::boundary_id(3)</code>). The Robin terms involve the
1652 * @f$\frac{k_n}{\omega}@f$, but in our configuration,
1653 * @f$\omega = k c_s@f$ with @f$c_s=1@f$, and the geometry of the domain implies
1654 * that @f$k_n =\mathbf{k} \cdot \mathbf{n}@f$ reduces to either
1655 * @f$k\cos{\theta}@f$ for the right boundary
1656 * (<code>types::boundary_id(1)</code>) or @f$k\sin{\theta}@f$ for the top
1657 * boundary (<code>types::boundary_id(3)</code>). Consequently, the
1658 * wavenumber cancels out, and we are left with the cosine and sine of
1659 * the propagation direction as the factor in front of the pressure
1664 * As for the cell-wise assembly, we loop over the face quadrature
1665 * points. We evaluate and cache the relevant quantities to assemble
1666 * both the Gram matrix face contributions and the face operator matrix
1667 * @f$\hat{B}@f$. We also store the shape function types for the test and
1668 * skeleton trial spaces for the current face dofs. After precomputing
1669 * these quantities, we loop over the test space degrees of freedom and
1670 * trial space face degrees of freedom to assemble the corresponding
1671 * contributions. The face contributions to the Gram matrix only arise
1672 * when the face lies on a Robin boundary and are assembled as follows:
1673 * - If both <code>i</code> and <code>j</code> are in test space
1674 * associated to the test functions @f$\mathbf{v}@f$ we build, @f$\langle
1675 * \mathbf{v} \cdot \mathbf{n}, \mathbf{v} \cdot \mathbf{n}
1676 * \rangle_{\Gamma_1 \cup \Gamma_3}@f$;
1677 * - If the dof <code>i</code> is in test function @f$\mathbf{v}@f$ and dof
1678 * <code>j</code> in test function @f$q@f$, we build @f$\langle
1679 * \mathbf{v} \cdot \mathbf{n},\frac{k_n}{\omega}q
1680 * \rangle_{\Gamma_1 \cup \Gamma_3}@f$
1681 * - If the dof <code>i</code> is in test function @f$q@f$ and the dof
1682 * <code>j</code> is in the test function @f$\mathbf{v}@f$, we build
1683 * @f$\langle \frac{k_n}{\omega}q, \mathbf{v}
1684 * \cdot \mathbf{n} \rangle_{\Gamma_1 \cup
1686 * - Finally, if both <code>i</code> and <code>j</code> are in test
1687 * space associated to the test functions @f$q@f$, we build @f$\langle
1688 * \frac{k_n}{\omega}q,
1689 * \frac{k_n}{\omega}q \rangle_{\Gamma_1
1690 * \cup \Gamma_3}@f$.
1691 * For all faces of the mesh (regardless of boundary type) we also
1692 * assemble the face operator matrix @f$\hat{B}@f$ by looping over the
1693 * skeleton trial space degrees of freedom. The two terms are:
1694 * - If dof <code>i</code> in test function @f$\mathbf{v}@f$ and dof
1695 * <code>j</code> in trial function @f$\hat{p}^*@f$ we build the
1696 * term @f$\left\langle
1697 * \mathbf{v} \cdot \mathbf{n}, \hat{p}^*
1698 * \right\rangle_{\partial \Omega_h}@f$;
1699 * - If dof <code>i</code> in test function @f$q@f$ and dof <code>j</code>
1700 * in trial function @f$\hat{u}_n@f$ we build the term
1701 * @f$\left\langle q, \hat{u}_n \right\rangle_{\partial \Omega_h}@f$.
1705 * An important detail when assembling the face contributions related to
1706 * the velocity trace @f$\hat{u}_n@f$. Indeed, since the FE_FaceQ elements
1707 * used to represent the trace of H(div) conforming fields do not encode
1708 * an intrinsic orientation, special care must be taken to ensure
1709 * consistency of the numerical flux across shared faces (i.e., the flux
1710 * that cross a face in a given cell is equal to the flux that crosses
1711 * the same face in the adjacent cell for which the normal is opposite).
1712 * To this end, we introduce a sign factor that enforces a unique
1713 * orientation rule: the flux is always oriented from the cell with the
1714 * smaller active cell index toward the cell with the larger one. On
1715 * boundary faces, we recover the standard convention in which the flux
1716 * is aligned with the outward normal by defining the neighbor cell
1717 * index as <code>std::numeric_limits@<unsigned int@>::%max()</code>.
1718 * This local rule guarantees that the flux contributions are consistent
1719 * across neighboring cells without requiring a global orientation of
1726 * for (const auto &face : cell_skeleton->face_iterators())
1728 * fe_face_values_test.reinit(cell_test, face);
1729 * fe_values_trial_skeleton.reinit(cell_skeleton, face);
1731 * const auto face_no = cell->face_iterator_to_index(face);
1732 * const auto current_boundary_id = face->boundary_id();
1734 * const double kn_omega =
1735 * (current_boundary_id == 1) ?
1737 * ((current_boundary_id == 3) ? std::sin(theta) : 1.);
1739 * for (unsigned int q_point = 0; q_point < n_face_q_points; ++q_point)
1741 * const Tensor<1, dim> normal =
1742 * fe_values_trial_skeleton.normal_vector(q_point);
1743 * const double JxW_face = fe_values_trial_skeleton.JxW(q_point);
1745 * for (unsigned int k : fe_face_values_test.dof_indices())
1749 * (fe_face_values_test[extractor_u_real].value(k, q_point) +
1751 * fe_face_values_test[extractor_u_imag].value(k,
1753 * v_face_n_conj[k] =
1755 * (fe_face_values_test[extractor_u_real].value(k, q_point) -
1757 * fe_face_values_test[extractor_u_imag].value(k,
1761 * fe_face_values_test[extractor_p_real].value(k, q_point) +
1763 * fe_face_values_test[extractor_p_imag].value(k, q_point);
1765 * fe_face_values_test[extractor_p_real].value(k, q_point) -
1767 * fe_face_values_test[extractor_p_imag].value(k, q_point);
1769 * if (fe_test.shape_function_belongs_to(k, extractor_u_real))
1770 * shape_function_type_test[k] |= velocity_real;
1771 * if (fe_test.shape_function_belongs_to(k, extractor_u_imag))
1772 * shape_function_type_test[k] |= velocity_imag;
1773 * if (fe_test.shape_function_belongs_to(k, extractor_p_real))
1774 * shape_function_type_test[k] |= pressure_real;
1775 * if (fe_test.shape_function_belongs_to(k, extractor_p_imag))
1776 * shape_function_type_test[k] |= pressure_imag;
1778 * for (unsigned int k : fe_values_trial_skeleton.dof_indices())
1781 * fe_values_trial_skeleton[extractor_u_hat_real].value(
1783 * imag * fe_values_trial_skeleton[extractor_u_hat_imag]
1784 * .value(k, q_point);
1786 * fe_values_trial_skeleton[extractor_u_hat_real].value(
1788 * imag * fe_values_trial_skeleton[extractor_u_hat_imag]
1789 * .value(k, q_point);
1792 * fe_values_trial_skeleton[extractor_p_hat_real].value(
1794 * imag * fe_values_trial_skeleton[extractor_p_hat_imag]
1795 * .value(k, q_point);
1797 * fe_values_trial_skeleton[extractor_p_hat_real].value(
1799 * imag * fe_values_trial_skeleton[extractor_p_hat_imag]
1800 * .value(k, q_point);
1802 * if (fe_trial_skeleton.shape_function_belongs_to(
1803 * k, extractor_u_hat_real))
1804 * shape_function_type_trial_skeleton[k] |= velocity_real;
1805 * if (fe_trial_skeleton.shape_function_belongs_to(
1806 * k, extractor_u_hat_imag))
1807 * shape_function_type_trial_skeleton[k] |= velocity_imag;
1808 * if (fe_trial_skeleton.shape_function_belongs_to(
1809 * k, extractor_p_hat_real))
1810 * shape_function_type_trial_skeleton[k] |= pressure_real;
1811 * if (fe_trial_skeleton.shape_function_belongs_to(
1812 * k, extractor_p_hat_imag))
1813 * shape_function_type_trial_skeleton[k] |= pressure_imag;
1816 * for (const auto i : fe_face_values_test.dof_indices())
1818 * const unsigned char face_test_type_i =
1819 * shape_function_type_test[i];
1821 * if (current_boundary_id == 1 || current_boundary_id == 3)
1823 * for (const auto j : fe_face_values_test.dof_indices())
1825 * const unsigned char face_test_type_j =
1826 * shape_function_type_test[j];
1827 * if ((face_test_type_i & is_velocity) &&
1828 * (face_test_type_j & is_velocity))
1831 * (v_face_n_conj[i] * v_face_n[j] * JxW_face)
1835 * else if ((face_test_type_i & is_velocity) &&
1836 * (face_test_type_j & is_pressure))
1838 * G_matrix(i, j) += (v_face_n_conj[i] * kn_omega *
1839 * q_face[j] * JxW_face)
1843 * else if ((face_test_type_i & is_pressure) &&
1844 * (face_test_type_j & is_velocity))
1846 * G_matrix(i, j) += (kn_omega * q_face_conj[i] *
1847 * v_face_n[j] * JxW_face)
1851 * else if ((face_test_type_i & is_pressure) &&
1852 * (face_test_type_j & is_pressure))
1855 * (kn_omega * q_face_conj[i] * kn_omega *
1856 * q_face[j] * JxW_face)
1862 * for (const auto j : fe_values_trial_skeleton.dof_indices())
1864 * const unsigned char face_trial_type_j =
1865 * shape_function_type_trial_skeleton[j];
1867 * if ((face_test_type_i & is_velocity) &&
1868 * (face_trial_type_j & is_pressure))
1870 * B_hat_matrix(i, j) +=
1871 * ((v_face_n_conj[i] * p_hat[j]) * JxW_face).real();
1874 * else if ((face_test_type_i & is_pressure) &&
1875 * (face_trial_type_j & is_velocity))
1877 * const unsigned int neighbor_cell_id =
1878 * face->at_boundary() ?
1879 * std::numeric_limits<unsigned int>::max() :
1880 * cell->neighbor(face_no)->active_cell_index();
1881 * const double flux_orientation =
1882 * neighbor_cell_id > cell->active_cell_index() ?
1886 * B_hat_matrix(i, j) +=
1887 * (q_face_conj[i] * flux_orientation * u_hat_n[j] *
1896 * Finally, we assemble the matrix @f$D@f$ and the corresponding
1897 * source-term vector @f$g@f$. Note that this is only required on
1898 * faces where Robin boundary conditions are applied and that
1899 * the orientation of @f$\hat{u}_n@f$ is unambiguous: since the face
1900 * lies on the exterior boundary of the domain, the flux is
1901 * always aligned with the outward normal. Consequently, the
1902 * flux orientation factor is set to +1. As for the other
1903 * matrices, we loop over the relevant <code>dof_indices</code>,
1904 * here the ones of the skeleton trial-space degrees. However,
1905 * here we already have stored the terms involving the skeleton
1906 * trial-space basis functions and the shape function types, so
1907 * we can directly assemble the matrix @f$D@f$ and vector @f$g@f$.:
1908 * - If both <code>i</code> and <code>j</code> are in the
1909 * skeleton trace associated to @f$\hat{u}_n@f$, we build the term
1910 * @f$- \langle \hat{u}_n, \hat{u}_n \rangle_{\Gamma_1 \cup
1912 * - If <code>i</code> is in the skeleton trace associated to
1913 * @f$\hat{u}_n@f$ and <code>j</code> in the skeleton trace
1915 * @f$\hat{p}^*@f$, we build the term @f$\langle \hat{u}_n,
1916 * \frac{k_n}{\omega} \hat{p}^* \rangle_{\Gamma_1 \cup
1918 * - If <code>i</code> is in the skeleton trace associated to
1919 * @f$\hat{p}^*@f$ and <code>j</code> in the skeleton trace
1921 * @f$\hat{u}_n@f$, we build the term @f$\langle \frac{k_n}{\omega}
1922 * \hat{p}^*, \hat{u}_n \rangle_{\Gamma_1 \cup \Gamma_3}@f$;
1923 * - If both <code>i</code> and <code>j</code> are in the
1924 * skeleton trace associated to @f$\hat{p}^*@f$, we build the term
1925 * @f$- \langle \frac{k_n}{\omega} \hat{p}^*, \frac{k_n}{\omega}
1926 * \hat{p}^* \rangle_{\Gamma_1 \cup \Gamma_3}@f$.
1927 * - If <code>i</code> is in the skeleton trace
1929 * @f$\hat{u}_n@f$, we assemble the term @f$- \langle \hat{u}_n, g_R
1930 * \rangle_{\Gamma_1 \cup \Gamma_3}@f$;
1931 * - If <code>i</code> is in the skeleton trace
1933 * @f$\hat{p}^*@f$, we assemble the term @f$\langle
1934 * \frac{k_n}{\omega} \hat{p}^*, g_R \rangle_{\Gamma_1 \cup
1944 * if (current_boundary_id == 1 || current_boundary_id == 3)
1946 * const double flux_orientation = 1.;
1948 * for (const auto i : fe_values_trial_skeleton.dof_indices())
1950 * const unsigned char face_trial_type_i =
1951 * shape_function_type_trial_skeleton[i];
1952 * for (const auto j :
1953 * fe_values_trial_skeleton.dof_indices())
1955 * const unsigned char face_trial_type_j =
1956 * shape_function_type_trial_skeleton[j];
1958 * if ((face_trial_type_i & is_velocity) &&
1959 * (face_trial_type_j & is_velocity))
1962 * (flux_orientation * u_hat_n_conj[i] *
1963 * flux_orientation * u_hat_n[j] * JxW_face)
1967 * else if ((face_trial_type_i & is_velocity) &&
1968 * (face_trial_type_j & is_pressure))
1971 * (flux_orientation * u_hat_n_conj[i] *
1972 * kn_omega * p_hat[j] * JxW_face)
1976 * else if ((face_trial_type_i & is_pressure) &&
1977 * (face_trial_type_j & is_velocity))
1980 * (kn_omega * p_hat_conj[i] * flux_orientation *
1981 * u_hat_n[j] * JxW_face)
1985 * else if ((face_trial_type_i & is_pressure) &&
1986 * (face_trial_type_j & is_pressure))
1989 * (kn_omega * p_hat_conj[i] * kn_omega *
1990 * p_hat[j] * JxW_face)
1995 * double source_term = 0.;
1996 * if (face_trial_type_i & is_velocity)
1999 * (u_hat_n_conj[i] * source_term).real() * JxW_face;
2001 * else if (face_trial_type_i & is_pressure)
2004 * (kn_omega * p_hat_conj[i] * source_term).real() *
2013 * After assembling all local matrices and vectors, we perform the
2014 * cell-wise static condensation associated with the DPG formulation. We
2015 * first invert the Gram matrix @f$G@f$ and use it to form the
2016 * auxiliary operators @f$M_4 = B^\dagger G^{-1}@f$ and
2017 * @f$M_5 = \hat{B}^\dagger G^{-1}@f$. These are then used to construct the
2018 * condensed blocks @f$M_1 = B^\dagger G^{-1} B@f$,
2019 * @f$M_2 = B^\dagger G^{-1} \hat{B}@f$ and
2020 * @f$M_3 = \hat{B}^\dagger G^{-1} \hat{B} - D@f$. The matrix @f$M_1@f$ is then
2021 * inverted as well. As both @f$G@f$ and @f$M_1@f$ were flagged as symmetric,
2022 * the two inversions rely on a Cholesky factorization. Then, if
2023 * <code>solve_interior</code> is <code>true</code>, the skeleton
2024 * solution @f$\hat{u}_h@f$ is assumed known and we recover the interior
2025 * unknowns on each cell by solving
2026 * @f$u_h = M_1^{-1} (M_4 l - M_2 \hat{u}_h)@f$, followed by distribution to
2027 * the global interior solution
2028 * vector. Otherwise, we assemble the fully condensed local system for
2029 * the skeleton unknowns by forming the Schur complement @f$(M_3 -
2030 * M_2^\dagger M_1^{-1} M_2)@f$, together with the corresponding
2031 * right-hand side @f$(M_5 - M_2^\dagger M_1^{-1} M_4) l - g@f$, and
2032 * distribute the resulting local matrix and vector to the global
2033 * skeleton system while enforcing constraints.
2036 * G_matrix.invert();
2038 * B_matrix.Tmmult(M4_matrix, G_matrix);
2039 * B_hat_matrix.Tmmult(M5_matrix, G_matrix);
2041 * M4_matrix.mmult(M1_matrix, B_matrix);
2042 * M4_matrix.mmult(M2_matrix, B_hat_matrix);
2044 * M5_matrix.mmult(M3_matrix, B_hat_matrix);
2045 * M3_matrix.add(-1.0, D_matrix);
2047 * M1_matrix.invert();
2049 * if (solve_interior)
2051 * cell_skeleton->get_dof_values(solution_skeleton,
2052 * cell_skeleton_solution);
2054 * M2_matrix.vmult(tmp_vector, cell_skeleton_solution);
2055 * M4_matrix.vmult(cell_interior_rhs, l_vector);
2056 * cell_interior_rhs -= tmp_vector;
2057 * M1_matrix.vmult(cell_interior_solution, cell_interior_rhs);
2059 * cell->distribute_local_to_global(cell_interior_solution,
2060 * solution_interior);
2065 * M2_matrix.Tmmult(tmp_matrix, M1_matrix);
2066 * tmp_matrix.mmult(tmp_matrix2, M2_matrix);
2067 * tmp_matrix2.add(-1.0, M3_matrix);
2068 * tmp_matrix2 *= -1.0;
2069 * cell_matrix = tmp_matrix2;
2071 * tmp_matrix.mmult(tmp_matrix3, M4_matrix);
2072 * M5_matrix.add(-1.0, tmp_matrix3);
2073 * M5_matrix.vmult(cell_skeleton_rhs, l_vector);
2074 * cell_skeleton_rhs -= g_vector;
2076 * cell_skeleton->get_dof_indices(local_dof_indices);
2077 * constraints.distribute_local_to_global(cell_matrix,
2078 * cell_skeleton_rhs,
2079 * local_dof_indices,
2089 * <a name="step_100-DPGHelmholtzsolve_linear_system_skeleton"></a>
2090 * <h3>DPGHelmholtz::solve_linear_system_skeleton</h3>
2091 * This function is in charge of solving the linear system assembled and has
2092 * nothing specific to DPG per se. Even though the original PDE was
2093 * indefinite, the way we solve it (using a least-squares approach) means that
2094 * the linear system is symmetric and positive definite. As a consequence, the
2095 * method allows us to use the Conjugate Gradient iterative solver. Note that
2096 * because we do not have any preconditioner, the number of iterations can be
2097 * quite high. For simplicity, we put a high upper limit on the number of
2098 * iterations, but in practice one would want to change this function to have
2099 * a more robust solver. The tolerance for the convergence here is defined
2100 * proportional to the @f$L^2@f$ norm of the RHS vector so the stopping criterion
2101 * is independent of whatever scaling we apply to the equation. The chosen
2102 * tolerance is rather stiff, but it is required to reproduce the convergence
2103 * plots of the results section.
2106 * template <int dim>
2107 * void DPGHelmholtz<dim>::solve_linear_system_skeleton()
2109 * std::cout << std::endl << "Solving the DPG system..." << std::endl;
2111 * SolverControl solver_control(100000, 1e-10 * system_rhs.l2_norm());
2112 * SolverCG<Vector<double>> solver(solver_control);
2113 * solver.solve(system_matrix,
2114 * solution_skeleton,
2116 * PreconditionIdentity());
2117 * constraints.distribute(solution_skeleton);
2119 * std::cout << " " << solver_control.last_step()
2120 * << " CG iterations needed to obtain convergence. \n"
2123 * error_table.add_value("n_iter", solver_control.last_step());
2129 * <a name="step_100-DPGHelmholtzoutput_results"></a>
2130 * <h3>DPGHelmholtz::output_results</h3>
2131 * This function outputs both the interior and skeleton solutions in VTU
2132 * format for visualization in ParaView or VisIt. The interior solution is
2133 * written using the standard DataOut class by attaching the interior
2134 * DoFHandler and providing appropriate component names and interpretations:
2135 * the real and imaginary parts of the velocity are treated as vector-valued
2136 * fields, while the real and imaginary parts of the pressure are treated as
2137 * scalar fields. The skeleton solution, which is defined only on mesh faces,
2138 * is handled separately using the DataOutFaces class as presented in
2139 * @ref step_51 "step-51" for the HDG method. For both outputs, visualization patches are
2140 * built using the corresponding polynomial degree, and the results are
2141 * written using names dependent on the current mesh adaption cycle.
2144 * template <int dim>
2145 * void DPGHelmholtz<dim>::output_results(const unsigned int cycle)
2147 * DataOut<dim> data_out;
2148 * data_out.attach_dof_handler(dof_handler_trial_interior);
2150 * std::vector<std::string> solution_interior_names;
2151 * for (unsigned int i = 0; i < dim; ++i)
2153 * solution_interior_names.emplace_back("velocity_real");
2155 * for (unsigned int i = 0; i < dim; ++i)
2157 * solution_interior_names.emplace_back("velocity_imag");
2159 * solution_interior_names.emplace_back("pressure_real");
2160 * solution_interior_names.emplace_back("pressure_imag");
2162 * std::vector<DataComponentInterpretation::DataComponentInterpretation>
2163 * data_component_interpretation;
2164 * for (unsigned int i = 0; i < dim; ++i)
2166 * data_component_interpretation.push_back(
2167 * DataComponentInterpretation::component_is_part_of_vector);
2169 * for (unsigned int i = 0; i < dim; ++i)
2171 * data_component_interpretation.push_back(
2172 * DataComponentInterpretation::component_is_part_of_vector);
2174 * data_component_interpretation.push_back(
2175 * DataComponentInterpretation::component_is_scalar);
2176 * data_component_interpretation.push_back(
2177 * DataComponentInterpretation::component_is_scalar);
2179 * data_out.add_data_vector(solution_interior,
2180 * solution_interior_names,
2181 * DataOut<dim>::type_automatic,
2182 * data_component_interpretation);
2183 * data_out.build_patches(fe_trial_interior.degree);
2184 * std::ofstream output("solution_planewave_square-" + std::to_string(cycle) +
2186 * data_out.write_vtu(output);
2188 * DataOutFaces<dim> data_out_faces(false);
2189 * data_out_faces.attach_dof_handler(dof_handler_trial_skeleton);
2191 * std::vector<std::string> solution_skeleton_names;
2192 * solution_skeleton_names.emplace_back("velocity_hat_real");
2193 * solution_skeleton_names.emplace_back("velocity_hat_imag");
2194 * solution_skeleton_names.emplace_back("pressure_hat_real");
2195 * solution_skeleton_names.emplace_back("pressure_hat_imag");
2197 * std::vector<DataComponentInterpretation::DataComponentInterpretation>
2198 * data_component_interpretation_skeleton(
2199 * 4, DataComponentInterpretation::component_is_scalar);
2201 * data_out_faces.add_data_vector(solution_skeleton,
2202 * solution_skeleton_names,
2203 * DataOutFaces<dim>::type_automatic,
2204 * data_component_interpretation_skeleton);
2206 * data_out_faces.build_patches(fe_trial_skeleton.degree);
2207 * std::ofstream output_face("solution_face_planewave_square-" +
2208 * std::to_string(cycle) + ".vtu");
2209 * data_out_faces.write_vtu(output_face);
2215 * <a name="step_100-DPGHelmholtzcalculate_L2_error"></a>
2216 * <h3>DPGHelmholtz::calculate_L2_error</h3>
2217 * In this function, we compute the @f$L^2@f$ error of each component of the
2218 * numerical solution, namely the real and imaginary parts of the velocity and
2219 * pressure, for both the interior and skeleton unknowns. Because we want to
2220 * have errors for the skeleton components, we cannot use the
2221 * VectorTools::integrate_difference function as it does not have a mechanism
2222 * to avoid visiting faces twice (i.e., counting the error on each cell
2223 * sharing the face). Therefore, we will perform the computation "by hand" for
2224 * both interior and skeleton solutions.
2230 * template <int dim>
2231 * void DPGHelmholtz<dim>::calculate_L2_error()
2233 * QGauss<dim> quadrature_formula(fe_test.degree + 1);
2234 * FEValues<dim> fe_values_trial_interior(fe_trial_interior,
2235 * quadrature_formula,
2237 * update_quadrature_points |
2238 * update_JxW_values);
2239 * const QGauss<dim - 1> face_quadrature_formula(fe_test.degree + 1);
2240 * FEFaceValues<dim> fe_values_trial_skeleton(fe_trial_skeleton,
2241 * face_quadrature_formula,
2243 * update_quadrature_points |
2244 * update_normal_vectors |
2245 * update_JxW_values);
2247 * const unsigned int n_q_points = quadrature_formula.size();
2248 * const unsigned int n_face_q_points = face_quadrature_formula.size();
2250 * double L2_error_p_real = 0;
2251 * double L2_error_p_imag = 0;
2252 * double L2_error_p_hat_real = 0;
2253 * double L2_error_p_hat_imag = 0;
2254 * double L2_error_u_real = 0;
2255 * double L2_error_u_imag = 0;
2256 * double L2_error_u_hat_real = 0;
2257 * double L2_error_u_hat_imag = 0;
2259 * std::vector<Tensor<1, dim>> local_u_real(n_q_points);
2260 * std::vector<Tensor<1, dim>> local_u_imag(n_q_points);
2261 * std::vector<double> local_p_real(n_q_points);
2262 * std::vector<double> local_p_imag(n_q_points);
2263 * std::vector<double> local_u_hat_real(n_face_q_points);
2264 * std::vector<double> local_u_hat_imag(n_face_q_points);
2265 * std::vector<double> local_p_hat_real(n_face_q_points);
2266 * std::vector<double> local_p_hat_imag(n_face_q_points);
2268 * const AnalyticalSolutionPressureReal<dim> analytical_solution_p_real(
2269 * wavenumber, theta);
2270 * const AnalyticalSolutionPressureImag<dim> analytical_solution_p_imag(
2271 * wavenumber, theta);
2272 * const AnalyticalSolutionVelocityReal<dim> analytical_solution_u_real(
2273 * wavenumber, theta);
2274 * const AnalyticalSolutionVelocityImag<dim> analytical_solution_u_imag(
2275 * wavenumber, theta);
2279 * To compute the @f$L^2@f$ error, we start by looping over all active cells of
2280 * the mesh and evaluating both the interior contributions. For
2281 * each cell, the interior velocity and pressure are first interpolated at
2282 * volume quadrature points using FEValues, and their squared differences
2283 * with the corresponding analytical solutions are accumulated using the
2284 * Jacobian quadrature weights. The skeleton error is then computed in a
2285 * similar way by looping over the faces of that same cell and interpolating
2286 * the trace unknowns at face quadrature points using FEFaceValues. However,
2287 * to avoid double-counting interior faces shared by two cells, we use a
2288 * similar idea to the one used to define the flux orientation during the
2289 * assembly: each face is integrated only once by retaining the contribution
2290 * from the cell with the smallest active cell index (this does not include
2291 * boundary faces which are always included). Finally, the accumulated
2292 * errors are printed to the terminal, and stored in the
2293 * <code>error_table</code> for post-processing.
2297 * An additional detail worth mentioning concerns the error computation for
2298 * the velocity trace variable. Indeed, for the normal flux trace variable
2299 * @f$\hat{u}_n@f$, the analytical velocity is projected onto the outward normal
2300 * at each quadrature point, and the error is computed using only the
2301 * magnitude (absolute value) of both numerical and analytical quantities.
2302 * This choice removes spurious sign changes induced by face-normal
2303 * orientation conventions, which may differ between neighboring cells and
2304 * are not physically meaningful for error estimation.
2307 * for (const auto &cell : dof_handler_trial_interior.active_cell_iterators())
2309 * fe_values_trial_interior.reinit(cell);
2311 * fe_values_trial_interior[extractor_u_real].get_function_values(
2312 * solution_interior, local_u_real);
2313 * fe_values_trial_interior[extractor_u_imag].get_function_values(
2314 * solution_interior, local_u_imag);
2315 * fe_values_trial_interior[extractor_p_real].get_function_values(
2316 * solution_interior, local_p_real);
2317 * fe_values_trial_interior[extractor_p_imag].get_function_values(
2318 * solution_interior, local_p_imag);
2320 * const auto &quadrature_points =
2321 * fe_values_trial_interior.get_quadrature_points();
2323 * for (const unsigned int q_index :
2324 * fe_values_trial_interior.quadrature_point_indices())
2326 * const double JxW = fe_values_trial_interior.JxW(q_index);
2327 * const auto &position = quadrature_points[q_index];
2329 * L2_error_u_real += (local_u_real[q_index] -
2330 * analytical_solution_u_real.value(position))
2333 * L2_error_u_imag += (local_u_imag[q_index] -
2334 * analytical_solution_u_imag.value(position))
2338 * L2_error_p_real +=
2339 * std::pow((local_p_real[q_index] -
2340 * analytical_solution_p_real.value(position, 0)),
2344 * L2_error_p_imag +=
2345 * std::pow((local_p_imag[q_index] -
2346 * analytical_solution_p_imag.value(position, 0)),
2351 * const typename DoFHandler<dim>::active_cell_iterator cell_skeleton =
2352 * cell->as_dof_handler_iterator(dof_handler_trial_skeleton);
2354 * for (const auto &face : cell->face_iterators())
2356 * fe_values_trial_skeleton.reinit(cell_skeleton, face);
2357 * const auto face_no = cell_skeleton->face_iterator_to_index(face);
2359 * fe_values_trial_skeleton[extractor_u_hat_real].get_function_values(
2360 * solution_skeleton, local_u_hat_real);
2361 * fe_values_trial_skeleton[extractor_u_hat_imag].get_function_values(
2362 * solution_skeleton, local_u_hat_imag);
2363 * fe_values_trial_skeleton[extractor_p_hat_real].get_function_values(
2364 * solution_skeleton, local_p_hat_real);
2365 * fe_values_trial_skeleton[extractor_p_hat_imag].get_function_values(
2366 * solution_skeleton, local_p_hat_imag);
2368 * const auto &face_quadrature_points =
2369 * fe_values_trial_skeleton.get_quadrature_points();
2371 * for (const unsigned int &q_index :
2372 * fe_values_trial_skeleton.quadrature_point_indices())
2374 * const double JxW = fe_values_trial_skeleton.JxW(q_index);
2375 * const auto &position = face_quadrature_points[q_index];
2376 * const Tensor<1, dim> normal =
2377 * fe_values_trial_skeleton.normal_vector(q_index);
2379 * const unsigned int neighbor_cell_id =
2380 * face->at_boundary() ?
2381 * std::numeric_limits<unsigned int>::max() :
2382 * cell->neighbor(face_no)->active_cell_index();
2383 * if (neighbor_cell_id < cell->active_cell_index())
2388 * double u_hat_n_analytical_real =
2389 * normal * analytical_solution_u_real.value(position);
2390 * double u_hat_n_analytical_imag =
2391 * normal * analytical_solution_u_imag.value(position);
2393 * L2_error_u_hat_real +=
2394 * std::pow(std::abs(local_u_hat_real[q_index]) -
2395 * std::abs(u_hat_n_analytical_real),
2398 * L2_error_u_hat_imag +=
2399 * std::pow(std::abs(local_u_hat_imag[q_index]) -
2400 * std::abs(u_hat_n_analytical_imag),
2404 * L2_error_p_hat_real +=
2405 * std::pow((local_p_hat_real[q_index] -
2406 * analytical_solution_p_real.value(position, 0)),
2409 * L2_error_p_hat_imag +=
2410 * std::pow((local_p_hat_imag[q_index] -
2411 * analytical_solution_p_imag.value(position, 0)),
2418 * std::cout << "Velocity real part L2 error is : "
2419 * << std::sqrt(L2_error_u_real) << std::endl;
2420 * std::cout << "Velocity imag part L2 error is : "
2421 * << std::sqrt(L2_error_u_imag) << std::endl;
2422 * std::cout << "Pressure real part L2 error is : "
2423 * << std::sqrt(L2_error_p_real) << std::endl;
2424 * std::cout << "Pressure imag part L2 error is : "
2425 * << std::sqrt(L2_error_p_imag) << std::endl;
2426 * std::cout << "Velocity skeleton real part L2 error is : "
2427 * << std::sqrt(L2_error_u_hat_real) << std::endl;
2428 * std::cout << "Velocity skeleton imag part L2 error is : "
2429 * << std::sqrt(L2_error_u_hat_imag) << std::endl;
2430 * std::cout << "Pressure skeleton real part L2 error is : "
2431 * << std::sqrt(L2_error_p_hat_real) << std::endl;
2432 * std::cout << "Pressure skeleton imag part L2 error is : "
2433 * << std::sqrt(L2_error_p_hat_imag) << std::endl;
2435 * error_table.add_value("eL2_u_r", std::sqrt(L2_error_u_real));
2436 * error_table.add_value("eL2_u_i", std::sqrt(L2_error_u_imag));
2437 * error_table.add_value("eL2_p_r", std::sqrt(L2_error_p_real));
2438 * error_table.add_value("eL2_p_i", std::sqrt(L2_error_p_imag));
2439 * error_table.add_value("eL2_u_hat_r", std::sqrt(L2_error_u_hat_real));
2440 * error_table.add_value("eL2_u_hat_i", std::sqrt(L2_error_u_hat_imag));
2441 * error_table.add_value("eL2_p_hat_r", std::sqrt(L2_error_p_hat_real));
2442 * error_table.add_value("eL2_p_hat_i", std::sqrt(L2_error_p_hat_imag));
2448 * <a name="step_100-DPGHelmholtzrefine_grid"></a>
2449 * <h3>DPGHelmholtz::refine_grid</h3>
2450 * This function creates the mesh for the first cycle and then refines it
2451 * uniformly for subsequent cycles. It also records the number of cells and
2452 * the maximum cell diameter in the error table for convergence analysis.
2455 * template <int dim>
2456 * void DPGHelmholtz<dim>::refine_grid(const unsigned int cycle)
2460 * const Point<dim> p1{0., 0.};
2461 * const Point<dim> p2{1., 1.};
2463 * std::vector<unsigned int> repetitions({2, 2});
2464 * GridGenerator::subdivided_hyper_rectangle(
2465 * triangulation, repetitions, p1, p2, true);
2466 * triangulation.refine_global(0);
2470 * triangulation.refine_global();
2473 * std::cout << "Number of active cells: " << triangulation.n_active_cells()
2476 * error_table.add_value("cycle", cycle);
2477 * error_table.add_value("n_cells", triangulation.n_active_cells());
2478 * error_table.add_value("cell_size",
2479 * GridTools::maximal_cell_diameter<dim>(triangulation));
2485 * <a name="step_100-DPGHelmholtzrun"></a>
2486 * <h3>DPGHelmholtz::run</h3>
2487 * This function is the main loop of the program using all the previously
2488 * defined functions. It is also where the convergence rates are obtained
2489 * after all the refinement cycles.
2492 * template <int dim>
2493 * void DPGHelmholtz<dim>::run()
2495 * for (unsigned int cycle = 0; cycle < 8; ++cycle)
2497 * std::cout << "===========================================" << std::endl
2498 * << "Cycle " << cycle << ':
' << std::endl;
2500 * refine_grid(cycle);
2502 * assemble_system(false);
2503 * solve_linear_system_skeleton();
2504 * assemble_system(true);
2505 * calculate_L2_error();
2506 * output_results(cycle);
2509 * error_table.evaluate_convergence_rates(
2510 * "eL2_u_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2511 * error_table.evaluate_convergence_rates(
2512 * "eL2_u_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2513 * error_table.evaluate_convergence_rates(
2514 * "eL2_p_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2515 * error_table.evaluate_convergence_rates(
2516 * "eL2_p_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2517 * error_table.evaluate_convergence_rates(
2518 * "eL2_u_hat_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2519 * error_table.evaluate_convergence_rates(
2520 * "eL2_u_hat_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2521 * error_table.evaluate_convergence_rates(
2522 * "eL2_p_hat_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2523 * error_table.evaluate_convergence_rates(
2524 * "eL2_p_hat_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2526 * std::cout << "===========================================" << std::endl;
2527 * std::cout << "Convergence table:" << std::endl;
2528 * error_table.write_text(std::cout);
2530 * } // End of namespace Step100
2535 * <a name="step_100-Thecodemaincodefunction"></a>
2536 * <h3>The <code>main</code> function</h3>
2540 * This is the main function of the program. It creates an instance of the
2541 * <code>DPGHelmholtz</code> class and calls its run method. It defines the
2542 * necessary variables for our 2D DPG Helmholtz, i.e., the <code>degree</code>
2543 * @f$p@f$ of the trial space, the degree difference <code>delta_degree</code>
2544 * between the test and trial spaces, the <code>wavenumber</code> @f$k@f$, and the
2545 * angle of incidence of the plane wave <code>theta</code> in radians.
2550 * const unsigned int dim = 2;
2554 * const int degree = 2;
2555 * const int delta_degree = 1;
2556 * const double wavenumber = 20 * pi;
2557 * const double theta = pi / 4.;
2559 * std::cout << "===========================================" << std::endl
2560 * << "Trial order: " << degree << std::endl
2561 * << "Test order: " << delta_degree + degree << std::endl
2562 * << "===========================================" << std::endl
2565 * Step100::DPGHelmholtz<dim> dpg_helmholtz(degree,
2570 * dpg_helmholtz.run();
2572 * std::cout << std::endl;
2574 * catch (std::exception &exc)
2576 * std::cerr << std::endl
2578 * << "----------------------------------------------------"
2580 * std::cerr << "Exception on processing: " << std::endl
2581 * << exc.what() << std::endl
2582 * << "Aborting!" << std::endl
2583 * << "----------------------------------------------------"
2589 * std::cerr << std::endl
2591 * << "----------------------------------------------------"
2593 * std::cerr << "Unknown exception!" << std::endl
2594 * << "Aborting!" << std::endl
2595 * << "----------------------------------------------------"
2603<a name="step_100-Results"></a><h1>Results</h1>
2605The solutions of the above program are written to .vtu files for both the
2606interior solution and the skeleton one. Each file contains four components,
2607the real and imaginary part of the pressure and velocity field. With a degree
2608@f$p=2@f$ polynomial space, a difference of @f$\Delta p =1@f$ degree between the test
2609and the trial space, a plane wave propagating in the direction @f$\theta = \pi/4@f$
2610and an angular frequency @f$\omega = 20 \pi@f$, the program should output the
2611following table at the end:
2613<table align="center" class="doxtable">
2615 <th rowspan="2">Cycle</th> <th rowspan="2">Cells</th> <th
2616 rowspan="2">h</th> <th rowspan="2">DoFs<br/>interior</th> <th
2617 rowspan="2">DoFs<br/>skeleton</th> <th rowspan="2">DoFs<br/>test</th> <th
2618 rowspan="2">Iterations</th> <th colspan="2">‖Re{u}‖<sub>L2</sub></th>
2619 <th colspan="2">‖Im{u}‖<sub>L2</sub></th>
2620 <th colspan="2">‖Re{p*}‖<sub>L2</sub></th>
2621 <th colspan="2">‖Im{p*}‖<sub>L2</sub></th> <th
2622 colspan="2">‖Re{û<sub>n</sub>}‖<sub>L2</sub></th>
2623 <th colspan="2">‖Im{û<sub>n</sub>}‖<sub>L2</sub></th>
2624 <th colspan="2">‖Re{p̂*}‖<sub>L2</sub></th> <th
2625 colspan="2">‖Im{p̂*}‖<sub>L2</sub></th>
2627 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2628 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2629 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2630 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2632 <td align="center">0</td><td align="center">4</td><td
2633 align="center">0.7071</td> <td align="center">216</td><td
2634 align="center">138</td><td align="center">450</td><td
2635 align="center">78</td> <td align="center">0.8139</td><td
2636 align="center">–</td> <td align="center">0.5738</td><td
2637 align="center">–</td> <td align="center">0.8061</td><td
2638 align="center">–</td> <td align="center">0.5736</td><td
2639 align="center">–</td> <td align="center">0.8489</td><td
2640 align="center">–</td> <td align="center">1.2310</td><td
2641 align="center">–</td> <td align="center">1.3819</td><td
2642 align="center">–</td> <td align="center">1.9864</td><td
2643 align="center">–</td>
2645 <td align="center">1</td><td align="center">16</td><td
2646 align="center">0.3536</td> <td align="center">864</td><td
2647 align="center">450</td><td align="center">1666</td><td
2648 align="center">102</td> <td align="center">0.7118</td><td
2649 align="center">0.19</td> <td align="center">0.7097</td><td
2650 align="center">-0.31</td> <td align="center">0.7106</td><td
2651 align="center">0.18</td> <td align="center">0.7087</td><td
2652 align="center">-0.31</td> <td align="center">1.4091</td><td
2653 align="center">-0.73</td> <td align="center">1.4210</td><td
2654 align="center">-0.21</td> <td align="center">2.3013</td><td
2655 align="center">-0.74</td> <td align="center">2.3266</td><td
2656 align="center">-0.23</td>
2658 <td align="center">2</td><td align="center">64</td><td
2659 align="center">0.1768</td> <td align="center">3456</td><td
2660 align="center">1602</td><td align="center">6402</td><td
2661 align="center">208</td> <td align="center">0.6600</td><td
2662 align="center">0.11</td> <td align="center">0.6641</td><td
2663 align="center">0.10</td> <td align="center">0.6618</td><td
2664 align="center">0.10</td> <td align="center">0.6597</td><td
2665 align="center">0.10</td> <td align="center">1.8172</td><td
2666 align="center">-0.37</td> <td align="center">1.7966</td><td
2667 align="center">-0.34</td> <td align="center">2.7158</td><td
2668 align="center">-0.24</td> <td align="center">2.7309</td><td
2669 align="center">-0.23</td>
2671 <td align="center">3</td><td align="center">256</td><td
2672 align="center">0.0884</td> <td align="center">13824</td><td
2673 align="center">6018</td><td align="center">25090</td><td
2674 align="center">1547</td> <td align="center">0.1334</td><td
2675 align="center">2.31</td> <td align="center">0.1091</td><td
2676 align="center">2.61</td> <td align="center">0.1093</td><td
2677 align="center">2.60</td> <td align="center">0.1337</td><td
2678 align="center">2.30</td> <td align="center">0.5128</td><td
2679 align="center">1.83</td> <td align="center">0.4169</td><td
2680 align="center">2.11</td> <td align="center">0.5232</td><td
2681 align="center">2.38</td> <td align="center">0.6859</td><td
2682 align="center">1.99</td>
2684 <td align="center">4</td><td align="center">1024</td><td
2685 align="center">0.0442</td> <td align="center">55296</td><td
2686 align="center">23298</td><td align="center">99330</td><td
2687 align="center">2233</td> <td align="center">0.0087</td><td
2688 align="center">3.94</td> <td align="center">0.0086</td><td
2689 align="center">3.66</td> <td align="center">0.0085</td><td
2690 align="center">3.69</td> <td align="center">0.0086</td><td
2691 align="center">3.96</td> <td align="center">0.0370</td><td
2692 align="center">3.79</td> <td align="center">0.0365</td><td
2693 align="center">3.51</td> <td align="center">0.0179</td><td
2694 align="center">4.87</td> <td align="center">0.0204</td><td
2695 align="center">5.07</td>
2697 <td align="center">5</td><td align="center">4096</td><td
2698 align="center">0.0221</td> <td align="center">221184</td><td
2699 align="center">91650</td><td align="center">395266</td><td
2700 align="center">5297</td> <td align="center">0.0011</td><td
2701 align="center">3.02</td> <td align="center">0.0011</td><td
2702 align="center">3.00</td> <td align="center">0.0011</td><td
2703 align="center">3.01</td> <td align="center">0.0011</td><td
2704 align="center">3.02</td> <td align="center">0.0061</td><td
2705 align="center">2.59</td> <td align="center">0.0061</td><td
2706 align="center">2.57</td> <td align="center">0.0015</td><td
2707 align="center">3.58</td> <td align="center">0.0015</td><td
2708 align="center">3.74</td>
2710 <td align="center">6</td><td align="center">16384</td><td
2711 align="center">0.0110</td> <td align="center">884736</td><td
2712 align="center">363522</td><td align="center">1576962</td><td
2713 align="center">10166</td> <td align="center">0.0001</td><td
2714 align="center">3.00</td> <td align="center">0.0001</td><td
2715 align="center">3.00</td> <td align="center">0.0001</td><td
2716 align="center">3.00</td> <td align="center">0.0001</td><td
2717 align="center">3.00</td> <td align="center">0.0011</td><td
2718 align="center">2.53</td> <td align="center">0.0011</td><td
2719 align="center">2.52</td> <td align="center">0.0001</td><td
2720 align="center">3.50</td> <td align="center">0.0001</td><td
2721 align="center">3.54</td>
2723 <td align="center">7</td><td align="center">65536</td><td
2724 align="center">0.0055</td> <td align="center">3538944</td><td
2725 align="center">1447938</td><td align="center">6299650</td><td
2726 align="center">19647</td> <td align="center">0.0000</td><td
2727 align="center">3.00</td> <td align="center">0.0000</td><td
2728 align="center">3.00</td> <td align="center">0.0000</td><td
2729 align="center">3.00</td> <td align="center">0.0000</td><td
2730 align="center">3.00</td> <td align="center">0.0002</td><td
2731 align="center">2.51</td> <td align="center">0.0002</td><td
2732 align="center">2.51</td> <td align="center">0.0000</td><td
2733 align="center">3.47</td> <td align="center">0.0000</td><td
2734 align="center">3.47</td>
2738The most refined solution should look similar to the following figure where
2739we present the pressure field solution -- the velocity fields have essentially
2740the same profile but are vector valued so we do not show them here --
2741depending on your visualization tool (here we used Paraview). We cropped the
2742domain in 4 to show both the real and imaginary components for the interior
2743and the mesh skeleton.
2745<img src="https://www.dealii.org/images/steps/developer/step-100-pressure.png"
2746style="width:80%" alt="">
2749<a name="step_100-Convergence"></a><h3>Convergence</h3>
2752To validate that the code falls back on the analytical solution of the plane
2753wave with the expected order, we did a convergence plot for all the different
2754fields which are presented below. As expected for degree @f$p=2@f$ polynomial
2755DGQ element in the interior, all fields converge to order 3. On the
2756faces, it is expected that we lose half an order compared to the interior
2757and that is what we observe for the normal velocity component. However,
2758the pressure skeleton unknowns are associated with the trace of Q
2759elements and for the same sequence of energy spaces the Q elements are one
2760degree higher than the DGQ element. It follows that for DGQ of order 2 as
2761used here, the corresponding Q element would be order 3 so the trace of
2762those elements should converge with a slope of 3.5 as observed.
2764<img src="https://www.dealii.org/images/steps/developer/step-100-error.svg"
2765style="width:80%" alt="">
2768<a name="step_100-Linearsolveriterations"></a><h3>Linear solver iterations</h3>
2771In time-harmonic problems, the number of iterations to solve the system
2772increases as the resolution of the wave increases (there are more dofs in
2773the linear system). The DPG method is no exception to this. To show this,
2774we recorded the number of iterations as we refined the mesh for 3 different
2775angular frequencies and we present the result in the last figure below. In it,
2776we show the number of iterations as a function of the number of dofs along
2777with the total error of the interior fields. From it, we can see that the
2778error does not start converging until the Nyquist criterion is respected,
2779which requires the spatial discretization to resolve the wave with at least
2780two points per wavelength. At that point there is a jump in the number
2781of iterations to achieve convergence. After that the number of iterations
2782approximately doubles each time we double the resolution. Nonetheless, because
2783the DPG method enables the use of a Conjugate Gradient solver we are able to
2784obtain a solution even for high frequencies without exceeding amounts of memory.
2786<img src="https://www.dealii.org/images/steps/developer/step-100-iteration.svg"
2787style="width:80%" alt="">
2789<a name="step_100-Possibilitiesforextension"></a><h3>Possibilities for extension </h3>
2792As an extension to get a feeling of the method, one could first try to
2793implement the 3D version of the plane wave problem in the unit cube by adding
2794a second angle @f$\phi@f$. The analytical solution to this problem is also known
2797 p^* & = e^{-i k (x \cos(\theta) \sin{\phi}
2798 + y \sin(\theta) \sin(\phi) + z \cos(\phi))},
2799 \\ \mathbf{u} & = \frac{1}{c_s} \begin{pmatrix} \cos(\theta) \sin(\phi) \\
2800 \sin(\theta) \sin(\phi) \\ \cos(\phi) \end{pmatrix} e^{-i k (x \cos(\theta)
2801 \sin{\phi} + y \sin(\theta) \sin(\phi) + z \cos(\phi))}.
2803Another interesting extension would be to reconstruct the residual @f$\Psi^r@f$
2804when the solutions on the faces and interior dofs are known and use it to
2805build an error estimator that can be utilized for adaptive hp-refinement
2806@cite petrides2017. Finally, if one is interested more specifically in
2807time-harmonic problems, the implementation of an adequate preconditioner to
2808improve the convergence of the linear solver would remedy one
2809of the inherent difficulties for this type of problem as stated by O. Ernst
2810and M. J. Gander @cite ernst2012.
2813<a name="step_100-PlainProg"></a>
2814<h1> The plain program</h1>
2815@include "step-100.cc"
* * for(const auto &cell :triangulation.active_cell_iterators())
* * int main(int argc, char **argv)
* * * struct InterferenceTaperTransform *
virtual RangeNumberType value(const Point< dim > &p, const unsigned int component=0) const
virtual value_type value(const Point< dim > &p) const
#define AssertDimension(dim1, dim2)
#define AssertThrow(cond, exc)
* * * ScaleZFunction< dim, Number, components >::ScaleZFunction * component(component)
int(&) functions(const void *v1, const void *v2)
::VectorizedArray< Number, width > cos(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)