Contents

Linear Elasticity

Introduction

This tutorial provides a step-by-step guide for implementing a linear elasticity problem using the Finite Element Method (FEM) in the deal.II library. Linear elasticity governs the behavior of solid materials under small deformations, where stress is linearly proportional to strain. We will focus on setting up the weak form of the elasticity equations, assembling the finite element system, applying appropriate boundary conditions, and solving the resulting linear system. This tutorial assumes familiarity with basic FEM concepts and provides a practical foundation for more advanced developments such as nonlinear elasticity or dynamic simulations within the deal.II framework.

Theory

Linear elasticity is a branch of solid mechanics that deals with the behavior of solid materials under small deformations, where stress is proportional to strain. It is governed by Hooke's Law, which relates the stress and strain through material properties such as Young's modulus and Poisson's ratio. In this section, we will discuss the foundational equations of linear elasticity, including the stress-strain relationship, the equilibrium equations, and boundary conditions used in finite element analysis.

Stress-Strain Relationship:
$$ \sigma = \mathbf{C} \epsilon $$

Where:

Equilibrium Equation:
$$ \nabla \cdot \sigma + \mathbf{f} = 0 $$

These equations are essential for solving the linear elasticity problem using the Finite Element Method. The solution involves discretizing the domain into finite elements and solving the corresponding system of equations to obtain the displacement field.

Implementation

Include Files

In this section, we import all the necessary deal.II library components as well as standard C++ headers required to set up and solve the linear elasticity problem. These include modules for finite element definitions, mesh handling, quadrature formulas, linear solvers, and other utilities. Including these files ensures that we have access to all the classes and functions needed throughout the program.

#include <deal.II/base/quadrature_lib.h>
#include <deal.II/base/function.h>
#include <deal.II/base/timer.h>
#include <deal.II/base/utilities.h>
#include <deal.II/base/conditional_ostream.h>
#include <deal.II/base/index_set.h>
#include <deal.II/base/quadrature_point_data.h>
#include <deal.II/base/tensor_function.h>

#include <deal.II/grid/grid_generator.h>
#include <deal.II/grid/tria_accessor.h>
#include <deal.II/grid/grid_refinement.h>

#include <deal.II/dofs/dof_handler.h>
#include <deal.II/dofs/dof_tools.h>
#include <deal.II/dofs/dof_accessor.h>

#include <deal.II/lac/generic_linear_algebra.h>
#include <deal.II/lac/vector.h>
#include <deal.II/lac/full_matrix.h>
#include <deal.II/lac/precondition.h>
#include <deal.II/lac/petsc_precondition.h>
#include <deal.II/lac/solver_cg.h>
#include <deal.II/lac/affine_constraints.h>
#include <deal.II/lac/sparsity_tools.h>
#include <deal.II/lac/dynamic_sparsity_pattern.h>

#include <deal.II/fe/fe_values.h>
#include <deal.II/fe/fe_system.h>
#include <deal.II/fe/fe_q.h>

#include <deal.II/numerics/vector_tools.h>
#include <deal.II/numerics/matrix_tools.h>
#include <deal.II/numerics/data_out.h>
#include <deal.II/numerics/error_estimator.h>

#include <deal.II/distributed/tria.h>
#include <deal.II/distributed/grid_refinement.h>
#include <deal.II/distributed/solution_transfer.h>

#include <fstream>
#include <iostream>
#include <random>
        

Namespace definition

We start by defining the namespace elas, which encapsulates all components related to the linear elasticity simulation. Namespaces in C++ help organize the code and prevent name conflicts, keeping related classes and functions grouped logically.

namespace elas
{
    using namespace dealii;
        

Elasticity class

The Elasticity class is templated on the problem dimension and manages the overall workflow.

Public functions:

Private functions:

    template <int dim>
    class Elasticity
    {
        public:
            Elasticity();
            void run();

        private:
            // Mesh generation and refinement
            void create_mesh();
            void refine_mesh(const unsigned int load_step);

            // Elasticity problem functions
            void setup_constraints (const unsigned int load_step); 
            void setup(const unsigned int load_step);
            void assemble();
            void solve_linear_problem();

            // Save the results - Disp, stress, reaction, energies
            void output_results(const unsigned int load_step) const;
            void output_stress(const unsigned int load_step);
            void reaction(Tensor<1, dim> &reaction_stress, const types::boundary_id &boundary_id);
        

Parallel and MPI setup

We define these objects in order to store all the relevant information that allow the program to operate efficiently across distributed memory systems, specifically:

            // Parallel setup and print on screen
            MPI_Comm mpi_communicator;
            const unsigned int n_mpi_processes;
            const unsigned int this_mpi_process;

            parallel::distributed::Triangulation<dim> triangulation;
        

Core finite element objects

These objects define the mathematical and computational framework for the simulation

            // Objects for elasticity
            const FESystem<dim> fe;                         // FE System
            DoFHandler<dim> dof_handler;                    // DoF Handler

            IndexSet locally_owned_dofs;                        // IndexSet - Locally Owned
            IndexSet locally_relevant_dofs;                     // IndexSet - Locally relevant 
            AffineConstraints<double> constraints;              // Affine Constraint  

            const QGauss<dim> quadrature_formula;           // Quadrature formula
        

System matrices and solution vectors:

            PETScWrappers::MPI::SparseMatrix system_matrix;     // Elastcitiy Matrix
            PETScWrappers::MPI::Vector system_rhs;              // RHS 

            PETScWrappers::MPI::Vector local_solution;          // MPI Split solution
            PETScWrappers::MPI::Vector distributed_solution;    // Full Solution

            PETScWrappers::MPI::Vector distributed_solution_old;   // This is for the refinement

            Tensor<1, dim> reaction_stress;
        

Material Properties and Simulation Parameters

This section defines the physical and numerical setup for the elasticity simulation. The domain dimensions are specified by L, H, and W, which represent the length, height, and width, respectively. The initial subdivisions of the domain along the x, y, and z axes are controlled by the nx, ny, and nz variables.

The material properties are defined using E for Young's modulus and nu for Poisson's ratio. These values are essential for simulating the material's response to stress. The simulation load is managed with the load_increment, which dictates the amount of load applied at each step, and the applied_load, which tracks the cumulative load over time. The num_load_steps variable defines the total number of load increments to be applied during the simulation.

Two auxiliary tools are utilized in this setup: pcout, which handles parallel printing, allowing output to be displayed during parallel computations, and computing_timer, which measures and reports the computation time for each simulation step.

This configuration forms the foundation for the elasticity simulation, specifying the material properties, domain dimensions, and numerical controls required to run the simulation efficiently and accurately.

            // Domain dimensions
            double L = 1;
            double H = 0.05;
            double W = 0.05;

            const unsigned int nx = 25;
            const unsigned int ny = 2;
            const unsigned int nz = 2;
            // Material properties
            double E = 100;
            double nu = 0.3;      

            double load_increment = 0.01;     // Load increment applied
            double applied_load = 0;          // Current applied_load += load_increment
            double num_load_steps = 5;        // Total load increment  
            
            ConditionalOStream pcout;
            TimerOutput computing_timer;       
    }; // End of Elast class
        

Constructor

The constructor initializes all essential components required for the elasticity problem, including the core finite element objects and the MPI communicator-related objects. Each object associated with the communicator is initialized in the same order in which it is declared within the class.

    // Constructor
    template <int dim>
    Elasticity<dim>::Elasticity()
        : mpi_communicator(MPI_COMM_WORLD)
        , n_mpi_processes(Utilities::MPI::n_mpi_processes(mpi_communicator))
        , this_mpi_process(Utilities::MPI::this_mpi_process(mpi_communicator))
        , triangulation (mpi_communicator)
        , fe (FE_Q<dim> (1), dim)
        , dof_handler (triangulation)
        , quadrature_formula (fe.degree + 1)
        , pcout (std::cout, this_mpi_process == 0)
        , computing_timer (mpi_communicator, pcout,
                            TimerOutput::never,
                            TimerOutput::wall_times)
        {}
        

Evaluation of Elastic Constants:

These two inline functions compute the Lamé constants, lambda and mu, based on the material's Young's modulus (E) and Poisson's ratio (nu). These constants are fundamental for solving elasticity problems in solid mechanics.

    // Evaluation of the elastic constant
    inline double lambda (const float E, const float nu)
    {
        return (E * nu) / ((1 + nu) * (1 - 2 * nu));
    }

