diff --git a/Changelog.md b/Changelog.md index f861e4d565..f42f22762e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,13 @@ All notable Changes to the Julia package `Manopt.jl` will be documented in this 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.4.65] June 13, 2024 + +### Changed + +* refactor stopping criteria to not store a `sc.reason` internally, but instead only + generate the reason (and hence allocate a string) when actually asked for a reason. + ## [0.4.64] June 4, 2024 ### Added diff --git a/Project.toml b/Project.toml index 693c3f0970..46723d0f0d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Manopt" uuid = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5" authors = ["Ronny Bergmann "] -version = "0.4.64" +version = "0.4.65" [deps] ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" diff --git a/docs/src/plans/stopping_criteria.md b/docs/src/plans/stopping_criteria.md index b1025da707..731dfa1caf 100644 --- a/docs/src/plans/stopping_criteria.md +++ b/docs/src/plans/stopping_criteria.md @@ -16,7 +16,8 @@ The stopping criteria `s` might have certain internal values/fields it uses to v This is done when calling them as a function `s(amp::AbstractManoptProblem, ams::AbstractManoptSolverState)`, where the [`AbstractManoptProblem`](@ref) and the [`AbstractManoptSolverState`](@ref) together represent the current state of the solver. The functor returns either `false` when the stopping criterion is not fulfilled or `true` otherwise. -One field all criteria should have is the `s.reason`, a string giving the reason to stop, see [`get_reason`](@ref). +One field all criteria should have is the `s.at_iteration`, to indicate at which iteration +the stopping criterion (last) indicated to stop. `0` refers to an indication _before_ starting the algorithm, while any negative number meant the stopping criterion is not (yet) fulfilled. To can access a string giving the reason of stopping see [`get_reason`](@ref). ## Generic stopping criteria diff --git a/src/plans/bundle_plan.jl b/src/plans/bundle_plan.jl index 6229e198dd..9a61c2635f 100644 --- a/src/plans/bundle_plan.jl +++ b/src/plans/bundle_plan.jl @@ -92,7 +92,7 @@ For the [`proximal_bundle_method`](@ref), the equation reads ``-ν = μ \lvert d # Constructors - StopWhenLagrangeMultiplierLess(tolerance=1e-6; mode::Symbol=:estimate) + StopWhenLagrangeMultiplierLess(tolerance=1e-6; mode::Symbol=:estimate, names=nothing) Create the stopping criterion for one of the `mode`s mentioned. Note that tolerance can be a single number for the `:estimate` case, @@ -100,32 +100,59 @@ but a vector of two values is required for the `:both` mode. Here the first entry specifies the tolerance for ``ε`` (``c``), the second the tolerance for ``\lvert g \rvert`` (``\lvert d \rvert``), respectively. """ -mutable struct StopWhenLagrangeMultiplierLess{T<:Real,A<:AbstractVector{<:T}} <: - StoppingCriterion - tolerance::A +mutable struct StopWhenLagrangeMultiplierLess{ + T<:Real,A<:AbstractVector{<:T},B<:Union{Nothing,<:AbstractVector{<:String}} +} <: StoppingCriterion + tolerances::A + values::A + names::B mode::Symbol - reason::String at_iteration::Int - function StopWhenLagrangeMultiplierLess(tol::T; mode::Symbol=:estimate) where {T<:Real} - return new{T,Vector{T}}([tol], mode, "", 0) + function StopWhenLagrangeMultiplierLess( + tol::T; mode::Symbol=:estimate, names::B=nothing + ) where {T<:Real,B<:Union{Nothing,<:AbstractVector{<:String}}} + return new{T,Vector{T},B}([tol], zero([tol]), names, mode, -1) end function StopWhenLagrangeMultiplierLess( - tols::A; mode::Symbol=:estimate - ) where {T<:Real,A<:AbstractVector{<:T}} - return new{T,A}(tols, mode, "", 0) + tols::A; mode::Symbol=:estimate, names::B=nothing + ) where {T<:Real,A<:AbstractVector{<:T},B<:Union{Nothing,<:AbstractVector{<:String}}} + return new{T,A,B}(tols, zero(tols), names, mode, -1) + end +end +function get_reason(sc::StopWhenLagrangeMultiplierLess) + if (sc.at_iteration >= 0) + if isnothing(sc.names) + tol_str = join( + ["$ai < $bi" for (ai, bi) in zip(sc.values, sc.tolerances)], ", " + ) + else + tol_str = join( + [ + "$si = $ai < $bi" for + (si, ai, bi) in zip(sc.names, sc.values, sc.tolerances) + ], + ", ", + ) + end + return "After $(sc.at_iteration) iterations the algorithm reached an approximate critical point with tolerances $tol_str.\n" end + return "" end + function status_summary(sc::StopWhenLagrangeMultiplierLess) - s = length(sc.reason) > 0 ? "reached" : "not reached" - msg = "" - (sc.mode === :both) && (msg = " ε ≤ $(sc.tolerance[1]) and |g| ≤ $(sc.tolerance[2])") - (sc.mode === :estimate) && (msg = " -ξ ≤ $(sc.tolerance[1])") - return "Stopping parameter: $(msg) :\t$(s)" + s = (sc.at_iteration >= 0) ? "reached" : "not reached" + msg = "Lagrange multipliers" + isnothing(sc.names) && (msg *= " with tolerances $(sc.tolerances)") + if !isnothing(sc.names) + msg *= join(["$si < $bi" for (si, bi) in zip(sc.names, sc.tolerances)], ", ") + end + return "$(msg) :\t$(s)" end function show(io::IO, sc::StopWhenLagrangeMultiplierLess) + n = isnothing(sc.names) ? "" : ", $(names)" return print( io, - "StopWhenLagrangeMultiplierLess($(sc.tolerance); mode=:$(sc.mode))\n $(status_summary(sc))", + "StopWhenLagrangeMultiplierLess($(sc.tolerances); mode=:$(sc.mode)$n)\n $(status_summary(sc))", ) end diff --git a/src/plans/stopping_criterion.jl b/src/plans/stopping_criterion.jl index 7b5e3d11dc..b2aeddd9bb 100644 --- a/src/plans/stopping_criterion.jl +++ b/src/plans/stopping_criterion.jl @@ -61,6 +61,14 @@ store a threshold when to stop looking at the complete runtime. It uses `time_ns()` to measure the time and you provide a `Period` as a time limit, for example `Minute(15)`. +# Fields + +* `threshold` stores the `Period` after which to stop +* `start` stores the starting time when the algorithm is started, that is a call with `i=0`. +* `time` stores the elapsed time +* `at_iteration` indicates at which iteration (including `i=0`) the stopping criterion + was fulfilled and is `-1` while it is not fulfilled. + # Constructor StopAfter(t) @@ -69,34 +77,39 @@ initialize the stopping criterion to a `Period t` to stop after. """ mutable struct StopAfter <: StoppingCriterion threshold::Period - reason::String start::Nanosecond + time::Nanosecond at_iteration::Int function StopAfter(t::Period) return if value(t) < 0 error("You must provide a positive time period") else - new(t, "", Nanosecond(0), 0) + new(t, Nanosecond(0), Nanosecond(0), -1) end end end function (c::StopAfter)(::AbstractManoptProblem, ::AbstractManoptSolverState, i::Int) if value(c.start) == 0 || i <= 0 # (re)start timer - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 c.start = Nanosecond(time_ns()) + c.time = Nanosecond(0) else - cTime = Nanosecond(time_ns()) - c.start - if i > 0 && (cTime > Nanosecond(c.threshold)) - c.reason = "The algorithm ran for about $(floor(cTime, typeof(c.threshold))) and has hence reached the threshold of $(c.threshold).\n" + c.time = Nanosecond(time_ns()) - c.start + if i > 0 && (c.time > Nanosecond(c.threshold)) c.at_iteration = i return true end end return false end +function get_reason(c::StopAfter) + if (c.at_iteration >= 0) + return "The algorithm ran for $(floor(c.time, typeof(c.threshold))) (threshold: $(c.threshold)).\n" + end + return "" +end function status_summary(c::StopAfter) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "stopped after $(c.threshold):\t$s" end @@ -123,9 +136,9 @@ A functor for a stopping criterion to stop after a maximal number of iterations. # Fields -* `maxIter` stores the maximal iteration number where to stop at -* `reason` stores a reason of stopping if the stopping criterion has one be reached, - see [`get_reason`](@ref). +* `max_iterations` stores the maximal iteration number where to stop at +* `at_iteration` indicates at which iteration (including `i=0`) the stopping criterion + was fulfilled and is `-1` while it is not fulfilled. # Constructor @@ -134,32 +147,35 @@ A functor for a stopping criterion to stop after a maximal number of iterations. initialize the functor to indicate to stop after `maxIter` iterations. """ mutable struct StopAfterIteration <: StoppingCriterion - maxIter::Int - reason::String + max_iterations::Int at_iteration::Int - StopAfterIteration(mIter::Int) = new(mIter, "", 0) + StopAfterIteration(i::Int) = new(i, -1) end function (c::StopAfterIteration)( ::P, ::S, i::Int ) where {P<:AbstractManoptProblem,S<:AbstractManoptSolverState} if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end - if i >= c.maxIter + if i >= c.max_iterations c.at_iteration = i - c.reason = "The algorithm reached its maximal number of iterations ($(c.maxIter)).\n" return true end return false end +function get_reason(c::StopAfterIteration) + if c.at_iteration >= c.max_iterations + return "The algorithm reached its maximal number of iterations ($(c.max_iterations)).\n" + end + return "" +end function status_summary(c::StopAfterIteration) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" - return "Max Iteration $(c.maxIter):\t$s" + return "Max Iteration $(c.max_iterations):\t$s" end function show(io::IO, c::StopAfterIteration) - return print(io, "StopAfterIteration($(c.maxIter))\n $(status_summary(c))") + return print(io, "StopAfterIteration($(c.max_iterations))\n $(status_summary(c))") end """ @@ -168,7 +184,7 @@ end Update the number of iterations after which the algorithm should stop. """ function update_stopping_criterion!(c::StopAfterIteration, ::Val{:MaxIteration}, v::Int) - c.maxIter = v + c.max_iterations = v return c end @@ -194,48 +210,46 @@ default. You can also provide an inverse_retraction_method for the `distance` or to use its default inverse retraction. """ mutable struct StopWhenChangeLess{ - IRT<:AbstractInverseRetractionMethod,TSSA<:StoreStateAction + F,IRT<:AbstractInverseRetractionMethod,TSSA<:StoreStateAction } <: StoppingCriterion - threshold::Float64 - reason::String + threshold::F + last_change::F storage::TSSA inverse_retraction::IRT at_iteration::Int end function StopWhenChangeLess( M::AbstractManifold, - ε::Float64; + ε::F; storage::StoreStateAction=StoreStateAction(M; store_points=Tuple{:Iterate}), inverse_retraction_method::IRT=default_inverse_retraction_method(M), -) where {IRT<:AbstractInverseRetractionMethod} - return StopWhenChangeLess{IRT,typeof(storage)}( - ε, "", storage, inverse_retraction_method, 0 +) where {F<:Real,IRT<:AbstractInverseRetractionMethod} + return StopWhenChangeLess{F,IRT,typeof(storage)}( + ε, zero(ε), storage, inverse_retraction_method, -1 ) end function StopWhenChangeLess( - ε::Float64; + ε::F; storage::StoreStateAction=StoreStateAction([:Iterate]), manifold::AbstractManifold=DefaultManifold(), inverse_retraction_method::IRT=default_inverse_retraction_method(manifold), -) where {IRT<:AbstractInverseRetractionMethod} +) where {F,IRT<:AbstractInverseRetractionMethod} if !(manifold isa DefaultManifold) @warn "The `manifold` keyword is deprecated, use the first positional argument `M` instead." end - return StopWhenChangeLess{IRT,typeof(storage)}( - ε, "", storage, inverse_retraction_method, 0 + return StopWhenChangeLess{F,IRT,typeof(storage)}( + ε, zero(ε), storage, inverse_retraction_method, -1 ) end function (c::StopWhenChangeLess)(mp::AbstractManoptProblem, s::AbstractManoptSolverState, i) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end if has_storage(c.storage, PointStorageKey(:Iterate)) M = get_manifold(mp) p_old = get_storage(c.storage, PointStorageKey(:Iterate)) - d = distance(M, get_iterate(s), p_old, c.inverse_retraction) - if d < c.threshold && i > 0 - c.reason = "The algorithm performed a step with a change ($d) less than $(c.threshold).\n" + c.last_change = distance(M, get_iterate(s), p_old, c.inverse_retraction) + if c.last_change < c.threshold && i > 0 c.at_iteration = i c.storage(mp, s, i) return true @@ -244,8 +258,14 @@ function (c::StopWhenChangeLess)(mp::AbstractManoptProblem, s::AbstractManoptSol c.storage(mp, s, i) return false end +function get_reason(c::StopWhenChangeLess) + if (c.last_change < c.threshold) && (c.at_iteration >= 0) + return "At iteration $(c.at_iteration) the algorithm performed a step with a change ($(c.last_change)) less than $(c.threshold).\n" + end + return "" +end function status_summary(c::StopWhenChangeLess) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "|Δp| < $(c.threshold): $s" end @@ -276,28 +296,35 @@ optimization problem from within a [`AbstractManoptProblem`](@ref), i.e `get_cos initialize the stopping criterion to a threshold `ε`. """ -mutable struct StopWhenCostLess <: StoppingCriterion - threshold::Float64 - reason::String +mutable struct StopWhenCostLess{F} <: StoppingCriterion + threshold::F + last_cost::F at_iteration::Int - StopWhenCostLess(ε::Float64) = new(ε, "", 0) + function StopWhenCostLess(ε::F) where {F<:Real} + return new{F}(ε, zero(ε), -1) + end end function (c::StopWhenCostLess)( p::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end - if i > 0 && get_cost(p, get_iterate(s)) < c.threshold - c.reason = "The algorithm reached a cost function value ($(get_cost(p,get_iterate(s)))) less than the threshold ($(c.threshold)).\n" + c.last_cost = get_cost(p, get_iterate(s)) + if c.last_cost < c.threshold c.at_iteration = i return true end return false end +function get_reason(c::StopWhenCostLess) + if (c.last_cost < c.threshold) && (c.at_iteration >= 0) + return "The algorithm reached a cost function value ($(c.last_cost)) less than the threshold ($(c.threshold)).\n" + end + return "" +end function status_summary(c::StopWhenCostLess) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "f(x) < $(c.threshold):\t$s" end @@ -329,7 +356,6 @@ Evaluate whether a certain fields change is less than a certain threshold # Internal fields -* `reason`: store a string reason when the stop was indicated * `at_iteration`: store the iteration at which the stop indication happened stores a threshold when to stop looking at the norm of the change of the @@ -350,28 +376,28 @@ mutable struct StopWhenEntryChangeLess{F,TF,TSSA<:StoreStateAction} <: StoppingC at_iteration::Int distance::F field::Symbol - reason::String storage::TSSA threshold::TF + last_change::TF end function StopWhenEntryChangeLess( field::Symbol, distance::F, threshold::TF; storage::TSSA=StoreStateAction([field]) ) where {F,TF,TSSA<:StoreStateAction} - return StopWhenEntryChangeLess{F,TF,TSSA}(0, distance, field, "", storage, threshold) + return StopWhenEntryChangeLess{F,TF,TSSA}( + -1, distance, field, storage, threshold, zero(threshold) + ) end function (sc::StopWhenEntryChangeLess)( mp::AbstractManoptProblem, s::AbstractManoptSolverState, i ) if i == 0 # reset on init - sc.reason = "" - sc.at_iteration = 0 + sc.at_iteration = -1 end if has_storage(sc.storage, sc.field) old_field_value = get_storage(sc.storage, sc.field) - ε = sc.distance(mp, s, old_field_value, getproperty(s, sc.field)) - if (i > 0) && (ε < sc.threshold) - sc.reason = "The algorithm performed a step with a change ($ε) in $(sc.field) less than $(sc.threshold).\n" + sc.last_change = sc.distance(mp, s, old_field_value, getproperty(s, sc.field)) + if (i > 0) && (sc.last_change < sc.threshold) sc.at_iteration = i sc.storage(mp, s, i) return true @@ -380,8 +406,14 @@ function (sc::StopWhenEntryChangeLess)( sc.storage(mp, s, i) return false end +function get_reason(sc::StopWhenEntryChangeLess) + if (sc.last_change < sc.threshold) && (sc.at_iteration >= 0) + return "At iteation $(sc.at_iteration) the algorithm performed a step with a change ($(sc.last_change)) in $(sc.field) less than $(sc.threshold).\n" + end + return "" +end function status_summary(sc::StopWhenEntryChangeLess) - has_stopped = length(sc.reason) > 0 + has_stopped = (sc.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "|Δ:$(sc.field)| < $(sc.threshold): $s" end @@ -422,24 +454,24 @@ indicates to stop when [`get_gradient`](@ref) is in (norm of) its change less th `vector_transport_method` denotes the vector transport ``\mathcal T`` used. """ mutable struct StopWhenGradientChangeLess{ - VTM<:AbstractVectorTransportMethod,TSSA<:StoreStateAction + F,VTM<:AbstractVectorTransportMethod,TSSA<:StoreStateAction } <: StoppingCriterion - threshold::Float64 - reason::String + threshold::F + last_change::F storage::TSSA vector_transport_method::VTM at_iteration::Int end function StopWhenGradientChangeLess( M::AbstractManifold, - ε::Float64; + ε::F; storage::StoreStateAction=StoreStateAction( M; store_points=Tuple{:Iterate}, store_vectors=Tuple{:Gradient} ), vector_transport_method::VTM=default_vector_transport_method(M), -) where {VTM<:AbstractVectorTransportMethod} - return StopWhenGradientChangeLess{VTM,typeof(storage)}( - ε, "", storage, vector_transport_method, 0 +) where {F,VTM<:AbstractVectorTransportMethod} + return StopWhenGradientChangeLess{F,VTM,typeof(storage)}( + ε, zero(ε), storage, vector_transport_method, -1 ) end function StopWhenGradientChangeLess( @@ -452,8 +484,7 @@ function (c::StopWhenGradientChangeLess)( ) M = get_manifold(mp) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end if has_storage(c.storage, PointStorageKey(:Iterate)) && has_storage(c.storage, VectorStorageKey(:Gradient)) @@ -462,9 +493,8 @@ function (c::StopWhenGradientChangeLess)( X_old = get_storage(c.storage, VectorStorageKey(:Gradient)) p = get_iterate(s) Xt = vector_transport_to(M, p_old, X_old, p, c.vector_transport_method) - d = norm(M, p, Xt - get_gradient(s)) - if d < c.threshold && i > 0 - c.reason = "At iteration $i the change of the gradient ($d) was less than $(c.threshold).\n" + c.last_change = norm(M, p, Xt - get_gradient(s)) + if c.last_change < c.threshold && i > 0 c.at_iteration = i c.storage(mp, s, i) return true @@ -473,8 +503,14 @@ function (c::StopWhenGradientChangeLess)( c.storage(mp, s, i) return false end +function get_reason(c::StopWhenGradientChangeLess) + if (c.last_change < c.threshold) && (c.at_iteration >= 0) + return "At iteration $(c.at_iteration) the change of the gradient ($(c.last_change)) was less than $(c.threshold).\n" + end + return "" +end function status_summary(c::StopWhenGradientChangeLess) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "|Δgrad f| < $(c.threshold): $s" end @@ -509,8 +545,8 @@ A stopping criterion based on the current gradient norm. # Internal fields -* `reason`: store a string reason when the stop was indicated -* `at_iteration`: store the iteration at which the stop indication happened +* `last_change` store the last change +* `at_iteration` store the iteration at which the stop indication happened # Constructor @@ -522,11 +558,11 @@ where the norm to use can be specified in the `norm=` keyword. """ mutable struct StopWhenGradientNormLess{F,TF} <: StoppingCriterion norm::F - threshold::Float64 - reason::String + threshold::TF + last_change::TF at_iteration::Int function StopWhenGradientNormLess(ε::TF; norm::F=norm) where {F,TF} - return new{F,TF}(norm, ε, "", 0) + return new{F,TF}(norm, ε, zero(ε), -1) end end @@ -535,21 +571,25 @@ function (sc::StopWhenGradientNormLess)( ) M = get_manifold(mp) if i == 0 # reset on init - sc.reason = "" - sc.at_iteration = 0 + sc.at_iteration = -1 end if (i > 0) - grad_norm = sc.norm(M, get_iterate(s), get_gradient(s)) - if grad_norm < sc.threshold - sc.reason = "The algorithm reached approximately critical point after $i iterations; the gradient norm ($(grad_norm)) is less than $(sc.threshold).\n" + sc.last_change = sc.norm(M, get_iterate(s), get_gradient(s)) + if sc.last_change < sc.threshold sc.at_iteration = i return true end end return false end +function get_reason(c::StopWhenGradientNormLess) + if (c.last_change < c.threshold) && (c.at_iteration >= 0) + return "The algorithm reached approximately critical point after $(c.at_iteration) iterations; the gradient norm ($(c.last_change)) is less than $(c.threshold).\n" + end + return "" +end function status_summary(c::StopWhenGradientNormLess) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "|grad f| < $(c.threshold): $s" end @@ -582,31 +622,35 @@ during the last iteration from within a [`AbstractManoptSolverState`](@ref). initialize the stopping criterion to a threshold `ε`. """ -mutable struct StopWhenStepsizeLess <: StoppingCriterion - threshold::Float64 - reason::String +mutable struct StopWhenStepsizeLess{F} <: StoppingCriterion + threshold::F + last_stepsize::F at_iteration::Int - function StopWhenStepsizeLess(ε::Float64) - return new(ε, "", 0) + function StopWhenStepsizeLess(ε::F) where {F<:Real} + return new{F}(ε, zero(ε), -1) end end function (c::StopWhenStepsizeLess)( p::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end - step = get_last_stepsize(p, s, i) - if step < c.threshold && i > 0 - c.reason = "The algorithm computed a step size ($step) less than $(c.threshold).\n" + c.last_stepsize = get_last_stepsize(p, s, i) + if c.last_stepsize < c.threshold && i > 0 c.at_iteration = i return true end return false end +function get_reason(c::StopWhenStepsizeLess) + if (c.last_stepsize < c.threshold) && (c.at_iteration >= 0) + return "The algorithm computed a step size ($(c.last_stepsize)) less than $(c.threshold).\n" + end + return "" +end function status_summary(c::StopWhenStepsizeLess) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "Stepsize s < $(c.threshold):\t$s" end @@ -635,26 +679,30 @@ stop looking at the cost function of the optimization problem from within a [`Ab initialize the stopping criterion to NaN. """ mutable struct StopWhenCostNaN <: StoppingCriterion - reason::String at_iteration::Int - StopWhenCostNaN() = new("", 0) + StopWhenCostNaN() = new(-1) end function (c::StopWhenCostNaN)( p::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end - if i > 0 && isnan(get_cost(p, get_iterate(s))) - c.reason = "The algorithm reached a cost function value ($(get_cost(p,get_iterate(s)))).\n" - c.at_iteration = 0 + # but still check + if isnan(get_cost(p, get_iterate(s))) + c.at_iteration = i return true end return false end +function get_reason(c::StopWhenCostNaN) + if c.at_iteration >= 0 + return "The algorithm reached a cost function value of NaN.\n" + end + return "" +end function status_summary(c::StopWhenCostNaN) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "f(x) is NaN:\t$s" end @@ -674,26 +722,29 @@ stop looking at the cost function of the optimization problem from within a [`Ab initialize the stopping criterion to NaN. """ mutable struct StopWhenIterateNaN <: StoppingCriterion - reason::String at_iteration::Int - StopWhenIterateNaN() = new("", 0) + StopWhenIterateNaN() = new(-1) end function (c::StopWhenIterateNaN)( p::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end - if i > 0 && any(isnan.(get_iterate(s))) - c.reason = "The algorithm reached a $(get_iterate(s)) iterate.\n" + if (i >= 0) && any(isnan.(get_iterate(s))) c.at_iteration = 0 return true end return false end +function get_reason(c::StopWhenIterateNaN) + if (c.at_iteration >= 0) + return "The algorithm reached an iterate containing NaNs iterate.\n" + end + return "" +end function status_summary(c::StopWhenIterateNaN) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "f(x) is NaN:\t$s" end @@ -710,8 +761,6 @@ A functor for an stopping criterion, where the algorithm if stopped when a varia * `value` stores the variable which has to fall under a threshold for the algorithm to stop * `minValue` stores the threshold where, if the value is smaller or equal to this threshold, the algorithm stops -* `reason` stores a reason of stopping if the stopping criterion has one be reached, - see [`get_reason`](@ref). # Constructor @@ -719,29 +768,34 @@ A functor for an stopping criterion, where the algorithm if stopped when a varia initialize the functor to indicate to stop after `value` is smaller than or equal to `minValue`. """ -mutable struct StopWhenSmallerOrEqual <: StoppingCriterion +mutable struct StopWhenSmallerOrEqual{R} <: StoppingCriterion value::Symbol - minValue::Real - reason::String + minValue::R at_iteration::Int - StopWhenSmallerOrEqual(value::Symbol, mValue::Real) = new(value, mValue, "", 0) + function StopWhenSmallerOrEqual(value::Symbol, mValue::R) where {R<:Real} + return new{R}(value, mValue, -1) + end end function (c::StopWhenSmallerOrEqual)( ::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end if getfield(s, c.value) <= c.minValue - c.reason = "The value of the variable ($(string(c.value))) is smaller than or equal to its threshold ($(c.minValue)).\n" c.at_iteration = i return true end return false end +function get_reason(c::StopWhenSmallerOrEqual) + if (c.at_iteration >= 0) + return "The value of the variable ($(string(c.value))) is smaller than or equal to its threshold ($(c.minValue)).\n" + end + return "" +end function status_summary(c::StopWhenSmallerOrEqual) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "Field :$(c.value) ≤ $(c.minValue):\t$s" end @@ -763,29 +817,34 @@ A stopping criterion based on the current subgradient norm. Create a stopping criterion with threshold `ε` for the subgradient, that is, this criterion indicates to stop when [`get_subgradient`](@ref) returns a subgradient vector of norm less than `ε`. """ -mutable struct StopWhenSubgradientNormLess <: StoppingCriterion +mutable struct StopWhenSubgradientNormLess{R} <: StoppingCriterion at_iteration::Int - threshold::Float64 - reason::String - StopWhenSubgradientNormLess(ε::Float64) = new(0, ε, "") + threshold::R + value::R + StopWhenSubgradientNormLess(ε::R) where {R<:Real} = new{R}(-1, ε, zero(ε)) end function (c::StopWhenSubgradientNormLess)( mp::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int ) M = get_manifold(mp) if (i == 0) # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end - if (norm(M, get_iterate(s), get_subgradient(s)) < c.threshold) && (i > 0) + c.value = norm(M, get_iterate(s), get_subgradient(s)) + if (c.value < c.threshold) && (i > 0) c.at_iteration = i - c.reason = "The algorithm reached approximately critical point after $i iterations; the subgradient norm ($(norm(M,get_iterate(s),get_subgradient(s)))) is less than $(c.threshold).\n" return true end return false end +function get_reason(c::StopWhenSubgradientNormLess) + if (c.value < c.threshold) && (c.at_iteration >= 0) + return "The algorithm reached approximately critical point after $(c.at_iteration) iterations; the subgradient norm ($(c.value)) is less than $(c.threshold).\n" + end + return "" +end function status_summary(c::StopWhenSubgradientNormLess) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "|∂f| < $(c.threshold): $s" end @@ -825,20 +884,26 @@ reasons. """ mutable struct StopWhenAll{TCriteria<:Tuple} <: StoppingCriterionSet criteria::TCriteria - reason::String - StopWhenAll(c::Vector{StoppingCriterion}) = new{typeof(tuple(c...))}(tuple(c...), "") - StopWhenAll(c...) = new{typeof(c)}(c, "") + at_iteration::Int + StopWhenAll(c::Vector{StoppingCriterion}) = new{typeof(tuple(c...))}(tuple(c...), -1) + StopWhenAll(c...) = new{typeof(c)}(c, -1) end function (c::StopWhenAll)(p::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int) - (i == 0) && (c.reason = "") # reset on init + (i == 0) && (c.at_iteration = -1) # reset on init if all(subC -> subC(p, s, i), c.criteria) - c.reason = string([get_reason(subC) for subC in c.criteria]...) + c.at_iteration = i return true end return false end +function get_reason(c::StopWhenAll) + if c.at_iteration >= 0 + return string([get_reason(subC) for subC in c.criteria]...) + end + return "" +end function status_summary(c::StopWhenAll) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" r = "Stop When _all_ of the following are fulfilled:\n" for cs in c.criteria @@ -900,9 +965,9 @@ concatenation of all reasons (assuming that all non-indicating return `""`). """ mutable struct StopWhenAny{TCriteria<:Tuple} <: StoppingCriterionSet criteria::TCriteria - reason::String - StopWhenAny(c::Vector{<:StoppingCriterion}) = new{typeof(tuple(c...))}(tuple(c...), "") - StopWhenAny(c::StoppingCriterion...) = new{typeof(c)}(c, "") + at_iteration::Int + StopWhenAny(c::Vector{<:StoppingCriterion}) = new{typeof(tuple(c...))}(tuple(c...), -1) + StopWhenAny(c::StoppingCriterion...) = new{typeof(c)}(c, -1) end # `_fast_any(f, tup::Tuple)`` is functionally equivalent to `any(f, tup)`` but on Julia 1.10 @@ -918,15 +983,21 @@ end end function (c::StopWhenAny)(p::AbstractManoptProblem, s::AbstractManoptSolverState, i::Int) - (i == 0) && (c.reason = "") # reset on init + (i == 0) && (c.at_iteration = -1) # reset on init if _fast_any(subC -> subC(p, s, i), c.criteria) - c.reason = string((get_reason(subC) for subC in c.criteria)...) + c.at_iteration = i return true end return false end +function get_reason(c::StopWhenAny) + if (c.at_iteration >= 0) + return string((get_reason(subC) for subC in c.criteria)...) + end + return "" +end function status_summary(c::StopWhenAny) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" r = "Stop When _one_ of the following are fulfilled:\n" for cs in c.criteria @@ -976,7 +1047,7 @@ function Base.:|(s1::StopWhenAny, s2::StopWhenAny) return StopWhenAny(s1.criteria..., s2.criteria...) end -is_active_stopping_criterion(c::StoppingCriterion) = !isempty(c.reason) +is_active_stopping_criterion(c::StoppingCriterion) = (c.at_iteration >= 0) @doc raw""" get_active_stopping_criteria(c) @@ -1002,23 +1073,6 @@ function get_active_stopping_criteria(c::sC) where {sC<:StoppingCriterion} end end -@doc raw""" - get_reason(c) - -return the current reason stored within a [`StoppingCriterion`](@ref) `c`. -This reason is empty if the criterion has never been met. -""" -get_reason(c::sC) where {sC<:StoppingCriterion} = c.reason - -@doc raw""" - get_reason(o) - -return the current reason stored within the [`StoppingCriterion`](@ref) from -within the [`AbstractManoptSolverState`](@ref) This reason is empty if the criterion has never -been met. -""" -get_reason(s::AbstractManoptSolverState) = get_reason(get_state(s).stop) - @doc raw""" get_stopping_criteria(c) @@ -1070,3 +1124,12 @@ end function update_stopping_criterion!(c::StoppingCriterion, ::Val, v) return c end + +@doc raw""" + get_reason(s::AbstractManoptSolverState) + +return the current reason stored within the [`StoppingCriterion`](@ref) from +within the [`AbstractManoptSolverState`](@ref). +This reason is empty (`""`) if the criterion has never been met. +""" +get_reason(s::AbstractManoptSolverState) = get_reason(get_state(s).stop) diff --git a/src/solvers/Lanczos.jl b/src/solvers/Lanczos.jl index 5486888bbc..121d65db35 100644 --- a/src/solvers/Lanczos.jl +++ b/src/solvers/Lanczos.jl @@ -244,22 +244,23 @@ m(X_k) \leq m(0) # Fields * `θ`: the factor ``θ`` in the second condition -* `reason`: a String indicating the reason if the criterion indicated to stop +* `at_iteration`: indicates at which iteration (including `i=0`) the stopping criterion + was fulfilled and is `-1` while it is not fulfilled. """ -mutable struct StopWhenFirstOrderProgress <: StoppingCriterion - θ::Float64 #θ - reason::String - StopWhenFirstOrderProgress(θ::Float64) = new(θ, "") +mutable struct StopWhenFirstOrderProgress{F} <: StoppingCriterion + θ::F + at_iteration::Int + StopWhenFirstOrderProgress(θ::F) where {F} = new{F}(θ, -1) end function (c::StopWhenFirstOrderProgress)( dmp::AbstractManoptProblem{<:TangentSpace}, ls::LanczosState, i::Int ) if (i == 0) if norm(ls.X) == zero(eltype(ls.X)) - c.reason = "The gradient of the gradient is zero." + c.at_iteration = 0 return true end - c.reason = "" + c.at_iteration = -1 return false end #Update Gradient @@ -272,14 +273,23 @@ function (c::StopWhenFirstOrderProgress)( ny = norm(y) model_grad_norm = norm(nX .* [1, zeros(i - 1)...] + Ty + ls.σ * ny * [y..., 0]) prog = (model_grad_norm <= c.θ * ny^2) - prog && (c.reason = "The algorithm has reduced the model grad norm by a factor $(c.θ).") + (prog) && (c.at_iteration = i) return prog end +function get_reason(c::StopWhenFirstOrderProgress) + if c.at_iteration > 0 + return "The algorithm has reduced the model grad norm by a factor $(c.θ)." + end + if c.at_iteration == 0 # gradient 0 + return "The gradient of the gradient is zero." + end + return "" +end function (c::StopWhenFirstOrderProgress)( dmp::AbstractManoptProblem{<:TangentSpace}, ams::AbstractManoptSolverState, i::Int ) if (i == 0) - c.reason = "" + c.at_iteration = -1 return false end TpM = get_manifold(dmp) @@ -290,12 +300,11 @@ function (c::StopWhenFirstOrderProgress)( # norm of current iterate nX = norm(base_manifold(TpM), p, q) prog = (nG <= c.θ * nX^2) - prog && (c.reason = "The algorithm has reduced the model grad norm by a factor $(c.θ).") + prog && (c.at_iteration = i) return prog end - function status_summary(c::StopWhenFirstOrderProgress) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "First order progress with θ=$(c.θ):\t$s" end @@ -317,7 +326,8 @@ Note that this stopping criterion (for now) is only implemented for the case tha # Fields * `maxLanczosVectors`: maximal number of Lanczos vectors -* `reason`: a String indicating the reason if the criterion indicated to stop +* `at_iteration` indicates at which iteration (including `i=0`) the stopping criterion + was fulfilled and is `-1` while it is not fulfilled. # Constructor @@ -326,23 +336,29 @@ Note that this stopping criterion (for now) is only implemented for the case tha """ mutable struct StopWhenAllLanczosVectorsUsed <: StoppingCriterion maxLanczosVectors::Int - reason::String - StopWhenAllLanczosVectorsUsed(maxLanczosVectors::Int) = new(maxLanczosVectors, "") + at_iteration::Int + StopWhenAllLanczosVectorsUsed(maxLanczosVectors::Int) = new(maxLanczosVectors, -1) end function (c::StopWhenAllLanczosVectorsUsed)( ::AbstractManoptProblem, arcs::AdaptiveRegularizationState{P,T,Pr,<:LanczosState}, i::Int, ) where {P,T,Pr} - (i == 0) && (c.reason = "") # reset on init + (i == 0) && (c.at_iteration = -1) # reset on init if (i > 0) && length(arcs.sub_state.Lanczos_vectors) == c.maxLanczosVectors - c.reason = "The algorithm used all ($(c.maxLanczosVectors)) preallocated Lanczos vectors and may have stagnated.\n Consider increasing this value.\n" + c.at_iteration = i return true end return false end +function get_reason(c::StopWhenAllLanczosVectorsUsed) + if (c.at_iteration >= 0) + return "The algorithm used all ($(c.maxLanczosVectors)) preallocated Lanczos vectors and may have stagnated.\n Consider increasing this value.\n" + end + return "" +end function status_summary(c::StopWhenAllLanczosVectorsUsed) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "All Lanczos vectors ($(c.maxLanczosVectors)) used:\t$s" end diff --git a/src/solvers/NelderMead.jl b/src/solvers/NelderMead.jl index d1c04997b5..519c286125 100644 --- a/src/solvers/NelderMead.jl +++ b/src/solvers/NelderMead.jl @@ -367,37 +367,42 @@ drops below a certain tolerance `tol_f` and `tol_p`, respectively. StopWhenPopulationConcentrated(tol_f::Real=1e-8, tol_x::Real=1e-8) """ -mutable struct StopWhenPopulationConcentrated{TF<:Real,TP<:Real} <: StoppingCriterion - tol_f::TF - tol_p::TP - reason::String +mutable struct StopWhenPopulationConcentrated{F<:Real} <: StoppingCriterion + tol_f::F + tol_p::F + value_f::F + value_p::F at_iteration::Int - function StopWhenPopulationConcentrated(tol_f::Real=1e-8, tol_p::Real=1e-8) - return new{typeof(tol_f),typeof(tol_p)}(tol_f, tol_p, "", 0) + function StopWhenPopulationConcentrated(tol_f::F=1e-8, tol_p::F=1e-8) where {F<:Real} + return new{F}(tol_f, tol_p, zero(tol_f), zero(tol_p), -1) end end function (c::StopWhenPopulationConcentrated)( mp::AbstractManoptProblem, s::NelderMeadState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end M = get_manifold(mp) - max_cdiff = maximum(cs -> abs(s.costs[1] - cs), s.costs[2:end]) - max_xdiff = maximum( + c.value_f = maximum(cs -> abs(s.costs[1] - cs), s.costs[2:end]) + c.value_p = maximum( p -> distance(M, s.population.pts[1], p, s.inverse_retraction_method), s.population.pts[2:end], ) - if max_cdiff < c.tol_f && max_xdiff < c.tol_p - c.reason = "After $i iterations the simplex has shrunk below the assumed level (maximum cost difference is $max_cdiff, maximum point distance is $max_xdiff).\n" + if c.value_f < c.tol_f && c.value_p < c.tol_p c.at_iteration = i return true end return false end +function get_reason(c::StopWhenPopulationConcentrated) + if (c.at_iteration >= 0) + return "After $(c.at_iteration) iterations the simplex has shrunk below the assumed level. Maximum cost difference is $(c.value_f) < $(c.tol_f), maximum point distance is $(c.value_p) < $(c.tol_p).\n" + end + return "" +end function status_summary(c::StopWhenPopulationConcentrated) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "Population concentration: in f < $(c.tol_f) and in p < $(c.tol_p):\t$s" end diff --git a/src/solvers/cma_es.jl b/src/solvers/cma_es.jl index 3b2929b480..16e49edbf3 100644 --- a/src/solvers/cma_es.jl +++ b/src/solvers/cma_es.jl @@ -531,7 +531,7 @@ mutable struct StopWhenCovarianceIllConditioned{T<:Real} <: StoppingCriterion at_iteration::Int end function StopWhenCovarianceIllConditioned(threshold::Real=1e14) - return StopWhenCovarianceIllConditioned{typeof(threshold)}(threshold, 1, 0) + return StopWhenCovarianceIllConditioned{typeof(threshold)}(threshold, 1, -1) end indicates_convergence(c::StopWhenCovarianceIllConditioned) = false @@ -540,7 +540,7 @@ function (c::StopWhenCovarianceIllConditioned)( ::AbstractManoptProblem, s::CMAESState, i::Int ) if i == 0 # reset on init - c.at_iteration = 0 + c.at_iteration = -1 return false end c.last_cond = s.covariance_matrix_cond @@ -556,7 +556,10 @@ function status_summary(c::StopWhenCovarianceIllConditioned) return "cond(s.covariance_matrix) > $(c.threshold):\t$s" end function get_reason(c::StopWhenCovarianceIllConditioned) - return "At iteration $(c.at_iteration) the condition number of covariance matrix ($(c.last_cond)) exceeded the threshold ($(c.threshold)).\n" + if c.at_iteration >= 0 + return "At iteration $(c.at_iteration) the condition number of covariance matrix ($(c.last_cond)) exceeded the threshold ($(c.threshold)).\n" + end + return "" end function show(io::IO, c::StopWhenCovarianceIllConditioned) return print( @@ -577,10 +580,11 @@ mutable struct StopWhenBestCostInGenerationConstant{TParam<:Real} <: StoppingCri iteration_range::Int best_objective_at_last_change::TParam iterations_since_change::Int + at_iteration::Int end function StopWhenBestCostInGenerationConstant{TParam}(iteration_range::Int) where {TParam} - return StopWhenBestCostInGenerationConstant{TParam}(iteration_range, Inf, 0) + return StopWhenBestCostInGenerationConstant{TParam}(iteration_range, Inf, 0, -1) end # It just indicates stagnation, not that convergence to a minimizer @@ -592,10 +596,12 @@ function (c::StopWhenBestCostInGenerationConstant)( ::AbstractManoptProblem, s::CMAESState, i::Int ) if i == 0 # reset on init + c.at_iteration = -1 c.best_objective_at_last_change = Inf return false end if c.iterations_since_change >= c.iteration_range + c.at_iteration = i return true else if c.best_objective_at_last_change != s.best_fitness_current_gen @@ -613,7 +619,10 @@ function status_summary(c::StopWhenBestCostInGenerationConstant) return "c.iterations_since_change > $(c.iteration_range):\t$s" end function get_reason(c::StopWhenBestCostInGenerationConstant) - return "For the last $(c.iterations_since_change) generation the best objective value in each generation was equal to $(c.best_objective_at_last_change).\n" + if c.at_iteration >= 0 + return "At iteration $(c.at_iteration): for the last $(c.iterations_since_change) generatiosn the best objective value in each generation was equal to $(c.best_objective_at_last_change).\n" + end + return "" end function show(io::IO, c::StopWhenBestCostInGenerationConstant) return print( @@ -636,6 +645,7 @@ mutable struct StopWhenEvolutionStagnates{TParam<:Real} <: StoppingCriterion fraction::TParam best_history::CircularBuffer{TParam} median_history::CircularBuffer{TParam} + at_iteration::Int end function StopWhenEvolutionStagnates( @@ -647,6 +657,7 @@ function StopWhenEvolutionStagnates( fraction, CircularBuffer{TParam}(max_size), CircularBuffer{TParam}(max_size), + -1, ) end @@ -669,9 +680,11 @@ function (c::StopWhenEvolutionStagnates)(::AbstractManoptProblem, s::CMAESState, if i == 0 # reset on init empty!(c.best_history) empty!(c.median_history) + c.at_iteration = -1 return false end if is_active_stopping_criterion(c) + c.at_iteration = i return true else push!(c.best_history, s.best_fitness_current_gen) @@ -683,6 +696,9 @@ function status_summary(c::StopWhenEvolutionStagnates) has_stopped = is_active_stopping_criterion(c) s = has_stopped ? "reached" : "not reached" N = length(c.best_history) + if N == 0 + return "best and median fitness not yet filled, stopping criterion:\t$s" + end thr_low = Int(ceil(N * c.fraction)) thr_high = Int(floor(N * (1 - c.fraction))) median_best_old = median(c.best_history[1:thr_low]) @@ -691,8 +707,11 @@ function status_summary(c::StopWhenEvolutionStagnates) median_median_new = median(c.median_history[thr_high:end]) return "generation >= $(c.min_size) && $(median_best_old) <= $(median_best_new) && $(median_median_old) <= $(median_median_new):\t$s" end -function get_reason(::StopWhenEvolutionStagnates) - return "Both median and best objective history became stagnant.\n" +function get_reason(c::StopWhenEvolutionStagnates) + if c.at_iteration >= 0 + return "Both median and best objective history became stagnant.\n" + end + return "" end function show(io::IO, c::StopWhenEvolutionStagnates) return print( @@ -701,37 +720,49 @@ function show(io::IO, c::StopWhenEvolutionStagnates) ) end -""" +@doc raw""" StopWhenPopulationStronglyConcentrated{TParam<:Real} <: StoppingCriterion Stop if the standard deviation in all coordinates is smaller than `tol` and norm of `σ * p_c` is smaller than `tol`. This corresponds to `TolX` condition from [Hansen:2023](@cite). + +# Fields + +* `tol` the tolerance to check against +* `at_iteration` an internal field to indicate at with iteration ``i \geq 0`` the tolerance was met. + +# Constructor + + StopWhenPopulationStronglyConcentrated(tol::Real) """ mutable struct StopWhenPopulationStronglyConcentrated{TParam<:Real} <: StoppingCriterion tol::TParam - is_active::Bool + at_iteration::Int end function StopWhenPopulationStronglyConcentrated(tol::Real) - return StopWhenPopulationStronglyConcentrated{typeof(tol)}(tol, false) + return StopWhenPopulationStronglyConcentrated{typeof(tol)}(tol, -1) end # It just indicates stagnation, not convergence to a minimizer indicates_convergence(c::StopWhenPopulationStronglyConcentrated) = true function is_active_stopping_criterion(c::StopWhenPopulationStronglyConcentrated) - return c.is_active + return c.at_iteration >= 0 end function (c::StopWhenPopulationStronglyConcentrated)( ::AbstractManoptProblem, s::CMAESState, i::Int ) if i == 0 # reset on init - c.is_active = false + c.at_iteration = -1 return false end norm_inf_dev = norm(s.deviations, Inf) norm_inf_p_c = norm(s.p_c, Inf) - c.is_active = norm_inf_dev < c.tol && s.σ * norm_inf_p_c < c.tol - return c.is_active + if norm_inf_dev < c.tol && s.σ * norm_inf_p_c < c.tol + c.at_iteration = i + return true + end + return false end function status_summary(c::StopWhenPopulationStronglyConcentrated) has_stopped = is_active_stopping_criterion(c) @@ -739,7 +770,10 @@ function status_summary(c::StopWhenPopulationStronglyConcentrated) return "norm(s.deviations, Inf) < $(c.tol) && norm(s.σ * s.p_c, Inf) < $(c.tol) :\t$s" end function get_reason(c::StopWhenPopulationStronglyConcentrated) - return "Standard deviation in all coordinates is smaller than $(c.tol) and `σ * p_c` has Inf norm lower than $(c.tol).\n" + if c.at_iteration >= 0 + return "Standard deviation in all coordinates is smaller than $(c.tol) and `σ * p_c` has Inf norm lower than $(c.tol).\n" + end + return "" end function show(io::IO, c::StopWhenPopulationStronglyConcentrated) return print( @@ -757,24 +791,24 @@ far too small `σ`, or divergent behavior. This corresponds to `TolXUp` conditio mutable struct StopWhenPopulationDiverges{TParam<:Real} <: StoppingCriterion tol::TParam last_σ_times_maxstddev::TParam - is_active::Bool + at_iteration::Int end function StopWhenPopulationDiverges(tol::Real) - return StopWhenPopulationDiverges{typeof(tol)}(tol, 1.0, false) + return StopWhenPopulationDiverges{typeof(tol)}(tol, 1.0, -1) end indicates_convergence(c::StopWhenPopulationDiverges) = false function is_active_stopping_criterion(c::StopWhenPopulationDiverges) - return c.is_active + return c.at_iteration >= 0 end function (c::StopWhenPopulationDiverges)(::AbstractManoptProblem, s::CMAESState, i::Int) if i == 0 # reset on init - c.is_active = false + c.at_iteration = -1 return false end cur_σ_times_maxstddev = s.σ * maximum(s.deviations) if cur_σ_times_maxstddev / c.last_σ_times_maxstddev > c.tol - c.is_active = true + c.at_iteration = i return true end return false @@ -785,7 +819,10 @@ function status_summary(c::StopWhenPopulationDiverges) return "cur_σ_times_maxstddev / c.last_σ_times_maxstddev > $(c.tol) :\t$s" end function get_reason(c::StopWhenPopulationDiverges) - return "σ times maximum standard deviation exceeded $(c.tol). This indicates either much too small σ or divergent behavior.\n" + if c.at_iteration >= 0 + return "σ times maximum standard deviation exceeded $(c.tol). This indicates either much too small σ or divergent behavior.\n" + end + return "" end function show(io::IO, c::StopWhenPopulationDiverges) return print(io, "StopWhenPopulationDiverges($(c.tol))\n $(status_summary(c))") @@ -805,24 +842,24 @@ and all function values in the current generation is below `tol`. This correspon mutable struct StopWhenPopulationCostConcentrated{TParam<:Real} <: StoppingCriterion tol::TParam best_value_history::CircularBuffer{TParam} - is_active::Bool + at_iteration::Int end function StopWhenPopulationCostConcentrated(tol::TParam, max_size::Int) where {TParam<:Real} return StopWhenPopulationCostConcentrated{TParam}( - tol, CircularBuffer{TParam}(max_size), false + tol, CircularBuffer{TParam}(max_size), -1 ) end # It just indicates stagnation, not convergence to a minimizer indicates_convergence(c::StopWhenPopulationCostConcentrated) = true function is_active_stopping_criterion(c::StopWhenPopulationCostConcentrated) - return c.is_active + return c.at_iteration >= 0 end function (c::StopWhenPopulationCostConcentrated)( ::AbstractManoptProblem, s::CMAESState, i::Int ) if i == 0 # reset on init - c.is_active = false + c.at_iteration = -1 return false end push!(c.best_value_history, s.best_fitness_current_gen) @@ -830,7 +867,7 @@ function (c::StopWhenPopulationCostConcentrated)( min_hist, max_hist = extrema(c.best_value_history) if max_hist - min_hist < c.tol && s.best_fitness_current_gen - s.worst_fitness_current_gen < c.tol - c.is_active = true + c.at_iteration = i return true end end @@ -842,7 +879,10 @@ function status_summary(c::StopWhenPopulationCostConcentrated) return "range of best objective values in the last $(length(c.best_value_history)) generations and all objective values in the current one < $(c.tol) :\t$s" end function get_reason(c::StopWhenPopulationCostConcentrated) - return "Range of best objective function values in the last $(length(c.best_value_history)) gnerations and all values in the current generation is below $(c.tol)\n" + if c.at_iteration >= 0 + return "Range of best objective function values in the last $(length(c.best_value_history)) gnerations and all values in the current generation is below $(c.tol)\n" + end + return "" end function show(io::IO, c::StopWhenPopulationCostConcentrated) return print( diff --git a/src/solvers/convex_bundle_method.jl b/src/solvers/convex_bundle_method.jl index bb94e303b7..72bf8ea20c 100644 --- a/src/solvers/convex_bundle_method.jl +++ b/src/solvers/convex_bundle_method.jl @@ -342,7 +342,7 @@ For more details, see [BergmannHerzogJasa:2024](@cite). allocation (default) form `∂f(M, q)` or [`InplaceEvaluation`](@ref) in place, that is of the form `∂f!(M, X, p)`. * `inverse_retraction_method`: (`default_inverse_retraction_method(M, typeof(p))`) an inverse retraction method to use * `retraction_method`: (`default_retraction_method(M, typeof(p))`) a `retraction(M, p, X)` to use. -* `stopping_criterion`: ([`StopWhenLagrangeMultiplierLess`](@ref)`(1e-8)`) a functor, see[`StoppingCriterion`](@ref), indicating when to stop +* `stopping_criterion`: ([`StopWhenLagrangeMultiplierLess`](@ref)`(1e-8; names=["-ξ"])`) a functor, see [`StoppingCriterion`](@ref), indicating when to stop * `vector_transport_method`: (`default_vector_transport_method(M, typeof(p))`) a vector transport method to use * `sub_problem`: a function evaluating with new allocations that solves the sub problem on `M` given the last serious iterate `p_last_serious`, the linearization errors `linearization_errors`, and the transported subgradients `transported_subgradients` @@ -394,7 +394,7 @@ function convex_bundle_method!( inverse_retraction_method::IR=default_inverse_retraction_method(M, typeof(p)), retraction_method::TRetr=default_retraction_method(M, typeof(p)), stopping_criterion::StoppingCriterion=StopWhenAny( - StopWhenLagrangeMultiplierLess(1e-8), StopAfterIteration(5000) + StopWhenLagrangeMultiplierLess(1e-8; names=["-ξ"]), StopAfterIteration(5000) ), vector_transport_method::VTransp=default_vector_transport_method(M, typeof(p)), sub_problem=convex_bundle_method_subsolver, @@ -550,24 +550,26 @@ function (sc::StopWhenLagrangeMultiplierLess)( mp::AbstractManoptProblem, bms::ConvexBundleMethodState, i::Int ) if i == 0 # reset on init - sc.reason = "" - sc.at_iteration = 0 + sc.at_iteration = -1 end M = get_manifold(mp) - if (sc.mode == :estimate) && (-bms.ξ ≤ sc.tolerance[1]) && (i > 0) - sc.reason = "After $i iterations the algorithm reached an approximate critical point: the parameter -ξ = $(-bms.ξ) ≤ $(sc.tolerance[1]).\n" + if (sc.mode == :estimate) && (-bms.ξ ≤ sc.tolerances[1]) && (i > 0) + sc.values[1] = -bms.ξ sc.at_iteration = i return true end ng = norm(M, bms.p_last_serious, bms.g) - if (sc.mode == :both) && (bms.ε ≤ sc.tolerance[1]) && (ng ≤ sc.tolerance[2]) && (i > 0) - sc.reason = "After $i iterations the algorithm reached an approximate critical point: the parameter ε = $(bms.ε) ≤ $(sc.tolerance[1]) and |g| = $(ng) ≤ $(sc.tolerance[2]).\n" + if (sc.mode == :both) && + (bms.ε ≤ sc.tolerances[1]) && + (ng ≤ sc.tolerances[2]) && + (i > 0) + sc.values[1] = bms.ε + sc.values[2] = ng sc.at_iteration = i return true end return false end - function (d::DebugWarnIfLagrangeMultiplierIncreases)( ::AbstractManoptProblem, st::ConvexBundleMethodState, i::Int ) diff --git a/src/solvers/particle_swarm.jl b/src/solvers/particle_swarm.jl index 0c91426256..90d2841edd 100644 --- a/src/solvers/particle_swarm.jl +++ b/src/solvers/particle_swarm.jl @@ -385,12 +385,11 @@ is less than a threshold. initialize the stopping criterion to a certain `tolerance`. """ -mutable struct StopWhenSwarmVelocityLess <: StoppingCriterion - threshold::Float64 - reason::String +mutable struct StopWhenSwarmVelocityLess{F<:Real} <: StoppingCriterion + threshold::F at_iteration::Int - velocity_norms::Vector{Float64} - StopWhenSwarmVelocityLess(tolerance::Float64) = new(tolerance, "", 0, Float64[]) + velocity_norms::Vector{F} + StopWhenSwarmVelocityLess(tolerance::F) where {F} = new{F}(tolerance, -1, F[]) end # It just indicates loss of velocity, not convergence to a minimizer indicates_convergence(c::StopWhenSwarmVelocityLess) = false @@ -398,22 +397,27 @@ function (c::StopWhenSwarmVelocityLess)( mp::AbstractManoptProblem, pss::ParticleSwarmState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 - c.velocity_norms = zeros(Float64, length(pss.swarm)) # init to correct length + c.at_iteration = -1 + # init to correct length + c.velocity_norms = zeros(eltype(c.velocity_norms), length(pss.swarm)) return false end M = get_manifold(mp) c.velocity_norms .= [norm(M, p, X) for (p, X) in zip(pss.swarm, pss.velocity)] if i > 0 && norm(c.velocity_norms) < c.threshold - c.reason = "At iteration $(i) the algorithm reached a velocity of the swarm ($(norm(c.velocity_norms))) less than the threshold ($(c.threshold)).\n" c.at_iteration = i return true end return false end +function get_reason(c::StopWhenSwarmVelocityLess) + if (c.at_iteration >= 0) && (norm(c.velocity_norms) < c.threshold) + return "At iteration $(c.at_iteration) the algorithm reached a velocity of the swarm ($(norm(c.velocity_norms))) less than the threshold ($(c.threshold)).\n" + end + return "" +end function status_summary(c::StopWhenSwarmVelocityLess) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) && (norm(c.velocity_norms) < c.threshold) s = has_stopped ? "reached" : "not reached" return "swarm velocity norm < $(c.threshold):\t$s" end diff --git a/src/solvers/proximal_bundle_method.jl b/src/solvers/proximal_bundle_method.jl index dd5c0b70c7..33118a91c7 100644 --- a/src/solvers/proximal_bundle_method.jl +++ b/src/solvers/proximal_bundle_method.jl @@ -256,8 +256,9 @@ function proximal_bundle_method!( inverse_retraction_method::IR=default_inverse_retraction_method(M, typeof(p)), retraction_method::TRetr=default_retraction_method(M, typeof(p)), bundle_size=50, - stopping_criterion::StoppingCriterion=StopWhenLagrangeMultiplierLess(1e-8) | - StopAfterIteration(5000), + stopping_criterion::StoppingCriterion=StopWhenLagrangeMultiplierLess( + 1e-8; names=["-ν"] + ) | StopAfterIteration(5000), vector_transport_method::VTransp=default_vector_transport_method(M, typeof(p)), α₀=1.2, ε=1e-2, @@ -437,18 +438,21 @@ function (sc::StopWhenLagrangeMultiplierLess)( mp::AbstractManoptProblem, pbms::ProximalBundleMethodState, i::Int ) if i == 0 # reset on init - sc.reason = "" - sc.at_iteration = 0 + sc.at_iteration = -1 end M = get_manifold(mp) - if (sc.mode == :estimate) && (-pbms.ν ≤ sc.tolerance[1]) && (i > 0) - sc.reason = "After $i iterations the algorithm reached an approximate critical point: the parameter -ν = $(-pbms.ν) ≤ $(sc.tolerance[1]).\n" + if (sc.mode == :estimate) && (-pbms.ν ≤ sc.tolerances[1]) && (i > 0) + sc.values[1] = -pbms.ν sc.at_iteration = i return true end nd = norm(M, pbms.p_last_serious, pbms.d) - if (sc.mode == :both) && (pbms.c ≤ sc.tolerance[1]) && (nd ≤ sc.tolerance[2]) && (i > 0) - sc.reason = "After $i iterations the algorithm reached an approximate critical point: the parameter c = $(pbms.c) ≤ $(sc.tolerance[1]) and |d| = $(nd) ≤ $(sc.tolerance[2]).\n" + if (sc.mode == :both) && + (pbms.c ≤ sc.tolerances[1]) && + (nd ≤ sc.tolerances[2]) && + (i > 0) + sc.values[1] = pbms.c + sc.values[2] = nd sc.at_iteration = i return true end diff --git a/src/solvers/truncated_conjugate_gradient_descent.jl b/src/solvers/truncated_conjugate_gradient_descent.jl index 54aaaa9552..cdb575dfa9 100644 --- a/src/solvers/truncated_conjugate_gradient_descent.jl +++ b/src/solvers/truncated_conjugate_gradient_descent.jl @@ -145,34 +145,38 @@ to the power of 1+θ or the norm of the initial residual times κ. [`truncated_conjugate_gradient_descent`](@ref), [`trust_regions`](@ref) """ -mutable struct StopWhenResidualIsReducedByFactorOrPower <: StoppingCriterion - κ::Float64 - θ::Float64 - reason::String +mutable struct StopWhenResidualIsReducedByFactorOrPower{F} <: StoppingCriterion + κ::F + θ::F at_iteration::Int - function StopWhenResidualIsReducedByFactorOrPower(; κ::Float64=0.1, θ::Float64=1.0) - return new(κ, θ, "", 0) + function StopWhenResidualIsReducedByFactorOrPower(; κ::F=0.1, θ::F=1.0) where {F<:Real} + return new{F}(κ, θ, -1) end end function (c::StopWhenResidualIsReducedByFactorOrPower)( mp::AbstractManoptProblem, tcgstate::TruncatedConjugateGradientState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end TpM = get_manifold(mp) M = base_manifold(TpM) p = TpM.point if norm(M, p, tcgstate.residual) <= tcgstate.initialResidualNorm * min(c.κ, tcgstate.initialResidualNorm^(c.θ)) && i > 0 - c.reason = "The norm of the residual is less than or equal either to κ=$(c.κ) times the norm of the initial residual or to the norm of the initial residual to the power 1 + θ=$(1+(c.θ)). \n" + c.at_iteration = i return true end return false end +function get_reason(c::StopWhenResidualIsReducedByFactorOrPower) + if (c.at_iteration >= 0) + return "The norm of the residual is less than or equal either to κ=$(c.κ) times the norm of the initial residual or to the norm of the initial residual to the power 1 + θ=$(1+(c.θ)). \n" + end + return "" +end function status_summary(c::StopWhenResidualIsReducedByFactorOrPower) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "Residual reduced by factor $(c.κ) or power $(c.θ):\t$s" end @@ -227,27 +231,35 @@ the norm of the next iterate is greater than the trust-region radius. [`truncated_conjugate_gradient_descent`](@ref), [`trust_regions`](@ref) """ -mutable struct StopWhenTrustRegionIsExceeded <: StoppingCriterion - reason::String +mutable struct StopWhenTrustRegionIsExceeded{F} <: StoppingCriterion at_iteration::Int + YPY::F + trr::F + StopWhenTrustRegionIsExceeded(v::F) where {F} = new{F}(-1, zero(v), zero(v)) end -StopWhenTrustRegionIsExceeded() = StopWhenTrustRegionIsExceeded("", 0) +StopWhenTrustRegionIsExceeded() = StopWhenTrustRegionIsExceeded(0.0) function (c::StopWhenTrustRegionIsExceeded)( ::AbstractManoptProblem, tcgs::TruncatedConjugateGradientState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end if tcgs.YPY >= tcgs.trust_region_radius^2 && i >= 0 - c.reason = "Trust-region radius violation (‖Y‖² = $(tcgs.YPY)) >= $(tcgs.trust_region_radius^2) = trust_region_radius²). \n" + c.YPY = tcgs.YPY + c.trr = tcgs.trust_region_radius c.at_iteration = i return true end return false end +function get_reason(c::StopWhenTrustRegionIsExceeded) + if c.at_iteration >= 0 + return "Trust-region radius violation (‖Y‖² = $(c.YPY)) >= $(c.trr^2) = trust_region_radius²). \n" + end + return "" +end function status_summary(c::StopWhenTrustRegionIsExceeded) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "Trust region exceeded:\t$s" end @@ -275,27 +287,33 @@ yield a reduction of the model. [`truncated_conjugate_gradient_descent`](@ref), [`trust_regions`](@ref) """ -mutable struct StopWhenCurvatureIsNegative <: StoppingCriterion - reason::String +mutable struct StopWhenCurvatureIsNegative{R} <: StoppingCriterion + value::R at_iteration::Int end -StopWhenCurvatureIsNegative() = StopWhenCurvatureIsNegative("", 0) +StopWhenCurvatureIsNegative() = StopWhenCurvatureIsNegative(0.0) +StopWhenCurvatureIsNegative(v::R) where {R<:Real} = StopWhenCurvatureIsNegative{R}(v, -1) function (c::StopWhenCurvatureIsNegative)( ::AbstractManoptProblem, tcgs::TruncatedConjugateGradientState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 end if tcgs.δHδ <= 0 && i > 0 - c.reason = "Negative curvature. The model is not strictly convex (⟨δ,Hδ⟩_x = $(tcgs.δHδ))) <= 0).\n" + c.value = tcgs.δHδ c.at_iteration = i return true end return false end +function get_reason(c::StopWhenCurvatureIsNegative) + if c.at_iteration >= 0 + return "Negative curvature. The model is not strictly convex (⟨δ,Hδ⟩_x = $(c.value))) <= 0).\n" + end + return "" +end function status_summary(c::StopWhenCurvatureIsNegative) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "Curvature is negative:\t$s" end @@ -320,29 +338,36 @@ A functor for testing if the curvature of the model value increased. [`truncated_conjugate_gradient_descent`](@ref), [`trust_regions`](@ref) """ -mutable struct StopWhenModelIncreased <: StoppingCriterion - reason::String +mutable struct StopWhenModelIncreased{F} <: StoppingCriterion at_iteration::Int - model_value::Float64 + model_value::F + inc_model_value::F end -StopWhenModelIncreased() = StopWhenModelIncreased("", 0, Inf) +StopWhenModelIncreased() = StopWhenModelIncreased(-1, Inf, Inf) function (c::StopWhenModelIncreased)( ::AbstractManoptProblem, tcgs::TruncatedConjugateGradientState, i::Int ) if i == 0 # reset on init - c.reason = "" - c.at_iteration = 0 + c.at_iteration = -1 c.model_value = Inf + c.inc_model_value = Inf end if i > 0 && (tcgs.model_value > c.model_value) - c.reason = "Model value increased from $(c.model_value) to $(tcgs.model_value).\n" + c.inc_model_value = tcgs.model_value + c.at_iteration = i return true end c.model_value = tcgs.model_value return false end +function get_reason(c::StopWhenModelIncreased) + if c.at_iteration >= 0 + return "Model value increased from $(c.model_value) to $( c.inc_model_value).\n" + end + return "" +end function status_summary(c::StopWhenModelIncreased) - has_stopped = length(c.reason) > 0 + has_stopped = (c.at_iteration >= 0) s = has_stopped ? "reached" : "not reached" return "Model Increased:\t$s" end diff --git a/test/plans/test_nelder_mead_plan.jl b/test/plans/test_nelder_mead_plan.jl index fba5d1b486..a8c5daa64d 100644 --- a/test/plans/test_nelder_mead_plan.jl +++ b/test/plans/test_nelder_mead_plan.jl @@ -1,13 +1,30 @@ using Manifolds, Manopt, Test -@testset "Nelder Mead State" begin - M = Euclidean(2) - o = NelderMeadState(M) - o2 = NelderMeadState(M, o.population) - @test o.p == o2.p - @test o.population == o2.population - @test get_state(o) == o - p = [1.0, 1.0] - set_iterate!(o, M, p) - @test get_iterate(o) == p +@testset "Nelder Mead" begin + @testset "Nelder Mead State" begin + M = Euclidean(2) + o = NelderMeadState(M) + o2 = NelderMeadState(M, o.population) + @test o.p == o2.p + @test o.population == o2.population + @test get_state(o) == o + p = [1.0, 1.0] + set_iterate!(o, M, p) + @test get_iterate(o) == p + + @testset "StopWhenPopulationConcentrated" begin + f(M, p) = norm(p) + obj = ManifoldCostObjective(f) + mp = DefaultManoptProblem(M, obj) + s = StopWhenPopulationConcentrated(0.1, 0.1) + # tweak an iteration + o.costs = [0.0, 0.1] + @test !s(mp, o, 1) + @test get_reason(s) == "" + s.value_f = 0.05 + s.value_p = 0.05 + s.at_iteration = 2 + @test length(get_reason(s)) > 0 + end + end end diff --git a/test/plans/test_stopping_criteria.jl b/test/plans/test_stopping_criteria.jl index ee1be831e3..926d1b1a1a 100644 --- a/test/plans/test_stopping_criteria.jl +++ b/test/plans/test_stopping_criteria.jl @@ -12,8 +12,14 @@ struct DummyStoppingCriterion <: StoppingCriterion end s = StopWhenAll(StopAfterIteration(10), StopWhenChangeLess(0.1)) @test Manopt.indicates_convergence(s) #due to all and change this is true @test startswith(repr(s), "StopWhenAll with the") + @test get_reason(s) === "" + # Trigger second one manually + s.criteria[2].last_change = 0.05 + s.criteria[2].at_iteration = 3 + @test length(get_reason(s.criteria[2])) > 0 s2 = StopWhenAll([StopAfterIteration(10), StopWhenChangeLess(0.1)]) - @test get_stopping_criteria(s)[1].maxIter == get_stopping_criteria(s2)[1].maxIter + @test get_stopping_criteria(s)[1].max_iterations == + get_stopping_criteria(s2)[1].max_iterations s3 = StopWhenCostLess(0.1) p = DefaultManoptProblem( @@ -21,19 +27,21 @@ struct DummyStoppingCriterion <: StoppingCriterion end ) s = GradientDescentState(Euclidean(), 1.0) @test !s3(p, s, 1) - @test length(s3.reason) == 0 + @test length(get_reason(s3)) == 0 s.p = 0.3 @test s3(p, s, 2) - @test length(s3.reason) > 0 + @test length(get_reason(s3)) > 0 # repack sn = StopWhenAny(StopAfterIteration(10), s3) + @test get_reason(sn) == "" @test !Manopt.indicates_convergence(sn) # since it might stop after 10 iterations @test startswith(repr(sn), "StopWhenAny with the") @test Manopt._fast_any(x -> false, ()) sn2 = StopAfterIteration(10) | s3 - @test get_stopping_criteria(sn)[1].maxIter == get_stopping_criteria(sn2)[1].maxIter + @test get_stopping_criteria(sn)[1].max_iterations == + get_stopping_criteria(sn2)[1].max_iterations @test get_stopping_criteria(sn)[2].threshold == get_stopping_criteria(sn2)[2].threshold # then s3 is the only active one @@ -46,7 +54,7 @@ struct DummyStoppingCriterion <: StoppingCriterion end @test repr(StopAfterIteration(10)) == s1 @test !sm(p, s, 9) @test sm(p, s, 11) - an = sm.reason + an = get_reason(sm) m = match(r"^((.*)\n)+", an) @test length(m.captures) == 2 # both have to be active update_stopping_criterion!(s3, :MinCost, 1e-2) @@ -67,8 +75,10 @@ struct DummyStoppingCriterion <: StoppingCriterion end @test repr(s) == "StopAfter(Millisecond(30))\n $(Manopt.status_summary(s))" s(p, o, 0) # Start @test s(p, o, 1) == false + @test get_reason(s) == "" sleep(0.05) @test s(p, o, 2) == true + @test length(get_reason(s)) > 0 @test_throws ErrorException StopAfter(Second(-1)) @test_throws ErrorException update_stopping_criterion!(s, :MaxTime, Second(-1)) update_stopping_criterion!(s, :MaxTime, Second(2)) @@ -80,11 +90,17 @@ struct DummyStoppingCriterion <: StoppingCriterion end b = StopWhenChangeLess(1e-6) sb = "StopWhenChangeLess(1.0e-6)\n $(Manopt.status_summary(b))" @test repr(b) == sb + @test get_reason(b) == "" b2 = StopWhenChangeLess(Euclidean(), 1e-6) # second constructor @test repr(b2) == sb c = StopWhenGradientNormLess(1e-6) sc = "StopWhenGradientNormLess(1.0e-6)\n $(Manopt.status_summary(c))" @test repr(c) == sc + @test get_reason(c) == "" + # Trigger manually + c.last_change = 1e-11 + c.at_iteration = 3 + @test length(get_reason(c)) > 0 c2 = StopWhenSubgradientNormLess(1e-6) sc2 = "StopWhenSubgradientNormLess(1.0e-6)\n $(Manopt.status_summary(c2))" @test repr(c2) == sc2 @@ -119,22 +135,28 @@ struct DummyStoppingCriterion <: StoppingCriterion end gs = GradientDescentState(Euclidean(2)) Manopt.set_iterate!(gs, Euclidean(2), [0.0, 1e-2]) g(gp, gs, 0) # reset - @test length(g.reason) == 0 + @test length(get_reason(g)) == 0 @test !g(gp, gs, 1) Manopt.set_iterate!(gs, Euclidean(2), [0.0, 1e-8]) @test g(gp, gs, 2) - @test length(g.reason) > 0 + @test length(get_reason(g)) > 0 h = StopWhenSmallerOrEqual(:p, 1e-4) @test repr(h) == "StopWhenSmallerOrEqual(:p, $(1e-4))\n $(Manopt.status_summary(h))" + @test get_reason(h) == "" + # Trigger manually + h.at_iteration = 1 + @test length(get_reason(h)) > 0 swgcl1 = StopWhenGradientChangeLess(Euclidean(2), 1e-8) swgcl2 = StopWhenGradientChangeLess(1e-8) for swgcl in [swgcl1, swgcl2] repr(swgcl) == "StopWhenGradientChangeLess($(1e-8); vector_transport_method=ParallelTransport())\n $(Manopt.status_summary(swgcl))" swgcl(gp, gs, 0) # reset + @test get_reason(swgcl) == "" @test swgcl(gp, gs, 1) # change 0 -> true @test endswith(Manopt.status_summary(swgcl), "reached") + @test length(get_reason(swgcl)) > 0 end update_stopping_criterion!(swgcl2, :MinGradientChange, 1e-9) @test swgcl2.threshold == 1e-9 @@ -150,21 +172,25 @@ struct DummyStoppingCriterion <: StoppingCriterion end tcgs.model_value = 1.0 s = StopWhenModelIncreased() @test !s(hp, tcgs, 0) - @test s.reason == "" + @test length(get_reason(s)) == 0 s.model_value = 0.5 # tweak the model value to trigger a test @test s(hp, tcgs, 1) - @test length(s.reason) > 0 + @test length(get_reason(s)) > 0 s2 = StopWhenCurvatureIsNegative() tcgs.δHδ = -1.0 @test !s2(hp, tcgs, 0) - @test s2.reason == "" + @test length(get_reason(s2)) == 0 @test s2(hp, tcgs, 1) - @test length(s2.reason) > 0 + @test length(get_reason(s2)) > 0 s3 = StopWhenResidualIsReducedByFactorOrPower() update_stopping_criterion!(s3, :ResidualFactor, 0.5) @test s3.κ == 0.5 update_stopping_criterion!(s3, :ResidualPower, 0.5) @test s3.θ == 0.5 + @test get_reason(s3) == "" + # Trigger manually + s3.at_iteration = 1 + @test length(get_reason(s3)) > 0 end @testset "Stop with step size" begin @@ -178,12 +204,12 @@ struct DummyStoppingCriterion <: StoppingCriterion end ) s1 = StopWhenStepsizeLess(0.5) @test !s1(dmp, gds, 1) - @test s1.reason == "" + @test length(get_reason(s1)) == 0 gds.stepsize = ConstantStepsize(; stepsize=0.25) @test s1(dmp, gds, 2) - @test length(s1.reason) > 0 + @test length(get_reason(s1)) > 0 update_stopping_criterion!(gds, :MaxIteration, 200) - @test gds.stop.maxIter == 200 + @test gds.stop.max_iterations == 200 update_stopping_criterion!(s1, :MinStepsize, 1e-1) @test s1.threshold == 1e-1 end @@ -202,9 +228,11 @@ struct DummyStoppingCriterion <: StoppingCriterion end update_stopping_criterion!(swecl, :Threshold, 1e-4) @test swecl.threshold == 1e-4 @test !swecl(dmp, gds, 1) #First call stores + @test length(get_reason(swecl)) == 0 @test swecl(dmp, gds, 2) #Second triggers (no change) + @test length(get_reason(swecl)) > 0 swecl(dmp, gds, 0) # reset - @test length(swecl.reason) == 0 + @test length(get_reason(swecl)) == 0 end @testset "Subgradient Norm Stopping Criterion" begin @@ -237,22 +265,34 @@ struct DummyStoppingCriterion <: StoppingCriterion end @testset "StopWhenCostNaN & StopWhenIterateNaN" begin sc1 = StopWhenCostNaN() - f(M, p) = NaN + f(M, p) = norm(p) > 2 ? NaN : 0 M = Euclidean(2) p = [1.0, 2.0] @test startswith(repr(sc1), "StopWhenCostNaN()\n") mco = ManifoldCostObjective(f) mp = DefaultManoptProblem(M, mco) s = NelderMeadState(M) + s.p = p @test sc1(mp, s, 1) #always returns true since `f` is always NaN - @test !sc1(mp, s, 0) # test reset - @test length(sc1.reason) == sc1.at_iteration # verify reset + s.p = [0.0, 0.1] + @test !sc1(mp, s, 0) # test reset – triggers again + @test length(get_reason(sc1)) == 0 + @test sc1.at_iteration == -1 + # Trigger manually + sc1.at_iteration = 1 + @test length(get_reason(sc1)) > 0 s.p .= NaN sc2 = StopWhenIterateNaN() @test startswith(repr(sc2), "StopWhenIterateNaN()\n") @test sc2(mp, s, 1) #always returns true since p was now set to NaN - @test !sc2(mp, s, 0) # test reset - @test length(sc2.reason) == sc2.at_iteration # verify reset + @test length(get_reason(sc2)) > 0 + s.p = p + @test !sc2(mp, s, 0) # test reset, though this als already triggers + @test length(get_reason(sc2)) == 0 # verify reset + @test sc2.at_iteration == -1 + # Trigger manually + sc1.at_iteration = 1 + @test length(get_reason(sc1)) > 0 end end diff --git a/test/solvers/test_adaptive_regularization_with_cubics.jl b/test/solvers/test_adaptive_regularization_with_cubics.jl index 9b0f585338..f2a11a4bfb 100644 --- a/test/solvers/test_adaptive_regularization_with_cubics.jl +++ b/test/solvers/test_adaptive_regularization_with_cubics.jl @@ -111,12 +111,23 @@ include("../utils/example_tasks.jl") st1 = StopWhenFirstOrderProgress(0.5) @test startswith(repr(st1), "StopWhenFirstOrderProgress(0.5)\n") @test Manopt.indicates_convergence(st1) + @test get_reason(st1) == "" + # fake a trigger + st1.at_iteration = 0 + @test length(get_reason(st1)) > 0 + st1.at_iteration = 1 + @test length(get_reason(st1)) > 0 + st2 = StopWhenAllLanczosVectorsUsed(2) @test startswith(repr(st2), "StopWhenAllLanczosVectorsUsed(2)\n") @test !Manopt.indicates_convergence(st2) @test startswith( repr(arcs2.sub_state), "# Solver state for `Manopt.jl`s Lanczos Iteration\n" ) + @test get_reason(st2) == "" + # manually trigger + st2.at_iteration = 1 + @test length(get_reason(st2)) > 0 f1(M, p) = p f1!(M, q, p) = copyto!(M, q, p) diff --git a/test/solvers/test_cma_es.jl b/test/solvers/test_cma_es.jl index a2ee61d8d8..61c036d79e 100644 --- a/test/solvers/test_cma_es.jl +++ b/test/solvers/test_cma_es.jl @@ -138,4 +138,19 @@ flat_example(::AbstractManifold, p) = 0.0 p1 = cma_es(M, griewank, [0.0, 1.0, 0.0]; σ=1.0, rng=MersenneTwister(123)) @test griewank(M, p1) < 0.17 end + @testset "Special Stopping Criteria" begin + sc1 = StopWhenBestCostInGenerationConstant{Float64}(10) + sc2 = StopWhenEvolutionStagnates(1, 2, 0.5) + @test contains(Manopt.status_summary(sc2), "not yet filled") + sc3 = StopWhenPopulationStronglyConcentrated(0.1) + sc4 = StopWhenPopulationCostConcentrated(0.1, 5) + sc5 = StopWhenCovarianceIllConditioned(1e-5) + sc6 = StopWhenPopulationDiverges(0.1) + for sc in [sc1, sc2, sc3, sc4, sc5, sc6] + @test get_reason(sc) == "" + # Manually set is active + sc.at_iteration = 10 + @test length(get_reason(sc)) > 0 + end + end end diff --git a/test/solvers/test_particle_swarm.jl b/test/solvers/test_particle_swarm.jl index dc8019a5de..90a78fc49d 100644 --- a/test/solvers/test_particle_swarm.jl +++ b/test/solvers/test_particle_swarm.jl @@ -70,5 +70,10 @@ using Random @testset "Specific Stopping criteria" begin sc = StopWhenSwarmVelocityLess(1.0) @test startswith(repr(sc), "StopWhenSwarmVelocityLess") + @test get_reason(sc) == "" + # Trigger manually + sc.at_iteration = 2 + sc.velocity_norms = [0.001, 0.001] + @test length(get_reason(sc)) > 0 end end diff --git a/test/solvers/test_proximal_bundle_method.jl b/test/solvers/test_proximal_bundle_method.jl index 9505a16f86..79c296d50b 100644 --- a/test/solvers/test_proximal_bundle_method.jl +++ b/test/solvers/test_proximal_bundle_method.jl @@ -14,10 +14,22 @@ import Manopt: proximal_bundle_method_subsolver, proximal_bundle_method_subsolve @test startswith( repr(sc1), "StopWhenLagrangeMultiplierLess([1.0e-8]; mode=:estimate)\n" ) + @test get_reason(sc1) == "" + # Trigger manually + sc1.at_iteration = 2 + @test length(get_reason(sc1)) > 0 sc2 = StopWhenLagrangeMultiplierLess([1e-8, 1e-8]; mode=:both) @test startswith( repr(sc2), "StopWhenLagrangeMultiplierLess([1.0e-8, 1.0e-8]; mode=:both)\n" ) + @test get_reason(sc2) == "" + # Trigger manually + sc2.at_iteration = 2 + @test length(get_reason(sc2)) > 0 + sc3 = StopWhenLagrangeMultiplierLess([1e-8, 1e-8]; mode=:both, names=["a", "b"]) + # Trigger manually + sc3.at_iteration = 2 + @test length(get_reason(sc3)) > 0 end @testset "Allocating Subgradient" begin f(M, q) = distance(M, q, p) diff --git a/test/solvers/test_truncated_cg.jl b/test/solvers/test_truncated_cg.jl index 73b08ee796..835db782f6 100644 --- a/test/solvers/test_truncated_cg.jl +++ b/test/solvers/test_truncated_cg.jl @@ -17,6 +17,10 @@ using Manifolds, Manopt, ManifoldsBase, Test str1 = Manopt.status_summary(str) @test str1 == "Trust region exceeded:\tnot reached" @test repr(str) == "StopWhenTrustRegionIsExceeded()\n $(str1)" + @test get_reason(str) == "" + # Trigger manually + str.at_iteration = 1 + @test length(get_reason(str)) > 0 scn = StopWhenCurvatureIsNegative() scn1 = Manopt.status_summary(scn) @test scn1 == "Curvature is negative:\tnot reached"