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.