    inline double mu (const float E, const float nu)
    {
        return E / (2 * (1 + nu));
    }        
        

Create mesh

This function generates the computational mesh for the elasticity problem, providing flexibility for both 2D and 3D cases. It starts by defining a coarse subdivision based on the given domain dimensions, which is stored in a standard C++ vector. The next step is to define the bottom-left point p1 and the top-right point p2 of the rectangular domain as Point objects. To ensure the solution is dimension-independent, a check is performed to determine the problem's dimensionality, and 2D or 3D points are created accordingly. Finally, the mesh is created using the GridGenerator::subdivided_hyper_rectangle function.

    template <int dim>
    void Elasticity<dim>::create_mesh()
    {
        // Define the initial coarse subdivision
        const std::vector<unsigned int> repetitions = {nx,ny,nz};
        
        // Create Mesh
        Point<dim> p1, p2;
        if constexpr (dim == 2)
        {
            p1 = Point<dim>(0, 0);
            p2 = Point<dim>(L, H);
        }
        else if constexpr (dim == 3)
        {
            p1 = Point<dim>(0, 0, 0);
            p2 = Point<dim>(L, H, W);
        }

        // create coarse mesh
        GridGenerator::subdivided_hyper_rectangle (triangulation, repetitions, p1, p2); 
        

Setting Boundary Face IDs:

The code iterates over all active cells in the triangulation object, and for each cell, it iterates over its faces using cell->face_iterators(), it checks if the face is at the boundary with face->at_boundary() and lastly, if the face is at the boundary, the center of the face is calculated using face->center().

Next, the code checks the location of the face's center to determine its position relative to the boundaries of the domain. It first checks if the face is located at the left, right, bottom, or top, assigning the IDs 0, 1, 2, and 3, respectively.

To remain dimension independent, the code uses if constexpr (dim == 3) to ensure that the check for the third coordinate is performed only in 3D cases. If the face is located at z = 0, it assigns a boundary ID of 4, and if the face is at z = W, it assigns a boundary ID of 5.

        // Set Face IDs
        for (const auto &cell : triangulation.active_cell_iterators ())
        {
            for (const auto &face : cell->face_iterators ())
            {
                if (face->at_boundary ())
                {
                    const auto center = face->center ();
                    if (std::fabs (center(0) - 0) < 1e-12)
                        face->set_boundary_id (0);

                    else if (std::fabs (center(0) - L) < 1e-12)
                        face->set_boundary_id (1);

                    else if (std::fabs (center(1) - 0) < 1e-12)
                        face->set_boundary_id (2);

                    else if (std::fabs (center(1) - H) < 1e-12)
                        face->set_boundary_id (3);

                    // Check if we are in 3D before accessing center(2)
                    if constexpr (dim == 3)
                    {
                        if (std::fabs(center(2) - 0) < 1e-12)
                            face->set_boundary_id(4);
                        else if (std::fabs(center(2) - W) < 1e-12)
                            face->set_boundary_id(5);
                    }
                }
            }
        }
        

Initialize DoF Handler and Solution Setup:

This section of the code initializes the degrees of freedom (DoFs) and sets up the solution for the elasticity problem in a parallel execution environment. The first operation performed is the distribution of DoFs across the mesh using dof_handler.distribute_dofs(fe). This method ensures that the degrees of freedom are allocated according to the finite element method configuration, which is crucial for efficient parallel execution, where each process is responsible for a part of the problem.

Next, the code retrieves the locally owned degrees of freedom using locally_owned_dofs = dof_handler.locally_owned_dofs(). This step ensures that each MPI process knows which part of the problem it is responsible for. Additionally, it extracts the locally relevant DoFs, which may include DoFs owned by neighboring processes. This is done using DoFTools::extract_locally_relevant_dofs(dof_handler), enabling the necessary inter-process communication for solving the problem.

To maintain continuity between load steps, especially when using adaptive mesh refinement techniques, the solution vector from the previous load step is reinitialized. The vector is called distributed_solution_old, and it is reinitialized using distributed_solution_old.reinit(locally_owned_dofs, mpi_communicator). This ensures that the previous solution is preserved for refinement purposes.

The code also prints some key information regarding the triangulation and the distribution of the mesh. Using the pcout stream, the number of global levels in the triangulation is displayed with the command. This gives an overview of the mesh refinement hierarchy. Similarly, it prints the number of locally owned cells, which informs the user about the number of cells assigned to the current MPI process. The total number of cells across the global domain is also printed.

            // Initialize dof handler and extact Owned/Relevant DoFs
        dof_handler.distribute_dofs (fe);
        
        locally_owned_dofs = dof_handler.locally_owned_dofs ();
        locally_relevant_dofs = DoFTools::extract_locally_relevant_dofs (dof_handler);
        
        // Initialize the old solution for refinement purposes
        distributed_solution_old.reinit (locally_owned_dofs, mpi_communicator);
        
        pcout << "No. of levels in triangulation: " 
              << triangulation.n_global_levels () << std::endl;
        
        pcout << "   Number of locally owned cells on the process:       " 
              << triangulation.n_locally_owned_active_cells () << std::endl;
        
        pcout << "Number of global cells:" << triangulation.n_global_active_cells () << std::endl;
    
        pcout << "  Total Number of globally active cells:       "  
              << triangulation.n_global_active_cells () << std::endl
              << "   Number of degrees of freedom for elasticity: " 
              << dof_handler.n_dofs () << std::endl;
    }
        

Mesh Refinement

This function prepares the computational mesh for adaptive refinement based on an error estimation from the current solution. It is typically called at the end of each load step to improve the solution's accuracy by refining the mesh where needed. The first step in the mesh refinement process is the creation of a vector, Vector<float> estimated_error_per_cell, which stores the estimated error for each locally owned active cell. The vector is initialized to the number of cells owned by the current MPI process, ensuring that each process has its own set of error estimates.

The next part of the process involves the use of the KellyErrorEstimator<dim>::estimate function. This function estimates the error for each cell in the mesh using the Kelly error estimator, which is a standard technique for error estimation in finite element methods. The estimator requires several inputs: the dof_handler (which contains the degrees of freedom), a quadrature rule QGauss<dim-1>(fe.degree + 1) that specifies the degree of quadrature for integration, a map of boundary IDs, a function representing boundary conditions, and the current solution local_solution. The estimated error for each cell is then stored in the estimated_error_per_cell vector.

Finally, the code initializes a SolutionTransfer object. The parallel::distributed::SolutionTransfer<dim, PETScWrappers::MPI::Vector> is responsible for transferring the solution from the old mesh to the refined mesh. This ensures that the solution is correctly mapped after the adaptive refinement, especially in a parallel execution environment. The SolutionTransfer object, named soltrans, is initialized with the dof_handler to enable the correct transfer of solution data.

