[ANN] OptimizationVariableInterface.jl: Zero-overhead manifold retractions and custom variables for numerical optimizers

Hello Julia Community!

I am pleased to announce OptimizationVariableInterface.jl (or OVI for short)—a lightweight, zero-overhead contract between numerical optimization algorithms and decision variable representations.


The Motivation: Manifolds and Constrained Parameters

In numerical optimization, solvers traditionally assume decision variables live in a flat Euclidean space \mathbb{R}^n, updating state via standard vector addition:

x_{k+1} = x_k + \Delta x

However, in many practical engineering, robotics, computer vision, and scientific problems, variables belong to non-Euclidean manifolds or constrained parameter spaces:

  • 3D rotations (SO(3)) and rigid-body poses (SE(3))
  • Unit vectors / directional parameters on spheres (S^2)
  • Strictly positive physical quantities (x > 0) or probabilities/fractions (x \in (0, 1))
  • Heterogeneous problem states (e.g., bundle adjustment combining thousands of camera poses, 3D landmarks, and calibration coefficients)

In these spaces, naive vector addition yields invalid parameters (e.g., breaking matrix orthogonality or violating boundary conditions). Instead, optimization algorithms calculate a search step \Delta x in the tangent space T_x\mathcal{M} \cong \mathbb{R}^d and map it back onto the manifold via a retraction:

x_{k+1} = \mathcal{R}_x(\Delta x)

Bridging the Manifold Gap in Julia Optimization

Despite Julia having a world-class optimization ecosystem, very few general-purpose optimization packages—chiefly Manopt.jl and NLLSsolver.jl—currently support optimization on manifolds.

Because most solvers strictly expect x::AbstractVector{<:Real}, practitioners working with non-Euclidean parameters often resort to fragile workarounds:

  • Flattening variables into raw vectors and applying ad-hoc post-step normalizations (e.g., re-normalizing quaternions after Euclidean steps, which can break convergence guarantees and line searches).
  • Adding artificial non-linear constraints or penalty functions to enforce manifold membership.
  • Being restricted solely to the specific algorithms implemented in dedicated manifold libraries.

Yet, supporting retraction-based manifold optimization in standard solvers is surprisingly simple! In algorithms like Gradient Descent, Newton, Gauss-Newton, quasi-Newton, or Levenberg-Marquardt, the linear systems and step directions already live in the local tangent space \mathbb{R}^d. The only missing piece is replacing x .+= step with a retraction step \mathcal{R}_x(\Delta x).

OptimizationVariableInterface.jl was designed to bridge this gap: solver packages don’t need to take on heavy dependencies or re-architect their algorithms, yet their users instantly gain the ability to optimize over arbitrary manifolds and composite states.


Why Use OptimizationVariableInterface.jl?

OVI was originally designed to interface between two as-yet unreleased packages—one for problem modeling (NLLSModel.jl) and one for numerical optimization (SecondOrderOptimization.jl / SOO).

However, the contract is completely generic and creates an open, two-sided bridge: any modeling package that implements the interface becomes instantly compatible with any optimizer that uses it.

Key benefits include:

  1. Decouples Solvers from Geometry: Solvers only operate on Euclidean tangent vectors \mathbb{R}^d and retractions, keeping solver code clean and geometry-agnostic.
  2. Zero Overhead & Allocation-Free: Built for high-performance loops with full support for compile-time static dimensions (Static.jl, StaticArrays.jl), in-place mutation for arrays, and zero-allocation passes for immutable structs.
  3. Composable Variable Blocks: Heterogeneous collections (e.g., Vector or SVector containing multiple distinct variable types) automatically compose their tangent degrees of freedom and unroll retractions.
  4. Lightweight Core: The core package has minimal dependencies (Static and StaticArrays), keeping overhead minimal for solvers that depend on it.

Seamless Integration with ManifoldsBase.jl & Manifolds.jl

You don’t need to write custom manifold geometry from scratch. Through a Julia 1.10+ package extension (OVIManifoldsBaseExt), any manifold from ManifoldsBase.jl or Manifolds.jl can be used as an optimization variable via ManifoldVar:

using OptimizationVariableInterface, ManifoldsBase, StaticArrays

# A 2-sphere manifold embedded in ℝ³
M = ManifoldsBase.DefaultManifold(3; parameter = :type)
p0 = @SVector [0.0, 0.0, 1.0]

# Wrap point with its manifold context
var = ManifoldVar(p0, M)
nvars(var) # 3 (intrinsic degrees of freedom)

# Retract along a 3D tangent vector
step = [0.1, -0.2, 0.0]
var_next = update(var, step)

Smart By-Value vs. By-Reference Storage

When optimizing large-scale problems with thousands of points (e.g., bundle adjustment or point-cloud registration), storing full manifold structs per variable can cause severe memory bloat. ManifoldVar intelligently handles this:

  • Small isbits manifolds (\le 16 bytes) or type-level singletons are stored inline by-value with zero pointer indirection.
  • Larger manifolds are automatically stored by-reference (byref = true). Every variable holds a single 8-byte pointer to a shared context, allowing 100,000 points to share one manifold instance without duplicate allocations.

