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
8 changes: 6 additions & 2 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ 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.23] unreleased
## [0.5.23] September 14, 2025

### Added
### Added

* `HybridCoefficient(args...)` conjugate gradient parameters.
* a function `has_converged(sc)` function for any `StoppingCriterion` to indicate that it _both_ has stopped and the reason is a convergence certificate.
Note that compared to the static evaluation of `indicates_convergence(sc)`, which is independent of the state of the criterion,
this is the dynamic variant to be used _after_ a solver has stopped.
* a `has_converged(::AbstractManoptSolverState)` function to check whether the solver has converged.

## [0.5.22] September 09, 2025

Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Manopt"
uuid = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5"
authors = ["Ronny Bergmann <manopt@ronnybergmann.net>"]
version = "0.5.22"
version = "0.5.23"

[deps]
ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
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 @@ -13,6 +13,7 @@ or a [`Stepsize`](@ref).
AbstractManoptSolverState
get_state
Manopt.get_count
Manopt.has_converged(::AbstractManoptSolverState)
```

Since every subtype of an [`AbstractManoptSolverState`](@ref) directly relate to a solver,
Expand Down
1 change: 1 addition & 0 deletions src/Manopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export get_state,
set_iterate!,
get_residuals,
get_residuals!,
has_converged,
linearized_forward_operator,
linearized_forward_operator!,
adjoint_linearized_operator,
Expand Down
7 changes: 7 additions & 0 deletions src/plans/solver_state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,13 @@ function get_count(ams::AbstractManoptSolverState, v::Val{:Iterations})
return get_count(ams.stop, v)
end

"""
has_converged(ams::AbstractManoptSolverState)

Return whether the solver has converged, based on the internal [`StoppingCriterion`](@ref).
"""
has_converged(ams::AbstractManoptSolverState) = has_converged(get_stopping_criterion(ams))

# in general, ignore printing the objective by default
function show(io::IO, t::Tuple{<:AbstractManifoldObjective, <:AbstractManoptSolverState})
return print(io, "$(t[2])")
Expand Down
5 changes: 4 additions & 1 deletion src/plans/stepsize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1844,7 +1844,7 @@ function show(io::IO, rdog::DistanceOverGradientsStepsize)
return print(io, s)
end

@doc raw"""
doc_DoG_main = raw"""
DistanceOverGradients(; kwargs...)
DistanceOverGradients(M::AbstractManifold; kwargs...)

Expand Down Expand Up @@ -1896,6 +1896,9 @@ On subsequent calls, the state is updated as implemented: ``G_t \leftarrow G_{t-
* `initial_distance=1e-3`: initial distance estimate ``ϵ``
* `use_curvature=false`: whether to include ``ζ_κ``
* `sectional_curvature_bound=0.0`: curvature lower bound ``κ`` (if known)
"""
@doc """
$(doc_DoG_main)

$(_note(:ManifoldDefaultFactory, "DistanceOverGradientsStepsize"))
"""
Expand Down
51 changes: 47 additions & 4 deletions src/plans/stopping_criterion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ abstract type StoppingCriterion end
"""
indicates_convergence(c::StoppingCriterion)

Return whether (true) or not (false) a [`StoppingCriterion`](@ref) does _always_
Return whether a [`StoppingCriterion`](@ref) does _always_
mean that, when it indicates to stop, the solver has converged to a
minimizer or critical point.

Expand All @@ -32,18 +32,41 @@ With `s1=StopAfterIteration(20)` and `s2=StopWhenGradientNormLess(1e-7)` the ind

* `indicates_convergence(s1)` is `false`
* `indicates_convergence(s2)` is `true`
* `indicates_convergence(s1 | s2)` is `false`, since this might also stop after 20 iterations
* `indicates_convergence(s1 | s2)` is `false`, since this might also stop after 20 iterations,
or in other words, for [`StopWhenAny`](@ref) _all_ its criteria have to indicate convergence, for this to return true.
* `indicates_convergence(s1 & s2)` is `true`, since `s2` is fulfilled if this stops.
"""
indicates_convergence(c::StoppingCriterion) = false

"""
has_converged(c::StoppingCriterion)

Return whether a [`StoppingCriterion`](@ref) that has indicated to stop _and_ is a stopping criterion
that allows to conclude that the corresponding solver has converged.

By default this is given by the static [`indicates_convergence`](@ref)`(c)` as well as
the test whether the stopping criterion has stopped.
For some stopping criteria, for example [`StopWhenAny`](@ref) a more advanced test can be done,
that is more precise.

# Examples
With `s1=StopAfterIteration(20)` and `s2=StopWhenGradientNormLess(1e-7)` we obtain

* `has_converged(s1)` is always `false` (even if it has stopped)
* `has_converged(s2)` is always `true` as soon as it has stopped
* `has_converged(s1 | s2)` is always `true` if it has stopped _and_ `s2` is the reason for that.
* `has_converged(s1 & s2)` is `true` as soon as the algorithm stopped, since here `s2` always
"""
has_converged(c::StoppingCriterion) = indicates_convergence(c) && (get_count(c, Val(:Iterations)) >= 0)

function get_count(c::StoppingCriterion, ::Val{:Iterations})
if hasfield(typeof(c), :at_iteration)
return getfield(c, :at_iteration)
else
return 0
end
end

@doc """
StoppingCriterionGroup <: StoppingCriterion

Expand Down Expand Up @@ -1057,12 +1080,20 @@ end
function indicates_convergence(c::StopWhenAll)
return any(indicates_convergence(ci) for ci in c.criteria)
end
function has_converged(c::StopWhenAll)
# All are active
if length(get_active_stopping_criteria(c)) == length(c.criteria)
# at least one of them does has_converged as well
return any(has_converged(ci) for ci in c.criteria)
end
return false
end
function get_count(c::StopWhenAll, v::Val{:Iterations})
return maximum(get_count(ci, v) for ci in c.criteria)
end
function show(io::IO, c::StopWhenAll)
s = replace(status_summary(c), "\n" => "\n ") #increase indent
return print(io, "StopWhenAll with the Stopping Criteria\n $(s)")
return print(io, "StopWhenAll with the stopping criteria\n $(s)")
end

"""
Expand Down Expand Up @@ -1149,7 +1180,11 @@ function status_summary(c::StopWhenAny)
return "$(r)Overall: $s"
end
function indicates_convergence(c::StopWhenAny)
return any(indicates_convergence(ci) for ci in get_active_stopping_criteria(c))
return all(indicates_convergence(ci) for ci in c.criteria)
end
function has_converged(c::StopWhenAny)
# If any of the active ones has_converged – we stop due to convergence
return any(has_converged(ci) for ci in get_active_stopping_criteria(c))
end
function get_count(c::StopWhenAny, v::Val{:Iterations})
iters = filter(x -> x > 0, [get_count(ci, v) for ci in c.criteria])
Expand Down Expand Up @@ -1338,6 +1373,10 @@ end
function indicates_convergence(sc::StopWhenRepeated)
return indicates_convergence(sc.stopping_criterion)
end
function has_converged(sc::StopWhenRepeated)
# When the inner one indicates convergence, this does as well
return has_converged(sc.stopping_criterion)
end
function show(io::IO, sc::StopWhenRepeated)
is = replace("$(sc.stopping_criterion)", "\n" => "\n ") #increase indent
return print(
Expand Down Expand Up @@ -1442,6 +1481,10 @@ end
function indicates_convergence(sc::StopWhenCriterionWithIterationCondition)
return indicates_convergence(sc.stopping_criterion)
end
function has_converged(sc::StopWhenCriterionWithIterationCondition)
# When the inner one indicates convergence, this does as well
return has_converged(sc.stopping_criterion)
end
function show(io::IO, sc::StopWhenCriterionWithIterationCondition)
has_stopped = (sc.at_iteration >= 0)
s = has_stopped ? "reached" : "not reached"
Expand Down
1 change: 1 addition & 0 deletions test/plans/test_state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ struct NoIterateState <: AbstractManoptSolverState end
set_gradient!(d2, M, p, X)
@test d2.state.X == ones(3)
@test get_stopping_criterion(d2) === s2.stop
@test has_converged(d2) === has_converged(s2)
end

@testset "Closed Form State" begin
Expand Down
37 changes: 37 additions & 0 deletions test/plans/test_stopping_criteria.jl
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ using Manifolds, ManifoldsBase, Manopt, ManoptTestSuite, Test, ManifoldsBase, Da
sc = StopWhenRepeated(s, 3)
sc2 = s × 3
@test Manopt.indicates_convergence(sc) == Manopt.indicates_convergence(s)
@test has_converged(sc) == has_converged(s)
@test get_reason(sc) == ""
@test startswith(repr(sc), "StopWhenRepeated with the Stopping Criterion:\n")
@test startswith(Manopt.status_summary(sc), "0 ≥ 3 (consecutive): not reached")
Expand All @@ -343,6 +344,7 @@ using Manifolds, ManifoldsBase, Manopt, ManoptTestSuite, Test, ManifoldsBase, Da

sc = StopWhenCriterionWithIterationCondition(s, 5)
@test Manopt.indicates_convergence(sc) == Manopt.indicates_convergence(s)
@test has_converged(sc) == has_converged(s)
@test get_reason(sc) == ""
@test startswith(
repr(sc),
Expand All @@ -368,4 +370,39 @@ using Manifolds, ManifoldsBase, Manopt, ManoptTestSuite, Test, ManifoldsBase, Da
sc(mp, st, 0) # reset
@test length(get_reason(sc)) == 0
end

@testset "has_converged" begin
M = Euclidean(1)
pr = ManoptTestSuite.DummyProblem{typeof(M)}()
s1 = StopWhenGradientNormLess(1.0e-4)
s2 = StopAfterIteration(10)
s3 = s1 & s2
s4 = s1 | s2
@test Manopt.indicates_convergence(s3)
@test !Manopt.indicates_convergence(s4)
@test !has_converged(s1)
@test !has_converged(s2)
@test !has_converged(s3)
@test !has_converged(s4)
# Tweak s1 to be active
s1.at_iteration = 1
# Check default
@test has_converged(s1)
# s3 does not trigger because s2 is not active yet
@test !has_converged(s3)
# But s4 does since we stopped with s1 due to convergence
@test has_converged(s4)
# Set s2 active as well
s2.at_iteration = 11
@test !has_converged(s2) # But it does not indicate we converged
@test has_converged(s3) # Now this is fine as well
@test has_converged(s4) # This continues to indicate that.
# Deactivate s1
s1.at_iteration = -1
# Now neither s3 (not all active) nor s4 (the active one not converged) indicate convergence
@test !has_converged(s1)
@test !has_converged(s3)
@test !has_converged(s4)
end

end
Loading