    template<int dim>
    void Elasticity<dim>::refine_mesh(const unsigned int load_step)
    {
        Vector<float> estimated_error_per_cell (triangulation.n_locally_owned_active_cells ());
        KellyErrorEstimator<dim>::estimate (dof_handler,
                                            QGauss<dim-1> (fe.degree + 1),
                                            std::map<types::boundary_id, const Function<dim> *>(),
                                            local_solution, estimated_error_per_cell);

        // Initialize SolutionTransfer object
        parallel::distributed::SolutionTransfer<dim, PETScWrappers::MPI::Vector>
            soltrans (dof_handler);
        

Mesh Refinement and Solution Transfer:

This section of the code performs adaptive mesh refinement based on error estimation, followed by transferring the solution across the updated mesh.

The parallel::distributed::GridRefinement::refine_and_coarsen_fixed_fraction function refines the top 30% of cells with the highest error and coarsens the bottom 1% to improve accuracy.

If the mesh is refined to level 4 or higher, further refinement is prevented at level 3 to avoid unnecessary computational overhead. This is controlled by checking the number of global levels with if (triangulation.n_global_levels() >= 4).

The triangulation is then prepared for coarsening and refinement using triangulation.prepare_coarsening_and_refinement(), and the SolutionTransfer object is initialized with soltrans.prepare_for_coarsening_and_refinement(local_solution) to ensure the current solution is correctly mapped onto the refined mesh.

After the mesh is refined, the degrees of freedom are redistributed using dof_handler.distribute_dofs(fe), ensuring the solution is consistent with the new mesh configuration.

        parallel::distributed::GridRefinement::refine_and_coarsen_fixed_fraction (
            triangulation, estimated_error_per_cell, 0.30, 0.01);

        if (triangulation.n_global_levels () >= 4)
        {
            for (const auto &cell : triangulation.active_cell_iterators_on_level (3))
            if (cell->is_locally_owned ())
                cell->clear_refine_flag ();
        }
        
        // Prepare the triangulation
        triangulation.prepare_coarsening_and_refinement ();

        // Prepare the SolutionTransfer object for coarsening and refinement
        // and give the solution vector that we intend to interpolate later,
        soltrans.prepare_for_coarsening_and_refinement (local_solution);

        // Do the refinement
        triangulation.execute_coarsening_and_refinement ();
        
        // redistribute dofs,
        dof_handler.distribute_dofs (fe);
        

Reconstruction of Degrees of Freedom and Solution Interpolation:

This part of the code manages the redistribution of degrees of freedom (DoFs) and interpolates the old solution onto the newly refined mesh. Constraints are then reapplied to ensure the solution remains valid and consistent with the updated mesh structure.

The locally_owned_dofs = dof_handler.locally_owned_dofs() updates the degrees of freedom owned by the current MPI process after mesh refinement. Similarly, locally_relevant_dofs = DoFTools::extract_locally_relevant_dofs(dof_handler) extracts the locally relevant degrees of freedom, including those needed for MPI communication.

The distributed_solution_old.reinit(locally_owned_dofs, mpi_communicator) reinitializes the distributed solution vector to align with the new DoF layout, followed by interpolation of the old solution onto the refined mesh with soltrans.interpolate(distributed_solution_old).

Next, the setup_constraints(load_step) function re-applies the boundary conditions and other constraints, and constraints.distribute(distributed_solution_old) ensures the constraints are applied to the interpolated solution. Finally, the new solution is copied into local_solution to maintain continuity in the simulation.

        // Recreate locally_owned_dofs and locally_relevant_dofs index sets
        locally_owned_dofs = dof_handler.locally_owned_dofs ();
        locally_relevant_dofs = DoFTools::extract_locally_relevant_dofs (dof_handler);

        distributed_solution_old.reinit (locally_owned_dofs, mpi_communicator);
        soltrans.interpolate (distributed_solution_old);

        // Apply constraints on the interpolated solution
        setup_constraints(load_step);
        constraints.distribute (distributed_solution_old);

        // Copy distributed_solution_old to local_solution
        local_solution.reinit(locally_owned_dofs,
                                locally_relevant_dofs,
                                mpi_communicator);
        local_solution = distributed_solution_old;
    }
        

Setup Constraints

This function configures the boundary constraints needed to enforce boundary conditions on the finite element solution. It starts by clearing and reinitializing the constraint system based on the currently relevant degrees of freedom. Hanging node constraints, which ensure continuity across mesh refinements, are automatically handled through DoFTools::make_hanging_node_constraints.

Specific displacement conditions are applied next. Displacement along the x-axis is extracted using u_x and its corresponding mask. The displacement values on the left and right boundaries are computed based on the current load step, introducing symmetrical loading: negative on the left side and positive on the right side.

Similarly, displacement along the y-axis is extracted using u_y, with a fixed (zero) displacement applied on the relevant boundary to prevent vertical motion.

If the simulation is in three dimensions, displacement along the z-axis (u_z) is also constrained to zero on a specified boundary to avoid out-of-plane motion. Finally, the constraint system is closed to finalize all imposed conditions before solving the system.

    template<int dim>
    void Elasticity<dim>::setup_constraints (const unsigned int load_step)
    {
        constraints.clear ();
        constraints.reinit (locally_relevant_dofs);

        DoFTools::make_hanging_node_constraints (dof_handler, constraints);

        const FEValuesExtractors::Scalar u_x (0);
        const FEValuesExtractors::Scalar u_y (1);

        const ComponentMask u_x_mask = fe.component_mask (u_x);
        const ComponentMask u_y_mask = fe.component_mask (u_y);

        applied_load = load_increment*load_step;
        const double u_x_values_right = 0.5*applied_load;
        const double u_x_values_left = -0.5*applied_load;

        const double u_fix = 0.0;

        VectorTools::interpolate_boundary_values (dof_handler, 0,
                                Functions::ConstantFunction<dim> (u_x_values_left, dim),
                                constraints, 
                                u_x_mask);

        VectorTools::interpolate_boundary_values (dof_handler, 1,
                                Functions::ConstantFunction<dim> (u_x_values_right, dim), 
                                constraints,
                                u_x_mask);

        VectorTools::interpolate_boundary_values (dof_handler, 2,
                                Functions::ConstantFunction<dim> (u_fix, dim), 
                                constraints,
                                u_y_mask);

        if constexpr (dim == 3)
        {
            const FEValuesExtractors::Scalar u_z (2);
            const ComponentMask u_z_mask = fe.component_mask (u_z);

            VectorTools::interpolate_boundary_values (dof_handler, 4,
                                Functions::ConstantFunction<dim> (u_fix, dim),
                                constraints,
                                u_z_mask);
        }

        constraints.close ();
    }
        

System setup

This function initializes all essential structures required for solving the finite element system at the current load step. It starts by creating and extracting the degrees of freedom that are locally owned and those that are locally relevant for communication across MPI processes. Solution vectors such as local_solution, system_rhs, and distributed_solution are reinitialized accordingly to match the new DoF layout.

Boundary conditions and hanging node constraints are enforced through a call to setup_constraints(load_step). A dynamic sparsity pattern is then generated, defining where non-zero entries are expected in the system matrix. This sparsity pattern is distributed across MPI processes to ensure efficient parallel assembly.

Finally, system_matrix is reinitialized based on the new sparsity pattern, completing the setup required before assembling and solving the elasticity problem for the given load step.

    template<int dim>
    void Elasticity<dim>::setup(const unsigned int load_step)
    {
        TimerOutput::Scope ts(computing_timer, "setup_system");
        
        // Initialize the dofs contained locally as well as those of the near cells
        locally_owned_dofs = dof_handler.locally_owned_dofs ();
        locally_relevant_dofs = DoFTools::extract_locally_relevant_dofs (dof_handler);
        
        local_solution.reinit (locally_owned_dofs,
                                locally_relevant_dofs,
                                mpi_communicator);

        // Initialize the contraints objects for BC and Hanging Nodes during refinement
        system_rhs.reinit (locally_owned_dofs, mpi_communicator);
        distributed_solution.reinit (locally_owned_dofs, mpi_communicator);
        
        // Initialize hanging nodes and constrains
        setup_constraints (load_step);  
        
        // Sparsity pattern initialization (where matrix has nonzero entries)
        DynamicSparsityPattern dsp(locally_relevant_dofs);
        DoFTools::make_sparsity_pattern (dof_handler,
                                            dsp, 
                                            constraints, 
                                            false);
        SparsityTools::distribute_sparsity_pattern (dsp,
                                                    dof_handler.locally_owned_dofs (),
                                                    mpi_communicator,
                                                    locally_relevant_dofs);
        
        // Elasticity Matrix
        system_matrix.reinit (locally_owned_dofs,
                                        locally_owned_dofs, 
                                        dsp, 
                                        mpi_communicator);
    }
        

Assembly

This function performs the assembly process of the system matrix and rhs. First it clears the previous system_rhs and system_matrix. It then initializes the FEValues object, which manages shape function evaluations, gradients, quadrature points, and Jacobian-weighted measures for each cell during integration.

The number of degrees of freedom per cell and the number of quadrature points are determined. Local objects, including the element cell_matrix and cell_rhs, are created and sized appropriately. Additionally, a vector local_dof_indices is initialized to map local degrees of freedom to the global system, setting the stage for the element-wise assembly process.

