Skip to content

Commit 0eb141a

Browse files
committed
stopping criterion docs and small fixes
1 parent e293ccd commit 0eb141a

2 files changed

Lines changed: 282 additions & 14 deletions

File tree

docs/src/stopping_criterion.md

Lines changed: 271 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,277 @@
1-
# Stopping criterion
1+
# Stopping criteria
2+
3+
Continuing the square‑root story from the [Interface](@ref sec_interface) page, we now decide **when** the iteration should halt.
4+
A stopping criterion encapsulates halting logic separately from the algorithm update rule.
5+
6+
## Why separate stopping logic?
7+
8+
Decoupling halting from stepping lets us:
9+
10+
* Reuse generic stopping (iteration caps, time limits) across algorithms.
11+
* Compose multiple conditions (stop after 1 second OR 100 iterations, etc.).
12+
* Query convergence indication vs. mere forced termination.
13+
* Store structured reasons and state (e.g. at which iteration a threshold was met).
14+
15+
16+
## Built-in criteria: Heron's method
17+
18+
The package ships several concrete [`StoppingCriterion`](@ref)s:
19+
20+
* [`StopAfterIteration`](@ref): stop after a maximum number of iterations.
21+
* [`StopAfter`](@ref): stop after a wall‑clock time `Period` (e.g. `Second(2)`, `Minute(1)`).
22+
* Combinations [`StopWhenAll`](@ref) (logical AND) and [`StopWhenAny`](@ref) (logical OR) built via `&` and `|` operators.
23+
24+
Each criterion has an associated [`StoppingCriterionState`](@ref) storing dynamic data (iteration when met, elapsed time, etc.).
25+
26+
Recall our [example implementation](@ref sec_heron) for Heron's method, where we we added a `stopping_criterion` to the `Algorithm`, as well as a `stopping_criterion_state` to the `State`.
27+
28+
```@example Heron
29+
using AlgorithmsInterface
30+
31+
struct SqrtProblem <: Problem
32+
S::Float64 # number whose square root we seek
33+
end
34+
35+
struct HeronAlgorithm <: Algorithm
36+
stopping_criterion # any StoppingCriterion
37+
end
38+
39+
mutable struct HeronState <: State
40+
iterate::Float64 # current iterate
41+
iteration::Int # current iteration count
42+
stopping_criterion_state # any StoppingCriterionState
43+
end
44+
```
45+
46+
Here, we delve a bit deeper into the core components of what made our algorithm stop, even though we had to add very little additional functionality.
47+
48+
### Initialization
49+
50+
The first core component to enable working with stopping criteria is to extend the initialization step to include initializing a [`StoppingCriterionState`](@ref) as well.
51+
This can conveniently be done through the same initialization functions we used for initializing the state:
52+
53+
- [`initialize_state`](@ref) constructs an entirely new stopping state for the algorithm
54+
- [`initialize_state!`](@ref) (in-place) reset of an existing stopping state.
55+
56+
```@example Heron
57+
function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
58+
x0 = rand() # random initial guess
59+
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion)
60+
return HeronState(x0, 0, stopping_criterion_state)
61+
end
62+
63+
function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
64+
# reset the state for the algorithm
65+
state.iterate = rand()
66+
state.iteration = 0
67+
68+
# reset the state for the stopping criterion
69+
state = AlgorithmsInterface.initialize_state!(
70+
problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state
71+
)
72+
return state
73+
end
74+
```
75+
76+
### Iteration
77+
78+
During the iteration procedure, as set out by our design principles, we do not have to modify any of the code, and the stopping criteria do not show up:
79+
80+
```@example Heron
81+
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
82+
S = problem.S
83+
x = state.iterate
84+
state.iterate = 0.5 * (x + S / x)
85+
return state
86+
end
87+
```
88+
89+
What is really going on is that behind the scenes, the loop of the iterative solver expands to code that is equivalent to:
90+
91+
```julia
92+
while !is_finished!(problem, algorithm, state)
93+
increment!(state)
94+
step!(problem, algorithm, state)
95+
end
96+
```
97+
98+
In other words, all of the logic is handled by the [`is_finished!`](@ref) function.
99+
The generic stopping criteria provided by this package have default implementations for this function that work out-of-the-box.
100+
This is partially because we used conventional names for the fields in the structs.
101+
There, `Algorithm` assumes the existence of `stopping_criterion`, while `State` assumes `iterate` and `iteration` and `stopping_criterion_state` to exist.
102+
103+
### Running the algorithm
104+
105+
We can again combine everything into a single function, but now make the stopping criterion accessible:
106+
107+
```@example Heron
108+
function heron_sqrt(x; stopping_criterion)
109+
prob = SqrtProblem(x)
110+
alg = HeronAlgorithm(stopping_criterion)
111+
state = solve(prob, alg) # allocates & runs
112+
return state.iterate, state.iteration
113+
end
114+
115+
heron_sqrt(2; stopping_criterion = StopAfterIteration(10))
116+
```
117+
118+
With this function, we are now ready to explore different ways of telling the algorithm to stop.
119+
For example, using the basic criteria provided by this package, we can alternatively do:
120+
121+
```@example Heron
122+
using Dates
123+
criterion = StopAfter(Millisecond(50))
124+
heron_sqrt(2; stopping_criterion = criterion)
125+
```
126+
127+
We can tighten the condition by combining criteria. Suppose we want to stop after either 25 iterations or 50 milliseconds, whichever comes first:
128+
129+
```@example Heron
130+
criterion = StopAfterIteration(25) | StopAfter(Millisecond(50)) # logical OR
131+
heron_sqrt(2; stopping_criterion = criterion)
132+
```
133+
134+
Conversely, to demand both a minimum iteration quality condition **and** a cap, use `&` (logical AND).
135+
136+
```@example Heron
137+
criterion = StopAfterIteration(25) & StopAfter(Millisecond(50)) # logical AND
138+
heron_sqrt(2; stopping_criterion = criterion)
139+
```
140+
141+
## Implementing a new criterion
142+
143+
It is of course possible that we are not satisfied by the stopping criteria that are provided by default.
144+
Suppose we want to stop when successive iterates change by less than `ϵ`, we could achieve this by implementing our own stopping criterion.
145+
In order to do so, we need to define our own structs and implement the required interface.
146+
Again, we split up the data into a _static_ part, the [`StoppingCriterion`](@ref), and a _dynamic_ part, the [`StoppingCriterionState`](@ref).
147+
148+
```@example Heron
149+
struct StopWhenStable <: StoppingCriterion
150+
tol::Float64 # when do we consider things converged
151+
end
152+
153+
mutable struct StopWhenStableState <: StoppingCriterionState
154+
previous_iterate::Float64 # previous value to compare to
155+
at_iteration::Int # iteration at which stability was reached
156+
delta::Float64 # difference between the values
157+
end
158+
```
159+
160+
Note that our mutable state holds both the `previous_iterate`, which we need to compare to,
161+
as well as the iteration at which the condition was satisfied.
162+
This is not strictly necessary, but can be convenient to have a persistent indication that convergence was reached.
163+
164+
### Initialization
165+
166+
In order to support these _stateful_ criteria, again an initialization phase is needed.
167+
This could be implemented as follows:
168+
169+
```@example Heron
170+
function AlgorithmsInterface.initialize_state(::Problem, ::Algorithm, c::StopWhenStable; kwargs...)
171+
return StopWhenStableState(NaN, -1, NaN)
172+
end
173+
174+
function AlgorithmsInterface.initialize_state!(
175+
::Problem, ::Algorithm, stop_when::StopWhenStable, st::StopWhenStableState;
176+
kwargs...
177+
)
178+
st.previous_iterate = NaN
179+
st.at_iteration = -1
180+
st.delta = NaN
181+
return st
182+
end
183+
```
184+
185+
### Checking for convergence
186+
187+
Then, we need to implement the logic that checks whether an algorithm has finished, which is achieved through [`is_finished`](@ref) and [`is_finished!`](@ref).
188+
Here, the mutating version alters the `stopping_criterion_state`, and should therefore be called exactly once per iteration, while the non-mutating version is simply used to inspect the current status.
189+
190+
```@example Heron
191+
function AlgorithmsInterface.is_finished!(
192+
::Problem, ::Algorithm, state::State, c::StopWhenStable, st::StopWhenStableState
193+
)
194+
k = state.iteration
195+
if k == 0
196+
st.previous_iterate = state.iterate
197+
st.at_iteration = -1
198+
return false
199+
end
200+
201+
st.delta = abs(state.iterate - st.previous_iterate)
202+
st.previous_iterate = state.iterate
203+
if st.delta < c.tol
204+
st.at_iteration = k
205+
return true
206+
end
207+
return false
208+
end
209+
210+
function AlgorithmsInterface.is_finished(
211+
::Problem, ::Algorithm, state::State, c::StopWhenStable, st::StopWhenStableState
212+
)
213+
k = state.iteration
214+
k == 0 && return false
215+
216+
Δ = abs(state.iterate - st.previous_iterate)
217+
return Δ < c.tol
218+
end
219+
```
220+
221+
### Reason and convergence reporting
222+
223+
Finally, we need to implement [`get_reason`](@ref) and [`indicates_convergence`](@ref).
224+
These helper functions are required to interact with the [logging system](@ref sec_logging), to distinguish between states that are considered ongoing, stopped and converged, or stopped without convergence.
225+
226+
```@example Heron
227+
function AlgorithmsInterface.get_reason(c::StopWhenStable, st::StopWhenStableState)
228+
(st.at_iteration >= 0 && st.delta < c.tol) || return nothing
229+
return "The algorithm reached an approximate stable point after $(st.at_iteration) iterations; the change $(st.delta) is less than $(c.tol)."
230+
end
231+
232+
AlgorithmsInterface.indicates_convergence(c::StopWhenStable, st::StopWhenStableState) = true
233+
```
234+
235+
### Convergence in action
236+
237+
Then we are finally ready to test out our new stopping criterion.
238+
239+
```@example Heron
240+
criterion = StopWhenStable(1e-8)
241+
heron_sqrt(16.0; stopping_criterion = criterion)
242+
```
243+
244+
Note that our work payed off, as we can still compose this stopping criterion with other criteria as well:
245+
246+
```@example Heron
247+
criterion = StopWhenStable(1e-8) | StopAfterIteration(5)
248+
heron_sqrt(16.0; stopping_criterion = criterion)
249+
```
250+
251+
### Summary
252+
253+
Implementing a criterion usually means defining:
254+
255+
1. A subtype of [`StoppingCriterion`](@ref).
256+
2. A state subtype of [`StoppingCriterionState`](@ref) capturing dynamic fields.
257+
3. `initialize_state` and `initialize_state!` for setup/reset.
258+
4. `is_finished!` (mutating) and optionally `is_finished` (non‑mutating) variants.
259+
5. `get_reason` (return `nothing` or a string) for user feedback.
260+
6. `indicates_convergence(::YourCriterion)` to mark if meeting it implies convergence.
261+
262+
You may also implement `Base.summary(io, criterion, criterion_state)` for compact status reports.
263+
264+
## Reference API
265+
266+
Below are the auto‑generated docs for all stopping criterion infrastructure.
2267

