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 * Finally, we define the local cell matrix and right-hand side vector
1219 * associated with the skeleton degrees of freedom that will be used to solve
1220 * our system, together with a local-to-global DoF index map used for
1221 * distribution into the global system. These are relevant to obtain the
1222 * solution when <code>solve_interior = false </code>, but when
1223 * <code>solve_interior = true</code>, we need to define additional vectors to
1224 * store the interior solution, interior right-hand side, and the skeleton
1225 * solution that will be used for the interior reconstruction step. Note that
1226 * since the Helmholtz problem is complex-valued, we also define the imaginary
1227 * unit and several complex constants that appear in the bilinear and linear
1228 * forms. Although the global linear system that is built is real-valued,
1229 * complex arithmetic from the C++ standard library is used locally to
1230 * simplify the formulation, in the same spirit as in @ref step_81 "step-81".
1236 * template <int dim>
1237 * void DPGHelmholtz<dim>::assemble_system(const bool solve_interior)
1239 * const QGauss<dim> quadrature_formula(fe_test.degree + 1);
1240 * const QGauss<dim - 1> face_quadrature_formula(fe_test.degree + 1);
1241 * const unsigned int n_q_points = quadrature_formula.size();
1242 * const unsigned int n_face_q_points = face_quadrature_formula.size();
1244 * FEValues<dim> fe_values_trial_interior(fe_trial_interior,
1245 * quadrature_formula,
1247 * update_quadrature_points |
1248 * update_JxW_values);
1249 * FEValues<dim> fe_values_test(fe_test,
1250 * quadrature_formula,
1251 * update_values | update_gradients);
1252 * FEFaceValues<dim> fe_values_trial_skeleton(fe_trial_skeleton,
1253 * face_quadrature_formula,
1255 * update_quadrature_points |
1256 * update_normal_vectors |
1257 * update_JxW_values);
1258 * FEFaceValues<dim> fe_face_values_test(fe_test,
1259 * face_quadrature_formula,
1262 * const unsigned int dofs_per_cell_test = fe_test.n_dofs_per_cell();
1263 * const unsigned int dofs_per_cell_trial_interior =
1264 * fe_trial_interior.n_dofs_per_cell();
1265 * const unsigned int dofs_per_cell_trial_skeleton =
1266 * fe_trial_skeleton.n_dofs_per_cell();
1268 * std::vector<Tensor<1, dim, std::complex<double>>> v(dofs_per_cell_test);
1269 * std::vector<Tensor<1, dim, std::complex<double>>> v_conj(
1270 * dofs_per_cell_test);
1271 * std::vector<std::complex<double>> div_v(dofs_per_cell_test);
1272 * std::vector<std::complex<double>> div_v_conj(dofs_per_cell_test);
1273 * std::vector<std::complex<double>> q(dofs_per_cell_test);
1274 * std::vector<std::complex<double>> q_conj(dofs_per_cell_test);
1275 * std::vector<Tensor<1, dim, std::complex<double>>> grad_q(
1276 * dofs_per_cell_test);
1277 * std::vector<Tensor<1, dim, std::complex<double>>> grad_q_conj(
1278 * dofs_per_cell_test);
1279 * std::vector<std::complex<double>> v_face_n(dofs_per_cell_test);
1280 * std::vector<std::complex<double>> v_face_n_conj(dofs_per_cell_test);
1281 * std::vector<std::complex<double>> q_face(dofs_per_cell_test);
1282 * std::vector<std::complex<double>> q_face_conj(dofs_per_cell_test);
1284 * std::vector<Tensor<1, dim, std::complex<double>>> u(
1285 * dofs_per_cell_trial_interior);
1286 * std::vector<std::complex<double>> p(dofs_per_cell_trial_interior);
1288 * std::vector<std::complex<double>> u_hat_n(dofs_per_cell_trial_skeleton);
1289 * std::vector<std::complex<double>> u_hat_n_conj(
1290 * dofs_per_cell_trial_skeleton);
1291 * std::vector<std::complex<double>> p_hat(dofs_per_cell_trial_skeleton);
1292 * std::vector<std::complex<double>> p_hat_conj(dofs_per_cell_trial_skeleton);
1294 * enum ShapeFunctionType : unsigned char
1296 * velocity_real = 1u << 0,
1297 * velocity_imag = 1u << 1,
1298 * pressure_real = 1u << 2,
1299 * pressure_imag = 1u << 3,
1301 * is_velocity = velocity_real | velocity_imag,
1302 * is_pressure = pressure_real | pressure_imag
1304 * std::vector<unsigned char> shape_function_type_test(dofs_per_cell_test);
1305 * std::vector<unsigned char> shape_function_type_trial_interior(
1306 * dofs_per_cell_trial_interior);
1307 * std::vector<unsigned char> shape_function_type_trial_skeleton(
1308 * dofs_per_cell_trial_skeleton);
1310 * LAPACKFullMatrix<double> G_matrix(dofs_per_cell_test, dofs_per_cell_test);
1311 * LAPACKFullMatrix<double> B_matrix(dofs_per_cell_test,
1312 * dofs_per_cell_trial_interior);
1313 * LAPACKFullMatrix<double> B_hat_matrix(dofs_per_cell_test,
1314 * dofs_per_cell_trial_skeleton);
1315 * LAPACKFullMatrix<double> D_matrix(dofs_per_cell_trial_skeleton,
1316 * dofs_per_cell_trial_skeleton);
1318 * Vector<double> g_vector(dofs_per_cell_trial_skeleton);
1319 * Vector<double> l_vector(dofs_per_cell_test);
1321 * LAPACKFullMatrix<double> M1_matrix(dofs_per_cell_trial_interior,
1322 * dofs_per_cell_trial_interior);
1323 * LAPACKFullMatrix<double> M2_matrix(dofs_per_cell_trial_interior,
1324 * dofs_per_cell_trial_skeleton);
1325 * LAPACKFullMatrix<double> M3_matrix(dofs_per_cell_trial_skeleton,
1326 * dofs_per_cell_trial_skeleton);
1327 * LAPACKFullMatrix<double> M4_matrix(dofs_per_cell_trial_interior,
1328 * dofs_per_cell_test);
1329 * LAPACKFullMatrix<double> M5_matrix(dofs_per_cell_trial_skeleton,
1330 * dofs_per_cell_test);
1332 * LAPACKFullMatrix<double> tmp_matrix(dofs_per_cell_trial_skeleton,
1333 * dofs_per_cell_trial_interior);
1334 * LAPACKFullMatrix<double> tmp_matrix2(dofs_per_cell_trial_skeleton,
1335 * dofs_per_cell_trial_skeleton);
1336 * LAPACKFullMatrix<double> tmp_matrix3(dofs_per_cell_trial_skeleton,
1337 * dofs_per_cell_test);
1338 * Vector<double> tmp_vector(dofs_per_cell_trial_interior);
1340 * FullMatrix<double> cell_matrix(dofs_per_cell_trial_skeleton,
1341 * dofs_per_cell_trial_skeleton);
1342 * Vector<double> cell_skeleton_rhs(dofs_per_cell_trial_skeleton);
1343 * std::vector<types::global_dof_index> local_dof_indices(
1344 * dofs_per_cell_trial_skeleton);
1346 * Vector<double> cell_interior_rhs(dofs_per_cell_trial_interior);
1347 * Vector<double> cell_interior_solution(dofs_per_cell_trial_interior);
1348 * Vector<double> cell_skeleton_solution(dofs_per_cell_trial_skeleton);
1350 * constexpr std::complex<double> imag(0., 1.);
1351 * const std::complex<double> iomega = imag * wavenumber;
1352 * const std::complex<double> iomega_conj = std::conj(iomega);
1356 * After defining all the variables for our assembly, we now assemble the
1357 * local contributions of the DPG formulation. As usual, we loop over all
1358 * active cells of the triangulation. We choose the DoFHandler associated
1359 * with the interior trial space as the primary iterator, since the
1360 * cell-wise assembly is naturally tied to the interior unknowns. For each
1361 * such cell, we explicitly obtain the corresponding iterators for the test
1362 * space and for the skeleton trial space to ensure that all FEValues
1363 * objects are reinitialized on the same physical cell. The loop on cells is
1364 * used to assemble all the local matrices and vectors entering the DPG
1365 * static condensation procedure (@f$G@f$, @f$B@f$, @f$\hat{B}@f$, @f$D@f$, @f$g@f$, @f$l@f$). It
1366 * follows that all these objects are reinitialized to zero at the beginning
1367 * of each cell loop. In addition, we need to reset the local condensation
1368 * matrix @f$M_1@f$ because LAPACKFullMatrix keeps track of its inverse status
1369 * between iterations and forbids to invert it again if it has the inverted
1374 * At each quadrature point, we evaluate
1375 * and cache the values, gradients, and divergences of the test functions
1376 * (@f$\mathbf{v}@f$ and @f$q@f$), as well as the values of the trial functions
1377 * (@f$\mathbf{u}@f$ and @f$p@f$) in the relevant containers. These quantities
1378 * are stored as complex-valued expressions, together with their complex
1379 * conjugates, in order to directly form the sesquilinear forms appearing in
1380 * the time-harmonic formulation. In addition, we check and store in the
1381 * <code>shape_function_type_test</code>,
1382 * <code>shape_function_type_trial_interior</code>, and
1383 * <code>shape_function_type_trial_skeleton</code> vectors to which field
1384 * each shape function is associated.
1388 * For each quadrature point, we then loop over the test space degrees of
1389 * freedom. In a first nested loop over test indices, we assemble the Gram
1390 * matrix @f$G@f$. Depending on whether the test basis functions correspond to
1391 * the velocity or pressure components, we add the appropriate
1393 * - If both <code>i</code> and <code>j</code> are in test space associated
1394 * to the test functions @f$\mathbf{v}@f$, we build the terms @f$(\mathbf{v},
1395 * \mathbf{v})_{\Omega_h} + (\nabla \cdot \mathbf{v}, \nabla \cdot
1396 * \mathbf{v})_{\Omega_h} + (i\omega\mathbf{v}, i\omega
1397 * \mathbf{v})_{\Omega_h}@f$;
1398 * - If the dof <code>i</code> is in test function @f$\mathbf{v}@f$ and dof
1399 * <code>j</code> in test function @f$q@f$ we build the terms @f$(i\omega
1400 * \mathbf{v}, \nabla q)_{\Omega_h} + (\nabla \cdot \mathbf{v}, i\omega
1402 * - If the dof <code>i</code> is in test function @f$q@f$ and the dof
1403 * <code>j</code> is in the test function @f$\mathbf{v}@f$, we build the terms
1404 * @f$(\nabla q, i\omega \mathbf{v})_{\Omega_h} + (i\omega q, \nabla \cdot
1405 * \mathbf{v})_{\Omega_h}@f$;
1406 * - Finally, <code>i</code> and <code>j</code> are in test space
1407 * associated to @f$q@f$, we build the terms @f$(q, q)_{\Omega_h} + (\nabla
1408 * q,\nabla q)_{\Omega_h} + (i\omega q, i\omega q)_{\Omega_h}@f$.
1412 * In a second nested loop over interior trial space degrees of freedom, we
1413 * assemble the operator matrix @f$B@f$. Here again, the contributions depend on
1414 * the pairing of test and trial components:
1415 * - If dof <code>i</code> in test function @f$\mathbf{v}@f$ and dof
1416 * <code>j</code> in trial function @f$\mathbf{u}@f$ we build the term
1417 * @f$(\mathbf{v}, i\omega \mathbf{u})_{\Omega_h}@f$;
1418 * - If dof <code>i</code> in test function @f$\mathbf{v}@f$ and dof
1419 * <code>j</code> in trial function @f$p@f$ we build the term @f$ -( \nabla \cdot
1420 * \mathbf{v}, p^*)_{\Omega_h}@f$;
1421 * - If dof <code>i</code> in test function @f$q@f$ and dof <code>j</code> in
1422 * trial function @f$\mathbf{u}@f$ we build the term @f$-(\nabla q,
1423 * \mathbf{u})_{\Omega_h}@f$;
1424 * - If dof <code>i</code> in test function @f$q@f$ and dof <code>j</code> in
1425 * trial function @f$p@f$ we build the term @f$(q, i\omega p^*)_{\Omega_h}@f$.
1429 * Finally, we assemble the load vector @f$l@f$. In the present plane wave
1430 * configuration, the volumetric source term is zero, but we nevertheless
1431 * assemble @f$(q, l)_{\Omega_h}@f$ over the cell for completeness.
1434 * for (const auto &cell : dof_handler_trial_interior.active_cell_iterators())
1436 * fe_values_trial_interior.reinit(cell);
1438 * const typename DoFHandler<dim>::active_cell_iterator cell_test =
1439 * cell->as_dof_handler_iterator(dof_handler_test);
1440 * fe_values_test.reinit(cell_test);
1442 * const typename DoFHandler<dim>::active_cell_iterator cell_skeleton =
1443 * cell->as_dof_handler_iterator(dof_handler_trial_skeleton);
1453 * for (unsigned int q_point = 0; q_point < n_q_points; ++q_point)
1455 * const double JxW = fe_values_trial_interior.JxW(q_point);
1457 * for (unsigned int k : fe_values_test.dof_indices())
1460 * fe_values_test[extractor_u_real].value(k, q_point) +
1461 * imag * fe_values_test[extractor_u_imag].value(k, q_point);
1463 * fe_values_test[extractor_u_real].value(k, q_point) -
1464 * imag * fe_values_test[extractor_u_imag].value(k, q_point);
1467 * fe_values_test[extractor_u_real].divergence(k, q_point) +
1469 * fe_values_test[extractor_u_imag].divergence(k, q_point);
1471 * fe_values_test[extractor_u_real].divergence(k, q_point) -
1473 * fe_values_test[extractor_u_imag].divergence(k, q_point);
1476 * fe_values_test[extractor_p_real].value(k, q_point) +
1477 * imag * fe_values_test[extractor_p_imag].value(k, q_point);
1479 * fe_values_test[extractor_p_real].value(k, q_point) -
1480 * imag * fe_values_test[extractor_p_imag].value(k, q_point);
1483 * fe_values_test[extractor_p_real].gradient(k, q_point) +
1484 * imag * fe_values_test[extractor_p_imag].gradient(k, q_point);
1486 * fe_values_test[extractor_p_real].gradient(k, q_point) -
1487 * imag * fe_values_test[extractor_p_imag].gradient(k, q_point);
1489 * if (fe_test.shape_function_belongs_to(k, extractor_u_real))
1490 * shape_function_type_test[k] |= velocity_real;
1491 * if (fe_test.shape_function_belongs_to(k, extractor_u_imag))
1492 * shape_function_type_test[k] |= velocity_imag;
1493 * if (fe_test.shape_function_belongs_to(k, extractor_p_real))
1494 * shape_function_type_test[k] |= pressure_real;
1495 * if (fe_test.shape_function_belongs_to(k, extractor_p_imag))
1496 * shape_function_type_test[k] |= pressure_imag;
1499 * for (unsigned int k : fe_values_trial_interior.dof_indices())
1502 * fe_values_trial_interior[extractor_u_real].value(k, q_point) +
1504 * fe_values_trial_interior[extractor_u_imag].value(k,
1508 * fe_values_trial_interior[extractor_p_real].value(k, q_point) +
1510 * fe_values_trial_interior[extractor_p_imag].value(k,
1513 * if (fe_trial_interior.shape_function_belongs_to(
1514 * k, extractor_u_real))
1515 * shape_function_type_trial_interior[k] |= velocity_real;
1516 * if (fe_trial_interior.shape_function_belongs_to(
1517 * k, extractor_u_imag))
1518 * shape_function_type_trial_interior[k] |= velocity_imag;
1519 * if (fe_trial_interior.shape_function_belongs_to(
1520 * k, extractor_p_real))
1521 * shape_function_type_trial_interior[k] |= pressure_real;
1522 * if (fe_trial_interior.shape_function_belongs_to(
1523 * k, extractor_p_imag))
1524 * shape_function_type_trial_interior[k] |= pressure_imag;
1527 * for (const auto i : fe_values_test.dof_indices())
1529 * const unsigned char test_type_i = shape_function_type_test[i];
1531 * for (const auto j : fe_values_test.dof_indices())
1533 * const unsigned char test_type_j =
1534 * shape_function_type_test[j];
1536 * if ((test_type_i & is_velocity) &&
1537 * (test_type_j & is_velocity))
1540 * (((v_conj[i] * v[j]) + (div_v_conj[i] * div_v[j]) +
1541 * (iomega_conj * v_conj[i] * iomega * v[j])) *
1546 * else if ((test_type_i & is_velocity) &&
1547 * (test_type_j & is_pressure))
1550 * (((iomega_conj * v_conj[i] * grad_q[j]) +
1551 * (div_v_conj[i] * iomega * q[j])) *
1556 * else if ((test_type_i & is_pressure) &&
1557 * (test_type_j & is_velocity))
1560 * (((grad_q_conj[i] * iomega * v[j]) +
1561 * (iomega_conj * q_conj[i] * div_v[j])) *
1566 * else if ((test_type_i & is_pressure) &&
1567 * (test_type_j & is_pressure))
1570 * (((q_conj[i] * q[j]) + (grad_q[j] * grad_q_conj[i]) +
1571 * (iomega_conj * q_conj[i] * iomega * q[j])) *
1577 * for (const auto j : fe_values_trial_interior.dof_indices())
1579 * const unsigned char trial_type_j =
1580 * shape_function_type_trial_interior[j];
1582 * if ((test_type_i & is_velocity) &&
1583 * (trial_type_j & is_velocity))
1586 * ((v_conj[i] * iomega * u[j]) * JxW).real();
1589 * else if ((test_type_i & is_velocity) &&
1590 * (trial_type_j & is_pressure))
1592 * B_matrix(i, j) -= ((div_v_conj[i] * p[j]) * JxW).real();
1595 * else if ((test_type_i & is_pressure) &&
1596 * (trial_type_j & is_velocity))
1599 * ((grad_q_conj[i] * u[j]) * JxW).real();
1602 * else if ((test_type_i & is_pressure) &&
1603 * (trial_type_j & is_pressure))
1606 * ((q_conj[i] * iomega * p[j]) * JxW).real();
1610 * if (test_type_i & is_pressure)
1612 * double source_term = 0.0;
1613 * l_vector(i) += (q_conj[i] * source_term * JxW).real();
1620 * We now need to assemble the skeleton (face) contributions of the DPG
1621 * formulation. For this purpose, we loop over all faces of the current
1622 * cell using the DoFHandler associated with the skeleton trial space.
1623 * On each face, we reinitialize the FEFaceValues objects for both the
1624 * test space and the skeleton trial space, ensuring that all quantities
1625 * are evaluated on the same geometric entity.
1629 * In addition to the standard face integrals, this loop also accounts
1630 * for Robin boundary conditions, which in the present plane wave
1631 * configuration are imposed on two boundaries of the domain
1632 * (<code>types::boundary_id(1)</code> and
1633 * <code>types::boundary_id(3)</code>). The Robin terms involve the
1635 * @f$\frac{k_n}{\omega}@f$, but in our configuration,
1636 * @f$\omega = k c_s@f$ with @f$c_s=1@f$, and the geometry of the domain implies
1637 * that @f$k_n =\mathbf{k} \cdot \mathbf{n}@f$ reduces to either
1638 * @f$k\cos{\theta}@f$ for the right boundary
1639 * (<code>types::boundary_id(1)</code>) or @f$k\sin{\theta}@f$ for the top
1640 * boundary (<code>types::boundary_id(3)</code>). Consequently, the
1641 * wavenumber cancels out, and we are left with the cosine and sine of
1642 * the propagation direction as the factor in front of the pressure
1647 * As for the cell-wise assembly, we loop over the face quadrature
1648 * points. We evaluate and cache the relevant quantities to assemble
1649 * both the Gram matrix face contributions and the face operator matrix
1650 * @f$\hat{B}@f$. We also store the shape function types for the test and
1651 * skeleton trial spaces for the current face dofs. After precomputing
1652 * these quantities, we loop over the test space degrees of freedom and
1653 * trial space face degrees of freedom to assemble the corresponding
1654 * contributions. The face contributions to the Gram matrix only arise
1655 * when the face lies on a Robin boundary and are assembled as follows:
1656 * - If both <code>i</code> and <code>j</code> are in test space
1657 * associated to the test functions @f$\mathbf{v}@f$ we build, @f$\langle
1658 * \mathbf{v} \cdot \mathbf{n}, \mathbf{v} \cdot \mathbf{n}
1659 * \rangle_{\Gamma_1 \cup \Gamma_3}@f$;
1660 * - If the dof <code>i</code> is in test function @f$\mathbf{v}@f$ and dof
1661 * <code>j</code> in test function @f$q@f$, we build @f$\langle
1662 * \mathbf{v} \cdot \mathbf{n},\frac{k_n}{\omega}q
1663 * \rangle_{\Gamma_1 \cup \Gamma_3}@f$
1664 * - If the dof <code>i</code> is in test function @f$q@f$ and the dof
1665 * <code>j</code> is in the test function @f$\mathbf{v}@f$, we build
1666 * @f$\langle \frac{k_n}{\omega}q, \mathbf{v}
1667 * \cdot \mathbf{n} \rangle_{\Gamma_1 \cup
1669 * - Finally, if both <code>i</code> and <code>j</code> are in test
1670 * space associated to the test functions @f$q@f$, we build @f$\langle
1671 * \frac{k_n}{\omega}q,
1672 * \frac{k_n}{\omega}q \rangle_{\Gamma_1
1673 * \cup \Gamma_3}@f$.
1674 * For all faces of the mesh (regardless of boundary type) we also
1675 * assemble the face operator matrix @f$\hat{B}@f$ by looping over the
1676 * skeleton trial space degrees of freedom. The two terms are:
1677 * - If dof <code>i</code> in test function @f$\mathbf{v}@f$ and dof
1678 * <code>j</code> in trial function @f$\hat{p}^*@f$ we build the
1679 * term @f$\left\langle
1680 * \mathbf{v} \cdot \mathbf{n}, \hat{p}^*
1681 * \right\rangle_{\partial \Omega_h}@f$;
1682 * - If dof <code>i</code> in test function @f$q@f$ and dof <code>j</code>
1683 * in trial function @f$\hat{u}_n@f$ we build the term
1684 * @f$\left\langle q, \hat{u}_n \right\rangle_{\partial \Omega_h}@f$.
1688 * An important detail when assembling the face contributions related to
1689 * the velocity trace @f$\hat{u}_n@f$. Indeed, since the FE_FaceQ elements
1690 * used to represent the trace of H(div) conforming fields do not encode
1691 * an intrinsic orientation, special care must be taken to ensure
1692 * consistency of the numerical flux across shared faces (i.e., the flux
1693 * that cross a face in a given cell is equal to the flux that crosses
1694 * the same face in the adjacent cell for which the normal is opposite).
1695 * To this end, we introduce a sign factor that enforces a unique
1696 * orientation rule: the flux is always oriented from the cell with the
1697 * smaller active cell index toward the cell with the larger one. On
1698 * boundary faces, we recover the standard convention in which the flux
1699 * is aligned with the outward normal by defining the neighbor cell
1700 * index as <code>std::numeric_limits@<unsigned int@>::%max()</code>.
1701 * This local rule guarantees that the flux contributions are consistent
1702 * across neighboring cells without requiring a global orientation of
1709 * for (const auto &face : cell_skeleton->face_iterators())
1711 * fe_face_values_test.reinit(cell_test, face);
1712 * fe_values_trial_skeleton.reinit(cell_skeleton, face);
1714 * const auto face_no = cell->face_iterator_to_index(face);
1715 * const auto current_boundary_id = face->boundary_id();
1717 * const double kn_omega =
1718 * (current_boundary_id == 1) ?
1720 * ((current_boundary_id == 3) ? std::sin(theta) : 1.);
1722 * for (unsigned int q_point = 0; q_point < n_face_q_points; ++q_point)
1724 * const Tensor<1, dim> normal =
1725 * fe_values_trial_skeleton.normal_vector(q_point);
1726 * const double JxW_face = fe_values_trial_skeleton.JxW(q_point);
1728 * for (unsigned int k : fe_face_values_test.dof_indices())
1732 * (fe_face_values_test[extractor_u_real].value(k, q_point) +
1734 * fe_face_values_test[extractor_u_imag].value(k,
1736 * v_face_n_conj[k] =
1738 * (fe_face_values_test[extractor_u_real].value(k, q_point) -
1740 * fe_face_values_test[extractor_u_imag].value(k,
1744 * fe_face_values_test[extractor_p_real].value(k, q_point) +
1746 * fe_face_values_test[extractor_p_imag].value(k, q_point);
1748 * fe_face_values_test[extractor_p_real].value(k, q_point) -
1750 * fe_face_values_test[extractor_p_imag].value(k, q_point);
1752 * if (fe_test.shape_function_belongs_to(k, extractor_u_real))
1753 * shape_function_type_test[k] |= velocity_real;
1754 * if (fe_test.shape_function_belongs_to(k, extractor_u_imag))
1755 * shape_function_type_test[k] |= velocity_imag;
1756 * if (fe_test.shape_function_belongs_to(k, extractor_p_real))
1757 * shape_function_type_test[k] |= pressure_real;
1758 * if (fe_test.shape_function_belongs_to(k, extractor_p_imag))
1759 * shape_function_type_test[k] |= pressure_imag;
1761 * for (unsigned int k : fe_values_trial_skeleton.dof_indices())
1764 * fe_values_trial_skeleton[extractor_u_hat_real].value(
1766 * imag * fe_values_trial_skeleton[extractor_u_hat_imag]
1767 * .value(k, q_point);
1769 * fe_values_trial_skeleton[extractor_u_hat_real].value(
1771 * imag * fe_values_trial_skeleton[extractor_u_hat_imag]
1772 * .value(k, q_point);
1775 * fe_values_trial_skeleton[extractor_p_hat_real].value(
1777 * imag * fe_values_trial_skeleton[extractor_p_hat_imag]
1778 * .value(k, q_point);
1780 * fe_values_trial_skeleton[extractor_p_hat_real].value(
1782 * imag * fe_values_trial_skeleton[extractor_p_hat_imag]
1783 * .value(k, q_point);
1785 * if (fe_trial_skeleton.shape_function_belongs_to(
1786 * k, extractor_u_hat_real))
1787 * shape_function_type_trial_skeleton[k] |= velocity_real;
1788 * if (fe_trial_skeleton.shape_function_belongs_to(
1789 * k, extractor_u_hat_imag))
1790 * shape_function_type_trial_skeleton[k] |= velocity_imag;
1791 * if (fe_trial_skeleton.shape_function_belongs_to(
1792 * k, extractor_p_hat_real))
1793 * shape_function_type_trial_skeleton[k] |= pressure_real;
1794 * if (fe_trial_skeleton.shape_function_belongs_to(
1795 * k, extractor_p_hat_imag))
1796 * shape_function_type_trial_skeleton[k] |= pressure_imag;
1799 * for (const auto i : fe_face_values_test.dof_indices())
1801 * const unsigned char face_test_type_i =
1802 * shape_function_type_test[i];
1804 * if (current_boundary_id == 1 || current_boundary_id == 3)
1806 * for (const auto j : fe_face_values_test.dof_indices())
1808 * const unsigned char face_test_type_j =
1809 * shape_function_type_test[j];
1810 * if ((face_test_type_i & is_velocity) &&
1811 * (face_test_type_j & is_velocity))
1814 * (v_face_n_conj[i] * v_face_n[j] * JxW_face)
1818 * else if ((face_test_type_i & is_velocity) &&
1819 * (face_test_type_j & is_pressure))
1821 * G_matrix(i, j) += (v_face_n_conj[i] * kn_omega *
1822 * q_face[j] * JxW_face)
1826 * else if ((face_test_type_i & is_pressure) &&
1827 * (face_test_type_j & is_velocity))
1829 * G_matrix(i, j) += (kn_omega * q_face_conj[i] *
1830 * v_face_n[j] * JxW_face)
1834 * else if ((face_test_type_i & is_pressure) &&
1835 * (face_test_type_j & is_pressure))
1838 * (kn_omega * q_face_conj[i] * kn_omega *
1839 * q_face[j] * JxW_face)
1845 * for (const auto j : fe_values_trial_skeleton.dof_indices())
1847 * const unsigned char face_trial_type_j =
1848 * shape_function_type_trial_skeleton[j];
1850 * if ((face_test_type_i & is_velocity) &&
1851 * (face_trial_type_j & is_pressure))
1853 * B_hat_matrix(i, j) +=
1854 * ((v_face_n_conj[i] * p_hat[j]) * JxW_face).real();
1857 * else if ((face_test_type_i & is_pressure) &&
1858 * (face_trial_type_j & is_velocity))
1860 * const unsigned int neighbor_cell_id =
1861 * face->at_boundary() ?
1862 * std::numeric_limits<unsigned int>::max() :
1863 * cell->neighbor(face_no)->active_cell_index();
1864 * const double flux_orientation =
1865 * neighbor_cell_id > cell->active_cell_index() ?
1869 * B_hat_matrix(i, j) +=
1870 * (q_face_conj[i] * flux_orientation * u_hat_n[j] *
1879 * Finally, we assemble the matrix @f$D@f$ and the corresponding
1880 * source-term vector @f$g@f$. Note that this is only required on
1881 * faces where Robin boundary conditions are applied and that
1882 * the orientation of @f$\hat{u}_n@f$ is unambiguous: since the face
1883 * lies on the exterior boundary of the domain, the flux is
1884 * always aligned with the outward normal. Consequently, the
1885 * flux orientation factor is set to +1. As for the other
1886 * matrices, we loop over the relevant <code>dof_indices</code>,
1887 * here the ones of the skeleton trial-space degrees. However,
1888 * here we already have stored the terms involving the skeleton
1889 * trial-space basis functions and the shape function types, so
1890 * we can directly assemble the matrix @f$D@f$ and vector @f$g@f$.:
1891 * - If both <code>i</code> and <code>j</code> are in the
1892 * skeleton trace associated to @f$\hat{u}_n@f$, we build the term
1893 * @f$- \langle \hat{u}_n, \hat{u}_n \rangle_{\Gamma_1 \cup
1895 * - If <code>i</code> is in the skeleton trace associated to
1896 * @f$\hat{u}_n@f$ and <code>j</code> in the skeleton trace
1898 * @f$\hat{p}^*@f$, we build the term @f$\langle \hat{u}_n,
1899 * \frac{k_n}{\omega} \hat{p}^* \rangle_{\Gamma_1 \cup
1901 * - If <code>i</code> is in the skeleton trace associated to
1902 * @f$\hat{p}^*@f$ and <code>j</code> in the skeleton trace
1904 * @f$\hat{u}_n@f$, we build the term @f$\langle \frac{k_n}{\omega}
1905 * \hat{p}^*, \hat{u}_n \rangle_{\Gamma_1 \cup \Gamma_3}@f$;
1906 * - If both <code>i</code> and <code>j</code> are in the
1907 * skeleton trace associated to @f$\hat{p}^*@f$, we build the term
1908 * @f$- \langle \frac{k_n}{\omega} \hat{p}^*, \frac{k_n}{\omega}
1909 * \hat{p}^* \rangle_{\Gamma_1 \cup \Gamma_3}@f$.
1910 * - If <code>i</code> is in the skeleton trace
1912 * @f$\hat{u}_n@f$, we assemble the term @f$- \langle \hat{u}_n, g_R
1913 * \rangle_{\Gamma_1 \cup \Gamma_3}@f$;
1914 * - If <code>i</code> is in the skeleton trace
1916 * @f$\hat{p}^*@f$, we assemble the term @f$\langle
1917 * \frac{k_n}{\omega} \hat{p}^*, g_R \rangle_{\Gamma_1 \cup
1927 * if (current_boundary_id == 1 || current_boundary_id == 3)
1929 * const double flux_orientation = 1.;
1931 * for (const auto i : fe_values_trial_skeleton.dof_indices())
1933 * const unsigned char face_trial_type_i =
1934 * shape_function_type_trial_skeleton[i];
1935 * for (const auto j :
1936 * fe_values_trial_skeleton.dof_indices())
1938 * const unsigned char face_trial_type_j =
1939 * shape_function_type_trial_skeleton[j];
1941 * if ((face_trial_type_i & is_velocity) &&
1942 * (face_trial_type_j & is_velocity))
1945 * (flux_orientation * u_hat_n_conj[i] *
1946 * flux_orientation * u_hat_n[j] * JxW_face)
1950 * else if ((face_trial_type_i & is_velocity) &&
1951 * (face_trial_type_j & is_pressure))
1954 * (flux_orientation * u_hat_n_conj[i] *
1955 * kn_omega * p_hat[j] * JxW_face)
1959 * else if ((face_trial_type_i & is_pressure) &&
1960 * (face_trial_type_j & is_velocity))
1963 * (kn_omega * p_hat_conj[i] * flux_orientation *
1964 * u_hat_n[j] * JxW_face)
1968 * else if ((face_trial_type_i & is_pressure) &&
1969 * (face_trial_type_j & is_pressure))
1972 * (kn_omega * p_hat_conj[i] * kn_omega *
1973 * p_hat[j] * JxW_face)
1978 * double source_term = 0.;
1979 * if (face_trial_type_i & is_velocity)
1982 * (u_hat_n_conj[i] * source_term).real() * JxW_face;
1984 * else if (face_trial_type_i & is_pressure)
1987 * (kn_omega * p_hat_conj[i] * source_term).real() *
1996 * After assembling all local matrices and vectors, we perform the
1997 * cell-wise static condensation associated with the DPG formulation. We
1998 * first invert the Gram matrix @f$G@f$ and use it to form the
1999 * auxiliary operators @f$M_4 = B^\dagger G^{-1}@f$ and
2000 * @f$M_5 = \hat{B}^\dagger G^{-1}@f$. These are then used to construct the
2001 * condensed blocks @f$M_1 = B^\dagger G^{-1} B@f$,
2002 * @f$M_2 = B^\dagger G^{-1} \hat{B}@f$ and
2003 * @f$M_3 = \hat{B}^\dagger G^{-1} \hat{B} - D@f$. Then, if
2004 * <code>solve_interior</code> is <code>true</code>, the skeleton
2005 * solution @f$\hat{u}_h@f$ is assumed known and we recover the interior
2006 * unknowns on each cell by solving
2007 * @f$u_h = M_1^{-1} (M_4 l - M_2 \hat{u}_h)@f$, followed by distribution to
2008 * the global interior solution
2009 * vector. Otherwise, we assemble the fully condensed local system for
2010 * the skeleton unknowns by forming the Schur complement @f$(M_3 -
2011 * M_2^\dagger M_1^{-1} M_2)@f$, together with the corresponding
2012 * right-hand side @f$(M_5 - M_2^\dagger M_1^{-1} M_4) l - g@f$, and
2013 * distribute the resulting local matrix and vector to the global
2014 * skeleton system while enforcing constraints.
2017 * G_matrix.invert();
2019 * B_matrix.Tmmult(M4_matrix, G_matrix);
2020 * B_hat_matrix.Tmmult(M5_matrix, G_matrix);
2022 * M4_matrix.mmult(M1_matrix, B_matrix);
2023 * M4_matrix.mmult(M2_matrix, B_hat_matrix);
2025 * M5_matrix.mmult(M3_matrix, B_hat_matrix);
2026 * M3_matrix.add(-1.0, D_matrix);
2028 * M1_matrix.invert();
2030 * if (solve_interior)
2032 * cell_skeleton->get_dof_values(solution_skeleton,
2033 * cell_skeleton_solution);
2035 * M2_matrix.vmult(tmp_vector, cell_skeleton_solution);
2036 * M4_matrix.vmult(cell_interior_rhs, l_vector);
2037 * cell_interior_rhs -= tmp_vector;
2038 * M1_matrix.vmult(cell_interior_solution, cell_interior_rhs);
2040 * cell->distribute_local_to_global(cell_interior_solution,
2041 * solution_interior);
2046 * M2_matrix.Tmmult(tmp_matrix, M1_matrix);
2047 * tmp_matrix.mmult(tmp_matrix2, M2_matrix);
2048 * tmp_matrix2.add(-1.0, M3_matrix);
2049 * tmp_matrix2 *= -1.0;
2050 * cell_matrix = tmp_matrix2;
2052 * tmp_matrix.mmult(tmp_matrix3, M4_matrix);
2053 * M5_matrix.add(-1.0, tmp_matrix3);
2054 * M5_matrix.vmult(cell_skeleton_rhs, l_vector);
2055 * cell_skeleton_rhs -= g_vector;
2057 * cell_skeleton->get_dof_indices(local_dof_indices);
2058 * constraints.distribute_local_to_global(cell_matrix,
2059 * cell_skeleton_rhs,
2060 * local_dof_indices,
2070 * <a name="step_100-DPGHelmholtzsolve_linear_system_skeleton"></a>
2071 * <h3>DPGHelmholtz::solve_linear_system_skeleton</h3>
2072 * This function is in charge of solving the linear system assembled and has
2073 * nothing specific to DPG per se. Even though the original PDE was
2074 * indefinite, the way we solve it (using a least-squares approach) means that
2075 * the linear system is symmetric and positive definite. As a consequence, the
2076 * method allows us to use the Conjugate Gradient iterative solver. Note that
2077 * because we do not have any preconditioner, the number of iterations can be
2078 * quite high. For simplicity, we put a high upper limit on the number of
2079 * iterations, but in practice one would want to change this function to have
2080 * a more robust solver. The tolerance for the convergence here is defined
2081 * proportional to the @f$L^2@f$ norm of the RHS vector so the stopping criterion
2082 * is independent of whatever scaling we apply to the equation. The chosen
2083 * tolerance is rather stiff, but it is required to reproduce the convergence
2084 * plots of the results section.
2087 * template <int dim>
2088 * void DPGHelmholtz<dim>::solve_linear_system_skeleton()
2090 * std::cout << std::endl << "Solving the DPG system..." << std::endl;
2092 * SolverControl solver_control(100000, 1e-10 * system_rhs.l2_norm());
2093 * SolverCG<Vector<double>> solver(solver_control);
2094 * solver.solve(system_matrix,
2095 * solution_skeleton,
2097 * PreconditionIdentity());
2098 * constraints.distribute(solution_skeleton);
2100 * std::cout << " " << solver_control.last_step()
2101 * << " CG iterations needed to obtain convergence. \n"
2104 * error_table.add_value("n_iter", solver_control.last_step());
2110 * <a name="step_100-DPGHelmholtzoutput_results"></a>
2111 * <h3>DPGHelmholtz::output_results</h3>
2112 * This function outputs both the interior and skeleton solutions in VTU
2113 * format for visualization in ParaView or VisIt. The interior solution is
2114 * written using the standard DataOut class by attaching the interior
2115 * DoFHandler and providing appropriate component names and interpretations:
2116 * the real and imaginary parts of the velocity are treated as vector-valued
2117 * fields, while the real and imaginary parts of the pressure are treated as
2118 * scalar fields. The skeleton solution, which is defined only on mesh faces,
2119 * is handled separately using the DataOutFaces class as presented in
2120 * @ref step_51 "step-51" for the HDG method. For both outputs, visualization patches are
2121 * built using the corresponding polynomial degree, and the results are
2122 * written using names dependent on the current mesh adaption cycle.
2125 * template <int dim>
2126 * void DPGHelmholtz<dim>::output_results(const unsigned int cycle)
2128 * DataOut<dim> data_out;
2129 * data_out.attach_dof_handler(dof_handler_trial_interior);
2131 * std::vector<std::string> solution_interior_names;
2132 * for (unsigned int i = 0; i < dim; ++i)
2134 * solution_interior_names.emplace_back("velocity_real");
2136 * for (unsigned int i = 0; i < dim; ++i)
2138 * solution_interior_names.emplace_back("velocity_imag");
2140 * solution_interior_names.emplace_back("pressure_real");
2141 * solution_interior_names.emplace_back("pressure_imag");
2143 * std::vector<DataComponentInterpretation::DataComponentInterpretation>
2144 * data_component_interpretation;
2145 * for (unsigned int i = 0; i < dim; ++i)
2147 * data_component_interpretation.push_back(
2148 * DataComponentInterpretation::component_is_part_of_vector);
2150 * for (unsigned int i = 0; i < dim; ++i)
2152 * data_component_interpretation.push_back(
2153 * DataComponentInterpretation::component_is_part_of_vector);
2155 * data_component_interpretation.push_back(
2156 * DataComponentInterpretation::component_is_scalar);
2157 * data_component_interpretation.push_back(
2158 * DataComponentInterpretation::component_is_scalar);
2160 * data_out.add_data_vector(solution_interior,
2161 * solution_interior_names,
2162 * DataOut<dim>::type_automatic,
2163 * data_component_interpretation);
2164 * data_out.build_patches(fe_trial_interior.degree);
2165 * std::ofstream output("solution_planewave_square-" + std::to_string(cycle) +
2167 * data_out.write_vtu(output);
2169 * DataOutFaces<dim> data_out_faces(false);
2170 * data_out_faces.attach_dof_handler(dof_handler_trial_skeleton);
2172 * std::vector<std::string> solution_skeleton_names;
2173 * solution_skeleton_names.emplace_back("velocity_hat_real");
2174 * solution_skeleton_names.emplace_back("velocity_hat_imag");
2175 * solution_skeleton_names.emplace_back("pressure_hat_real");
2176 * solution_skeleton_names.emplace_back("pressure_hat_imag");
2178 * std::vector<DataComponentInterpretation::DataComponentInterpretation>
2179 * data_component_interpretation_skeleton(
2180 * 4, DataComponentInterpretation::component_is_scalar);
2182 * data_out_faces.add_data_vector(solution_skeleton,
2183 * solution_skeleton_names,
2184 * DataOutFaces<dim>::type_automatic,
2185 * data_component_interpretation_skeleton);
2187 * data_out_faces.build_patches(fe_trial_skeleton.degree);
2188 * std::ofstream output_face("solution_face_planewave_square-" +
2189 * std::to_string(cycle) + ".vtu");
2190 * data_out_faces.write_vtu(output_face);
2196 * <a name="step_100-DPGHelmholtzcalculate_L2_error"></a>
2197 * <h3>DPGHelmholtz::calculate_L2_error</h3>
2198 * In this function, we compute the @f$L^2@f$ error of each component of the
2199 * numerical solution, namely the real and imaginary parts of the velocity and
2200 * pressure, for both the interior and skeleton unknowns. Because we want to
2201 * have errors for the skeleton components, we cannot use the
2202 * VectorTools::integrate_difference function as it does not have a mechanism
2203 * to avoid visiting faces twice (i.e., counting the error on each cell
2204 * sharing the face). Therefore, we will perform the computation "by hand" for
2205 * both interior and skeleton solutions.
2211 * template <int dim>
2212 * void DPGHelmholtz<dim>::calculate_L2_error()
2214 * QGauss<dim> quadrature_formula(fe_test.degree + 1);
2215 * FEValues<dim> fe_values_trial_interior(fe_trial_interior,
2216 * quadrature_formula,
2218 * update_quadrature_points |
2219 * update_JxW_values);
2220 * const QGauss<dim - 1> face_quadrature_formula(fe_test.degree + 1);
2221 * FEFaceValues<dim> fe_values_trial_skeleton(fe_trial_skeleton,
2222 * face_quadrature_formula,
2224 * update_quadrature_points |
2225 * update_normal_vectors |
2226 * update_JxW_values);
2228 * const unsigned int n_q_points = quadrature_formula.size();
2229 * const unsigned int n_face_q_points = face_quadrature_formula.size();
2231 * double L2_error_p_real = 0;
2232 * double L2_error_p_imag = 0;
2233 * double L2_error_p_hat_real = 0;
2234 * double L2_error_p_hat_imag = 0;
2235 * double L2_error_u_real = 0;
2236 * double L2_error_u_imag = 0;
2237 * double L2_error_u_hat_real = 0;
2238 * double L2_error_u_hat_imag = 0;
2240 * std::vector<Tensor<1, dim>> local_u_real(n_q_points);
2241 * std::vector<Tensor<1, dim>> local_u_imag(n_q_points);
2242 * std::vector<double> local_p_real(n_q_points);
2243 * std::vector<double> local_p_imag(n_q_points);
2244 * std::vector<double> local_u_hat_real(n_face_q_points);
2245 * std::vector<double> local_u_hat_imag(n_face_q_points);
2246 * std::vector<double> local_p_hat_real(n_face_q_points);
2247 * std::vector<double> local_p_hat_imag(n_face_q_points);
2249 * const AnalyticalSolutionPressureReal<dim> analytical_solution_p_real(
2250 * wavenumber, theta);
2251 * const AnalyticalSolutionPressureImag<dim> analytical_solution_p_imag(
2252 * wavenumber, theta);
2253 * const AnalyticalSolutionVelocityReal<dim> analytical_solution_u_real(
2254 * wavenumber, theta);
2255 * const AnalyticalSolutionVelocityImag<dim> analytical_solution_u_imag(
2256 * wavenumber, theta);
2260 * To compute the @f$L^2@f$ error, we start by looping over all active cells of
2261 * the mesh and evaluating both the interior contributions. For
2262 * each cell, the interior velocity and pressure are first interpolated at
2263 * volume quadrature points using FEValues, and their squared differences
2264 * with the corresponding analytical solutions are accumulated using the
2265 * Jacobian quadrature weights. The skeleton error is then computed in a
2266 * similar way by looping over the faces of that same cell and interpolating
2267 * the trace unknowns at face quadrature points using FEFaceValues. However,
2268 * to avoid double-counting interior faces shared by two cells, we use a
2269 * similar idea to the one used to define the flux orientation during the
2270 * assembly: each face is integrated only once by retaining the contribution
2271 * from the cell with the smallest active cell index (this does not include
2272 * boundary faces which are always included). Finally, the accumulated
2273 * errors are printed to the terminal, and stored in the
2274 * <code>error_table</code> for post-processing.
2278 * An additional detail worth mentioning concerns the error computation for
2279 * the velocity trace variable. Indeed, for the normal flux trace variable
2280 * @f$\hat{u}_n@f$, the analytical velocity is projected onto the outward normal
2281 * at each quadrature point, and the error is computed using only the
2282 * magnitude (absolute value) of both numerical and analytical quantities.
2283 * This choice removes spurious sign changes induced by face-normal
2284 * orientation conventions, which may differ between neighboring cells and
2285 * are not physically meaningful for error estimation.
2288 * for (const auto &cell : dof_handler_trial_interior.active_cell_iterators())
2290 * fe_values_trial_interior.reinit(cell);
2292 * fe_values_trial_interior[extractor_u_real].get_function_values(
2293 * solution_interior, local_u_real);
2294 * fe_values_trial_interior[extractor_u_imag].get_function_values(
2295 * solution_interior, local_u_imag);
2296 * fe_values_trial_interior[extractor_p_real].get_function_values(
2297 * solution_interior, local_p_real);
2298 * fe_values_trial_interior[extractor_p_imag].get_function_values(
2299 * solution_interior, local_p_imag);
2301 * const auto &quadrature_points =
2302 * fe_values_trial_interior.get_quadrature_points();
2304 * for (const unsigned int q_index :
2305 * fe_values_trial_interior.quadrature_point_indices())
2307 * const double JxW = fe_values_trial_interior.JxW(q_index);
2308 * const auto &position = quadrature_points[q_index];
2310 * L2_error_u_real += (local_u_real[q_index] -
2311 * analytical_solution_u_real.value(position))
2314 * L2_error_u_imag += (local_u_imag[q_index] -
2315 * analytical_solution_u_imag.value(position))
2319 * L2_error_p_real +=
2320 * std::pow((local_p_real[q_index] -
2321 * analytical_solution_p_real.value(position, 0)),
2325 * L2_error_p_imag +=
2326 * std::pow((local_p_imag[q_index] -
2327 * analytical_solution_p_imag.value(position, 0)),
2332 * const typename DoFHandler<dim>::active_cell_iterator cell_skeleton =
2333 * cell->as_dof_handler_iterator(dof_handler_trial_skeleton);
2335 * for (const auto &face : cell->face_iterators())
2337 * fe_values_trial_skeleton.reinit(cell_skeleton, face);
2338 * const auto face_no = cell_skeleton->face_iterator_to_index(face);
2340 * fe_values_trial_skeleton[extractor_u_hat_real].get_function_values(
2341 * solution_skeleton, local_u_hat_real);
2342 * fe_values_trial_skeleton[extractor_u_hat_imag].get_function_values(
2343 * solution_skeleton, local_u_hat_imag);
2344 * fe_values_trial_skeleton[extractor_p_hat_real].get_function_values(
2345 * solution_skeleton, local_p_hat_real);
2346 * fe_values_trial_skeleton[extractor_p_hat_imag].get_function_values(
2347 * solution_skeleton, local_p_hat_imag);
2349 * const auto &face_quadrature_points =
2350 * fe_values_trial_skeleton.get_quadrature_points();
2352 * for (const unsigned int &q_index :
2353 * fe_values_trial_skeleton.quadrature_point_indices())
2355 * const double JxW = fe_values_trial_skeleton.JxW(q_index);
2356 * const auto &position = face_quadrature_points[q_index];
2357 * const Tensor<1, dim> normal =
2358 * fe_values_trial_skeleton.normal_vector(q_index);
2360 * const unsigned int neighbor_cell_id =
2361 * face->at_boundary() ?
2362 * std::numeric_limits<unsigned int>::max() :
2363 * cell->neighbor(face_no)->active_cell_index();
2364 * if (neighbor_cell_id < cell->active_cell_index())
2369 * double u_hat_n_analytical_real =
2370 * normal * analytical_solution_u_real.value(position);
2371 * double u_hat_n_analytical_imag =
2372 * normal * analytical_solution_u_imag.value(position);
2374 * L2_error_u_hat_real +=
2375 * std::pow(std::abs(local_u_hat_real[q_index]) -
2376 * std::abs(u_hat_n_analytical_real),
2379 * L2_error_u_hat_imag +=
2380 * std::pow(std::abs(local_u_hat_imag[q_index]) -
2381 * std::abs(u_hat_n_analytical_imag),
2385 * L2_error_p_hat_real +=
2386 * std::pow((local_p_hat_real[q_index] -
2387 * analytical_solution_p_real.value(position, 0)),
2390 * L2_error_p_hat_imag +=
2391 * std::pow((local_p_hat_imag[q_index] -
2392 * analytical_solution_p_imag.value(position, 0)),
2399 * std::cout << "Velocity real part L2 error is : "
2400 * << std::sqrt(L2_error_u_real) << std::endl;
2401 * std::cout << "Velocity imag part L2 error is : "
2402 * << std::sqrt(L2_error_u_imag) << std::endl;
2403 * std::cout << "Pressure real part L2 error is : "
2404 * << std::sqrt(L2_error_p_real) << std::endl;
2405 * std::cout << "Pressure imag part L2 error is : "
2406 * << std::sqrt(L2_error_p_imag) << std::endl;
2407 * std::cout << "Velocity skeleton real part L2 error is : "
2408 * << std::sqrt(L2_error_u_hat_real) << std::endl;
2409 * std::cout << "Velocity skeleton imag part L2 error is : "
2410 * << std::sqrt(L2_error_u_hat_imag) << std::endl;
2411 * std::cout << "Pressure skeleton real part L2 error is : "
2412 * << std::sqrt(L2_error_p_hat_real) << std::endl;
2413 * std::cout << "Pressure skeleton imag part L2 error is : "
2414 * << std::sqrt(L2_error_p_hat_imag) << std::endl;
2416 * error_table.add_value("eL2_u_r", std::sqrt(L2_error_u_real));
2417 * error_table.add_value("eL2_u_i", std::sqrt(L2_error_u_imag));
2418 * error_table.add_value("eL2_p_r", std::sqrt(L2_error_p_real));
2419 * error_table.add_value("eL2_p_i", std::sqrt(L2_error_p_imag));
2420 * error_table.add_value("eL2_u_hat_r", std::sqrt(L2_error_u_hat_real));
2421 * error_table.add_value("eL2_u_hat_i", std::sqrt(L2_error_u_hat_imag));
2422 * error_table.add_value("eL2_p_hat_r", std::sqrt(L2_error_p_hat_real));
2423 * error_table.add_value("eL2_p_hat_i", std::sqrt(L2_error_p_hat_imag));
2429 * <a name="step_100-DPGHelmholtzrefine_grid"></a>
2430 * <h3>DPGHelmholtz::refine_grid</h3>
2431 * This function creates the mesh for the first cycle and then refines it
2432 * uniformly for subsequent cycles. It also records the number of cells and
2433 * the maximum cell diameter in the error table for convergence analysis.
2436 * template <int dim>
2437 * void DPGHelmholtz<dim>::refine_grid(const unsigned int cycle)
2441 * const Point<dim> p1{0., 0.};
2442 * const Point<dim> p2{1., 1.};
2444 * std::vector<unsigned int> repetitions({2, 2});
2445 * GridGenerator::subdivided_hyper_rectangle(
2446 * triangulation, repetitions, p1, p2, true);
2447 * triangulation.refine_global(0);
2451 * triangulation.refine_global();
2454 * std::cout << "Number of active cells: " << triangulation.n_active_cells()
2457 * error_table.add_value("cycle", cycle);
2458 * error_table.add_value("n_cells", triangulation.n_active_cells());
2459 * error_table.add_value("cell_size",
2460 * GridTools::maximal_cell_diameter<dim>(triangulation));
2466 * <a name="step_100-DPGHelmholtzrun"></a>
2467 * <h3>DPGHelmholtz::run</h3>
2468 * This function is the main loop of the program using all the previously
2469 * defined functions. It is also where the convergence rates are obtained
2470 * after all the refinement cycles.
2473 * template <int dim>
2474 * void DPGHelmholtz<dim>::run()
2476 * for (unsigned int cycle = 0; cycle < 8; ++cycle)
2478 * std::cout << "===========================================" << std::endl
2479 * << "Cycle " << cycle << ':
' << std::endl;
2481 * refine_grid(cycle);
2483 * assemble_system(false);
2484 * solve_linear_system_skeleton();
2485 * assemble_system(true);
2486 * calculate_L2_error();
2487 * output_results(cycle);
2490 * error_table.evaluate_convergence_rates(
2491 * "eL2_u_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2492 * error_table.evaluate_convergence_rates(
2493 * "eL2_u_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2494 * error_table.evaluate_convergence_rates(
2495 * "eL2_p_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2496 * error_table.evaluate_convergence_rates(
2497 * "eL2_p_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2498 * error_table.evaluate_convergence_rates(
2499 * "eL2_u_hat_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2500 * error_table.evaluate_convergence_rates(
2501 * "eL2_u_hat_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2502 * error_table.evaluate_convergence_rates(
2503 * "eL2_p_hat_r", "n_cells", ConvergenceTable::reduction_rate_log2);
2504 * error_table.evaluate_convergence_rates(
2505 * "eL2_p_hat_i", "n_cells", ConvergenceTable::reduction_rate_log2);
2507 * std::cout << "===========================================" << std::endl;
2508 * std::cout << "Convergence table:" << std::endl;
2509 * error_table.write_text(std::cout);
2511 * } // End of namespace Step100
2516 * <a name="step_100-Thecodemaincodefunction"></a>
2517 * <h3>The <code>main</code> function</h3>
2521 * This is the main function of the program. It creates an instance of the
2522 * <code>DPGHelmholtz</code> class and calls its run method. It defines the
2523 * necessary variables for our 2D DPG Helmholtz, i.e., the <code>degree</code>
2524 * @f$p@f$ of the trial space, the degree difference <code>delta_degree</code>
2525 * between the test and trial spaces, the <code>wavenumber</code> @f$k@f$, and the
2526 * angle of incidence of the plane wave <code>theta</code> in radians.
2531 * const unsigned int dim = 2;
2535 * const int degree = 2;
2536 * const int delta_degree = 1;
2537 * const double wavenumber = 20 * pi;
2538 * const double theta = pi / 4.;
2540 * std::cout << "===========================================" << std::endl
2541 * << "Trial order: " << degree << std::endl
2542 * << "Test order: " << delta_degree + degree << std::endl
2543 * << "===========================================" << std::endl
2546 * Step100::DPGHelmholtz<dim> dpg_helmholtz(degree,
2551 * dpg_helmholtz.run();
2553 * std::cout << std::endl;
2555 * catch (std::exception &exc)
2557 * std::cerr << std::endl
2559 * << "----------------------------------------------------"
2561 * std::cerr << "Exception on processing: " << std::endl
2562 * << exc.what() << std::endl
2563 * << "Aborting!" << std::endl
2564 * << "----------------------------------------------------"
2570 * std::cerr << std::endl
2572 * << "----------------------------------------------------"
2574 * std::cerr << "Unknown exception!" << std::endl
2575 * << "Aborting!" << std::endl
2576 * << "----------------------------------------------------"
2584<a name="step_100-Results"></a><h1>Results</h1>
2586The solutions of the above program are written to .vtu files for both the
2587interior solution and the skeleton one. Each file contains four components,
2588the real and imaginary part of the pressure and velocity field. With a degree
2589@f$p=2@f$ polynomial space, a difference of @f$\Delta p =1@f$ degree between the test
2590and the trial space, a plane wave propagating in the direction @f$\theta = \pi/4@f$
2591and an angular frequency @f$\omega = 20 \pi@f$, the program should output the
2592following table at the end:
2594<table align="center" class="doxtable">
2596 <th rowspan="2">Cycle</th> <th rowspan="2">Cells</th> <th
2597 rowspan="2">h</th> <th rowspan="2">DoFs<br/>interior</th> <th
2598 rowspan="2">DoFs<br/>skeleton</th> <th rowspan="2">DoFs<br/>test</th> <th
2599 rowspan="2">Iterations</th> <th colspan="2">‖Re{u}‖<sub>L2</sub></th>
2600 <th colspan="2">‖Im{u}‖<sub>L2</sub></th>
2601 <th colspan="2">‖Re{p*}‖<sub>L2</sub></th>
2602 <th colspan="2">‖Im{p*}‖<sub>L2</sub></th> <th
2603 colspan="2">‖Re{û<sub>n</sub>}‖<sub>L2</sub></th>
2604 <th colspan="2">‖Im{û<sub>n</sub>}‖<sub>L2</sub></th>
2605 <th colspan="2">‖Re{p̂*}‖<sub>L2</sub></th> <th
2606 colspan="2">‖Im{p̂*}‖<sub>L2</sub></th>
2608 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2609 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2610 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2611 <th>Norm</th><th>Order</th> <th>Norm</th><th>Order</th>
2613 <td align="center">0</td><td align="center">4</td><td
2614 align="center">0.7071</td> <td align="center">216</td><td
2615 align="center">138</td><td align="center">450</td><td
2616 align="center">78</td> <td align="center">0.8139</td><td
2617 align="center">–</td> <td align="center">0.5738</td><td
2618 align="center">–</td> <td align="center">0.8061</td><td
2619 align="center">–</td> <td align="center">0.5736</td><td
2620 align="center">–</td> <td align="center">0.8489</td><td
2621 align="center">–</td> <td align="center">1.2310</td><td
2622 align="center">–</td> <td align="center">1.3819</td><td
2623 align="center">–</td> <td align="center">1.9864</td><td
2624 align="center">–</td>
2626 <td align="center">1</td><td align="center">16</td><td
2627 align="center">0.3536</td> <td align="center">864</td><td
2628 align="center">450</td><td align="center">1666</td><td
2629 align="center">102</td> <td align="center">0.7118</td><td
2630 align="center">0.19</td> <td align="center">0.7097</td><td
2631 align="center">-0.31</td> <td align="center">0.7106</td><td
2632 align="center">0.18</td> <td align="center">0.7087</td><td
2633 align="center">-0.31</td> <td align="center">1.4091</td><td
2634 align="center">-0.73</td> <td align="center">1.4210</td><td
2635 align="center">-0.21</td> <td align="center">2.3013</td><td
2636 align="center">-0.74</td> <td align="center">2.3266</td><td
2637 align="center">-0.23</td>
2639 <td align="center">2</td><td align="center">64</td><td
2640 align="center">0.1768</td> <td align="center">3456</td><td
2641 align="center">1602</td><td align="center">6402</td><td
2642 align="center">208</td> <td align="center">0.6600</td><td
2643 align="center">0.11</td> <td align="center">0.6641</td><td
2644 align="center">0.10</td> <td align="center">0.6618</td><td
2645 align="center">0.10</td> <td align="center">0.6597</td><td
2646 align="center">0.10</td> <td align="center">1.8172</td><td
2647 align="center">-0.37</td> <td align="center">1.7966</td><td
2648 align="center">-0.34</td> <td align="center">2.7158</td><td
2649 align="center">-0.24</td> <td align="center">2.7309</td><td
2650 align="center">-0.23</td>
2652 <td align="center">3</td><td align="center">256</td><td
2653 align="center">0.0884</td> <td align="center">13824</td><td
2654 align="center">6018</td><td align="center">25090</td><td
2655 align="center">1547</td> <td align="center">0.1334</td><td
2656 align="center">2.31</td> <td align="center">0.1091</td><td
2657 align="center">2.61</td> <td align="center">0.1093</td><td
2658 align="center">2.60</td> <td align="center">0.1337</td><td
2659 align="center">2.30</td> <td align="center">0.5128</td><td
2660 align="center">1.83</td> <td align="center">0.4169</td><td
2661 align="center">2.11</td> <td align="center">0.5232</td><td
2662 align="center">2.38</td> <td align="center">0.6859</td><td
2663 align="center">1.99</td>
2665 <td align="center">4</td><td align="center">1024</td><td
2666 align="center">0.0442</td> <td align="center">55296</td><td
2667 align="center">23298</td><td align="center">99330</td><td
2668 align="center">2233</td> <td align="center">0.0087</td><td
2669 align="center">3.94</td> <td align="center">0.0086</td><td
2670 align="center">3.66</td> <td align="center">0.0085</td><td
2671 align="center">3.69</td> <td align="center">0.0086</td><td
2672 align="center">3.96</td> <td align="center">0.0370</td><td
2673 align="center">3.79</td> <td align="center">0.0365</td><td
2674 align="center">3.51</td> <td align="center">0.0179</td><td
2675 align="center">4.87</td> <td align="center">0.0204</td><td
2676 align="center">5.07</td>
2678 <td align="center">5</td><td align="center">4096</td><td
2679 align="center">0.0221</td> <td align="center">221184</td><td
2680 align="center">91650</td><td align="center">395266</td><td
2681 align="center">5297</td> <td align="center">0.0011</td><td
2682 align="center">3.02</td> <td align="center">0.0011</td><td
2683 align="center">3.00</td> <td align="center">0.0011</td><td
2684 align="center">3.01</td> <td align="center">0.0011</td><td
2685 align="center">3.02</td> <td align="center">0.0061</td><td
2686 align="center">2.59</td> <td align="center">0.0061</td><td
2687 align="center">2.57</td> <td align="center">0.0015</td><td
2688 align="center">3.58</td> <td align="center">0.0015</td><td
2689 align="center">3.74</td>
2691 <td align="center">6</td><td align="center">16384</td><td
2692 align="center">0.0110</td> <td align="center">884736</td><td
2693 align="center">363522</td><td align="center">1576962</td><td
2694 align="center">10166</td> <td align="center">0.0001</td><td
2695 align="center">3.00</td> <td align="center">0.0001</td><td
2696 align="center">3.00</td> <td align="center">0.0001</td><td
2697 align="center">3.00</td> <td align="center">0.0001</td><td
2698 align="center">3.00</td> <td align="center">0.0011</td><td
2699 align="center">2.53</td> <td align="center">0.0011</td><td
2700 align="center">2.52</td> <td align="center">0.0001</td><td
2701 align="center">3.50</td> <td align="center">0.0001</td><td
2702 align="center">3.54</td>
2704 <td align="center">7</td><td align="center">65536</td><td
2705 align="center">0.0055</td> <td align="center">3538944</td><td
2706 align="center">1447938</td><td align="center">6299650</td><td
2707 align="center">19647</td> <td align="center">0.0000</td><td
2708 align="center">3.00</td> <td align="center">0.0000</td><td
2709 align="center">3.00</td> <td align="center">0.0000</td><td
2710 align="center">3.00</td> <td align="center">0.0000</td><td
2711 align="center">3.00</td> <td align="center">0.0002</td><td
2712 align="center">2.51</td> <td align="center">0.0002</td><td
2713 align="center">2.51</td> <td align="center">0.0000</td><td
2714 align="center">3.47</td> <td align="center">0.0000</td><td
2715 align="center">3.47</td>
2719The most refined solution should look similar to the following figure where
2720we present the pressure field solution -- the velocity fields have essentially
2721the same profile but are vector valued so we do not show them here --
2722depending on your visualization tool (here we used Paraview). We cropped the
2723domain in 4 to show both the real and imaginary components for the interior
2724and the mesh skeleton.
2726<img src="https://www.dealii.org/images/steps/developer/step-100-pressure.png"
2727style="width:80%" alt="">
2730<a name="step_100-Convergence"></a><h3>Convergence</h3>
2733To validate that the code falls back on the analytical solution of the plane
2734wave with the expected order, we did a convergence plot for all the different
2735fields which are presented below. As expected for degree @f$p=2@f$ polynomial
2736DGQ element in the interior, all fields converge to order 3. On the
2737faces, it is expected that we lose half an order compared to the interior
2738and that is what we observe for the normal velocity component. However,
2739the pressure skeleton unknowns are associated with the trace of Q
2740elements and for the same sequence of energy spaces the Q elements are one
2741degree higher than the DGQ element. It follows that for DGQ of order 2 as
2742used here, the corresponding Q element would be order 3 so the trace of
2743those elements should converge with a slope of 3.5 as observed.
2745<img src="https://www.dealii.org/images/steps/developer/step-100-error.svg"
2746style="width:80%" alt="">
2749<a name="step_100-Linearsolveriterations"></a><h3>Linear solver iterations</h3>
2752In time-harmonic problems, the number of iterations to solve the system
2753increases as the resolution of the wave increases (there are more dofs in
2754the linear system). The DPG method is no exception to this. To show this,
2755we recorded the number of iterations as we refined the mesh for 3 different
2756angular frequencies and we present the result in the last figure below. In it,
2757we show the number of iterations as a function of the number of dofs along
2758with the total error of the interior fields. From it, we can see that the
2759error does not start converging until the Nyquist criterion is respected,
2760which requires the spatial discretization to resolve the wave with at least
2761two points per wavelength. At that point there is a jump in the number
2762of iterations to achieve convergence. After that the number of iterations
2763approximately doubles each time we double the resolution. Nonetheless, because
2764the DPG method enables the use of a Conjugate Gradient solver we are able to
2765obtain a solution even for high frequencies without exceeding amounts of memory.
2767<img src="https://www.dealii.org/images/steps/developer/step-100-iteration.svg"
2768style="width:80%" alt="">
2770<a name="step_100-Possibilitiesforextension"></a><h3>Possibilities for extension </h3>
2773As an extension to get a feeling of the method, one could first try to
2774implement the 3D version of the plane wave problem in the unit cube by adding
2775a second angle @f$\phi@f$. The analytical solution to this problem is also known
2778 p^* & = e^{-i k (x \cos(\theta) \sin{\phi}
2779 + y \sin(\theta) \sin(\phi) + z \cos(\phi))},
2780 \\ \mathbf{u} & = \frac{1}{c_s} \begin{pmatrix} \cos(\theta) \sin(\phi) \\
2781 \sin(\theta) \sin(\phi) \\ \cos(\phi) \end{pmatrix} e^{-i k (x \cos(\theta)
2782 \sin{\phi} + y \sin(\theta) \sin(\phi) + z \cos(\phi))}.
2784Another interesting extension would be to reconstruct the residual @f$\Psi^r@f$
2785when the solutions on the faces and interior dofs are known and use it to
2786build an error estimator that can be utilized for adaptive hp-refinement
2787@cite petrides2017. Finally, if one is interested more specifically in
2788time-harmonic problems, the implementation of an adequate preconditioner to
2789improve the convergence of the linear solver would remedy one
2790of the inherent difficulties for this type of problem as stated by O. Ernst
2791and M. J. Gander @cite ernst2012.
2794<a name="step_100-PlainProg"></a>
2795<h1> The plain program</h1>
2796@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 > &)