    template<int dim>
    void Elasticity<dim>::assemble()
    {
        TimerOutput::Scope ts (computing_timer, "assembly");

        // Clean the current matrices 
        system_rhs = 0;
        system_matrix = 0;

        // Initialize the FEValues Objects
        FEValues<dim> fe_values (fe, quadrature_formula,
                                update_values | update_gradients |
                                update_quadrature_points | update_JxW_values);

        // Number of DoFs and Gauss points per cell
        const unsigned int dofs_per_cell = fe.n_dofs_per_cell ();
        
        // Local Matrices and Vector
        FullMatrix<double> cell_matrix (dofs_per_cell, dofs_per_cell);
        Vector<double> cell_rhs (dofs_per_cell);

        // Create vector to store the local dof indices
        std::vector<types::global_dof_index> local_dof_indices (dofs_per_cell);
        

The function loops over all locally owned active cells. For each cell, the local stiffness matrix cell_matrix and local right-hand side vector cell_rhs are reset to zero. The finite element information is updated for the current cell using fe_values.reinit(cell), preparing the shape functions and their gradients for integration.

At each Gauss quadrature point, the code computes contributions to the local matrix by assembling the linear elasticity terms based on Lame parameters lambda and mu. It also calculates contributions to the local right-hand side vector, though in this case the body force is zero.

To apply boundary forces, you need to loop over the faces of the cells and check if each face belongs to a boundary. If the face is part of the boundary, you can then add the corresponding contribution to the system. This requires to setup and use a FEFaceValues object in addition to the FEValues already in place, which evaluates the shape functions and gradients at the domain boundaries. By using FEFaceValues, you can compute the necessary values (such as shape functions, gradients, and normal vectors) on the boundary faces, allowing you to correctly apply the boundary forces to the right-hand side vector (system_rhs) of the linear system.

Afterward, the local degrees of freedom are mapped to their global indices, and the local contributions are inserted into the global system matrix and right-hand side using constraints.distribute_local_to_global, which also enforces boundary and hanging node conditions.

Finally, after all cells have been processed, the system matrix and right-hand side vector are compressed to finalize assembly across all MPI processes, ensuring a consistent global system ready for solving.

        // loop over the cells
        for (const auto &cell : dof_handler.active_cell_iterators ())
        {
            if (cell->is_locally_owned ())
            {
                // Reset the local matrix/rhs of the cell
                cell_matrix = 0;
                cell_rhs = 0;

                fe_values.reinit (cell);
            
                // loop over the gauss point of the cell
                for (const unsigned int q_point : fe_values.quadrature_point_indices ())
                {
                    for (const unsigned int i : fe_values.dof_indices ())
                    {
                        const unsigned int component_i = fe.system_to_component_index (i).first;

                        for (const unsigned int j : fe_values.dof_indices ())
                        {
                            const unsigned int component_j = fe.system_to_component_index (j).first;
                            cell_matrix (i, j) +=  ((fe_values.shape_grad (i, q_point)[component_i] 
                                                    * lambda (E, nu) *
                                                    fe_values.shape_grad (j, q_point)[component_j])
                                                    +
                                                    (fe_values.shape_grad (i, q_point)[component_j] 
                                                    * mu (E, nu) *
                                                    fe_values.shape_grad (j, q_point)[component_i])
                                                    +
                                                    ((component_i == component_j) ?
                                                    (fe_values.shape_grad (i, q_point) *  mu (E, nu) 
                                                    * fe_values.shape_grad (j, q_point)) 
                                                                                    : 0)                              
                                                    ) * fe_values.JxW (q_point); //
                        }
                        
                        // Cell rhs set to zero for the current example
                        // Modify this entry to account for body forces
                        cell_rhs (i) += fe_values.shape_value (i, q_point) * 0
                                        * fe_values.JxW (q_point);

                                        
                        // To apply boundary forces you need to loop over the exterior boundaries
                        // to populate the corresponding entries of the rhs
                    
                    }
                }
                cell->get_dof_indices (local_dof_indices);
                constraints.distribute_local_to_global (cell_matrix,
                                                        cell_rhs, 
                                                        local_dof_indices, 
                                                        system_matrix,
                                                        system_rhs);
            }
        }
        system_matrix.compress (VectorOperation::add);
        system_rhs.compress (VectorOperation::add);
    }
        

Solve system

The function begins by measuring the computation time for solving the linear system. A Conjugate Gradient (CG) solver is initialized with a maximum iteration count and a stopping tolerance proportional to the norm of the right-hand side vector. The system is solved using the CG method with a block Jacobi preconditioner built from the system matrix to accelerate convergence.

After solving, the number of iterations taken to converge is printed using the parallel output stream. Finally, the solution vector is adjusted by applying the constraints, ensuring the solution respects boundary conditions and hanging node continuity requirements.

    template<int dim>
    void Elasticity<dim>::solve_linear_problem()
    {
        TimerOutput::Scope ts (computing_timer, "solve_linear_system");
        // CG
        SolverControl solver_control (100000, 1e-12* system_rhs.l2_norm());
        SolverCG<PETScWrappers::MPI::Vector> solver (solver_control);

        PETScWrappers::PreconditionBlockJacobi preconditioner(system_matrix);
        
        solver.solve (system_matrix,
                        distributed_solution,
                        system_rhs,
                        preconditioner);
    
        pcout << "   Solved in " << solver_control.last_step () << " iterations." << std::endl;
    
        constraints.distribute (distributed_solution);
    }
        

Output results

This function handles writing the displacement solution and subdomain information after each load step. Displacement components are labeled appropriately and interpreted as vector fields for visualization. Each processor’s subdomain is also recorded to distinguish parallel ownership of cells.

The data is organized into patches and written into VTU format files, with a PVTU master record created for parallel output. On the root MPI process, a PVD file is updated to index all the time steps, enabling easy loading of the full simulation history in visualization tools like ParaView.

    template <int dim>
    void Elasticity<dim>::output_results (const unsigned int load_step) const
    {
        std::vector<std::string> displacement_names (dim, "displacement");
        std::vector<DataComponentInterpretation::DataComponentInterpretation> 
            displacement_component_interpretation (dim, 
                        DataComponentInterpretation::component_is_part_of_vector);

        DataOut<dim> data_out_phasefield;
        data_out_phasefield.add_data_vector (dof_handler,
                                            local_solution,
                                            displacement_names,
                                            displacement_component_interpretation);

        Vector<double> subdomain (triangulation.n_active_cells ());
        for (unsigned int i = 0; i < subdomain.size (); ++i)
        {
            subdomain (i) = triangulation.locally_owned_subdomain ();
        }

        data_out_phasefield.add_data_vector (subdomain, "subdomain");
        data_out_phasefield.build_patches ();
        const std::string pvtu_filename = data_out_phasefield.write_vtu_with_pvtu_record ("./", 
                                                                                    "solution",
                                                                    load_step, mpi_communicator, 
                                                                                                2, 
                                                                                                0);

        if (this_mpi_process == 0)
        {
            static std::vector<std::pair<double, std::string>> times_and_names;
            times_and_names.push_back(std::pair<double, std::string>(load_step, pvtu_filename));
            std::ofstream pvd_output("./solution_u.pvd");
            DataOutBase::write_pvd_record(pvd_output, times_and_names);
        }
    }
        

Output stresses

This function calculates the stress tensor at the quadrature points by post-processing the displacement solution. The stress components are averaged per cell and stored in vectors. Separate stress components such as s_xx, s_xy, and s_yy (and s_zz, s_xz, s_yz in 3D) are created and attached to the mesh for visualization.

Each stress component is exported into VTU files using DataOut. A parallel VTU (PVTU) record is generated for distributed visualization, and a PVD file is updated on the root process to maintain the simulation timeline.

    template <int dim>
    void Elasticity<dim>::output_stress(const unsigned int load_step)
    {
        // Vettori per cella in Vector<float>
        Vector<float> s_xx(triangulation.n_active_cells());
        Vector<float> s_xy(triangulation.n_active_cells());
        Vector<float> s_yy(triangulation.n_active_cells());
        Vector<float> s_zz(triangulation.n_active_cells());
        Vector<float> s_xz(triangulation.n_active_cells());
        Vector<float> s_yz(triangulation.n_active_cells());
    
        FEValues<dim> fe_values(fe, quadrature_formula,
                                update_gradients | update_JxW_values);
    
        const unsigned int n_q_points = quadrature_formula.size();
        const FEValuesExtractors::Vector displacements(0);
        std::vector<SymmetricTensor<2, dim>> strain_tensor(n_q_points);
    
        for (const auto &cell : dof_handler.active_cell_iterators())
        {
            if (cell->is_locally_owned())
            {
                fe_values.reinit(cell);
                fe_values[displacements].get_function_symmetric_gradients(local_solution,
                                                                            strain_tensor);
    
                double c_s_xx = 0, c_s_xy = 0, c_s_yy = 0;
                double c_s_zz = 0, c_s_xz = 0, c_s_yz = 0;
    
                for (unsigned int q = 0; q < n_q_points; ++q)
                {
                    const auto &eps_u = strain_tensor[q];
                    const double tr = trace(eps_u);
                    const auto stress = 2.0 * mu(E, nu) * eps_u + lambda(E, nu)
                                        * tr * unit_symmetric_tensor<dim>();
    
                    c_s_xx += stress[0][0];
                    c_s_xy += stress[0][1];
                    c_s_yy += stress[1][1];
    
                    if constexpr (dim == 3)
                    {
                        c_s_zz += stress[2][2];
                        c_s_xz += stress[0][2];
                        c_s_yz += stress[1][2];
                    }
                }
    
                const unsigned int cell_index = cell->active_cell_index();
                s_xx[cell_index] = c_s_xx / n_q_points;
                s_xy[cell_index] = c_s_xy / n_q_points;
                s_yy[cell_index] = c_s_yy / n_q_points;
    
                if constexpr (dim == 3)
                {
                    s_zz[cell_index] = c_s_zz / n_q_points;
                    s_xz[cell_index] = c_s_xz / n_q_points;
                    s_yz[cell_index] = c_s_yz / n_q_points;
                }
            }
        }
    
        DataOut<dim> data_out;
        data_out.attach_triangulation(triangulation);
    
        data_out.add_data_vector(s_xx, "s_xx", DataOut<dim>::type_cell_data);
        data_out.add_data_vector(s_xy, "s_xy", DataOut<dim>::type_cell_data);
        data_out.add_data_vector(s_yy, "s_yy", DataOut<dim>::type_cell_data);
    
        if constexpr (dim == 3)
        {
            data_out.add_data_vector(s_zz, "s_zz", DataOut<dim>::type_cell_data);
            data_out.add_data_vector(s_xz, "s_xz", DataOut<dim>::type_cell_data);
            data_out.add_data_vector(s_yz, "s_yz", DataOut<dim>::type_cell_data);
        }
    
        data_out.build_patches();
    
        const std::string pvtu_filename = data_out.write_vtu_with_pvtu_record(
            "./", "stresses", load_step, mpi_communicator, 4);
    
        if (this_mpi_process == 0)
        {
            static std::vector<std::pair<double, std::string>> times_and_names;
            times_and_names.emplace_back(load_step, pvtu_filename);
    
            std::ofstream pvd_output("./stresses.pvd");
            DataOutBase::write_pvd_record(pvd_output, times_and_names);
        }
    }
        

Compute reactions

This function computes the total reaction force on a specified boundary by integrating the stress tensor over the boundary faces. The stress is derived from the strain, which is computed from the displacement solution at quadrature points on the faces.

Only faces matching the given boundary_id are considered. Local contributions are accumulated per MPI rank and summed globally. The resulting reaction vector components are printed using parallel output utilities.