3268
```@autodocs
4269
Modules = [AlgorithmsInterface]
5270
Pages = ["stopping_criterion.jl"]
6271
Order = [:type, :function]
7272
Private = true
8-
```
273+
```
274+
275+
### Next: Logging
276+
277+
With halting logic done, proceed to the [logging section](@ref sec_logging) to instrument the same example and capture intermediate diagnostics.

src/stopping_criterion.jl

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function get_reason end
3737
get_reason(stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)
3838
3939
Provide a reason in human readable text as to why a [`StoppingCriterion`](@ref) with [`StoppingCriterionState`](@ref) indicated to stop.
40-
If it does not indicate to stop, this should return an empty string.
40+
If it does not indicate to stop, this should return `nothing`.
4141
4242
Providing the iteration at which this indicated to stop in the reason would be preferable.
4343
"""
@@ -54,8 +54,7 @@ indicates_convergence(stopping_criterion::StoppingCriterion)
5454
@doc """
5555
indicates_convergence(stopping_criterion::StoppingCriterion, ::StoppingCriterionState)
5656
57-
Return whether or not a [`StoppingCriterion`](@ref) indicates convergence
58-
when it is in [`StoppingCriterionState`](@ref)
57+
Return whether or not a [`StoppingCriterion`](@ref) indicates convergence when it is in [`StoppingCriterionState`](@ref).
5958
6059
By default this checks whether the [`StoppingCriterion`](@ref) has actually stopped.
6160
If so it returns whether `stopping_criterion` itself indicates convergence, otherwise it returns `false`,
@@ -275,19 +274,19 @@ function initialize_state(
275274
)
276275
end
277276
function initialize_state!(
278-
stopping_criterion_states::GroupStoppingCriterionState,
279277
problem::Problem,
280278
algorithm::Algorithm,
281-
stop_when::Union{StopWhenAll, StopWhenAny};
279+
stop_when::Union{StopWhenAll, StopWhenAny},
280+
stopping_criterion_states::GroupStoppingCriterionState;
282281
kwargs...,
283282
)
284283
for (stopping_criterion_state, stopping_criterion) in
285284
zip(stopping_criterion_states.criteria_states, stop_when.criteria)
286285
initialize_state!(
287-
stopping_criterion_state,
288286
problem,
289287
algorithm,
290-
stopping_criterion;
288+
stopping_criterion,
289+
stopping_criterion_state;
291290
kwargs...,
292291
)
293292
end
@@ -441,10 +440,10 @@ end
441440
initialize_state(::Problem, ::Algorithm, ::StopAfterIteration; kwargs...) =
442441
DefaultStoppingCriterionState()
443442
function initialize_state!(
444-
stopping_criterion_state::DefaultStoppingCriterionState,
445443
::Problem,
446444
::Algorithm,
447-
::StopAfterIteration;
445+
::StopAfterIteration,
446+
stopping_criterion_state::DefaultStoppingCriterionState;
448447
kwargs...,
449448
)
450449
stopping_criterion_state.at_iteration = -1
@@ -550,10 +549,10 @@ initialize_state(::Problem, ::Algorithm, ::StopAfter; kwargs...) =
550549
StopAfterTimePeriodState()
551550

552551
function initialize_state!(
553-
stopping_criterion_state::DefaultStoppingCriterionState,
554552
::Problem,
555553
::Algorithm,
556-
::StopAfter;
554+
::StopAfter,
555+
stopping_criterion_state::StopAfterTimePeriodState;
557556
kwargs...,
558557
)
559558
stopping_criterion_state.start = Nanosecond(0)
@@ -586,7 +585,7 @@ function is_finished!(
586585
stop_after_state.start = Nanosecond(time_ns())
587586
stop_after_state.time = Nanosecond(0)
588587
else
589-
stop_after_state.time = Nanosecond(time_ns()) - stopping_criterion_state.start
588+
stop_after_state.time = Nanosecond(time_ns()) - stop_after_state.start
590589
if k > 0 && (stop_after_state.time > Nanosecond(stop_after.threshold))
591590
stop_after_state.at_iteration = k
592591
return true

0 commit comments

Comments
 (0)