AdaptiveSimplex is a header-only C++20 library for adaptive integration of discontinuous or localized non-smooth functions on simplex meshes.
It is built for high-performance computing workflows where function or vertex evaluation is expensive, and the important structure lives near an interface. A typical example is a level-set or occupation integral:
Why AdaptiveSimplex?
-
β‘ Low overhead: mesh, cache, and cut-simplex bookkeeping stay near the microsecond scale in the supplied benchmarks, so expensive application evaluations can dominate.
-
π― Adaptive by target: set an error target and refinement budget; the algorithm samples where the current estimator says more information is needed, instead of committing to a fixed tensor grid up front.
-
π Strong vertex reuse: every evaluated vertex is stored in
VertexCache<T>, so shared simplex vertices are reused instead of re-evaluated. -
π Incremental updates: refinement updates the integral through local corrections, evaluating only newly introduced vertices and avoiding full recomputation.
-
π Dimension-general: the adaptive simplex machinery is not tied to 2D or level sets; it works for general simplex integrals in any dimension.
-
π§΅ Parallel vertex evaluation: missing vertices can be evaluated in parallel with OpenMP when available.
-
βοΈ Interface-aware simplex integration: for level-set problems, each cut simplex contributes the clipped volume or barycentric moments implied by the vertex data.
Good fits:
- discontinuous or non-smooth integrands with localized structure
- level-set-defined regions, cut cells, and occupation functions
- Fermi-sea or zero-temperature occupation calculations
- expensive vertex evaluations where reuse matters
- problems with a meaningful local error or interface indicator
Poor fits:
- smooth integrals
For smooth integrands, use standard quadrature methods, sparse grids, or even uniform grids. They are usually simpler, faster, and more accurate for that problem class. The smooth two-peak comparison below is included as a reminder: AdaptiveSimplex is not a smooth-quadrature silver bullet, and tensor grids can be competitive or better when the function is cheap and smooth.
This example integrates the occupied part of a rotated ellipse:
The executable constructs a mesh, a vertex cache, and an integrand built from three domain-specific pieces:
#include "adaptivesimplex/adaptive/adaptive_loop.h"
#include "adaptivesimplex/adaptive/simplex_integrand.h"
#include "adaptivesimplex/cut.h"
#include "adaptivesimplex/core/root_mesh.h"
#include "adaptivesimplex/core/vertex_cache.h"
#include "adaptivesimplex_examples/level_set_function.h"
namespace adaptive = adaptivesimplex::adaptive;
namespace core = adaptivesimplex::core;
namespace examples = adaptivesimplex::examples;
auto function = examples::LevelSetIndicator2D{};
auto geometry = core::root_geometry(2, 2);
auto cache = core::VertexCache<examples::LevelSetVertexValue>{};
auto integrand = adaptive::simplex_integrand(
cache,
// 1. Evaluate one mesh vertex.
[function](std::span<const double> point) {
return examples::LevelSetVertexValue{
.level_value = function.level_value(point[0], point[1]),
};
},
// 2. Compute one simplex contribution from cached vertex data.
[function](
const core::Geometry& geometry,
core::SimplexId simplex_id,
const core::VertexCache<examples::LevelSetVertexValue>& vertex_cache
) {
const auto moments = adaptivesimplex::cut::simplex_moments(
geometry,
simplex_id,
[&](core::VertexId vertex_id) {
return vertex_cache.get(vertex_id).level_value;
},
adaptivesimplex::cut::LevelOptions{.level = function.level()}
);
return moments.volume;
}
);
auto options = adaptive::Options{
.target_error = 5e-4,
.max_refinements = 2048,
.preview_depth = 2,
.min_refinement_batch_size = 1,
.max_refinement_batch_size = 4,
};
const auto result = adaptive::run(geometry, integrand, options);For the same example with the helper from examples/common, the executable is
shorter:
auto function = examples::LevelSetIndicator2D{};
auto geometry = examples::level_set_geometry();
auto cache = core::VertexCache<examples::LevelSetVertexValue>{};
auto integrand = examples::make_level_set_integrand(cache, function);
const auto result =
adaptive::run(geometry, integrand, examples::level_set_options());
std::cout << result.integral << "\n";
std::cout << result.stopping_error << "\n";The important data flow is still explicit:
adaptive::run(...)asks the mesh which vertices are missing.- The vertex evaluator computes data for each missing vertex.
VertexCache<T>stores that data byVertexId.- The simplex rule computes contributions for the coarse simplex and its preview mesh.
- Estimation policies turn the resulting correction into global stopping error and a local refinement score.
- The adaptive loop consumes estimates individually, retains one local value per active simplex, and updates totals incrementally.
For scalar integrals, the default policies are:
adaptive::estimation_policies{
adaptive::signed_correction_error{},
adaptive::absolute_local_correction{},
};signed_correction_error uses the accumulated result correction directly and
applies std::abs, without retaining a second copy. Use
adaptive::unsigned_correction_error{} when you want a scalar accumulator of
sum(std::abs(local.correction)) with no cancellation.
Set preview_depth to zero to evaluate only the active mesh, without
speculative child simplices or preview-only vertices. In this mode,
simplex_integrand(...) returns the coarse contribution and a shape-compatible
zero correction. Refinement still bisects selected simplices by one level, and
the returned integral represents the active mesh.
Depth zero must be paired with stopping-error and refinement-score policies
that use an intrinsic simplex estimate; the built-in correction-only policies
otherwise report zero error immediately.
See examples/scalar_integral.cpp for the minimal scalar factory example and examples/level_set_integral.cpp for the runnable level-set example.
simplex_integrand(...) builds the ordinary integrand protocol for you. If you
need full control, a hand-written integrand supplies:
using vertex_value_type = ...using integral_value_type = ...cache()stopping_global_error()evaluate_vertex(geometry, vertex_id)estimate_simplex(geometry, simplex_id, preview_depth)
The adaptive loop evaluates missing vertices for a complete batch, then calls
estimate_simplex(...) and consumes its result one simplex at a time:
adaptive::SimplexEstimate<T>{
.coarse = coarse,
.correction = preview - coarse,
.refinement_score = score,
};The preview can be reconstructed as coarse + correction when needed. Stopping
policies receive the single value retained for each active simplex through:
add_local_estimate(accumulator, local_estimate);
remove_local_estimate(accumulator, local_estimate);
error(accumulator, global_correction);For positive preview depth, local_estimate is the simplex correction, so a
stopping policy must be expressible from that retained value. At depth zero it
is the coarse simplex integral, allowing custom policies to use intrinsic error
data stored in that value. The refinement-score context still exposes both
coarse and correction while the estimate is in flight.
adaptive::IntegrationResult<T> contains the final accumulated values:
integral: current integral estimatecorrection: accumulated signed correction statestopping_error: scalar stopping criterionevaluations: number of vertex evaluationsrefinements: number of selected parent refinementsconverged: whether the target was reached
Adaptive refinement decides where to refine. Cut-simplex moments decide what a simplex contributes when a discontinuity crosses it.
For a level-set integral, simply averaging vertex indicator values is a poor local quadrature rule: the function jumps across an interface. Instead, AdaptiveSimplex linearly interpolates the level value on each simplex (T):
clips the simplex to the occupied region
and computes barycentric moments
For an indicator-only volume calculation, the moment routine already returns
the occupied simplex volume. That is why the simplex rule above returns
moments.volume.
This is the main reason adaptivesimplex::cut exists: it turns a
level-set-defined discontinuity into a deterministic simplex contribution that
can be reused by the adaptive algorithm.
-
core::GeometryOwns the dimension, dyadic vertices, active simplices, refinement, and simplex volumes. Create a basic domain with
core::root_geometry(dimension, depth). -
core::VertexCache<T>Stores user-defined vertex data keyed by
VertexId. Missing vertices are evaluated once; reused vertices are not counted again as evaluations. -
adaptive::simplex_integrand(...)Factory that connects a cache, vertex evaluator, simplex rule, and estimation policies to the adaptive loop.
-
Vertex evaluator
Computes the cache value for one vertex. In a physics code, this is where an expensive Hamiltonian diagonalization or band calculation would usually live.
-
Simplex rule
Computes the domain-specific integral contribution for one simplex. For level-set integrals this usually calls
cut::simplex_moments(...). -
adaptive::estimation_policiesSelects the global stopping-error accumulator and local refinement-score policy.
-
adaptive::SimplexEstimate<T>Contains
coarse,correction, andrefinement_scorefor one active simplex. -
adaptive::IntegrationResult<T>Contains
integral,correction,stopping_error,evaluations,refinements, andconvergedfor the full adaptive run. -
adaptive::OptionsControls stopping and refinement behavior: target error, maximum refinements, preview depth, and refinement batch size.
-
adaptive::run(...)Evaluates missing vertices, updates estimates, refines high-priority simplices, and returns the integration result.
The package exports:
adaptivesimplex::coreadaptivesimplex::cutadaptivesimplex::adaptive
The adaptive layer depends on core, but it does not depend on cut. Link
adaptivesimplex::cut when your code directly includes or uses the cut-simplex
utility.
Public headers are under include/adaptivesimplex:
#include <adaptivesimplex/core.h>
#include <adaptivesimplex/cut.h>
#include <adaptivesimplex/adaptive.h>
#include <adaptivesimplex/adaptivesimplex.h>After installing AdaptiveSimplex:
find_package(adaptivesimplex CONFIG REQUIRED)
add_executable(my_integrator main.cpp)
target_compile_features(my_integrator PRIVATE cxx_std_20)
target_link_libraries(my_integrator PRIVATE adaptivesimplex::adaptive)If your code uses cut-simplex moments directly:
target_link_libraries(
my_integrator PRIVATE
adaptivesimplex::cut
adaptivesimplex::adaptive
)With CMake FetchContent:
include(FetchContent)
FetchContent_Declare(
adaptivesimplex
GIT_REPOSITORY <repository-url>
GIT_TAG v0.1.0
)
FetchContent_MakeAvailable(adaptivesimplex)
target_link_libraries(my_integrator PRIVATE adaptivesimplex::adaptive)When used as a subproject, tests, examples, benchmarks, and documentation tools default to off. Main CMake options:
ADAPTIVESIMPLEX_BUILD_TESTINGADAPTIVESIMPLEX_BUILD_EXAMPLESADAPTIVESIMPLEX_BUILD_BENCHMARKSADAPTIVESIMPLEX_BUILD_DOC_TOOLSADAPTIVESIMPLEX_ENABLE_OPENMP
User-facing examples:
examples/level_set_integral.cpp: adaptive integration of a level-set indicator usingadaptivesimplex::cutexamples/two_peak_integral.cpp: smooth two-peak integral, useful as a counterexample where tensor grids can be competitiveexamples/scalar_integral.cpp: minimalsimplex_integrandsetupexamples/vector_integral.cpp: vector-valued result with explicit policies
Build and run examples:
cmake -S . -B build/examples -DADAPTIVESIMPLEX_BUILD_EXAMPLES=ON
cmake --build build/examples
build/examples/adaptivesimplex_level_set_integral
build/examples/adaptivesimplex_two_peak_integralcmake -S . -B build/local \
-DADAPTIVESIMPLEX_BUILD_TESTING=ON \
-DADAPTIVESIMPLEX_BUILD_EXAMPLES=ON \
-DADAPTIVESIMPLEX_BUILD_BENCHMARKS=ON
cmake --build build/local
ctest --test-dir build/local --output-on-failureCI runs the package check on Ubuntu GCC, Ubuntu Clang, and macOS Clang. The package check builds tests/examples/benchmarks, runs the tests, smoke-runs the benchmarks, installs the package, and builds a downstream package consumer.
OpenMP is optional. If CMake finds it and ADAPTIVESIMPLEX_ENABLE_OPENMP is on,
adaptivesimplex::adaptive links OpenMP::OpenMP_CXX and the missing-vertex
loop in adaptive/evaluation.h can run in parallel.