    // Reaction
    template <int dim>
    void Elasticity<dim>::reaction(Tensor<1, dim> &reaction_stress,
                                        const types::boundary_id &boundary_id)
    {
        // Face quadrature formula
        QGauss<dim - 1> face_quadrature_formula(5);

        // Set up FE face values for vector-valued and scalar-valued solution
        FEFaceValues<dim> fe_face_values(fe, face_quadrature_formula,
                                        UpdateFlags(update_values |
                                                    update_gradients |
                                                    update_quadrature_points |
                                                    update_normal_vectors |
                                                    update_JxW_values));

        const unsigned int n_face_q_points = face_quadrature_formula.size();

        // Extract displacements for gradient calculation
        const FEValuesExtractors::Vector displacements(0);
        std::vector<SymmetricTensor<2, dim>> strain_tensor(n_face_q_points);

        // Parallel DoFHandler iterator
        typename DoFHandler<dim>::active_cell_iterator cell = dof_handler.begin_active(),
                                                        endc = dof_handler.end();

        Tensor<1, dim> reaction_mpi;

        // Loop over all cells in the triangulation
        for (; cell != endc; ++cell)
        {
            if (cell->is_locally_owned())
            {
                // Loop over all faces of the current cell
                for (unsigned int face = 0; face < GeometryInfo<dim>::faces_per_cell; ++face)
                {
                    // Check if the current face belongs to the specified boundary
                    if (cell->face(face)->boundary_id() == boundary_id)
                    {
                        // Reinitialize FE face values for the current face
                        fe_face_values.reinit(cell, face);
                        fe_face_values[displacements].get_function_symmetric_gradients(local_solution, strain_tensor);

                        // Loop over quadrature points on the face
                        for (unsigned int q_point = 0; q_point < n_face_q_points; ++q_point)
                        {
                            const auto &eps_u = strain_tensor[q_point];
                            const Tensor<1, dim> &N = fe_face_values.normal_vector(q_point);
                            const double JxW_f = fe_face_values.JxW(q_point);

                            // Compute the stress tensor using the strain
                            const double tr = trace(eps_u);
                            const auto stress = 2.0 * mu(E, nu) * eps_u + lambda(E, nu)
                                                    * tr * unit_symmetric_tensor<dim>();

                            // Accumulate the contribution to the reaction stress
                            reaction_mpi += stress * N * JxW_f;
                        }
                    }
                }
            }
        }

        // Sum the reaction stresses from all MPI ranks and print it
        reaction_stress = Utilities::MPI::sum(reaction_mpi, mpi_communicator);
        for (unsigned int i = 0; i < dim; ++i)
        {
            pcout << "   R_" << (char)('x' + i) << ": " << reaction_stress[i] << std::endl;
        }
    }
        

Run

This is the main driver function. It starts by creating the mesh and then iterates over each load step. For each step, it sets up the system, assembles the linear system, solves it, updates the solution, refines the mesh, and outputs displacement, stress, and reaction results. Performance information is printed at each load step, and the overall runtime is recorded at the end.

    template <int dim>
    void Elasticity<dim>::run()
    {
        Timer timer;
        timer.start ();
        pcout << "Running on " << Utilities::MPI::n_mpi_processes (mpi_communicator) 
                << " MPI rank(s)..." << std::endl;

        create_mesh();

        // Loop over load steps
        for (unsigned int load_step = 0; load_step <= num_load_steps; load_step++)
        {
            pcout << " --- Load increment number : " << load_step << std::endl;

            setup (load_step);
            assemble ();
            solve_linear_problem ();
            
            // Update ghost values after the solution
            local_solution = distributed_solution;
            local_solution.update_ghost_values();
            
            // Prepare for refinement
            distributed_solution_old = local_solution;
            
            // Refine
            refine_mesh(load_step);

            pcout << "  Total New Number of globally active cells:       "  
                    << triangulation.n_global_active_cells () << std::endl
                    << "   New Number of degrees of freedom for elasticity: " 
                    << dof_handler.n_dofs () << std::endl;
            
            // Save results
            output_results (load_step);
            output_stress (load_step);
            reaction(reaction_stress, 1);

            // Print the table of cpmputing times
            computing_timer.print_summary ();
            computing_timer.reset ();
            pcout << std::endl;
        }

        timer.stop ();
        pcout << "Total run time: " << timer.wall_time () << " seconds." << std::endl;
    }
} // End of namespace Elasticity     
        

Main

The entry point of the program. It initializes the MPI environment and creates an instance of the Elasticity<dim> class (with <dim> to be set equal to 2 for the 2D problem or 3 for 3D one). It then calls the run() method to execute the simulation. Exceptions are caught and printed clearly to standard error to ensure that any runtime issues are reported properly before the program aborts.

int main (int argc, char *argv[])
{
    try
    {
        using namespace dealii;
        using namespace elas;

        Utilities::MPI::MPI_InitFinalize mpi_initialization (argc, argv, 1);

        Elasticity<2> elasticity;  // For the 2D problem
        // Elasticity<3> elasticity;  // For the 3D problem
        elasticity.run ();
    }
    catch (std::exception &exc)
    {
        std::cerr << std::endl << std::endl
        << "----------------------------------------------------" << std::endl;
        std::cerr << "Exception on processing: " << std::endl << exc.what ()
        << std::endl << "Aborting!" << std::endl
        << "----------------------------------------------------" << std::endl;

        return 1;
    }
    catch (...)
    {
        std::cerr << std::endl << std::endl
        << "----------------------------------------------------" << std::endl;
        std::cerr << "Unknown exception!" << std::endl << "Aborting!" << std::endl
        << "----------------------------------------------------" << std::endl;
        return 1;
    }

    return 0;
}
        

Results

Sezione dei risultati in cui andare a mettere:

Complete code

#include <deal.II/base/quadrature_lib.h>
#include <deal.II/base/function.h>
#include <deal.II/base/timer.h>
#include <deal.II/base/utilities.h>
#include <deal.II/base/conditional_ostream.h>
#include <deal.II/base/index_set.h>
#include <deal.II/base/quadrature_point_data.h>
#include <deal.II/base/tensor_function.h>
#include <deal.II/grid/grid_generator.h>
#include <deal.II/grid/tria_accessor.h>
#include <deal.II/grid/grid_refinement.h>
#include <deal.II/dofs/dof_handler.h>
#include <deal.II/dofs/dof_tools.h>
#include <deal.II/dofs/dof_accessor.h>
#include <deal.II/lac/generic_linear_algebra.h>
#include <deal.II/lac/vector.h>
#include <deal.II/lac/full_matrix.h>
#include <deal.II/lac/precondition.h>
#include <deal.II/lac/petsc_precondition.h>
#include <deal.II/lac/solver_cg.h>
#include <deal.II/lac/affine_constraints.h>
#include <deal.II/lac/sparsity_tools.h>
#include <deal.II/lac/dynamic_sparsity_pattern.h>
#include <deal.II/fe/fe_values.h>
#include <deal.II/fe/fe_system.h>
#include <deal.II/fe/fe_q.h>
#include <deal.II/numerics/vector_tools.h>
#include <deal.II/numerics/matrix_tools.h>
#include <deal.II/numerics/data_out.h>
#include <deal.II/numerics/error_estimator.h>
#include <deal.II/distributed/tria.h>
#include <deal.II/distributed/grid_refinement.h>
#include <deal.II/distributed/solution_transfer.h>
#include <fstream>
#include <iostream>
#include <random>

