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

**URL:** <https://discourse.julialang.org/t/ann-optimizationvariableinterface-jl-zero-overhead-manifold-retractions-and-custom-variables-for-numerical-optimizers/142831>\
**Category:** Package Announcements\
**Tags:** announcement, optimization, manifolds\
**Created:** [November 9, 2026, 2:12pm UTC](https://discourse.julialang.org/t/ann-optimizationvariableinterface-jl-zero-overhead-manifold-retractions-and-custom-variables-for-numerical-optimizers/142831 "2026-11-09T14:12:50Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [November 9, 2026, 2:12pm UTC](https://discourse.julialang.org/t/ann-optimizationvariableinterface-jl-zero-overhead-manifold-retractions-and-custom-variables-for-numerical-optimizers/142831/1 "2026-11-09T14:12:50Z")

</div>

Hello Julia Community!

I am pleased to announce [**OptimizationVariableInterface.jl**](https://github.com/ojwoodford/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**](https://github.com/JuliaManifolds/Manopt.jl) and [**NLLSsolver.jl**](https://github.com/ojwoodford/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**](https://github.com/ojwoodford/NLLSModel.jl)) and one for numerical optimization ([**SecondOrderOptimization.jl**](https://github.com/ojwoodford/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`](https://github.com/JuliaManifolds/ManifoldsBase.jl) or [`Manifolds.jl`](https://github.com/JuliaManifolds/Manifolds.jl) can be used as an optimization variable via `ManifoldVar`:

```julia
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 `Vector`s; returns new instance for immutables. |
| `syncvars!(to, from)` | State synchronization | Copies elements in-place for mutable `Vector`s; 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}`:

```diff
  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:

```julia
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:

```julia
# 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:

```julia
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**](https://github.com/ojwoodford/NLLSModel.jl): for problem modeling (defining nonlinear least squares cost functions and composite manifold-aware variable states).
- [**SecondOrderOptimization.jl**](https://github.com/ojwoodford/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.

- **Repository:** [https://github.com/ojwoodford/OptimizationVariableInterface.jl](https://github.com/ojwoodford/OptimizationVariableInterface.jl)
- **License:** MIT

---

<div class="post-metadata">

**Author:** ![kellertuer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kellertuer/32/220707_2.png) [@kellertuer](https://discourse.julialang.org/u/kellertuer)\
**Post date:** [November 9, 2026, 2:31pm UTC](https://discourse.julialang.org/t/ann-optimizationvariableinterface-jl-zero-overhead-manifold-retractions-and-custom-variables-for-numerical-optimizers/142831/2 "2026-11-09T14:31:31Z")

</div>

> [@user664303](#):
>
> quasi-Newton

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.

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [November 9, 2026, 4:14pm UTC](https://discourse.julialang.org/t/ann-optimizationvariableinterface-jl-zero-overhead-manifold-retractions-and-custom-variables-for-numerical-optimizers/142831/3 "2026-11-09T16:14:03Z")

</div>

> [@kellertuer](#):
>
> [if] one “combines” gradients, i.e. tangent vectors, then one would additionally need a vector transport

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.

---

<div class="post-metadata">

**Author:** ![kellertuer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kellertuer/32/220707_2.png) [@kellertuer](https://discourse.julialang.org/u/kellertuer)\
**Post date:** [November 9, 2026, 4:58pm UTC](https://discourse.julialang.org/t/ann-optimizationvariableinterface-jl-zero-overhead-manifold-retractions-and-custom-variables-for-numerical-optimizers/142831/4 "2026-11-09T16:58:35Z")

</div>

And that is also fine with me. I am also not saying this never works – in the right representation on Lie groups, parallel transport is the identity, same on Euclidean spaces – otherwise QN for example, some trust region methods or Bundle methods might not work well or not work at all.

And sure in other areas, Gradient descent for example, fits your scheme very well and there it is great to see another perspective on how manifolds can be made more accessible.

_edit:_ I think Optim.jl s approach to manifolds with their retractions is also quite close to what you are doing.
