Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The following is a set of guidelines to [`Manopt.jl`](https://juliamanifolds.git
- [Code style](#Code-style)
- [Concerning the documentation](#Concerning-the-documentation)
- [Spell checking](#Spell-checking)
- [On the use of AI](#On-the-use-of-AI)

## I just have a question

The developer can most easily be reached in the Julia Slack channel [#manifolds](https://julialang.slack.com/archives/CP4QF0K5Z).
Expand Down
10 changes: 10 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The file was started with Version `0.4`.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.36] April 24, 2026

### Added

* a function `stopped_at(state)` to access the number of iterations it took a solver to stop. (#599)

### Fixed

* a small bug where `get_count(sc::StopWhenAny, Val(:Iteration))` wrongly reported it stopped before the first iteration when it actually did not yet stop. (#599)

## [0.5.35] April 16, 2026

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Manopt"
uuid = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5"
version = "0.5.35"
version = "0.5.36"
authors = [{family-names = "Bergmann", given-names = "Ronny", alias = "kellertuer", city = "Trondheim", affiliation = "Norwegian University of Science and Technology", country = "NO", email = "manopt@ronnybergmann.net", orcid = "https://orcid.org/0000-0001-8342-7218", website = "https://ronnybergmann.net"}]

[workspace]
Expand Down
1 change: 1 addition & 0 deletions docs/src/plans/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ AbstractManoptSolverState
get_state
Manopt.get_count
Manopt.has_converged(::AbstractManoptSolverState)
stopped_at
```

Since every subtype of an [`AbstractManoptSolverState`](@ref) directly relate to a solver,
Expand Down
2 changes: 1 addition & 1 deletion src/Manopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ export StopAfter,
StopWhenSwarmVelocityLess,
StopWhenTrustRegionIsExceeded
export get_active_stopping_criteria,
get_stopping_criteria, get_reason, get_stopping_criterion
get_stopping_criteria, get_reason, get_stopping_criterion, stopped_at
#
# Exports
export asymptote_export_S2_signals, asymptote_export_S2_data, asymptote_export_SPD
Expand Down
12 changes: 12 additions & 0 deletions src/plans/solver_state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@ Make a copy of tangent vector `X` from manifold `M` for storage in [`StoreStateA
_storage_copy_vector(M::AbstractManifold, X) = copy(M, X)
_storage_copy_vector(::AbstractManifold, X::Number) = StorageRef(X)

@doc """
stopped_at(state::AbstractManoptSolverState)

Return the number of iterations the solver represented by the `state` took to stop.
If the solver has not yet stopped, this function returns `-1`.

By default, this function calls `get_count` function on the state's stopping criterion to access its `:Iteration` count.
"""
function stopped_at(state::AbstractManoptSolverState)
return get_count(get_stopping_criterion(state), Val(:Iterations))
end

@doc """
StoreStateAction <: AbstractStateAction

Expand Down
2 changes: 1 addition & 1 deletion src/plans/stopping_criterion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,7 @@ function has_converged(c::StopWhenAny)
end
function get_count(c::StopWhenAny, v::Val{:Iterations})
iters = filter(x -> x > 0, [get_count(ci, v) for ci in c.criteria])
(length(iters) == 0) && (return 0)
(length(iters) == 0) && (return -1) # None indicated to stop yet, so we also do not
return minimum(iters)
end
function show(io::IO, c::StopWhenAny)
Expand Down
1 change: 1 addition & 0 deletions test/plans/test_gradient_plan.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ using ManifoldsBase, Manopt, Test
stopping_criterion = StopAfterIteration(20),
stepsize = Manopt.ConstantStepsize(M),
)
@test stopped_at(gst) == -1
Comment thread
kellertuer marked this conversation as resolved.
set_iterate!(gst, M, q)
@test get_iterate(gst) == q
set_gradient!(gst, M, p, [1.0, 0.0])
Expand Down
2 changes: 1 addition & 1 deletion test/plans/test_state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct NoIterateState <: AbstractManoptSolverState end
@test_throws ErrorException get_iterate(s2)
end

@testset "Iteration and Gradient setters" begin
@testset "Iterate and Gradient setters" begin
M = Euclidean(3)
s1 = NelderMeadState(M)
s2 = GradientDescentState(M)
Expand Down
56 changes: 12 additions & 44 deletions test/solvers/test_gradient_descent.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ using ManifoldDiff: grad_distance
500,
)
s = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(200) | StopWhenChangeLess(M, 1.0e-16),
stepsize = ArmijoLinesearch(; contraction_factor = 0.99),
debug = d,
Expand All @@ -38,10 +35,7 @@ using ManifoldDiff: grad_distance
res_debug = String(take!(my_io))
@test res_debug === " f(x): 1.357071\n"
p2 = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(200) | StopWhenChangeLess(M, 1.0e-16),
stepsize = ArmijoLinesearch(; contraction_factor = 0.99),
)
Expand All @@ -56,10 +50,7 @@ using ManifoldDiff: grad_distance
stop_when_stepsize_exceeds = 0.9 * π,
)
p3 = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
stepsize = step,
debug = [], # do not warn about increasing step here
Expand All @@ -75,10 +66,7 @@ using ManifoldDiff: grad_distance
stop_when_stepsize_exceeds = 0.9 * π,
)
p4 = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
stepsize = step2,
debug = [], # do not warn about increasing step here
Expand All @@ -94,40 +82,28 @@ using ManifoldDiff: grad_distance
stop_when_stepsize_exceeds = 0.9 * π,
)
p5 = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
stepsize = step3,
debug = [], # do not warn about increasing step here
)
@test isapprox(M, p, p5; atol = 1.0e-13)
p6 = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
direction = Nesterov(; p = copy(M, data[1])),
)
@test isapprox(M, p, p6; atol = 1.0e-13)
# Precon in simple scale down by 2
p7 = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
direction = PreconditionedDirection((M, p, X) -> 0.5 .* X),
)
@test isapprox(M, p, p7; atol = 1.0e-13)
# Precon in simple scale down by 2 – inplace
p8 = gradient_descent(
M,
f,
grad_f,
data[1];
M, f, grad_f, data[1];
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
direction = PreconditionedDirection(
(M, Y, p, X) -> (Y .= 0.5 .* X); evaluation = InplaceEvaluation()
Expand Down Expand Up @@ -170,20 +146,14 @@ using ManifoldDiff: grad_distance
# `gradient_descent` allocated n2 newly
@test isapprox(M, north, n2a)
n3 = gradient_descent(
M,
f,
grad_f,
pts[1];
M, f, grad_f, pts[1];
direction = MomentumGradient(),
stepsize = ConstantLength(),
debug = [], # do not warn about increasing step here
)
@test isapprox(M, north, n3)
n4 = gradient_descent(
M,
f,
grad_f,
pts[1];
M, f, grad_f, pts[1];
direction = AverageGradient(M; n = 5),
stopping_criterion = StopAfterIteration(800),
)
Expand All @@ -194,14 +164,12 @@ using ManifoldDiff: grad_distance
@test startswith(repr(r), "# Solver state for `Manopt.jl`s Gradient Descent")
# State and a count objective, putting stats behind print
n6 = gradient_descent(
M,
f,
grad_f,
pts[1];
M, f, grad_f, pts[1];
count = [:Gradient],
return_objective = true,
return_state = true,
)
@test stopped_at(n6[2]) > 0
@test repr(n6) == "$(n6[2])\n\n$(n6[1])"
end
@testset "Tutorial mode" begin
Expand Down
Loading