namespace elas
{
    using namespace dealii;

    template <int dim>
    class Elasticity
    {
        public:
            Elasticity();
            void run();

        private:
            // Mesh generation and refinement
            void create_mesh();
            void refine_mesh(const unsigned int load_step);

            // Elasticity problem functions
            void setup_constraints (const unsigned int load_step); 
            void setup(const unsigned int load_step);
            void assemble();
            void solve_linear_problem();

            // Save the results - Disp, stress, reaction
            void output_results(const unsigned int load_step) const;
            void output_stress(const unsigned int load_step);
            void reaction(Tensor<1, dim> &reaction_stress, const types::boundary_id &boundary_id);
            
            MPI_Comm mpi_communicator;
            const unsigned int n_mpi_processes;
            const unsigned int this_mpi_process;

            parallel::distributed::Triangulation<dim> triangulation;
            
            // Elasticity objects
            const FESystem<dim> fe;                         // FE System
            DoFHandler<dim> dof_handler;                    // DoF Handler

            IndexSet locally_owned_dofs;                        // IndexSet - Locally Owned
            IndexSet locally_relevant_dofs;                     // IndexSet - Locally relevant 
            AffineConstraints<double> constraints;              // Affine Constraint  

            PETScWrappers::MPI::SparseMatrix system_matrix;     // Elastcitiy Matrix
            PETScWrappers::MPI::Vector system_rhs;              // RHS 

            PETScWrappers::MPI::Vector local_solution;          // MPI Split solution
            PETScWrappers::MPI::Vector distributed_solution;    // Full Solution

            PETScWrappers::MPI::Vector distributed_solution_old;   // This is for the refinement

            const QGauss<dim> quadrature_formula;           // Quadrature formula

            Tensor<1, dim> reaction_stress;

            // Domain dimensions
            double L = 1;
            double H = 0.05;
            double W = 0.05;

            const unsigned int nx = 25;
            const unsigned int ny = 2;
            const unsigned int nz = 2;
            // Material properties
            double E = 100;
            double nu = 0.3;      

            double load_increment = 0.01;     // Load increment applied
            double applied_load = 0;          // Current applied_load += load_increment
            double num_load_steps = 5;        // Total load increment  
            
            ConditionalOStream pcout;
            TimerOutput computing_timer;       
    }; // End of Elast class

    // Constructor
    template <int dim>
    Elasticity<dim>::Elasticity()
        : mpi_communicator(MPI_COMM_WORLD)
        , n_mpi_processes(Utilities::MPI::n_mpi_processes(mpi_communicator))
        , this_mpi_process(Utilities::MPI::this_mpi_process(mpi_communicator))
        , triangulation (mpi_communicator)
        , fe (FE_Q<dim> (1), dim)
        , dof_handler (triangulation)
        , quadrature_formula (fe.degree + 1)
        , pcout (std::cout, this_mpi_process == 0)
        , computing_timer (mpi_communicator, pcout, TimerOutput::never, TimerOutput::wall_times)
    {}

    // Evaluation of the elastic constant
    inline double lambda (const float E, const float nu)
    {
        return (E * nu) / ((1 + nu) * (1 - 2 * nu));
    }

    inline double mu (const float E, const float nu)
    {
        return E / (2 * (1 + nu));
    }

    // -----------------------------
    // ----------- MESH ------------    
    // -----------------------------
    template <int dim>
    void Elasticity<dim>::create_mesh()
    {
        // Define the initial coarse subdivision
        const std::vector<unsigned int> repetitions = {nx,ny,nz};
        
        // Create Mesh
        Point<dim> p1, p2;
        if constexpr (dim == 2)
        {
            p1 = Point<dim>(0, 0);
            p2 = Point<dim>(L, H);
        }
        else if constexpr (dim == 3)
        {
            p1 = Point<dim>(0, 0, 0);
            p2 = Point<dim>(L, H, W);
        }

        // create coarse mesh
        GridGenerator::subdivided_hyper_rectangle (triangulation, repetitions, p1, p2); 
        
        // Set Fce IDs
        for (const auto &cell : triangulation.active_cell_iterators ())
        {
            for (const auto &face : cell->face_iterators ())
            {
                if (face->at_boundary ())
                {
                    const auto center = face->center ();
                    if (std::fabs (center(0) - 0) < 1e-12)
                        face->set_boundary_id (0);

                    else if (std::fabs (center(0) - L) < 1e-12)
                        face->set_boundary_id (1);

                    else if (std::fabs (center(1) - 0) < 1e-12)
                        face->set_boundary_id (2);

                    else if (std::fabs (center(1) - H) < 1e-12)
                        face->set_boundary_id (3);

                    // Check if we are in 3D before accessing center(2)
                    if constexpr (dim == 3)
                    {
                        if (std::fabs(center(2) - 0) < 1e-12)
                            face->set_boundary_id(4);
                        else if (std::fabs(center(2) - W) < 1e-12)
                            face->set_boundary_id(5);
                    }
                }
            }
        }

        // Initialize dof handler and extact Owned/Relevant DoFs
        dof_handler.distribute_dofs (fe);
        
        locally_owned_dofs = dof_handler.locally_owned_dofs ();
        locally_relevant_dofs = DoFTools::extract_locally_relevant_dofs (dof_handler);
        
        // Initialize the old solution for refinement purposes
        distributed_solution_old.reinit (locally_owned_dofs, mpi_communicator);
        
        pcout << "No. of levels in triangulation: " 
              << triangulation.n_global_levels () << std::endl;
        
        pcout << "   Number of locally owned cells on the process:       " 
              << triangulation.n_locally_owned_active_cells () << std::endl;
        
        pcout << "Number of global cells:" << triangulation.n_global_active_cells () << std::endl;
    
        pcout << "  Total Number of globally active cells:       "  
              << triangulation.n_global_active_cells () << std::endl
              << "   Number of degrees of freedom for elasticity: " 
              << dof_handler.n_dofs () << std::endl;
    }

    template<int dim>
    void Elasticity<dim>::refine_mesh(const unsigned int load_step)
    {
        Vector<float> estimated_error_per_cell (triangulation.n_locally_owned_active_cells ());
        KellyErrorEstimator<dim>::estimate (dof_handler,
                                            QGauss<dim-1> (fe.degree + 1),
                                            std::map<types::boundary_id, const Function<dim> *>(),
                                            local_solution, estimated_error_per_cell);

        // Initialize SolutionTransfer object
        parallel::distributed::SolutionTransfer<dim, PETScWrappers::MPI::Vector>
            soltrans (dof_handler);
        
        parallel::distributed::GridRefinement::refine_and_coarsen_fixed_fraction (
            triangulation, estimated_error_per_cell, 0.30, 0.01);

        if (triangulation.n_global_levels () >= 4)
        {
            for (const auto &cell : triangulation.active_cell_iterators_on_level (3))
            if (cell->is_locally_owned ())
                cell->clear_refine_flag ();
        }
        
        // Prepare the triangulation
        triangulation.prepare_coarsening_and_refinement ();

        // Prepare the SolutionTransfer object for coarsening and refinement
        // and give the solution vector that we intend to interpolate later,
        soltrans.prepare_for_coarsening_and_refinement (local_solution);

        // Do the refinement
        triangulation.execute_coarsening_and_refinement ();
        
        // redistribute dofs,
        dof_handler.distribute_dofs (fe);

        // Recreate locally_owned_dofs and locally_relevant_dofs index sets - ELASTICITY
        locally_owned_dofs = dof_handler.locally_owned_dofs ();
        locally_relevant_dofs = DoFTools::extract_locally_relevant_dofs (dof_handler);

        distributed_solution_old.reinit (locally_owned_dofs, mpi_communicator);
        soltrans.interpolate (distributed_solution_old);

        // Apply constraints on the interpolated solution
        setup_constraints(load_step);
        constraints.distribute (distributed_solution_old);

        // Copy distributed_solution_old to local_solution
        local_solution.reinit(locally_owned_dofs,
                                locally_relevant_dofs,
                                mpi_communicator);
        local_solution = distributed_solution_old;
    }