How Easy Is It to Modify an Existing Optimization Package?

Adopting OptimizationVariableInterface.jl in an existing optimizer is straightforward. An optimizer only interacts with four core functions:

Function Role Invariant / Behavior
nvars(x) Tangent dimension Determines degrees of freedom to size gradients, Hessians, and linear systems. Falls back to Base.length(x).
copyvars(x) Buffer allocation Preallocates candidate/scratch buffers (0 allocations for immutables; shallow copy for isbits arrays).
update!(to, from, step) Unified retraction Retracts state along tangent step. Mutates in-place for Vectors; returns new instance for immutables.
syncvars!(to, from) State synchronization Copies elements in-place for mutable Vectors; returns from for immutables.

The “Before vs. After” Diff

Because default fallbacks are provided (nvars(x) = length(x) and update falls back to +), updating an existing solver requires replacing just a few lines—while remaining 100% backward-compatible with standard Vector{Float64}:

  function optimize(fdf!, x0)
-     n = length(x0)
+     n = dynamic(nvars(x0))
      g = zeros(Float64, n)
      H = zeros(Float64, n, n)

-     x = copy(x0)
-     x_next = copy(x0)
+     x = copyvars(x0)
+     x_next = copyvars(x0)

      while !converged
          fdf!(H, g, x)
          step = -(H \ g)

-         x_next .= x .+ step
+         x_next = update!(x_next, x, step)

          if accepted
-             x .= x_next
+             x = syncvars!(x, x_next)
          end
      end
      return x
  end

Example: Defining a Custom Variable Type

Defining your own variable (such as an angle on the circle S^1 that wraps modulo 2\pi) takes just two method extensions:

using OptimizationVariableInterface, Static
import OptimizationVariableInterface: nvars, update

# Custom variable type: an angle on the circle S¹
struct CircleAngle{T<:AbstractFloat}
    theta::T
end

# 1. Tangent-space degrees of freedom (static allows compile-time optimizations)
nvars(::CircleAngle) = static(1)

# 2. Retraction: step starts at offset `start` in the concatenated step vector
update(var::CircleAngle, step, start=1) = CircleAngle(mod2pi(var.theta + step[start]))

CircleAngle can now be passed directly to any optimizer that speaks OptimizationVariableInterface, or mixed into heterogeneous composite states:

# Composite problem state with mixed variable types:
vars = [CircleAngle(0.0), [1.0, 2.0], ZeroToInfScalar(5.0)]
nvars(vars) # 1 + 2 + 1 = 4 total degrees of freedom

Example: A Minimal Manifold Newton Optimizer in Action

Here is a complete, working Newton optimizer demonstrating how clean solver code remains:

using OptimizationVariableInterface, LinearAlgebra

function optimize(fdf!, x0; max_iters=20, tol=1e-6)
    n = dynamic(nvars(x0))
    g = zeros(Float64, n)
    H = zeros(Float64, n, n)

    x = copyvars(x0)
    x_next = copyvars(x0)

    for iter in 1:max_iters
        fdf!(H, g, x)
        if norm(g) < tol
            break
        end

        step = -(H \ g)
        x_next = update!(x_next, x, step)
        x = syncvars!(x, x_next)
    end

    return x
end

# Minimize f(θ) = 1 - cos(θ - 1.0) on CircleAngle
function cost_grad_hess!(H, g, a::CircleAngle)
    d = a.theta - 1.0
    g[1] = sin(d)
    H[1, 1] = max(cos(d), 1e-4)
    return 1.0 - cos(d)
end

x_opt = optimize(cost_grad_hess!, CircleAngle(0.2))
# Result: CircleAngle(1.0)

Because update!, copyvars, and syncvars! handle both immutable structs and mutable vectors transparently, this single function works out of the box with standard floats, static arrays, custom manifolds, or vectors of heterogeneous blocks.


Ecosystem and Feedback

OptimizationVariableInterface.jl was designed to connect two upcoming, soon-to-be-released packages:

  • NLLSModel.jl: for problem modeling (defining nonlinear least squares cost functions and composite manifold-aware variable states).
  • SecondOrderOptimization.jl (SOO): for numerical optimization (providing second-order Newton, Levenberg-Marquardt, dogleg, and trust-region solvers).

However, the broader goal is to foster an interoperable ecosystem where any modeling package can talk to any optimizer without bespoke adapters.

I would love to hear thoughts, feedback, and ideas from authors across the optimization, modeling, and manifold communities!

For Package Maintainers: If you maintain an existing optimization package (or a modeling library) and are interested in adopting the interface, please leave a comment below—I would be very happy to open a PR on your package implementing the required integration.

just a small comment for some solvers – like (quasi) Newton e.g. in a limited memory approach or trust region methods usually – one “combines” gradients, i.e. tangent vectors, then one would additionally need a vector transport (to approximate the parallel transport) additionally to the retraction (to approximate the exponential map), at least as soon as one leaves Lie groups, where one can represent all these in the same space, the Lie algebra.

For the lay-person: this means that the OVI interface is (currently) only suitable for optimizers that do not combine gradient information computed at different locations in the state space.