Skip to content

Commit 571b909

Browse files
authored
introduce a stopped_at function for solver states. (#599)
* introduce a stopped_at function for solver states. * Add stopped_at to the docs. * Add the AI section to the initial TOC. * a bit of code formatting and one test. * Fix a test.
1 parent 0e7305a commit 571b909

10 files changed

Lines changed: 42 additions & 48 deletions

File tree

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ The following is a set of guidelines to [`Manopt.jl`](https://juliamanifolds.git
1818
- [Code style](#Code-style)
1919
- [Concerning the documentation](#Concerning-the-documentation)
2020
- [Spell checking](#Spell-checking)
21+
- [On the use of AI](#On-the-use-of-AI)
22+
2123
## I just have a question
2224

2325
The developer can most easily be reached in the Julia Slack channel [#manifolds](https://julialang.slack.com/archives/CP4QF0K5Z).

Changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ The file was started with Version `0.4`.
66
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

9+
## [0.5.36] April 24, 2026
10+
11+
### Added
12+
13+
* a function `stopped_at(state)` to access the number of iterations it took a solver to stop. (#599)
14+
15+
### Fixed
16+
17+
* a small bug where `get_count(sc::StopWhenAny, Val(:Iteration))` wrongly reported it stopped before the first iteration when it actually did not yet stop. (#599)
18+
919
## [0.5.35] April 16, 2026
1020

1121
### Changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "Manopt"
22
uuid = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5"
3-
version = "0.5.35"
3+
version = "0.5.36"
44
authors = [{family-names = "Bergmann", given-names = "Ronny", alias = "kellertuer", city = "Trondheim", affiliation = "Norwegian University of Science and Technology", country = "NO", email = "manopt@ronnybergmann.net", orcid = "https://orcid.org/0000-0001-8342-7218", website = "https://ronnybergmann.net"}]
55

66
[workspace]

docs/src/plans/state.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ AbstractManoptSolverState
1414
get_state
1515
Manopt.get_count
1616
Manopt.has_converged(::AbstractManoptSolverState)
17+
stopped_at
1718
```
1819

1920
Since every subtype of an [`AbstractManoptSolverState`](@ref) directly relate to a solver,

src/Manopt.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ export StopAfter,
587587
StopWhenSwarmVelocityLess,
588588
StopWhenTrustRegionIsExceeded
589589
export get_active_stopping_criteria,
590-
get_stopping_criteria, get_reason, get_stopping_criterion
590+
get_stopping_criteria, get_reason, get_stopping_criterion, stopped_at
591591
#
592592
# Exports
593593
export asymptote_export_S2_signals, asymptote_export_S2_data, asymptote_export_SPD

src/plans/solver_state.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,18 @@ Make a copy of tangent vector `X` from manifold `M` for storage in [`StoreStateA
329329
_storage_copy_vector(M::AbstractManifold, X) = copy(M, X)
330330
_storage_copy_vector(::AbstractManifold, X::Number) = StorageRef(X)
331331

332+
@doc """
333+
stopped_at(state::AbstractManoptSolverState)
334+
335+
Return the number of iterations the solver represented by the `state` took to stop.
336+
If the solver has not yet stopped, this function returns `-1`.
337+
338+
By default, this function calls `get_count` function on the state's stopping criterion to access its `:Iteration` count.
339+
"""
340+
function stopped_at(state::AbstractManoptSolverState)
341+
return get_count(get_stopping_criterion(state), Val(:Iterations))
342+
end
343+
332344
@doc """
333345
StoreStateAction <: AbstractStateAction
334346

src/plans/stopping_criterion.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ function has_converged(c::StopWhenAny)
11811181
end
11821182
function get_count(c::StopWhenAny, v::Val{:Iterations})
11831183
iters = filter(x -> x > 0, [get_count(ci, v) for ci in c.criteria])
1184-
(length(iters) == 0) && (return 0)
1184+
(length(iters) == 0) && (return -1) # None indicated to stop yet, so we also do not
11851185
return minimum(iters)
11861186
end
11871187
function show(io::IO, c::StopWhenAny)

test/plans/test_gradient_plan.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ using ManifoldsBase, Manopt, Test
1616
stopping_criterion = StopAfterIteration(20),
1717
stepsize = Manopt.ConstantStepsize(M),
1818
)
19+
@test stopped_at(gst) == -1
1920
set_iterate!(gst, M, q)
2021
@test get_iterate(gst) == q
2122
set_gradient!(gst, M, p, [1.0, 0.0])

test/plans/test_state.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ struct NoIterateState <: AbstractManoptSolverState end
7272
@test_throws ErrorException get_iterate(s2)
7373
end
7474

75-
@testset "Iteration and Gradient setters" begin
75+
@testset "Iterate and Gradient setters" begin
7676
M = Euclidean(3)
7777
s1 = NelderMeadState(M)
7878
s2 = GradientDescentState(M)

test/solvers/test_gradient_descent.jl

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ using ManifoldDiff: grad_distance
2424
500,
2525
)
2626
s = gradient_descent(
27-
M,
28-
f,
29-
grad_f,
30-
data[1];
27+
M, f, grad_f, data[1];
3128
stopping_criterion = StopAfterIteration(200) | StopWhenChangeLess(M, 1.0e-16),
3229
stepsize = ArmijoLinesearch(; contraction_factor = 0.99),
3330
debug = d,
@@ -38,10 +35,7 @@ using ManifoldDiff: grad_distance
3835
res_debug = String(take!(my_io))
3936
@test res_debug === " f(x): 1.357071\n"
4037
p2 = gradient_descent(
41-
M,
42-
f,
43-
grad_f,
44-
data[1];
38+
M, f, grad_f, data[1];
4539
stopping_criterion = StopAfterIteration(200) | StopWhenChangeLess(M, 1.0e-16),
4640
stepsize = ArmijoLinesearch(; contraction_factor = 0.99),
4741
)
@@ -56,10 +50,7 @@ using ManifoldDiff: grad_distance
5650
stop_when_stepsize_exceeds = 0.9 * π,
5751
)
5852
p3 = gradient_descent(
59-
M,
60-
f,
61-
grad_f,
62-
data[1];
53+
M, f, grad_f, data[1];
6354
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
6455
stepsize = step,
6556
debug = [], # do not warn about increasing step here
@@ -75,10 +66,7 @@ using ManifoldDiff: grad_distance
7566
stop_when_stepsize_exceeds = 0.9 * π,
7667
)
7768
p4 = gradient_descent(
78-
M,
79-
f,
80-
grad_f,
81-
data[1];
69+
M, f, grad_f, data[1];
8270
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
8371
stepsize = step2,
8472
debug = [], # do not warn about increasing step here
@@ -94,40 +82,28 @@ using ManifoldDiff: grad_distance
9482
stop_when_stepsize_exceeds = 0.9 * π,
9583
)
9684
p5 = gradient_descent(
97-
M,
98-
f,
99-
grad_f,
100-
data[1];
85+
M, f, grad_f, data[1];
10186
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
10287
stepsize = step3,
10388
debug = [], # do not warn about increasing step here
10489
)
10590
@test isapprox(M, p, p5; atol = 1.0e-13)
10691
p6 = gradient_descent(
107-
M,
108-
f,
109-
grad_f,
110-
data[1];
92+
M, f, grad_f, data[1];
11193
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
11294
direction = Nesterov(; p = copy(M, data[1])),
11395
)
11496
@test isapprox(M, p, p6; atol = 1.0e-13)
11597
# Precon in simple scale down by 2
11698
p7 = gradient_descent(
117-
M,
118-
f,
119-
grad_f,
120-
data[1];
99+
M, f, grad_f, data[1];
121100
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
122101
direction = PreconditionedDirection((M, p, X) -> 0.5 .* X),
123102
)
124103
@test isapprox(M, p, p7; atol = 1.0e-13)
125104
# Precon in simple scale down by 2 – inplace
126105
p8 = gradient_descent(
127-
M,
128-
f,
129-
grad_f,
130-
data[1];
106+
M, f, grad_f, data[1];
131107
stopping_criterion = StopAfterIteration(1000) | StopWhenChangeLess(M, 1.0e-16),
132108
direction = PreconditionedDirection(
133109
(M, Y, p, X) -> (Y .= 0.5 .* X); evaluation = InplaceEvaluation()
@@ -170,20 +146,14 @@ using ManifoldDiff: grad_distance
170146
# `gradient_descent` allocated n2 newly
171147
@test isapprox(M, north, n2a)
172148
n3 = gradient_descent(
173-
M,
174-
f,
175-
grad_f,
176-
pts[1];
149+
M, f, grad_f, pts[1];
177150
direction = MomentumGradient(),
178151
stepsize = ConstantLength(),
179152
debug = [], # do not warn about increasing step here
180153
)
181154
@test isapprox(M, north, n3)
182155
n4 = gradient_descent(
183-
M,
184-
f,
185-
grad_f,
186-
pts[1];
156+
M, f, grad_f, pts[1];
187157
direction = AverageGradient(M; n = 5),
188158
stopping_criterion = StopAfterIteration(800),
189159
)
@@ -194,14 +164,12 @@ using ManifoldDiff: grad_distance
194164
@test startswith(repr(r), "# Solver state for `Manopt.jl`s Gradient Descent")
195165
# State and a count objective, putting stats behind print
196166
n6 = gradient_descent(
197-
M,
198-
f,
199-
grad_f,
200-
pts[1];
167+
M, f, grad_f, pts[1];
201168
count = [:Gradient],
202169
return_objective = true,
203170
return_state = true,
204171
)
172+
@test stopped_at(n6[2]) > 0
205173
@test repr(n6) == "$(n6[2])\n\n$(n6[1])"
206174
end
207175
@testset "Tutorial mode" begin

0 commit comments

Comments
 (0)