    // -----------------------------
    // -------- ELASTICITY ---------
    // -----------------------------
    template<int dim>
    void Elasticity<dim>::setup_constraints (const unsigned int load_step)
    {
        constraints.clear ();
        constraints.reinit (locally_relevant_dofs);

        DoFTools::make_hanging_node_constraints (dof_handler, constraints);

        const FEValuesExtractors::Scalar u_x (0);
        const FEValuesExtractors::Scalar u_y (1);

        const ComponentMask u_x_mask = fe.component_mask (u_x);
        const ComponentMask u_y_mask = fe.component_mask (u_y);

        applied_load = load_increment*load_step;
        const double u_x_values_right = 0.5*applied_load;
        const double u_x_values_left = -0.5*applied_load;

        const double u_fix = 0.0;

        VectorTools::interpolate_boundary_values (dof_handler, 0,
                                Functions::ConstantFunction<dim> (u_x_values_left, dim),
                                constraints, 
                                u_x_mask);

        VectorTools::interpolate_boundary_values (dof_handler, 1,
                                Functions::ConstantFunction<dim> (u_x_values_right, dim), 
                                constraints,
                                u_x_mask);

        VectorTools::interpolate_boundary_values (dof_handler, 2,
                                Functions::ConstantFunction<dim> (u_fix, dim), 
                                constraints,
                                u_y_mask);

        if constexpr (dim == 3)
        {
            const FEValuesExtractors::Scalar u_z (2);
            const ComponentMask u_z_mask = fe.component_mask (u_z);

            VectorTools::interpolate_boundary_values (dof_handler, 4,
                                Functions::ConstantFunction<dim> (u_fix, dim),
                                constraints,
                                u_z_mask);
        }

        constraints.close ();
    }

    template<int dim>
    void Elasticity<dim>::setup(const unsigned int load_step)
    {
        TimerOutput::Scope ts(computing_timer, "setup_system");
        
        // Initialize the dofs contained locally as well as those of the near cells
        locally_owned_dofs = dof_handler.locally_owned_dofs ();
        locally_relevant_dofs = DoFTools::extract_locally_relevant_dofs (dof_handler);
        
        local_solution.reinit (locally_owned_dofs,
                                locally_relevant_dofs,
                                mpi_communicator);

        // Initialize the contraints objects for BC and Hanging Nodes during refinement
        system_rhs.reinit (locally_owned_dofs, mpi_communicator);
        distributed_solution.reinit (locally_owned_dofs, mpi_communicator);
        
        // Initialize hanging nodes and constrains
        setup_constraints (load_step);  
        
        // Sparsity pattern initialization (where matrix has nonzero entries)
        DynamicSparsityPattern dsp(locally_relevant_dofs);
        DoFTools::make_sparsity_pattern (dof_handler,
                                            dsp, 
                                            constraints, 
                                            false);
        SparsityTools::distribute_sparsity_pattern (dsp,
                                                    dof_handler.locally_owned_dofs (),
                                                    mpi_communicator,
                                                    locally_relevant_dofs);
        
        // Elasticity Matrix
        system_matrix.reinit (locally_owned_dofs,
                                        locally_owned_dofs, 
                                        dsp, 
                                        mpi_communicator);
    }

    template<int dim>
    void Elasticity<dim>::assemble()
    {
        TimerOutput::Scope ts (computing_timer, "assembly");

        // Clean the current matrices 
        system_rhs = 0;
        system_matrix = 0;

        // Initialize the FEValues Objects
        FEValues<dim> fe_values (fe, quadrature_formula,
                                update_values | update_gradients |
                                update_quadrature_points | update_JxW_values);

        // Number of DoFs and Gauss points per cell
        const unsigned int dofs_per_cell = fe.n_dofs_per_cell ();
        
        // Local Matrices and Vector
        FullMatrix<double> cell_matrix (dofs_per_cell, dofs_per_cell);
        Vector<double> cell_rhs (dofs_per_cell);

        // Create vector to store the local dof indices
        std::vector<types::global_dof_index> local_dof_indices (dofs_per_cell);

        // loop over the cells
        for (const auto &cell : dof_handler.active_cell_iterators ())
        {
            if (cell->is_locally_owned ())
            {
                // Reset the local matrix/rhs of the cell
                cell_matrix = 0;
                cell_rhs = 0;

                fe_values.reinit (cell);
            
                // loop over the gauss point of the cell
                for (const unsigned int q_point : fe_values.quadrature_point_indices ())
                {
                    for (const unsigned int i : fe_values.dof_indices ())
                    {
                        const unsigned int component_i = fe.system_to_component_index (i).first;

                        for (const unsigned int j : fe_values.dof_indices ())
                        {
                            const unsigned int component_j = fe.system_to_component_index (j).first;
                            cell_matrix (i, j) +=  ((fe_values.shape_grad (i, q_point)[component_i] 
                                                    * lambda (E, nu) *
                                                    fe_values.shape_grad (j, q_point)[component_j])
                                                    +
                                                    (fe_values.shape_grad (i, q_point)[component_j] 
                                                    * mu (E, nu) *
                                                    fe_values.shape_grad (j, q_point)[component_i])
                                                    +
                                                    ((component_i == component_j) ?
                                                    (fe_values.shape_grad (i, q_point) 
                                                    *  mu (E, nu) * 
                                                    fe_values.shape_grad (j, q_point)) 
                                                    : 0)                              
                                                    ) * fe_values.JxW (q_point); //
                        }
                        
                        // Cell rhs set to zero for the current example
                        // Modify this entry to account for body forces
                        cell_rhs (i) += fe_values.shape_value (i, q_point) * 0
                                        * fe_values.JxW (q_point);

                                        
                        // To apply boundary forces you need to loop over the exterior boundaries
                        // to populate the corresponding entries of the rhs
                    
                    }
                }
                cell->get_dof_indices (local_dof_indices);
                constraints.distribute_local_to_global (cell_matrix,
                                                        cell_rhs, 
                                                        local_dof_indices, 
                                                        system_matrix,
                                                        system_rhs);
            }
        }
        system_matrix.compress (VectorOperation::add);
        system_rhs.compress (VectorOperation::add);
    }

    template<int dim>
    void Elasticity<dim>::solve_linear_problem()
    {
        TimerOutput::Scope ts (computing_timer, "solve_linear_system");
        // CG
        SolverControl solver_control (100000, 1e-12* system_rhs.l2_norm());
        SolverCG<PETScWrappers::MPI::Vector> solver (solver_control);

        PETScWrappers::PreconditionBlockJacobi preconditioner(system_matrix);
        
        solver.solve (system_matrix,
                        distributed_solution,
                        system_rhs,
                        preconditioner);
    
        pcout << "   Solved in " << solver_control.last_step () << " iterations." << std::endl;
    
        constraints.distribute (distributed_solution);
    }

    // -----------------------------
    // ---------- OUTPUT -----------
    // -----------------------------
    // Solution
    template <int dim>
    void Elasticity<dim>::output_results (const unsigned int load_step) const
    {
        std::vector<std::string> displacement_names (dim, "displacement");
        std::vector<DataComponentInterpretation::DataComponentInterpretation> 
            displacement_component_interpretation (dim, 
                        DataComponentInterpretation::component_is_part_of_vector);

        DataOut<dim> data_out_phasefield;
        data_out_phasefield.add_data_vector (dof_handler,
                                            local_solution,
                                            displacement_names,
                                            displacement_component_interpretation);

        Vector<double> subdomain (triangulation.n_active_cells ());
        for (unsigned int i = 0; i < subdomain.size (); ++i)
        {
            subdomain (i) = triangulation.locally_owned_subdomain ();
        }

        data_out_phasefield.add_data_vector (subdomain, "subdomain");
        data_out_phasefield.build_patches ();
        const std::string pvtu_filename = data_out_phasefield.write_vtu_with_pvtu_record ("./", 
                                                                                    "solution",
                                                                   load_step, mpi_communicator, 
                                                                                             2, 
                                                                                             0);

        if (this_mpi_process == 0)
        {
            static std::vector<std::pair<double, std::string>> times_and_names;
            times_and_names.push_back(std::pair<double, std::string>(load_step, pvtu_filename));
            std::ofstream pvd_output("./solution_u.pvd");
            DataOutBase::write_pvd_record(pvd_output, times_and_names);
        }
    }

