Adaptive Refinement with deal.II + Ginkgo#

Solves the Poisson equation $-\Delta u = 1$ on a disk with adaptive mesh refinement (AMR). After each refinement cycle, the mesh changes, the system is re-assembled, and a new Ginkgo solver is generated from the updated matrix.

Based on deal.II tutorial step-6.

Integration Overview#

The key difference from the basic Poisson example is that AMR changes the matrix dimensions and sparsity pattern every cycle. The Ginkgo solver (and its preconditioner) must be re-created from scratch after each refinement. This example shows the pattern for solver re-generation.

Step-by-Step#

CSR Extraction Helper#

A reusable function that extracts CSR arrays from a deal.II SparseMatrix:

// Extracts CSR arrays from a deal.II SparseMatrix for Ginkgo consumption.
void extract_csr(const SparseMatrix<double>& matrix, std::vector<int>& row_ptrs,
                 std::vector<int>& col_idxs, std::vector<double>& values)
{
    const auto n = matrix.m();
    const auto nnz = matrix.n_nonzero_elements();

    row_ptrs.resize(n + 1);
    col_idxs.resize(nnz);
    values.resize(nnz);

    row_ptrs[0] = 0;
    for (unsigned int row = 0; row < n; ++row) {
        int idx = row_ptrs[row];
        for (auto it = matrix.begin(row); it != matrix.end(row); ++it) {
            col_idxs[idx] = static_cast<int>(it->column());
            values[idx] = it->value();
            ++idx;
        }
        row_ptrs[row + 1] = idx;
    }
}

Solve Function with Ginkgo#

Each cycle calls this function, which creates a fresh Ginkgo solver. After solving, constraints.distribute() enforces hanging node constraints.

// Builds a new Ginkgo solver from the current system matrix and solves.
// A new solver must be generated each cycle because the matrix changes
// after refinement (different size, different sparsity pattern).
void solve_with_ginkgo(const SparseMatrix<double>& system_matrix,
                       const Vector<double>& system_rhs,
                       Vector<double>& solution,
                       const AffineConstraints<double>& constraints)
{
    auto exec = gko::ReferenceExecutor::create();

    std::vector<int> row_ptrs;
    std::vector<int> col_idxs;
    std::vector<double> values;
    extract_csr(system_matrix, row_ptrs, col_idxs, values);

    const auto n = system_matrix.m();
    const auto nnz = system_matrix.n_nonzero_elements();

    auto mtx = gko::share(gko::matrix::Csr<double, int>::create(
        exec, gko::dim<2>(n, n),
        gko::array<double>::view(exec, nnz, values.data()),
        gko::array<int>::view(exec, nnz, col_idxs.data()),
        gko::array<int>::view(exec, n + 1, row_ptrs.data())));

    auto b = gko::matrix::Dense<double>::create(
        exec, gko::dim<2>(n, 1),
        gko::array<double>::view(exec, n,
                                 const_cast<double*>(system_rhs.begin())),
        1);
    auto x = gko::matrix::Dense<double>::create(
        exec, gko::dim<2>(n, 1),
        gko::array<double>::view(exec, n, solution.begin()), 1);

    auto solver =
        gko::solver::Cg<double>::build()
            .with_preconditioner(gko::preconditioner::Jacobi<double>::build())
            .with_criteria(
                gko::stop::Iteration::build().with_max_iters(1000u),
                gko::stop::ResidualNorm<double>::build().with_reduction_factor(
                    1e-12))
            .on(exec)
            ->generate(mtx);

    solver->apply(b, x);

    // Enforce hanging node constraints on the solution.
    constraints.distribute(solution);
}

The Refinement Loop#

