Beispiel #1
0
// We now define the matrix and rhs vector assembly function
// for the shell system.  This function implements the
// linear Kirchhoff-Love theory for thin shells.  At the
// end we also take into account the boundary conditions
// here, using the penalty method.
void assemble_shell (EquationSystems& es, const std::string& system_name)
{
  // It is a good idea to make sure we are assembling
  // the proper system.
  libmesh_assert_equal_to (system_name, "Shell");

  // Get a constant reference to the mesh object.
  const MeshBase& mesh = es.get_mesh();

  // Get a reference to the shell system object.
  LinearImplicitSystem & system = es.get_system<LinearImplicitSystem> ("Shell");

  // Get the shell parameters that we need during assembly.
  const Real h  = es.parameters.get<Real> ("thickness");
  const Real E  = es.parameters.get<Real> ("young's modulus");
  const Real nu = es.parameters.get<Real> ("poisson ratio");
  const Real q  = es.parameters.get<Real> ("uniform load");

  // Compute the membrane stiffness \p K and the bending
  // rigidity \p D from these parameters.
  const Real K = E * h     /     (1-nu*nu);
  const Real D = E * h*h*h / (12*(1-nu*nu));

  // Numeric ids corresponding to each variable in the system.
  const unsigned int u_var = system.variable_number ("u");
  const unsigned int v_var = system.variable_number ("v");
  const unsigned int w_var = system.variable_number ("w");

  // Get the Finite Element type for "u".  Note this will be
  // the same as the type for "v" and "w".
  FEType fe_type = system.variable_type (u_var);

  // Build a Finite Element object of the specified type.
  AutoPtr<FEBase> fe (FEBase::build(2, fe_type));

  // A Gauss quadrature rule for numerical integration.
  // For subdivision shell elements, a single Gauss point per
  // element is sufficient, hence we use extraorder = 0.
  const int extraorder = 0;
  AutoPtr<QBase> qrule (fe_type.default_quadrature_rule (2, extraorder));

  // Tell the finite element object to use our quadrature rule.
  fe->attach_quadrature_rule (qrule.get());

  // The element Jacobian * quadrature weight at each integration point.
  const std::vector<Real>& JxW = fe->get_JxW();

  // The surface tangents in both directions at the quadrature points.
  const std::vector<RealGradient>& dxyzdxi  = fe->get_dxyzdxi();
  const std::vector<RealGradient>& dxyzdeta = fe->get_dxyzdeta();

  // The second partial derivatives at the quadrature points.
  const std::vector<RealGradient>& d2xyzdxi2    = fe->get_d2xyzdxi2();
  const std::vector<RealGradient>& d2xyzdeta2   = fe->get_d2xyzdeta2();
  const std::vector<RealGradient>& d2xyzdxideta = fe->get_d2xyzdxideta();

  // The element shape function and its derivatives evaluated at the
  // quadrature points.
  const std::vector<std::vector<Real> >&          phi = fe->get_phi();
  const std::vector<std::vector<RealGradient> >& dphi = fe->get_dphi();
  const std::vector<std::vector<RealTensor> >&  d2phi = fe->get_d2phi();

  // A reference to the \p DofMap object for this system.  The \p DofMap
  // object handles the index translation from node and element numbers
  // to degree of freedom numbers.
  const DofMap & dof_map = system.get_dof_map();

  // Define data structures to contain the element stiffness matrix
  // and right-hand-side vector contribution.  Following
  // basic finite element terminology we will denote these
  // "Ke" and "Fe".
  DenseMatrix<Number> Ke;
  DenseVector<Number> Fe;

  DenseSubMatrix<Number>
    Kuu(Ke), Kuv(Ke), Kuw(Ke),
    Kvu(Ke), Kvv(Ke), Kvw(Ke),
    Kwu(Ke), Kwv(Ke), Kww(Ke);

  DenseSubVector<Number>
    Fu(Fe),
    Fv(Fe),
    Fw(Fe);

  // This vector will hold the degree of freedom indices for
  // the element.  These define where in the global system
  // the element degrees of freedom get mapped.
  std::vector<dof_id_type> dof_indices;
  std::vector<dof_id_type> dof_indices_u;
  std::vector<dof_id_type> dof_indices_v;
  std::vector<dof_id_type> dof_indices_w;

  // Now we will loop over all the elements in the mesh.  We will
  // compute the element matrix and right-hand-side contribution.
  MeshBase::const_element_iterator       el     = mesh.active_local_elements_begin();
  const MeshBase::const_element_iterator end_el = mesh.active_local_elements_end();

  for (; el != end_el; ++el)
  {
    // Store a pointer to the element we are currently
    // working on.  This allows for nicer syntax later.
    const Elem* elem = *el;

    // The ghost elements at the boundaries need to be excluded
    // here, as they don't belong to the physical shell,
    // but serve for a proper boundary treatment only.
    libmesh_assert_equal_to (elem->type(), TRI3SUBDIVISION);
    const Tri3Subdivision* sd_elem = static_cast<const Tri3Subdivision*> (elem);
    if (sd_elem->is_ghost())
      continue;

    // Get the degree of freedom indices for the
    // current element.  These define where in the global
    // matrix and right-hand-side this element will
    // contribute to.
    dof_map.dof_indices (elem, dof_indices);
    dof_map.dof_indices (elem, dof_indices_u, u_var);
    dof_map.dof_indices (elem, dof_indices_v, v_var);
    dof_map.dof_indices (elem, dof_indices_w, w_var);

    const std::size_t n_dofs   = dof_indices.size();
    const std::size_t n_u_dofs = dof_indices_u.size();
    const std::size_t n_v_dofs = dof_indices_v.size();
    const std::size_t n_w_dofs = dof_indices_w.size();

    // Compute the element-specific data for the current
    // element.  This involves computing the location of the
    // quadrature points and the shape functions
    // (phi, dphi, d2phi) for the current element.
    fe->reinit (elem);

    // Zero the element matrix and right-hand side before
    // summing them.  We use the resize member here because
    // the number of degrees of freedom might have changed from
    // the last element.
    Ke.resize (n_dofs, n_dofs);
    Fe.resize (n_dofs);

    // Reposition the submatrices...  The idea is this:
    //
    //         -           -          -  -
    //        | Kuu Kuv Kuw |        | Fu |
    //   Ke = | Kvu Kvv Kvw |;  Fe = | Fv |
    //        | Kwu Kwv Kww |        | Fw |
    //         -           -          -  -
    //
    // The \p DenseSubMatrix.repostition () member takes the
    // (row_offset, column_offset, row_size, column_size).
    //
    // Similarly, the \p DenseSubVector.reposition () member
    // takes the (row_offset, row_size)
    Kuu.reposition (u_var*n_u_dofs, u_var*n_u_dofs, n_u_dofs, n_u_dofs);
    Kuv.reposition (u_var*n_u_dofs, v_var*n_u_dofs, n_u_dofs, n_v_dofs);
    Kuw.reposition (u_var*n_u_dofs, w_var*n_u_dofs, n_u_dofs, n_w_dofs);

    Kvu.reposition (v_var*n_v_dofs, u_var*n_v_dofs, n_v_dofs, n_u_dofs);
    Kvv.reposition (v_var*n_v_dofs, v_var*n_v_dofs, n_v_dofs, n_v_dofs);
    Kvw.reposition (v_var*n_v_dofs, w_var*n_v_dofs, n_v_dofs, n_w_dofs);

    Kwu.reposition (w_var*n_w_dofs, u_var*n_w_dofs, n_w_dofs, n_u_dofs);
    Kwv.reposition (w_var*n_w_dofs, v_var*n_w_dofs, n_w_dofs, n_v_dofs);
    Kww.reposition (w_var*n_w_dofs, w_var*n_w_dofs, n_w_dofs, n_w_dofs);

    Fu.reposition (u_var*n_u_dofs, n_u_dofs);
    Fv.reposition (v_var*n_u_dofs, n_v_dofs);
    Fw.reposition (w_var*n_u_dofs, n_w_dofs);

    // Now we will build the element matrix and right-hand-side.
    for (unsigned int qp=0; qp<qrule->n_points(); ++qp)
    {
      // First, we compute the external force resulting
      // from a load q distributed uniformly across the plate.
      // Since the load is supposed to be transverse to the plate,
      // it affects the z-direction, i.e. the "w" variable.
      for (unsigned int i=0; i<n_u_dofs; ++i)
        Fw(i) += JxW[qp] * phi[i][qp] * q;

      // Next, we assemble the stiffness matrix.  This is only valid
      // for the linear theory, i.e., for small deformations, where
      // reference and deformed surface metrics are indistinguishable.

      // Get the three surface basis vectors.
      const RealVectorValue & a1 = dxyzdxi[qp];
      const RealVectorValue & a2 = dxyzdeta[qp];
            RealVectorValue   a3 = a1.cross(a2);
      const Real jac = a3.size(); // the surface Jacobian
      libmesh_assert_greater (jac, 0);
      a3 /= jac; // the shell director a3 is normalized to unit length

      // Get the derivatives of the surface tangents.
      const RealVectorValue & a11 = d2xyzdxi2[qp];
      const RealVectorValue & a22 = d2xyzdeta2[qp];
      const RealVectorValue & a12 = d2xyzdxideta[qp];

      // Compute the three covariant components of the first
      // fundamental form of the surface.
      const RealVectorValue a(a1*a1, a2*a2, a1*a2);

      // The elastic H matrix in Voigt's notation, computed from the
      // covariant components of the first fundamental form rather
      // than the contravariant components, exploiting that the
      // contravariant first fundamental form is the inverse of the
      // covatiant first fundamental form (hence the determinant etc.).
      RealTensorValue H;
      H(0,0)          =  a(1) * a(1);
      H(0,1) = H(1,0) =   nu  * a(1) * a(0) + (1-nu) * a(2) * a(2);
      H(0,2) = H(2,0) = -a(1) * a(2);
      H(1,1)          =  a(0) * a(0);
      H(1,2) = H(2,1) = -a(0) * a(2);
      H(2,2)          = 0.5 * ((1-nu) * a(1) * a(0) + (1+nu) * a(2) * a(2));
      const Real det = a(0) * a(1) - a(2) * a(2);
      libmesh_assert_not_equal_to (det * det, 0);
      H /= det * det;

      // Precompute come cross products for the bending part below.
      const RealVectorValue a11xa2 = a11.cross(a2);
      const RealVectorValue a22xa2 = a22.cross(a2);
      const RealVectorValue a12xa2 = a12.cross(a2);
      const RealVectorValue a1xa11 =  a1.cross(a11);
      const RealVectorValue a1xa22 =  a1.cross(a22);
      const RealVectorValue a1xa12 =  a1.cross(a12);
      const RealVectorValue a2xa3  =  a2.cross(a3);
      const RealVectorValue a3xa1  =  a3.cross(a1);

      // Loop over all pairs of nodes I,J.
      for (unsigned int i=0; i<n_u_dofs; ++i)
      {
        for (unsigned int j=0; j<n_u_dofs; ++j)
        {
          // The membrane strain matrices in Voigt's notation.
          RealTensorValue MI, MJ;
          for (unsigned int k=0; k<3; ++k)
          {
            MI(0,k) = dphi[i][qp](0) * a1(k);
            MI(1,k) = dphi[i][qp](1) * a2(k);
            MI(2,k) = dphi[i][qp](1) * a1(k)
                    + dphi[i][qp](0) * a2(k);

            MJ(0,k) = dphi[j][qp](0) * a1(k);
            MJ(1,k) = dphi[j][qp](1) * a2(k);
            MJ(2,k) = dphi[j][qp](1) * a1(k)
                    + dphi[j][qp](0) * a2(k);
          }

          // The bending strain matrices in Voigt's notation.
          RealTensorValue BI, BJ;
          for (unsigned int k=0; k<3; ++k)
          {
            const Real term_ik = dphi[i][qp](0) * a2xa3(k)
                               + dphi[i][qp](1) * a3xa1(k);
            BI(0,k) = -d2phi[i][qp](0,0) * a3(k)
                      +(dphi[i][qp](0) * a11xa2(k)
                      + dphi[i][qp](1) * a1xa11(k)
                      + (a3*a11) * term_ik) / jac;
            BI(1,k) = -d2phi[i][qp](1,1) * a3(k)
                      +(dphi[i][qp](0) * a22xa2(k)
                      + dphi[i][qp](1) * a1xa22(k)
                      + (a3*a22) * term_ik) / jac;
            BI(2,k) = 2 * (-d2phi[i][qp](0,1) * a3(k)
                           +(dphi[i][qp](0) * a12xa2(k)
                           + dphi[i][qp](1) * a1xa12(k)
                           + (a3*a12) * term_ik) / jac);

            const Real term_jk = dphi[j][qp](0) * a2xa3(k)
                               + dphi[j][qp](1) * a3xa1(k);
            BJ(0,k) = -d2phi[j][qp](0,0) * a3(k)
                      +(dphi[j][qp](0) * a11xa2(k)
                      + dphi[j][qp](1) * a1xa11(k)
                      + (a3*a11) * term_jk) / jac;
            BJ(1,k) = -d2phi[j][qp](1,1) * a3(k)
                      +(dphi[j][qp](0) * a22xa2(k)
                      + dphi[j][qp](1) * a1xa22(k)
                      + (a3*a22) * term_jk) / jac;
            BJ(2,k) = 2 * (-d2phi[j][qp](0,1) * a3(k)
                           +(dphi[j][qp](0) * a12xa2(k)
                           + dphi[j][qp](1) * a1xa12(k)
                           + (a3*a12) * term_jk) / jac);
          }

          // The total stiffness matrix coupling the nodes
          // I and J is a sum of membrane and bending
          // contributions according to the following formula.
          const RealTensorValue KIJ = JxW[qp] * K * MI.transpose() * H * MJ
                                    + JxW[qp] * D * BI.transpose() * H * BJ;

          // Insert the components of the coupling stiffness
          // matrix \p KIJ into the corresponding directional
          // submatrices.
          Kuu(i,j) += KIJ(0,0);
          Kuv(i,j) += KIJ(0,1);
          Kuw(i,j) += KIJ(0,2);

          Kvu(i,j) += KIJ(1,0);
          Kvv(i,j) += KIJ(1,1);
          Kvw(i,j) += KIJ(1,2);

          Kwu(i,j) += KIJ(2,0);
          Kwv(i,j) += KIJ(2,1);
          Kww(i,j) += KIJ(2,2);
        }
      }

    } // end of the quadrature point qp-loop

    // The element matrix and right-hand-side are now built
    // for this element.  Add them to the global matrix and
    // right-hand-side vector.  The \p NumericMatrix::add_matrix()
    // and \p NumericVector::add_vector() members do this for us.
    system.matrix->add_matrix (Ke, dof_indices);
    system.rhs->add_vector    (Fe, dof_indices);
  } // end of non-ghost element loop

  // Next, we apply the boundary conditions.  In this case,
  // all boundaries are clamped by the penalty method, using
  // the special "ghost" nodes along the boundaries.  Note
  // that there are better ways to implement boundary conditions
  // for subdivision shells.  We use the simplest way here,
  // which is known to be overly restrictive and will lead to
  // a slightly too small deformation of the plate.
  el = mesh.active_local_elements_begin();

  for (; el != end_el; ++el)
  {
    // Store a pointer to the element we are currently
    // working on.  This allows for nicer syntax later.
    const Elem* elem = *el;

    // For the boundary conditions, we only need to loop over
    // the ghost elements.
    libmesh_assert_equal_to (elem->type(), TRI3SUBDIVISION);
    const Tri3Subdivision* gh_elem = static_cast<const Tri3Subdivision*> (elem);
    if (!gh_elem->is_ghost())
      continue;

    // Find the side which is part of the physical plate boundary,
    // that is, the boundary of the original mesh without ghosts.
    for (unsigned int s=0; s<elem->n_sides(); ++s)
    {
      const Tri3Subdivision* nb_elem = static_cast<const Tri3Subdivision*> (elem->neighbor(s));
      if (nb_elem == NULL || nb_elem->is_ghost())
        continue;

      /*
       * Determine the four nodes involved in the boundary
       * condition treatment of this side.  The \p MeshTools::Subdiv
       * namespace provides lookup tables \p next and \p prev
       * for an efficient determination of the next and previous
       * nodes of an element, respectively.
       *
       *      n4
       *     /  \
       *    / gh \
       *  n2 ---- n3
       *    \ nb /
       *     \  /
       *      n1
       */
      Node* nodes [4]; // n1, n2, n3, n4
      nodes[1] = gh_elem->get_node(s); // n2
      nodes[2] = gh_elem->get_node(MeshTools::Subdivision::next[s]); // n3
      nodes[3] = gh_elem->get_node(MeshTools::Subdivision::prev[s]); // n4

      // The node in the interior of the domain, \p n1, is the
      // hardest to find.  Walk along the edges of element \p nb until
      // we have identified it.
      unsigned int n_int = 0;
      nodes[0] = nb_elem->get_node(0);
      while (nodes[0]->id() == nodes[1]->id() || nodes[0]->id() == nodes[2]->id())
        nodes[0] = nb_elem->get_node(++n_int);

      // The penalty value.  \f$ \frac{1}{\epsilon} \f$
      const Real penalty = 1.e10;

      // With this simple method, clamped boundary conditions are
      // obtained by penalizing the displacements of all four nodes.
      // This ensures that the displacement field vanishes on the
      // boundary side \p s.
      for (unsigned int n=0; n<4; ++n)
      {
        const dof_id_type u_dof = nodes[n]->dof_number (system.number(), u_var, 0);
        const dof_id_type v_dof = nodes[n]->dof_number (system.number(), v_var, 0);
        const dof_id_type w_dof = nodes[n]->dof_number (system.number(), w_var, 0);
        system.matrix->add (u_dof, u_dof, penalty);
        system.matrix->add (v_dof, v_dof, penalty);
        system.matrix->add (w_dof, w_dof, penalty);
      }
    }
  } // end of ghost element loop
}
Beispiel #2
0
// We now define the matrix assembly function for the
// Laplace system.  We need to first compute element
// matrices and right-hand sides, and then take into
// account the boundary conditions, which will be handled
// via a penalty method.
void assemble_laplace(EquationSystems & es,
                      const std::string & system_name)
{
#ifdef LIBMESH_ENABLE_AMR
  // It is a good idea to make sure we are assembling
  // the proper system.
  libmesh_assert_equal_to (system_name, "Laplace");


  // Declare a performance log.  Give it a descriptive
  // string to identify what part of the code we are
  // logging, since there may be many PerfLogs in an
  // application.
  PerfLog perf_log ("Matrix Assembly", false);

  // Get a constant reference to the mesh object.
  const MeshBase & mesh = es.get_mesh();

  // The dimension that we are running
  const unsigned int mesh_dim = mesh.mesh_dimension();

  // Get a reference to the LinearImplicitSystem we are solving
  LinearImplicitSystem & system = es.get_system<LinearImplicitSystem>("Laplace");

  // A reference to the DofMap object for this system.  The DofMap
  // object handles the index translation from node and element numbers
  // to degree of freedom numbers.  We will talk more about the DofMap
  // in future examples.
  const DofMap & dof_map = system.get_dof_map();

  // Get a constant reference to the Finite Element type
  // for the first (and only) variable in the system.
  FEType fe_type = dof_map.variable_type(0);

  // Build a Finite Element object of the specified type.  Since the
  // FEBase::build() member dynamically creates memory we will
  // store the object as a UniquePtr<FEBase>.  This can be thought
  // of as a pointer that will clean up after itself.
  UniquePtr<FEBase> fe      (FEBase::build(mesh_dim, fe_type));
  UniquePtr<FEBase> fe_face (FEBase::build(mesh_dim, fe_type));

  // Quadrature rules for numerical integration.
  UniquePtr<QBase> qrule(fe_type.default_quadrature_rule(mesh_dim));
  UniquePtr<QBase> qface(fe_type.default_quadrature_rule(mesh_dim-1));

  // Tell the finite element object to use our quadrature rule.
  fe->attach_quadrature_rule      (qrule.get());
  fe_face->attach_quadrature_rule (qface.get());

  // Here we define some references to cell-specific data that
  // will be used to assemble the linear system.
  // We begin with the element Jacobian * quadrature weight at each
  // integration point.
  const std::vector<Real> & JxW      = fe->get_JxW();
  const std::vector<Real> & JxW_face = fe_face->get_JxW();

  // The physical XY locations of the quadrature points on the element.
  // These might be useful for evaluating spatially varying material
  // properties or forcing functions at the quadrature points.
  const std::vector<Point> & q_point = fe->get_xyz();

  // The element shape functions evaluated at the quadrature points.
  // For this simple problem we usually only need them on element
  // boundaries.
  const std::vector<std::vector<Real> > & phi = fe->get_phi();
  const std::vector<std::vector<Real> > & psi = fe_face->get_phi();

  // The element shape function gradients evaluated at the quadrature
  // points.
  const std::vector<std::vector<RealGradient> > & dphi = fe->get_dphi();

  // The XY locations of the quadrature points used for face integration
  const std::vector<Point> & qface_points = fe_face->get_xyz();

  // Define data structures to contain the element matrix
  // and right-hand-side vector contribution.  Following
  // basic finite element terminology we will denote these
  // "Ke" and "Fe". More detail is in example 3.
  DenseMatrix<Number> Ke;
  DenseVector<Number> Fe;

  // This vector will hold the degree of freedom indices for
  // the element.  These define where in the global system
  // the element degrees of freedom get mapped.
  std::vector<dof_id_type> dof_indices;

  // Now we will loop over all the elements in the mesh.  We will
  // compute the element matrix and right-hand-side contribution.  See
  // example 3 for a discussion of the element iterators.  Here we use
  // the const_active_local_elem_iterator to indicate we only want
  // to loop over elements that are assigned to the local processor
  // which are "active" in the sense of AMR.  This allows each
  // processor to compute its components of the global matrix for
  // active elements while ignoring parent elements which have been
  // refined.
  MeshBase::const_element_iterator       el     = mesh.active_local_elements_begin();
  const MeshBase::const_element_iterator end_el = mesh.active_local_elements_end();

  for ( ; el != end_el; ++el)
    {
      // Start logging the shape function initialization.
      // This is done through a simple function call with
      // the name of the event to log.
      perf_log.push("elem init");

      // Store a pointer to the element we are currently
      // working on.  This allows for nicer syntax later.
      const Elem * elem = *el;

      // Get the degree of freedom indices for the
      // current element.  These define where in the global
      // matrix and right-hand-side this element will
      // contribute to.
      dof_map.dof_indices (elem, dof_indices);

      // Compute the element-specific data for the current
      // element.  This involves computing the location of the
      // quadrature points (q_point) and the shape functions
      // (phi, dphi) for the current element.
      fe->reinit (elem);

      // Zero the element matrix and right-hand side before
      // summing them.  We use the resize member here because
      // the number of degrees of freedom might have changed from
      // the last element.  Note that this will be the case if the
      // element type is different (i.e. the last element was a
      // triangle, now we are on a quadrilateral).
      Ke.resize (dof_indices.size(),
                 dof_indices.size());

      Fe.resize (dof_indices.size());

      // Stop logging the shape function initialization.
      // If you forget to stop logging an event the PerfLog
      // object will probably catch the error and abort.
      perf_log.pop("elem init");

      // Now we will build the element matrix.  This involves
      // a double loop to integrate the test funcions (i) against
      // the trial functions (j).
      //
      // Now start logging the element matrix computation
      perf_log.push ("Ke");

      for (unsigned int qp=0; qp<qrule->n_points(); qp++)
        for (unsigned int i=0; i<dphi.size(); i++)
          for (unsigned int j=0; j<dphi.size(); j++)
            Ke(i,j) += JxW[qp]*(dphi[i][qp]*dphi[j][qp]);

      // We need a forcing function to make the 1D case interesting
      if (mesh_dim == 1)
        for (unsigned int qp=0; qp<qrule->n_points(); qp++)
          {
            Real x = q_point[qp](0);
            Real f = singularity ? sqrt(3.)/9.*pow(-x, -4./3.) :
              cos(x);
            for (unsigned int i=0; i<dphi.size(); ++i)
              Fe(i) += JxW[qp]*phi[i][qp]*f;
          }

      // Stop logging the matrix computation
      perf_log.pop ("Ke");


      // At this point the interior element integration has
      // been completed.  However, we have not yet addressed
      // boundary conditions.  For this example we will only
      // consider simple Dirichlet boundary conditions imposed
      // via the penalty method.
      //
      // This approach adds the L2 projection of the boundary
      // data in penalty form to the weak statement.  This is
      // a more generic approach for applying Dirichlet BCs
      // which is applicable to non-Lagrange finite element
      // discretizations.
      {
        // Start logging the boundary condition computation
        perf_log.push ("BCs");

        // The penalty value.
        const Real penalty = 1.e10;

        // The following loops over the sides of the element.
        // If the element has no neighbor on a side then that
        // side MUST live on a boundary of the domain.
        for (unsigned int s=0; s<elem->n_sides(); s++)
          if (elem->neighbor(s) == libmesh_nullptr)
            {
              fe_face->reinit(elem, s);

              for (unsigned int qp=0; qp<qface->n_points(); qp++)
                {
                  const Number value = exact_solution (qface_points[qp],
                                                       es.parameters,
                                                       "null",
                                                       "void");

                  // RHS contribution
                  for (unsigned int i=0; i<psi.size(); i++)
                    Fe(i) += penalty*JxW_face[qp]*value*psi[i][qp];

                  // Matrix contribution
                  for (unsigned int i=0; i<psi.size(); i++)
                    for (unsigned int j=0; j<psi.size(); j++)
                      Ke(i,j) += penalty*JxW_face[qp]*psi[i][qp]*psi[j][qp];
                }
            }

        // Stop logging the boundary condition computation
        perf_log.pop ("BCs");
      }


      // The element matrix and right-hand-side are now built
      // for this element.  Add them to the global matrix and
      // right-hand-side vector.  The SparseMatrix::add_matrix()
      // and NumericVector::add_vector() members do this for us.
      // Start logging the insertion of the local (element)
      // matrix and vector into the global matrix and vector
      perf_log.push ("matrix insertion");

      dof_map.constrain_element_matrix_and_vector(Ke, Fe, dof_indices);
      system.matrix->add_matrix (Ke, dof_indices);
      system.rhs->add_vector    (Fe, dof_indices);

      // Start logging the insertion of the local (element)
      // matrix and vector into the global matrix and vector
      perf_log.pop ("matrix insertion");
    }

  // That's it.  We don't need to do anything else to the
  // PerfLog.  When it goes out of scope (at this function return)
  // it will print its log to the screen. Pretty easy, huh?
#endif // #ifdef LIBMESH_ENABLE_AMR
}