    // Stresses
    template <int dim>
    void Elasticity<dim>::output_stress(const unsigned int load_step)
    {
        // Vettori per cella in Vector<float>
        Vector<float> s_xx(triangulation.n_active_cells());
        Vector<float> s_xy(triangulation.n_active_cells());
        Vector<float> s_yy(triangulation.n_active_cells());
        Vector<float> s_zz(triangulation.n_active_cells());
        Vector<float> s_xz(triangulation.n_active_cells());
        Vector<float> s_yz(triangulation.n_active_cells());
    
        FEValues<dim> fe_values(fe, quadrature_formula,
                                update_gradients | update_JxW_values);
    
        const unsigned int n_q_points = quadrature_formula.size();
        const FEValuesExtractors::Vector displacements(0);
        std::vector<SymmetricTensor<2, dim>> strain_tensor(n_q_points);
    
        for (const auto &cell : dof_handler.active_cell_iterators())
        {
            if (cell->is_locally_owned())
            {
                fe_values.reinit(cell);
                fe_values[displacements].get_function_symmetric_gradients(local_solution,
                                                                          strain_tensor);
    
                double c_s_xx = 0, c_s_xy = 0, c_s_yy = 0;
                double c_s_zz = 0, c_s_xz = 0, c_s_yz = 0;
    
                for (unsigned int q = 0; q < n_q_points; ++q)
                {
                    const auto &eps_u = strain_tensor[q];
                    const double tr = trace(eps_u);
                    const auto stress = 2.0 * mu(E, nu) * eps_u + lambda(E, nu)
                                        * tr * unit_symmetric_tensor<dim>();
    
                    c_s_xx += stress[0][0];
                    c_s_xy += stress[0][1];
                    c_s_yy += stress[1][1];
    
                    if constexpr (dim == 3)
                    {
                        c_s_zz += stress[2][2];
                        c_s_xz += stress[0][2];
                        c_s_yz += stress[1][2];
                    }
                }
    
                const unsigned int cell_index = cell->active_cell_index();
                s_xx[cell_index] = c_s_xx / n_q_points;
                s_xy[cell_index] = c_s_xy / n_q_points;
                s_yy[cell_index] = c_s_yy / n_q_points;
    
                if constexpr (dim == 3)
                {
                    s_zz[cell_index] = c_s_zz / n_q_points;
                    s_xz[cell_index] = c_s_xz / n_q_points;
                    s_yz[cell_index] = c_s_yz / n_q_points;
                }
            }
        }
    
        DataOut<dim> data_out;
        data_out.attach_triangulation(triangulation);
    
        data_out.add_data_vector(s_xx, "s_xx", DataOut<dim>::type_cell_data);
        data_out.add_data_vector(s_xy, "s_xy", DataOut<dim>::type_cell_data);
        data_out.add_data_vector(s_yy, "s_yy", DataOut<dim>::type_cell_data);
    
        if constexpr (dim == 3)
        {
            data_out.add_data_vector(s_zz, "s_zz", DataOut<dim>::type_cell_data);
            data_out.add_data_vector(s_xz, "s_xz", DataOut<dim>::type_cell_data);
            data_out.add_data_vector(s_yz, "s_yz", DataOut<dim>::type_cell_data);
        }
    
        data_out.build_patches();
    
        const std::string pvtu_filename = data_out.write_vtu_with_pvtu_record(
            "./", "stresses", load_step, mpi_communicator, 4);
    
        if (this_mpi_process == 0)
        {
            static std::vector<std::pair<double, std::string>> times_and_names;
            times_and_names.emplace_back(load_step, pvtu_filename);
    
            std::ofstream pvd_output("./stresses.pvd");
            DataOutBase::write_pvd_record(pvd_output, times_and_names);
        }
    }
    
    // Reaction
    template <int dim>
    void Elasticity<dim>::reaction(Tensor<1, dim> &reaction_stress,
                                       const types::boundary_id &boundary_id)
    {
        // Face quadrature formula
        QGauss<dim - 1> face_quadrature_formula(5);

        // Set up FE face values for vector-valued and scalar-valued solution
        FEFaceValues<dim> fe_face_values(fe, face_quadrature_formula,
                                        UpdateFlags(update_values |
                                                    update_gradients |
                                                    update_quadrature_points |
                                                    update_normal_vectors |
                                                    update_JxW_values));

        const unsigned int n_face_q_points = face_quadrature_formula.size();

        // Extract displacements for gradient calculation
        const FEValuesExtractors::Vector displacements(0);
        std::vector<SymmetricTensor<2, dim>> strain_tensor(n_face_q_points);

        // Parallel DoFHandler iterator
        typename DoFHandler<dim>::active_cell_iterator cell = dof_handler.begin_active(),
                                                        endc = dof_handler.end();

        Tensor<1, dim> reaction_mpi;

        // Loop over all cells in the triangulation
        for (; cell != endc; ++cell)
        {
            if (cell->is_locally_owned())
            {
                // Loop over all faces of the current cell
                for (unsigned int face = 0; face < GeometryInfo<dim>::faces_per_cell; ++face)
                {
                    // Check if the current face belongs to the specified boundary
                    if (cell->face(face)->boundary_id() == boundary_id)
                    {
                        // Reinitialize FE face values for the current face
                        fe_face_values.reinit(cell, face);
                        fe_face_values[displacements].get_function_symmetric_gradients(local_solution, strain_tensor);

                        // Loop over quadrature points on the face
                        for (unsigned int q_point = 0; q_point < n_face_q_points; ++q_point)
                        {
                            const auto &eps_u = strain_tensor[q_point];
                            const Tensor<1, dim> &N = fe_face_values.normal_vector(q_point);
                            const double JxW_f = fe_face_values.JxW(q_point);

                            // Compute the stress tensor using the strain
                            const double tr = trace(eps_u);
                            const auto stress = 2.0 * mu(E, nu) * eps_u + lambda(E, nu)
                                                 * tr * unit_symmetric_tensor<dim>();

                            // Accumulate the contribution to the reaction stress
                            reaction_mpi += stress * N * JxW_f;
                        }
                    }
                }
            }
        }

        // Sum the reaction stresses from all MPI ranks and print it
        reaction_stress = Utilities::MPI::sum(reaction_mpi, mpi_communicator);
        for (unsigned int i = 0; i < dim; ++i)
        {
            pcout << "   R_" << (char)('x' + i) << ": " << reaction_stress[i] << std::endl;
        }
    }

    // -----------------------------
    // ----------- RUN -------------
    // -----------------------------
    template <int dim>
    void Elasticity<dim>::run()
    {
        Timer timer;
        timer.start ();
        pcout << "Running on " << Utilities::MPI::n_mpi_processes (mpi_communicator) 
              << " MPI rank(s)..." << std::endl;

        create_mesh();

        // Loop over load steps
        for (unsigned int load_step = 0; load_step <= num_load_steps; load_step++)
        {
            pcout << " --- Load increment number : " << load_step << std::endl;

            setup (load_step);
            assemble ();
            solve_linear_problem ();
            
            // Update ghost values after the solution
            local_solution = distributed_solution;
            local_solution.update_ghost_values();
            
            // Prepare for refinement
            distributed_solution_old = local_solution;
            
            // Refine
            refine_mesh(load_step);

            pcout << "  Total New Number of globally active cells:       "  
                  << triangulation.n_global_active_cells () << std::endl
                  << "   New Number of degrees of freedom for elasticity: " 
                  << dof_handler.n_dofs () << std::endl;
            
            // Save results
            output_results (load_step);
            output_stress (load_step);
            reaction(reaction_stress, 1);

            // Print the table of cpmputing times
            computing_timer.print_summary ();
            computing_timer.reset ();
            pcout << std::endl;
        }

        timer.stop ();
        pcout << "Total run time: " << timer.wall_time () << " seconds." << std::endl;
    }
} // End of namespace Elasticity

// -----------------------------
// ---------- MAIIN ------------
// -----------------------------
int main (int argc, char *argv[])
{
    try
    {
        using namespace dealii;
        using namespace elas;

        Utilities::MPI::MPI_InitFinalize mpi_initialization (argc, argv, 1);

        Elasticity<3> elasticity;
        elasticity.run ();
    }
    catch (std::exception &exc)
    {
        std::cerr << std::endl << std::endl
        << "----------------------------------------------------" << std::endl;
        std::cerr << "Exception on processing: " << std::endl << exc.what ()
        << std::endl << "Aborting!" << std::endl
        << "----------------------------------------------------" << std::endl;

        return 1;
    }
    catch (...)
    {
        std::cerr << std::endl << std::endl
        << "----------------------------------------------------" << std::endl;
        std::cerr << "Unknown exception!" << std::endl << "Aborting!" << std::endl
        << "----------------------------------------------------" << std::endl;
        return 1;
    }

    return 0;
}
        
Back