How to use callbacks
Ronny Bergmann
This tutorial illustrates how to use custom callbacks. Manopt.jl 0.6.2 introduces a generic way to work with callbacks within solvers. While simple debug and recording are available, callbacks offer a more flexible way to interact with a solver run.
The framework of callbacks allows to call user-defined functions to be called at specific places, referred to as “hooks” in the following, during the run of a solver.
using Manopt, Manifolds, Random, LinearAlgebra, ManifoldDiff
using ManifoldDiff: grad_distance
Random.seed!(42);We consider the same example as in the Introductory tutorial to compute the Riemannian center of mass
n = 100
σ = π / 8
M = Sphere(2)
p = 1 / sqrt(2) * [1.0, 0.0, 1.0]
data = [exp(M, p, σ * rand(M; vector_at=p)) for i in 1:n];
f(M, p) = sum(1 / (2 * n) * distance.(Ref(M), Ref(p), data) .^ 2)
grad_f(M, p) = sum(1 / n * grad_distance.(Ref(M), data, Ref(p)));And we also use gradient_descent as the example solver for this tutorial.
The general callback function is called at every hook a solver has. By default there are at least
:BeforeInitand:Initbefore and afterinitialize_solver!is executed,:BeforeStepand:Stepbefore and after astep_solver!is performed.:BeforeStopbefore the stopping criterion is evaluated, and:Stopwhen the stopping criterion indicated to stop.
There are two different variants to introduce callbacks: A single callback that is called at every hook and specific callbacks for single hooks.
A general callback for all hooks
The general callback function has to have the signature of the form
cb(hook::Symbol, problem::AbstractManoptProblem, state::AbstractManoptSolverState, k::Int)where the hook is the specific hook name currently calling cb, and problem and state can of course be more precisely specified to fit the solver you run.
As an example here, we generate a list of the Hooks visited when only performing the first step of the solver run. We collect them in a string, that starts before the initialisation, and generates a list for every step. All this is collected in a globak string
vhs = ""
function visited_hooks(hook, problem, state, k)
if hook === :BeforeInit # Reset string on start
global vhs = """
# Visited Hooks during this solver run
## Initialisation
"""
end
if hook === :BeforeStep # Start new list every step
vhs *= """
## Step #$(k)
"""
end # For all: Add an item of the current hook name
vhs *= "* :$(hook)\n"
return nothing
endTo pass this to the solver, the general callbacks = can be used. For the callback for all hooks, we can just pass the function we just defined. This fills the string vhs as follows
gradient_descent(
M, f, grad_f, data[1];
callbacks = visited_hooks,
stopping_criterion = StopAfterIteration(1),
)
vhs |> print# Visited Hooks during this solver run
## Initialisation
* :BeforeInit
* :Init
* :BeforeStop
## Step #1
* :BeforeStep
* :Stepsize
* :Step
* :BeforeStop
* :StopAnd we see that an additional hook is available: :Stepsize. This is placed after the step size is computed but before the actual step is taken.
Solvers might offer several further hooks. Given a state type, these can be seen calling
provided_callbacks(GradientDescentState)8-element Vector{Symbol}:
:Any
:BeforeInit
:BeforeStep
:BeforeStop
:Init
:Step
:Stop
:StepsizeWe further see the :Any hook. This is used internally to store the general fallback we constructed in this section.
Specific callbacks for single hooks.
If there is some computation that shall only be performed on a single hook, one can of course use the method from the last section and use the if variant as already used there as well. Alternatively, one can also specify a callback with a signature not requiring the symbol as before, that is
cb(problem::AbstractManoptProblem, state::AbstractManoptSolverState, k::Int)and add it to just one of the hooks. As an example, while recording allows to record fields, we maybe want to modify the state as well. Here – as a toy example – we want to modify the descent direction after the step size is computed but before the step is taken. We could for example shorten it a bit, but also only for the first 10 steps.
function stepsize_callback(problem, state, k)
if k <= 10
state.X ./= 2
(k==10) && (@info "Stop shortening direction at k=10")
end
endAdding this to a certain hook is done by passing a vector of Pairs to the callbacks = keyword.
gradient_descent(
M, f, grad_f, data[1]; callbacks = [:Stepsize => stepsize_callback]
);[ Info: Stop shortening direction at k=10Both approaches can also be mixed, by just adding visited_hooks into the callbacks = array. This is equivalent to passing :Any => visited_hooks as a pair therein as well.
Technical Details
This tutorial is cached. It was last run on the following package versions.
Status `~/work/Manopt.jl/Manopt.jl/tutorials/Project.toml`
[47edcb42] ADTypes v1.22.2
[6e4b80f9] BenchmarkTools v1.8.0
[5ae59095] Colors v0.13.1
[a0c0ee7d] DifferentiationInterface v0.7.20
[31c24e10] Distributions v0.25.129
[26cc04aa] FiniteDifferences v0.12.34
[f6369f11] ForwardDiff v1.4.1
[8ac3fa9e] LRUCache v1.6.2
[af67fdf4] ManifoldDiff v0.4.5
[1cead3c2] Manifolds v0.11.28
[3362f125] ManifoldsBase v2.5.0
[0fc0a36d] Manopt v0.6.2 `.`
[91a5bcdd] Plots v1.41.6
[731186ca] RecursiveArrayTools v4.3.4
[37e2e46d] LinearAlgebra v1.12.0
[9a3f8284] Random v1.11.0This tutorial was last rendered July 17, 2026, 6:51:56.