Involute is a hardware-accelerated Consensus-Based Optimization (CBO) library for finding global minima of non-convex objective functions constrained to Riemannian manifolds — including
A pre-print describing the theoretical foundations is expected in July 2026.
- Test Results: Benchmarks and convergence performance on high-dimensional manifolds
- Library Documentation
- Advanced Setup: Customizing adapters and backend routing for SYCL/oneMKL
Involute depends on two companion libraries — isomorphism (hardware-accelerated tensor DSL) and riemannian-gaussian-sampler (exact spectral samplers for SO(d) and V(n,k)). Both are declared as Homebrew dependencies and installed automatically.
brew tap c0rmac/homebrew-isomorphism
brew tap c0rmac/homebrew-riemannian-gaussian-sampler
brew tap c0rmac/homebrew-involuteApple MLX — recommended on Apple Silicon (M1/M2/M3/M4), uses the Metal GPU:
brew install involute-mlxLibTorch — for LibTorch / PyTorch users or non-Apple hardware:
brew install involute-torchEigen — lightweight CPU-only, no large framework dependency:
brew install involute-eigenEach formula pulls in the matching backend variant of isomorphism and riemannian-gaussian-sampler automatically.
find_package(involute REQUIRED)
add_executable(my_solver main.cpp)
target_link_libraries(my_solver PRIVATE involute::involute)All solvers share the same pattern:
- Define an objective function via
FuncObj(single manifold) orFuncProductObj(product manifold). - Construct a solver config using designated initializers.
- Run
solver.solve(&objective)and readresult.min_energy.
Each manifold type ships two pre-wired solver variants that differ only in how the noise scale evolves across iterations:
| Variant | Adapter | When to use |
|---|---|---|
| CMAES | CMAESParameterAdapter |
The default choice. Infers the noise schedule from the swarm's covariance structure — no learning-rate tuning required. Works well across a wide range of problem dimensions. |
| ADAM | AdamParameterAdapter |
Use when you want direct control over how aggressively the swarm contracts. Exposes delta (initial noise scale) as the primary knob; the Adam-style moment estimates then modulate it step-by-step. |
Both variants accept the same objective function and convergence criterion. Switching is a one-line change to the config type and solver class name, as shown in each example below.
Minimize a function
CMAES infers the noise schedule from the swarm's covariance structure — no learning rate needs to be specified. ADAM drives the noise scale via Adam-style moment estimates; use it when you want direct control over the contraction rate via delta.
#include <involute/solvers/isotropic/so_isotropic_solver_cmaes.hpp>
#include <involute/solvers/isotropic/so_isotropic_solver_adam.hpp>
#include <involute/core/math.hpp>
#include <iostream>
using namespace involute;
using namespace involute::core;
using namespace involute::solvers;
int main() {
sampler::set_num_threads(8);
math::set_default_device_gpu();
const int d = 5;
// Rastrigin on SO(d), global minimum f(I) = 0
FuncObj objective([d](const Tensor& X) {
const float A = 10.0f;
const float two_pi = 2.0f * 3.14159265f;
Tensor I = math::eye(d, DType::Float32);
Tensor diff = math::subtract(X, I);
return math::sum(
math::subtract(
math::add(math::square(diff), Tensor(A, DType::Float32)),
math::multiply(math::cos(math::multiply(diff, Tensor(two_pi, DType::Float32))),
Tensor(A, DType::Float32))),
{1, 2});
});
// --- CMAES: noise schedule derived from swarm covariance, no learning rate needed ---
SOIsotropicSolverCMAESConfig cmaes_config{
.N = 200,
.d = d,
.convergence = std::make_shared<MaxStepsCriterion>(500),
.delta = 2.0,
.debug = {Debugger::Log}
};
SOIsotropicSolverCMAES cmaes_solver(cmaes_config);
CBOResult cmaes_result = cmaes_solver.solve(&objective);
std::cout << "[CMAES] Energy: " << cmaes_result.min_energy
<< " | Steps: " << cmaes_result.iterations_run << "\n";
// --- ADAM: noise scale driven by Adam moment estimates; tune delta to control contraction ---
SOIsotropicSolverADAMConfig adam_config{
.N = 200,
.d = d,
.convergence = std::make_shared<MaxStepsCriterion>(500),
.delta = 2.0,
.debug = {Debugger::Log}
};
SOIsotropicSolverADAM adam_solver(adam_config);
CBOResult adam_result = adam_solver.solve(&objective);
std::cout << "[ADAM] Energy: " << adam_result.min_energy
<< " | Steps: " << adam_result.iterations_run << "\n";
return 0;
}Minimize a function
CMAES adapts the noise scale per-step without any learning rate. ADAM applies moment-based modulation to the noise decay; supply a larger delta for a wider initial search radius.
#include <involute/solvers/isotropic/stiefel_isotropic_solver_cmaes.hpp>
#include <involute/solvers/isotropic/stiefel_isotropic_solver_adam.hpp>
#include <involute/core/math.hpp>
#include <iostream>
using namespace involute;
using namespace involute::core;
using namespace involute::solvers;
int main() {
sampler::set_num_threads(8);
math::set_default_device_gpu();
const int n = 10, k = 4;
// Rastrigin on V(n,k), global minimum f(I_{n,k}) = 0
Tensor I_nk = math::slice(math::eye(n, DType::Float32), 0, k, 1);
Tensor two_pi = Tensor(2.0f * 3.14159265f, DType::Float32);
Tensor A = Tensor(10.0f, DType::Float32);
FuncObj objective([I_nk, two_pi, A](const Tensor& X) {
Tensor diff = math::subtract(X, I_nk);
return math::sum(
math::subtract(
math::add(math::square(diff), A),
math::multiply(math::cos(math::multiply(diff, two_pi)), A)),
{1, 2});
});
// --- CMAES: noise schedule derived from swarm covariance, no learning rate needed ---
StiefelIsotropicSolverCMAESConfig cmaes_config{
.N = 200,
.n = n,
.k = k,
.convergence = std::make_shared<MaxStepsCriterion>(400),
.delta = 1.5,
.debug = {Debugger::Log}
};
StiefelIsotropicSolverCMAES cmaes_solver(cmaes_config);
CBOResult cmaes_result = cmaes_solver.solve(&objective);
std::cout << "[CMAES] Energy: " << cmaes_result.min_energy
<< " | Steps: " << cmaes_result.iterations_run << "\n";
// --- ADAM: noise scale driven by Adam moment estimates; tune delta to control contraction ---
StiefelIsotropicSolverADAMConfig adam_config{
.N = 200,
.n = n,
.k = k,
.convergence = std::make_shared<MaxStepsCriterion>(400),
.delta = 1.5,
.debug = {Debugger::Log}
};
StiefelIsotropicSolverADAM adam_solver(adam_config);
CBOResult adam_result = adam_solver.solve(&objective);
std::cout << "[ADAM] Energy: " << adam_result.min_energy
<< " | Steps: " << adam_result.iterations_run << "\n";
return 0;
}The product solver optimizes over a Cartesian product of manifold components simultaneously. Each component is specified with ManifoldSpec::SO(n, lambda, delta) or ManifoldSpec::Stiefel(n, k, lambda, delta). The objective receives a std::vector<Tensor> — one tensor per component — and returns a single energy tensor over the particle batch.
CMAES adapts each component's noise scale independently from its local covariance, making it especially well suited to mixed-dimension products where components have very different geometries. ADAM applies a shared Adam-style schedule across all components; useful when the landscape is smooth and you want the noise to decay faster.
#include <involute/solvers/isotropic/product_isotropic_solver_cmaes.hpp>
#include <involute/solvers/isotropic/product_isotropic_solver_adam.hpp>
#include <involute/core/math.hpp>
#include <iostream>
#include <vector>
using namespace involute;
using namespace involute::core;
using namespace involute::solvers;
int main() {
sampler::set_num_threads(8);
math::set_default_device_gpu();
// SO(3) x SO(2)^23 — 24-joint humanoid configuration space
std::vector<int> dims = {
3, // base orientation SO(3)
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // left arm + spine SO(2)^11
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 // right arm + legs SO(2)^12
};
// Build identity targets for each joint
std::vector<Tensor> targets;
for (int d : dims)
targets.push_back(math::eye(d, DType::Float32));
// Rastrigin on the product space, minimum at all-identity
FuncProductObj objective([dims, targets](const std::vector<Tensor>& X) {
const float A = 10.0f;
const float two_pi = 2.0f * 3.14159265f;
Tensor total = Tensor(0.0f, DType::Float32);
for (size_t i = 0; i < X.size(); ++i) {
Tensor diff = math::divide(math::subtract(X[i], targets[i]),
Tensor(4.0f, DType::Float32));
Tensor cos_term = math::cos(math::multiply(diff, Tensor(two_pi, DType::Float32)));
Tensor rastrigin = math::multiply(
Tensor(A, DType::Float32),
math::subtract(Tensor(static_cast<float>(dims[i] * dims[i]), DType::Float32),
math::sum(cos_term, {1, 2})));
total = math::add(total,
math::add(math::sum(math::square(diff), {1, 2}), rastrigin));
}
return total;
});
std::vector<ManifoldSpec> manifolds;
for (int d : dims)
manifolds.push_back(ManifoldSpec::SO(d, 1.0, 2.5));
// --- CMAES: each component's noise scale adapted independently, no learning rate needed ---
ProductIsotropicSolverCMAESConfig cmaes_config{
.manifolds = manifolds,
.N = 500,
.convergence = std::make_shared<MaxStepsCriterion>(1000),
.debug = {Debugger::Log}
};
ProductIsotropicSolverCMAES cmaes_solver(cmaes_config);
ProductCBOResult cmaes_result = cmaes_solver.solve(&objective);
std::cout << "[CMAES] Energy: " << cmaes_result.min_energy
<< " | Steps: " << cmaes_result.iterations_run << "\n";
// --- ADAM: shared Adam moment schedule; tune delta per ManifoldSpec for faster contraction ---
ProductIsotropicSolverADAMConfig adam_config{
.manifolds = manifolds,
.N = 500,
.convergence = std::make_shared<MaxStepsCriterion>(1000),
.debug = {Debugger::Log}
};
ProductIsotropicSolverADAM adam_solver(adam_config);
ProductCBOResult adam_result = adam_solver.solve(&objective);
std::cout << "[ADAM] Energy: " << adam_result.min_energy
<< " | Steps: " << adam_result.iterations_run << "\n";
return 0;
}A pre-print is expected in July 2026.
Consensus-Based Optimization (CBO) maintains a swarm of
The metric-adjusted coefficient
These dynamics inspire a more efficient class of algorithms via consensus freezing (Fornasier et al.). Holding
With
Rather than integrating this SDE forward — accumulating Euler–Maruyama error and risking constraint violation — each particle is replaced at each step by an exact i.i.d. draw from
Exact sampling from
where
Replacing the metric-adjusted noise with a fixed
The name refers to a core property of globally symmetric spaces. A manifold is locally symmetric if its Riemann curvature tensor is covariantly constant (