The main loop: generate/refine mesh, set up DOFs, assemble, solve, repeat.

    // Adaptive refinement loop: refine mesh, rebuild system, re-solve.
    // Each cycle requires a fresh Ginkgo solver because the matrix
    // dimensions and sparsity pattern change.
    const unsigned int n_cycles = 8;
    for (unsigned int cycle = 0; cycle < n_cycles; ++cycle) {
        std::cout << "Cycle " << cycle << ":" << std::endl;

        if (cycle == 0) {
            GridGenerator::hyper_ball(triangulation);
            triangulation.refine_global(1);
        } else {
            // Estimate error and refine.
            Vector<float> estimated_error(triangulation.n_active_cells());
            KellyErrorEstimator<2>::estimate(dof_handler,
                                             QGauss<1>(fe.degree + 1), {},
                                             solution, estimated_error);

            GridRefinement::refine_and_coarsen_fixed_number(
                triangulation, estimated_error, 0.3, 0.03);
            triangulation.execute_coarsening_and_refinement();
        }

        // Setup DOFs and constraints for hanging nodes + BCs.
        dof_handler.distribute_dofs(fe);
        solution.reinit(dof_handler.n_dofs());
        system_rhs.reinit(dof_handler.n_dofs());

        constraints.clear();
        DoFTools::make_hanging_node_constraints(dof_handler, constraints);
        VectorTools::interpolate_boundary_values(
            dof_handler, 0, Functions::ZeroFunction<2>(), constraints);
        constraints.close();

        DynamicSparsityPattern dsp(dof_handler.n_dofs());
        DoFTools::make_sparsity_pattern(dof_handler, dsp, constraints, false);
        sparsity_pattern.copy_from(dsp);
        system_matrix.reinit(sparsity_pattern);

        // Assemble.
        const QGauss<2> quadrature_formula(fe.degree + 1);
        FEValues<2> fe_values(
            fe, quadrature_formula,
            update_values | update_gradients | update_JxW_values);

        const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
        FullMatrix<double> cell_matrix(dofs_per_cell, dofs_per_cell);
        Vector<double> cell_rhs_local(dofs_per_cell);
        std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);

        for (const auto& cell : dof_handler.active_cell_iterators()) {
            fe_values.reinit(cell);
            cell_matrix = 0;
            cell_rhs_local = 0;

            for (const unsigned int q : fe_values.quadrature_point_indices())
                for (const unsigned int i : fe_values.dof_indices()) {
                    for (const unsigned int j : fe_values.dof_indices())
                        cell_matrix(i, j) += fe_values.shape_grad(i, q) *
                                             fe_values.shape_grad(j, q) *
                                             fe_values.JxW(q);

                    cell_rhs_local(i) +=
                        fe_values.shape_value(i, q) * 1.0 * fe_values.JxW(q);
                }

            cell->get_dof_indices(local_dof_indices);
            constraints.distribute_local_to_global(cell_matrix, cell_rhs_local,
                                                   local_dof_indices,
                                                   system_matrix, system_rhs);
        }

        std::cout << "  Cells: " << triangulation.n_active_cells()
                  << "  DOFs: " << dof_handler.n_dofs() << std::endl;

        // Solve with Ginkgo — a new solver is created each cycle.
        solve_with_ginkgo(system_matrix, system_rhs, solution, constraints);
        std::cout << "  Solved." << std::endl;

        // Write output for this cycle.
        DataOut<2> data_out;
        data_out.attach_dof_handler(dof_handler);
        data_out.add_data_vector(solution, "solution");
        data_out.build_patches();
        std::ofstream output("solution-" + std::to_string(cycle) + ".vtk");
        data_out.write_vtk(output);
    }

Building#

cmake -S . -B build --preset default
cmake --build build
./build/adaptive_refinement

Expected Output#

Cycle 0:
  Cells: 20  DOFs: 89
  Solved.
Cycle 1:
  Cells: 44  DOFs: 209
  Solved.
...
Cycle 7:
  Cells: 2513  DOFs: 12725
  Solved.

Each cycle produces a solution-N.vtk file for visualization.