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
11 changes: 9 additions & 2 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ 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.27] November 11, 2025

### Added

* In `WolfePowellLinesearchStepsize`, two new keyword arguments `stop_increasing_at_step=` and `stop_decreasing_at_step=` were added to limit the number of increase/decrease steps in the initial bracketing phase for s_plus and s_minus, respectively. (resolves (#495))
* refactor `get_message` to only allocate a string when it is asked to deliver one, not every time a message is actually stored. This makes the message system align more with `get_reason`.

## [0.5.26] November 5, 2025

### Added
Expand Down Expand Up @@ -235,7 +242,7 @@ present; they were changed to `retact_fused!`.
* add a `PreconditionedDirection` variant to the `direction` gradient processor
keyword argument and its corresponding `PreconditionedDirectionRule`
* make the preconditioner available in quasi Newton.
* in `gradient_descent` and `conjugate_gradient_descent` the rule can be added anyways.
* in `gradient_descent` and `conjugate_gradient_descent` the rule can be added anyway.

### Fixed

Expand Down Expand Up @@ -370,7 +377,7 @@ In general this introduces a few factories, that avoid having to pass the manifo
* index for equality constraints is unified to `j` running from `1,...,n`
* iterations are using now `k`
* `get_manopt_parameter` has been renamed to `get_parameter` since it is internal,
so internally that is clear; accessing it from outside hence reads anyways `Manopt.get_parameter`
so internally that is clear; accessing it from outside hence reads anyway `Manopt.get_parameter`
* `set_manopt_parameter!` has been renamed to `set_parameter!` since it is internal,
so internally that is clear; accessing it from outside hence reads `Manopt.set_parameter!`
* changed the `stabilize::Bool=` keyword in `quasi_Newton` to the more flexible `project!=`
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 <[email protected]>"]
version = "0.5.26"
version = "0.5.27"

[deps]
ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
Expand Down
4 changes: 2 additions & 2 deletions docs/src/plans/stepsize.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ Internally these use

```@autodocs
Modules = [Manopt]
Pages = ["plans/stepsize.jl"]
Pages = ["plans/stepsize/stepsize_message.jl", "plans/stepsize/linesearch.jl", "plans/stepsize/stepsize.jl"]
Private = true
Order = [:function, :type]
Filter = t -> !(t in [Stepsize, AdaptiveWNGradient, ArmijoLinesearch, ConstantLength, CubicBracketingLinesearch, DecreasingLength, DistanceOverGradients, NonmonotoneLinesearch, Polyak, WolfePowellLinesearch, WolfePowellBinaryLinesearch ])
Filter = t -> !(t in [Manopt.get_message, Stepsize, AdaptiveWNGradient, ArmijoLinesearch, ConstantLength, CubicBracketingLinesearch, DecreasingLength, DistanceOverGradients, NonmonotoneLinesearch, Polyak, WolfePowellLinesearch, WolfePowellBinaryLinesearch ])
```


Expand Down
5 changes: 4 additions & 1 deletion src/plans/plan.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ include("debug.jl")
include("record.jl")

include("stopping_criterion.jl")
include("stepsize.jl")

include("stepsize/stepsize_message.jl")
include("stepsize/linesearch.jl")
include("stepsize/stepsize.jl")

include("keywords.jl")

Expand Down
188 changes: 188 additions & 0 deletions src/plans/stepsize/linesearch.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""
Stepsize

An abstract type for the functors representing step sizes. These are callable
structures. The naming scheme is `TypeOfStepSize`, for example `ConstantStepsize`.

Every Stepsize has to provide a constructor and its function has to have
the interface `(p,o,i)` where a [`AbstractManoptProblem`](@ref) as well as [`AbstractManoptSolverState`](@ref)
and the current number of iterations are the arguments
and returns a number, namely the stepsize to use.

The functor usually should accept arbitrary keyword arguments. Common ones used are
* `gradient=nothing`: to pass a pre-calculated gradient, otherwise it is computed.

For most it is advisable to employ a [`ManifoldDefaultsFactory`](@ref). Then
the function creating the factory should either be called `TypeOf` or if that is confusing or too generic, `TypeOfLength`

# See also

[`Linesearch`](@ref)
"""
abstract type Stepsize end

get_message(::S) where {S <: Stepsize} = ""

"""
default_stepsize(M::AbstractManifold, ams::AbstractManoptSolverState)

Returns the default [`Stepsize`](@ref) functor used when running the solver specified by the
[`AbstractManoptSolverState`](@ref) `ams` running with an objective on the `AbstractManifold M`.
"""
default_stepsize(M::AbstractManifold, sT::Type{<:AbstractManoptSolverState})

"""
max_stepsize(M::AbstractManifold, p)
max_stepsize(M::AbstractManifold)

Get the maximum stepsize (at point `p`) on manifold `M`. It should be used to limit the
distance an algorithm is trying to move in a single step.

By default, this returns $(_link(:injectivity_radius))`(M)`, if this exists.
If this is not available on the the method returns `Inf`.
"""
function max_stepsize(M::AbstractManifold, p)
s = try
injectivity_radius(M, p)
catch
is_tutorial_mode() &&
@warn "`max_stepsize was called, but there seems to not be an `injectivity_raidus` available on $M."
Inf
end
return s
end
function max_stepsize(M::AbstractManifold)
s = try
injectivity_radius(M)
catch
is_tutorial_mode() &&
@warn "`max_stepsize was called, but there seems to not be an `injectivity_raidus` available on $M."
Inf
end
return s
end

"""
Linesearch <: Stepsize

An abstract functor to represent line search type step size determinations, see
[`Stepsize`](@ref) for details. One example is the [`ArmijoLinesearchStepsize`](@ref)
functor.

Compared to simple step sizes, the line search functors provide an interface of
the form `(p,o,i,X) -> s` with an additional (but optional) fourth parameter to
provide a search direction; this should default to something reasonable,
most prominently the negative gradient.
"""
abstract type Linesearch <: Stepsize end

@doc """
s = linesearch_backtrack(M, F, p, X, s, decrease, contract η = -X, f0 = f(p); kwargs...)
s = linesearch_backtrack!(M, q, F, p, X, s, decrease, contract η = -X, f0 = f(p); kwargs...)

perform a line search

* on manifold `M`
* for the cost function `f`,
* at the current point `p`
* with current gradient provided in `X`
* an initial stepsize `s`
* a sufficient `decrease`
* a `contract`ion factor ``σ``
* a search direction ``η = -X``
* an offset, ``f_0 = F(x)``

## Keyword arguments

$(_var(:Keyword, :retraction_method))
* `stop_when_stepsize_less=0.0`: to avoid numerical underflow
* `stop_when_stepsize_exceeds=`[`max_stepsize`](@ref)`(M, p) / norm(M, p, η)`) to avoid leaving the injectivity radius on a manifold
* `stop_increasing_at_step=100`: stop the initial increase of step size after these many steps
* `stop_decreasing_at_step=`1000`: stop the decreasing search after these many steps
* `report_messages_in::NamedTuple = (; )`: a named tuple of [`StepsizeMessage`](@ref)s to report messages in.
currently supported keywords are `:non_descent_direction`, `:stepsize_exceeds`, `:stepsize_less`, `:stop_increasing`, `:stop_decreasing`
* `additional_increase_condition=(M,p) -> true`: impose an additional condition for an increased step size to be accepted
* `additional_decrease_condition=(M,p) -> true`: impose an additional condition for an decreased step size to be accepted

These keywords are used as safeguards, where only the max stepsize is a very manifold specific one.

# Return value

A stepsize `s` and a message `msg` (in case any of the 4 criteria hit)
"""
function linesearch_backtrack(
M::AbstractManifold, f, p, X::T, s, decrease, contract, η::T = (-X), f0 = f(M, p); kwargs...
) where {T}
q = allocate(M, p)
return linesearch_backtrack!(M, q, f, p, X, s, decrease, contract, η, f0; kwargs...)
end

"""
s = linesearch_backtrack!(M, q, F, p, X, s, decrease, contract η = -X, f0 = f(p))

Perform a line search backtrack in-place of `q`.
For all details and options, see [`linesearch_backtrack`](@ref)
"""
function linesearch_backtrack!(
M::AbstractManifold,
q,
f::TF,
p,
X::T,
s,
decrease,
contract,
η::T = (-X),
f0 = f(M, p);
retraction_method::AbstractRetractionMethod = default_retraction_method(M, typeof(p)),
additional_increase_condition = (M, p) -> true,
additional_decrease_condition = (M, p) -> true,
stop_when_stepsize_less = 0.0,
stop_when_stepsize_exceeds = max_stepsize(M, p) / norm(M, p, η),
stop_increasing_at_step = 100,
stop_decreasing_at_step = 1000,
report_messages_in::NamedTuple = (;),
) where {TF, T}
ManifoldsBase.retract_fused!(M, q, p, η, s, retraction_method)
f_q = f(M, q)
search_dir_inner = real(inner(M, p, η, X))
if search_dir_inner >= 0
set_message!(report_messages_in, :non_descent_direction, at = 0, value = search_dir_inner)
end

i = 0
# Ensure that both the original condition and the additional one are fulfilled afterwards
while f_q < f0 + decrease * s * search_dir_inner || !additional_increase_condition(M, q)
(stop_increasing_at_step == 0) && break
i = i + 1
s = s / contract
ManifoldsBase.retract_fused!(M, q, p, η, s, retraction_method)
f_q = f(M, q)
if i == stop_increasing_at_step
set_message!(report_messages_in, :stop_increasing, at = i, bound = stop_increasing_at_step, value = s)
break
end
if s > stop_when_stepsize_exceeds
set_message!(report_messages_in, :stepsize_exceeds, at = i, bound = stop_when_stepsize_exceeds, value = s)
break
end
end
i = 0
# Ensure that both the original condition and the additional one are fulfilled afterwards
while (f_q > f0 + decrease * s * search_dir_inner) ||
(!additional_decrease_condition(M, q))
i = i + 1
s = contract * s
ManifoldsBase.retract_fused!(M, q, p, η, s, retraction_method)
f_q = f(M, q)
if i == stop_decreasing_at_step
set_message!(report_messages_in, :stop_decreasing, at = i, bound = stop_decreasing_at_step, value = s)
break
end
if s < stop_when_stepsize_less
set_message!(report_messages_in, :stepsize_less, at = i, bound = stop_when_stepsize_less, value = s)
break
end
end
return s
end
Loading
Loading