-
Notifications
You must be signed in to change notification settings - Fork 8
Example adaptive interpolation
In this example we adaptively refine a grid to minimze the L^2 interpolation error when interpolating cosh in a piecewise constant finite volume space.
This only serves to study how to keep data which is associated with the grid (say a DoF vector) across adaptation, that is: what is required for restriction/prolongation.
(Thus it is a quite artificial example.)
We simply use the local interpolation error as indicators to refine grid elements with a Dörfler strategy (and also coarsen, to have a reason to study restriction).
Update begins: parts of the original example have found their way into the library by now. The updated example can be found in the new-master branch (the old example is kept below for future reference). The relevant changes in the main are the use of the AdaptiveHelper, parts of the main now look like:
// fake solution to demonstrate restriction/prolongation
auto fv_space = GDT::make_finite_volume_space<1>(grid_view);
auto current_solution = GDT::make_discrete_function<V>(fv_space);
GDT::interpolate(cosh, current_solution);
// the main adaptation loop
auto helper = make_adaptation_helper(grid, current_solution);
const double tolerance = DXTC_CONFIG_GET("tolerance", 1e-3);
size_t counter = 0;
while (true) {
logger.info() << "step " << counter << ", space has " << fv_space.mapper().size() << " DoFs" << std::endl;
current_solution.visualize("interpolated_function_" + XT::Common::to_string(counter));
// compute local L^2 errors
const auto local_errors = compute_local_l2_norms(current_solution - cosh.as_grid_function(grid_view), grid_view);
logger.info() << " relative interpolation error: " << local_errors.l2_norm() / reference_norm << std::endl;
if (local_errors.l2_norm() / reference_norm < tolerance) {
logger.info() << "target accuracy reached, terminating!" << std::endl;
break;
}
// use these as indicators for grid refinement
mark_elements(grid,
grid_view,
local_errors,
DXTC_CONFIG_GET("mark.refine_factor", 0.25),
DXTC_CONFIG_GET("mark.coarsen_factor", 0.01));
// adapt the grid (restricts/prolongs the solution)
helper.preAdapt();
helper.adapt();
helper.postAdapt();
// now that the grid is adapted, interpolate the function on the new grid
GDT::interpolate(cosh, current_solution);
++counter;
}update ends: below is the original code
The following code is simply the complete dune-gdt/examples/adaptive-integration.cc file, which currently resides somewhere in Felix' repository, but I copy it here for future reference.
Whenever you encounter something like
DXTC_CONFIG_GET("key", value)in the example, you can pass these as runtime arguments to the executable in the sense of
./examples__adaptive-interpolation -key value
See the very bottom for a possible output. This is the example:
// This file is part of the dune-gdt project:
// https://github.com/dune-community/dune-gdt
// Copyright 2010-2018 dune-gdt developers and contributors. All rights reserved.
// License: Dual licensed as BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)
// or GPL-2.0+ (http://opensource.org/licenses/gpl-license)
// with "runtime exception" (http://www.dune-project.org/license.html)
// Authors:
// Felix Schindler (2018)
#include "config.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <utility>
#include <dune/common/dynvector.hh>
#include <dune/common/exceptions.hh>
#include <dune/common/parallel/mpihelper.hh>
#include <dune/grid/common/rangegenerators.hh>
#include <dune/grid/utility/persistentcontainer.hh>
#include <dune/xt/common/float_cmp.hh>
#include <dune/xt/common/string.hh>
#include <dune/xt/common/timedlogging.hh>
#include <dune/xt/la/container/conversion.hh>
#include <dune/xt/la/container/common/vector/dense.hh>
#include <dune/xt/grid/entity.hh>
#include <dune/xt/grid/grids.hh>
#include <dune/xt/grid/gridprovider/cube.hh>
#include <dune/xt/grid/type_traits.hh>
#include <dune/xt/grid/walker.hh>
#include <dune/xt/functions/lambda/function.hh>
#include <dune/gdt/discretefunction/default.hh>
#include <dune/gdt/interpolations.hh>
#include <dune/gdt/local/bilinear-forms/integrals.hh>
#include <dune/gdt/local/integrands/product.hh>
#include <dune/gdt/spaces/l2/finite-volume.hh>
using namespace Dune;
// some global defines
using G = ONED_1D;
static const constexpr size_t d = G::dimension;
using GV = typename G::LeafGridView;
using E = XT::Grid::extract_entity_t<GV>;
using V = XT::LA::CommonDenseVector<double>;
V compute_local_l2_norms(const XT::Functions::GridFunctionInterface<E>& func, const GV& grid_view)
{
// the best index set for a grid view of arbitrary elements is a scalar FV space ...
auto fv_space = GDT::make_finite_volume_space<1>(grid_view);
// ... and the best way to associate data with grid elements is a corresponding discrete function
auto df = GDT::make_discrete_function<V>(fv_space);
// compute local L^2 products
auto walker = XT::Grid::make_walker(grid_view);
walker.append(/*prepare nothing*/ []() {},
/*apply local*/
[&](const auto& element) {
auto local_func = func.local_function();
local_func->bind(element);
// models \int_element 1*phi*psi dx for any phi/psi
const GDT::LocalElementIntegralBilinearForm<E> local_l2_product(
GDT::LocalElementProductIntegrand<E>(1.));
// evaluate local product with phi = psi = local_func
const auto element_l2_error2 = local_l2_product.apply2(*local_func, *local_func)[0][0];
// store in entry for this element (we keep them unsquared, makes for easier computation of total)
df.local_discrete_function(element)->dofs()[0] = std::sqrt(element_l2_error2);
},
/*finalize*/ []() {});
walker.walk(/*thread_parallel=*/true);
return df.dofs().vector();
} // ... compute_local_l2_norms(...)
void mark_elements(
G& grid, const GV& grid_view, const V& local_indicators, const double& refine_factor, const double& coarsen_factor)
{
// mark all elements with a contribution above coarsen_threashhold for coarsening
// * therefore: as above, use a scalar FV space as index mapper
auto fv_space = GDT::make_finite_volume_space<1>(grid_view);
// ... and a corresponding discrete function to associate data with grid elements
// auto local_indicators_function = GDT::make_discrete_function(fv_space, local_indicators);
// auto local_indicators_local_function = local_indicators_function.local_discrete_function();
// prepare dörfler marking
std::vector<std::pair<double, size_t>> accumulated_sorted_local_indicators(local_indicators.size());
for (size_t ii = 0; ii < local_indicators.size(); ++ii)
accumulated_sorted_local_indicators[ii] = {std::pow(local_indicators[ii], 2), ii};
std::sort(accumulated_sorted_local_indicators.begin(),
accumulated_sorted_local_indicators.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
for (size_t ii = 1; ii < accumulated_sorted_local_indicators.size(); ++ii)
accumulated_sorted_local_indicators[ii].first += accumulated_sorted_local_indicators[ii - 1].first;
// select smallest coarsen_factor contributions for coarsening
std::set<size_t> elements_to_be_coarsened;
const double total_indicators = std::pow(local_indicators.l2_norm(), 2);
for (const auto& indicator : accumulated_sorted_local_indicators)
if (indicator.first < (coarsen_factor * total_indicators))
elements_to_be_coarsened.insert(indicator.second);
// select largest refine_factor contributions
std::set<size_t> elements_to_be_refined;
for (const auto& indicator : accumulated_sorted_local_indicators)
if (indicator.first > ((1 - refine_factor) * total_indicators))
elements_to_be_refined.insert(indicator.second);
// mark elements
size_t corsend_elements = 0;
size_t refined_elements = 0;
for (auto&& element : elements(grid_view)) {
const size_t index = fv_space.mapper().global_indices(element)[0];
bool coarsened = false;
if (std::find(elements_to_be_coarsened.begin(), elements_to_be_coarsened.end(), index)
!= elements_to_be_coarsened.end()) {
grid.mark(/*coarsen*/ -1, element);
coarsened = true;
}
if (std::find(elements_to_be_refined.begin(), elements_to_be_refined.end(), index)
!= elements_to_be_refined.end()) {
grid.mark(/*refine and overwrite coarsening if present*/ 2, element);
refined_elements += 1;
coarsened = false;
}
if (coarsened)
++corsend_elements;
}
auto logger = XT::Common::TimedLogger().get("mark_elements");
logger.info() << "marked " << corsend_elements << "/" << fv_space.mapper().size() << " elements for coarsening and "
<< refined_elements << "/" << fv_space.mapper().size() << " elements for refinement" << std::endl;
} // ... mark_elements(...)
/**
* \note At some point this functionality should find its way into the spaces.
*
* On the domain covered by the coarser element, this computes an L^2 projection of the function defined on the finer
* elements (which corresponds to weighted averaging in the FV case).
*/
void compute_restriction(const E& element, PersistentContainer<G, DynamicVector<double>>& persistent_data)
{
auto& element_restriction_data = persistent_data[element];
if (element_restriction_data.size() == 0) {
DUNE_THROW_IF(element.isLeaf(), InvalidStateException, "");
for (auto&& child_element : descendantElements(element, element.level() + 1)) {
// ensure we have data on all descendant elements of the next level
compute_restriction(child_element, persistent_data);
// compute restriction
auto child_restriction_data = persistent_data[child_element];
child_restriction_data *= child_element.geometry().volume();
if (element_restriction_data.size() == 0)
element_restriction_data = child_restriction_data; // initialize with child data
else
element_restriction_data += child_restriction_data;
}
// now we have assembled h*value
element_restriction_data /= element.geometry().volume();
}
} // ... compute_restriction(...)
int main(int argc, char* argv[])
{
try {
MPIHelper::instance(argc, argv);
if (argc > 1)
DXTC_CONFIG.read_options(argc, argv);
XT::Common::TimedLogger().create(DXTC_CONFIG_GET("logger.info", 1), DXTC_CONFIG_GET("logger.debug", -1));
auto logger = XT::Common::TimedLogger().get("main");
// initial grid
const double domain_length = 10;
auto grid_ptr =
XT::Grid::make_cube_grid<G>(/*lower_left=*/0., /*upper_right=*/domain_length, /*num_elements=*/1).grid_ptr();
auto& grid = *grid_ptr;
auto grid_view = grid.leafGridView();
// funtion to interpolate
const XT::Functions::LambdaFunction<d> cosh(/*approx_pol_order=*/10,
[](const auto& x, const auto& /*param*/) { return std::cosh(x); });
const auto reference_norm = compute_local_l2_norms(cosh.as_grid_function(grid_view), grid_view).l2_norm();
// fake solution vector to demonstrate prolongation
V current_solution_vector = GDT::interpolate<V>(cosh, GDT::make_finite_volume_space<1>(grid_view)).dofs().vector();
// the main adaptation loop
const double tolerance = DXTC_CONFIG_GET("tolerance", 1e-3);
size_t counter = 0;
while (true) {
// interpret current_solution as a discrete FV function on the current grid
auto fv_space = GDT::make_finite_volume_space<1>(grid_view);
auto current_solution = GDT::make_discrete_function(fv_space, current_solution_vector);
logger.info() << "step " << counter << ", space has " << fv_space.mapper().size() << " DoFs" << std::endl;
current_solution.visualize("interpolated_function_" + XT::Common::to_string(counter));
// compute local L^2 errors
const auto local_errors = compute_local_l2_norms(current_solution - cosh.as_grid_function(grid_view), grid_view);
logger.info() << " relative interpolation error: " << local_errors.l2_norm() / reference_norm << std::endl;
if (local_errors.l2_norm() / reference_norm < tolerance) {
logger.info() << "target accuracy reached, terminating!" << std::endl;
break;
}
// use these as indicators for grid refinement
mark_elements(grid,
grid_view,
local_errors,
DXTC_CONFIG_GET("mark.refine_factor", 0.25),
DXTC_CONFIG_GET("mark.coarsen_factor", 0.01));
// now the actual adaptation
// * call preadapt, this will mark elements which might vanish due to coarsening
const bool elements_may_be_coarsened = grid.preAdapt();
// - now we need some persistent storage to keep our data: all kept leaf elements might change their indices and
// all coarsened elements might vanish
// this should keep local DoF vectors, which can be converted to DynamicVector<double>
PersistentContainer<G, DynamicVector<double>> persistent_data(grid, 0);
// - we also need a container to recall those elements where we need to restrict to
PersistentContainer<G, bool> restriction_required(grid, 0, false);
// - first of all, walk the current leaf of the grid ...
auto current_local_solution = current_solution.local_discrete_function();
for (auto&& element : elements(grid_view)) {
current_local_solution->bind(element);
// ... to store the local DoFs ...
persistent_data[element] = XT::LA::convert_to<DynamicVector<double>>(current_local_solution->dofs());
// ... and to mark father elements
if (element.mightVanish())
restriction_required[element.father()] = true;
}
// - now walk the grid up all coarser levels ...
if (elements_may_be_coarsened) {
for (int level = grid.maxLevel() - 1; level >= 0; --level) {
auto level_view = grid.levelGridView(level);
for (auto&& element : elements(level_view)) {
// ... to compute restrictions ...
if (restriction_required[element])
compute_restriction(element, persistent_data);
// ... and to mark father elements
if (element.mightVanish())
restriction_required[element.father()] = true;
}
}
}
// from here on, restriction_required is not required any more
// * next, we actually adapt the grid
grid.adapt();
// - from now on, fv_space, current_solution and current_local_solution must not be used any more,
// this is not yet implemented!
// - clean up data structures
persistent_data.resize();
persistent_data.shrinkToFit();
// * create a new space, resize the solution vector and create a new discrete function to hold the
// prolongated/restricted values
auto new_fv_space = GDT::make_finite_volume_space<1>(grid_view);
current_solution_vector.resize(new_fv_space.mapper().size());
auto new_solution = GDT::make_discrete_function(new_fv_space, current_solution_vector);
// * copy the persistent data back to the discrete function ...
auto new_local_solution = new_solution.local_discrete_function();
for (auto&& element : elements(grid_view)) {
new_local_solution->bind(element);
if (element.isNew()) {
// ... by prolongation from the father element ...
const auto& father_data = persistent_data[element.father()];
DUNE_THROW_IF(father_data.size() == 0, InvalidStateException, "");
new_local_solution->dofs().assign_from(father_data);
} else {
// ... or by copying the data from this unchanged element
const auto& element_data = persistent_data[element];
DUNE_THROW_IF(element_data.size() == 0, InvalidStateException, "");
new_local_solution->dofs().assign_from(element_data);
}
}
// * clean up the grid
grid.postAdapt();
// from here on, persistent_data is not required any more
new_solution.visualize("interpolated_function_after_refinement_" + XT::Common::to_string(counter));
// now that the grid is adapted, interpolate the function on the new grid
GDT::interpolate(cosh, new_solution);
++counter;
}
double min_h = std::numeric_limits<double>::max();
for (auto&& element : elements(grid_view))
min_h = std::min(min_h, XT::Grid::diameter(element));
logger.info() << "\nA uniformly refinement grid of similar accuracy would have roughly required "
<< int(domain_length / min_h) << " DoFs" << std::endl;
} catch (Exception& e) {
std::cerr << "\nDUNE reported error: " << e.what() << std::endl;
return EXIT_FAILURE;
} catch (std::exception& e) {
std::cerr << "\nstl reported error: " << e.what() << std::endl;
return EXIT_FAILURE;
} catch (...) {
std::cerr << "Unknown error occured!" << std::endl;
return EXIT_FAILURE;
} // try
return EXIT_SUCCESS;
} // ... main(...)And this is the output on my machine:
00:00|main: step 0, space has 1 DoFs
00:00|main: relative interpolation error: 0.986892
00:00|mark_elements: marked 0/1 elements for coarsening and 1/1 elements for refinement
00:00|main: step 1, space has 2 DoFs
00:00|main: relative interpolation error: 0.860953
00:00|mark_elements: marked 1/2 elements for coarsening and 1/2 elements for refinement
00:00|main: step 2, space has 3 DoFs
00:00|main: relative interpolation error: 0.595099
00:00|mark_elements: marked 2/3 elements for coarsening and 1/3 elements for refinement
00:00|main: step 3, space has 4 DoFs
00:00|main: relative interpolation error: 0.343076
00:00|mark_elements: marked 1/4 elements for coarsening and 1/4 elements for refinement
00:00|main: step 4, space has 5 DoFs
00:00|main: relative interpolation error: 0.200343
00:00|mark_elements: marked 1/5 elements for coarsening and 1/5 elements for refinement
00:00|main: step 5, space has 6 DoFs
00:00|main: relative interpolation error: 0.152863
00:00|mark_elements: marked 1/6 elements for coarsening and 1/6 elements for refinement
00:00|main: step 6, space has 7 DoFs
00:00|main: relative interpolation error: 0.130386
00:00|mark_elements: marked 1/7 elements for coarsening and 1/7 elements for refinement
00:00|main: step 7, space has 8 DoFs
00:00|main: relative interpolation error: 0.110434
00:00|mark_elements: marked 1/8 elements for coarsening and 1/8 elements for refinement
00:00|main: step 8, space has 9 DoFs
00:00|main: relative interpolation error: 0.0968832
00:00|mark_elements: marked 1/9 elements for coarsening and 1/9 elements for refinement
00:00|main: step 9, space has 10 DoFs
00:00|main: relative interpolation error: 0.0882792
00:00|mark_elements: marked 1/10 elements for coarsening and 1/10 elements for refinement
00:00|main: step 10, space has 11 DoFs
00:00|main: relative interpolation error: 0.0793062
00:00|mark_elements: marked 1/11 elements for coarsening and 1/11 elements for refinement
00:00|main: step 11, space has 12 DoFs
00:00|main: relative interpolation error: 0.0700921
00:00|mark_elements: marked 1/12 elements for coarsening and 2/12 elements for refinement
00:00|main: step 12, space has 14 DoFs
00:00|main: relative interpolation error: 0.0598781
00:00|mark_elements: marked 1/14 elements for coarsening and 2/14 elements for refinement
00:00|main: step 13, space has 16 DoFs
00:00|main: relative interpolation error: 0.0524025
00:00|mark_elements: marked 0/16 elements for coarsening and 2/16 elements for refinement
00:00|main: step 14, space has 18 DoFs
00:00|main: relative interpolation error: 0.0453071
00:00|mark_elements: marked 0/18 elements for coarsening and 2/18 elements for refinement
00:00|main: step 15, space has 20 DoFs
00:00|main: relative interpolation error: 0.0400431
00:00|mark_elements: marked 0/20 elements for coarsening and 3/20 elements for refinement
00:00|main: step 16, space has 23 DoFs
00:00|main: relative interpolation error: 0.0346841
00:00|mark_elements: marked 0/23 elements for coarsening and 3/23 elements for refinement
00:00|main: step 17, space has 26 DoFs
00:00|main: relative interpolation error: 0.0306315
00:00|mark_elements: marked 1/26 elements for coarsening and 4/26 elements for refinement
00:00|main: step 18, space has 30 DoFs
00:00|main: relative interpolation error: 0.0269718
00:00|mark_elements: marked 1/30 elements for coarsening and 4/30 elements for refinement
00:00|main: step 19, space has 34 DoFs
00:00|main: relative interpolation error: 0.0239658
00:00|mark_elements: marked 1/34 elements for coarsening and 4/34 elements for refinement
00:00|main: step 20, space has 38 DoFs
00:00|main: relative interpolation error: 0.0212999
00:00|mark_elements: marked 1/38 elements for coarsening and 4/38 elements for refinement
00:00|main: step 21, space has 42 DoFs
00:00|main: relative interpolation error: 0.0192651
00:00|mark_elements: marked 2/42 elements for coarsening and 5/42 elements for refinement
00:00|main: step 22, space has 47 DoFs
00:00|main: relative interpolation error: 0.017096
00:00|mark_elements: marked 2/47 elements for coarsening and 6/47 elements for refinement
00:00|main: step 23, space has 53 DoFs
00:00|main: relative interpolation error: 0.0152015
00:00|mark_elements: marked 3/53 elements for coarsening and 7/53 elements for refinement
00:00|main: step 24, space has 60 DoFs
00:00|main: relative interpolation error: 0.0135002
00:00|mark_elements: marked 3/60 elements for coarsening and 7/60 elements for refinement
00:00|main: step 25, space has 67 DoFs
00:00|main: relative interpolation error: 0.0120876
00:00|mark_elements: marked 3/67 elements for coarsening and 8/67 elements for refinement
00:00|main: step 26, space has 75 DoFs
00:00|main: relative interpolation error: 0.0107531
00:00|mark_elements: marked 3/75 elements for coarsening and 8/75 elements for refinement
00:00|main: step 27, space has 83 DoFs
00:00|main: relative interpolation error: 0.00963154
00:00|mark_elements: marked 3/83 elements for coarsening and 9/83 elements for refinement
00:00|main: step 28, space has 92 DoFs
00:00|main: relative interpolation error: 0.0086114
00:00|mark_elements: marked 3/92 elements for coarsening and 10/92 elements for refinement
00:00|main: step 29, space has 102 DoFs
00:00|main: relative interpolation error: 0.00776004
00:00|mark_elements: marked 3/102 elements for coarsening and 13/102 elements for refinement
00:00|main: step 30, space has 114 DoFs
00:00|main: relative interpolation error: 0.00703421
00:00|mark_elements: marked 4/114 elements for coarsening and 13/114 elements for refinement
00:00|main: step 31, space has 127 DoFs
00:00|main: relative interpolation error: 0.00630862
00:00|mark_elements: marked 4/127 elements for coarsening and 14/127 elements for refinement
00:00|main: step 32, space has 141 DoFs
00:00|main: relative interpolation error: 0.00567056
00:00|mark_elements: marked 4/141 elements for coarsening and 15/141 elements for refinement
00:00|main: step 33, space has 155 DoFs
00:00|main: relative interpolation error: 0.00514459
00:00|mark_elements: marked 4/155 elements for coarsening and 16/155 elements for refinement
00:00|main: step 34, space has 170 DoFs
00:00|main: relative interpolation error: 0.00466947
00:00|mark_elements: marked 5/170 elements for coarsening and 18/170 elements for refinement
00:00|main: step 35, space has 187 DoFs
00:00|main: relative interpolation error: 0.00422839
00:00|mark_elements: marked 5/187 elements for coarsening and 21/187 elements for refinement
00:00|main: step 36, space has 208 DoFs
00:00|main: relative interpolation error: 0.00380419
00:00|mark_elements: marked 6/208 elements for coarsening and 25/208 elements for refinement
00:00|main: step 37, space has 231 DoFs
00:00|main: relative interpolation error: 0.00346587
00:00|mark_elements: marked 7/231 elements for coarsening and 26/231 elements for refinement
00:00|main: step 38, space has 255 DoFs
00:00|main: relative interpolation error: 0.00314821
00:00|mark_elements: marked 7/255 elements for coarsening and 27/255 elements for refinement
00:00|main: step 39, space has 280 DoFs
00:00|main: relative interpolation error: 0.00286403
00:00|mark_elements: marked 9/280 elements for coarsening and 29/280 elements for refinement
00:00|main: step 40, space has 308 DoFs
00:00|main: relative interpolation error: 0.00258791
00:00|mark_elements: marked 9/308 elements for coarsening and 32/308 elements for refinement
00:00|main: step 41, space has 338 DoFs
00:00|main: relative interpolation error: 0.00234796
00:00|mark_elements: marked 10/338 elements for coarsening and 35/338 elements for refinement
00:00|main: step 42, space has 371 DoFs
00:00|main: relative interpolation error: 0.00213133
00:00|mark_elements: marked 11/371 elements for coarsening and 41/371 elements for refinement
00:00|main: step 43, space has 409 DoFs
00:00|main: relative interpolation error: 0.00193855
00:00|mark_elements: marked 14/409 elements for coarsening and 48/409 elements for refinement
00:00|main: step 44, space has 454 DoFs
00:00|main: relative interpolation error: 0.0017599
00:00|mark_elements: marked 15/454 elements for coarsening and 51/454 elements for refinement
00:00|main: step 45, space has 501 DoFs
00:00|main: relative interpolation error: 0.00160077
00:00|mark_elements: marked 15/501 elements for coarsening and 53/501 elements for refinement
00:00|main: step 46, space has 550 DoFs
00:00|main: relative interpolation error: 0.0014565
00:00|mark_elements: marked 16/550 elements for coarsening and 57/550 elements for refinement
00:00|main: step 47, space has 601 DoFs
00:00|main: relative interpolation error: 0.00132909
00:00|mark_elements: marked 17/601 elements for coarsening and 60/601 elements for refinement
00:00|main: step 48, space has 654 DoFs
00:00|main: relative interpolation error: 0.00121564
00:00|mark_elements: marked 19/654 elements for coarsening and 66/654 elements for refinement
00:00|main: step 49, space has 712 DoFs
00:00|main: relative interpolation error: 0.00111226
00:00|mark_elements: marked 21/712 elements for coarsening and 75/712 elements for refinement
00:00|main: step 50, space has 778 DoFs
00:00|main: relative interpolation error: 0.00101703
00:00|mark_elements: marked 24/778 elements for coarsening and 88/778 elements for refinement
00:00|main: step 51, space has 856 DoFs
00:00|main: relative interpolation error: 0.000930272
00:00|main: target accuracy reached, terminating!
00:00|main:
00:00|main: To reach the desired tolerance using uniform refinement would have roughly required 8192 DoFs