Skip to content

Commit ae2c1fd

Browse files
committed
update docs
1 parent 54fa9c7 commit ae2c1fd

3 files changed

Lines changed: 149 additions & 62 deletions

File tree

docs/src/interface.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ end
6767

6868
### Initialization
6969

70-
In order to start implementing the core parts of our algorithm, we start at the very beginning.
70+
In order to begin implementing the core parts of our algorithm, we start at the very beginning.
7171
There are two main entry points provided by the interface:
7272

7373
- [`initialize_state`](@ref) constructs an entirely new state for the algorithm
@@ -76,21 +76,21 @@ There are two main entry points provided by the interface:
7676
An example implementation might look like:
7777

7878
```@example Heron
79-
function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
80-
x0 = rand() # random initial guess
81-
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion)
79+
function AlgorithmsInterface.initialize_state(
80+
problem::SqrtProblem, algorithm::HeronAlgorithm,
81+
stopping_criterion_state::StoppingCriterionState;
82+
kwargs...
83+
)
84+
x0 = rand()
85+
iteration = 0
8286
return HeronState(x0, 0, stopping_criterion_state)
8387
end
8488
85-
function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
86-
# reset the state for the algorithm
87-
state.iterate = rand()
88-
state.iteration = 0
89-
90-
# reset the state for the stopping criterion
91-
state = AlgorithmsInterface.initialize_state!(
92-
problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state
89+
function AlgorithmsInterface.initialize_state!(
90+
problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState;
91+
kwargs...
9392
)
93+
state.iteration = 0
9494
return state
9595
end
9696
```
@@ -175,6 +175,15 @@ Order = [:type, :function]
175175
Private = true
176176
```
177177

178+
### Stopping Criteria
179+
180+
```@autodocs
181+
Modules = [AlgorithmsInterface]
182+
Pages = ["interface/stopping.jl"]
183+
Order = [:type, :function]
184+
Private = true
185+
```
186+
178187
### Next: Stopping criteria
179188

180189
Proceed to the stopping criteria section to add robust halting logic (iteration caps, time limits, tolerance on successive iterates, and combinations) to this square‑root example.

docs/src/logging.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,21 @@ mutable struct HeronState <: State
5252
stopping_criterion_state
5353
end
5454
55-
function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
55+
function AlgorithmsInterface.initialize_state(
56+
problem::SqrtProblem, algorithm::HeronAlgorithm,
57+
stopping_criterion_state::StoppingCriterionState;
58+
kwargs...
59+
)
5660
x0 = rand()
57-
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion)
61+
iteration = 0
5862
return HeronState(x0, 0, stopping_criterion_state)
5963
end
6064
61-
function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
62-
state.iterate = rand()
65+
function AlgorithmsInterface.initialize_state!(
66+
problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState;
67+
kwargs...
68+
)
6369
state.iteration = 0
64-
initialize_state!(problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state)
6570
return state
6671
end
6772

docs/src/stopping_criterion.md

Lines changed: 118 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,31 @@ Here, we delve a bit deeper into the core components of what made our algorithm
5252
### Initialization
5353

5454
The first core component to enable working with stopping criteria is to extend the initialization step to include initializing a [`StoppingCriterionState`](@ref) as well.
55-
This can conveniently be done through the same initialization functions we used for initializing the state:
56-
57-
- [`initialize_state`](@ref) constructs an entirely new stopping state for the algorithm
58-
- [`initialize_state!`](@ref) (in-place) reset of an existing stopping state.
55+
Since some of these may require _stateful_ implementations, we also keep a `stopping_criterion_state` that captures this, and thus needs to be initialized.
56+
By default, the initialization happens automatically and the only thing that is left for us to do is to attach this `stopping_criterion_state` to the `state` in the [`initialize_state`](@ref) function, as we already saw before:
5957

6058
```@example Heron
61-
function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
62-
x0 = rand() # random initial guess
63-
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion)
59+
function AlgorithmsInterface.initialize_state(
60+
problem::SqrtProblem, algorithm::HeronAlgorithm,
61+
stopping_criterion_state::StoppingCriterionState;
62+
kwargs...
63+
)
64+
x0 = rand()
65+
iteration = 0
6466
return HeronState(x0, 0, stopping_criterion_state)
6567
end
6668
67-
function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
68-
# reset the state for the algorithm
69-
state.iterate = rand()
70-
state.iteration = 0
71-
72-
# reset the state for the stopping criterion
73-
state = AlgorithmsInterface.initialize_state!(
74-
problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state
69+
function AlgorithmsInterface.initialize_state!(
70+
problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState;
71+
kwargs...
7572
)
73+
state.iteration = 0
7674
return state
7775
end
7876
```
7977

78+
Note that we do not need to handle any stopping criteria in the [`initialize_state!`](@ref) function, as a separate call to [`AlgorithmsInterface.initialize_stopping_state!`](@ref) is made independently.
79+
8080
### Iteration
8181

8282
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:
@@ -145,9 +145,75 @@ heron_sqrt(2; stopping_criterion = criterion)
145145
## Implementing a new criterion
146146

147147
It is of course possible that we are not satisfied by the stopping criteria that are provided by default.
148-
Suppose we want to stop when successive iterates change by less than `ϵ`, we could achieve this by implementing our own stopping criterion.
149-
In order to do so, we need to define our own structs and implement the required interface.
150-
Again, we split up the data into a _static_ part, the [`StoppingCriterion`](@ref), and a _dynamic_ part, the [`StoppingCriterionState`](@ref).
148+
For example, we might check for convergence by squaring our current `iterate` and seeing if it equals the input value.
149+
In order to do so, we need to define our own struct and implement the required interface.
150+
151+
```@example Heron
152+
struct StopWhenSquared <: StoppingCriterion
153+
tol::Float64 # when do we consider things to be converged
154+
end
155+
```
156+
157+
### Checking for convergence
158+
159+
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).
160+
161+
```@example Heron
162+
using AlgorithmsInterface: DefaultStoppingCriterionState
163+
164+
function AlgorithmsInterface.is_finished(
165+
problem::SqrtProblem, ::Algorithm, state::State,
166+
stopping_criterion::StopWhenSquared, ::DefaultStoppingCriterionState
167+
)
168+
return state.iteration > 0 && isapprox(state.iterate^2, problem.S; atol = stopping_criterion.tol)
169+
end
170+
```
171+
172+
Note that we automatically obtain a `DefaultStoppingCriterionState` as the final argument, in which we have to store the iteration at which convergence is reached.
173+
As this is a mutating operation that alters the `stopping_criterion_state`, we ensure that it is called exactly once per iteration, while the non-mutating version is simply used to inspect the current status.
174+
175+
```@example Heron
176+
function AlgorithmsInterface.is_finished!(
177+
problem::SqrtProblem, ::Algorithm, state::State,
178+
stopping_criterion::StopWhenSquared, stopping_criterion_state::DefaultStoppingCriterionState
179+
)
180+
if state.iteration > 0 && isapprox(state.iterate^2, problem.S; atol = criterion.tol)
181+
stopping_criterion_state.at_iteration = state.iteration
182+
return true
183+
else
184+
return false
185+
end
186+
end
187+
```
188+
189+
### Reason and convergence reporting
190+
191+
Finally, we need to implement [`get_reason`](@ref) and [`indicates_convergence`](@ref).
192+
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.
193+
194+
```@example Heron
195+
function AlgorithmsInterface.get_reason(stopping_criterion::StopWhenSquared, stopping_criterion_state::DefaultStoppingCriterionState)
196+
stopping_criterion_state.at_iteration >= 0 || return nothing
197+
return "The algorithm reached a square root after $(stopping_criterion_state.at_iteration) iterations up to a tolerance of $(stopping_criterion.tol)."
198+
end
199+
200+
AlgorithmsInterface.indicates_convergence(::StopWhenSquared, ::DefaultStoppingCriterionState) = true
201+
```
202+
203+
### Convergence in action
204+
205+
Then we are finally ready to test out our new stopping criteria.
206+
207+
```@example Heron
208+
criterion = StopWhenSquared(1e-8)
209+
heron_sqrt(16.0; stopping_criterion = criterion)
210+
```
211+
212+
### Initialization
213+
214+
Now suppose we want to stop when successive iterates change by less than `ϵ`.
215+
This can be achieved by introducing a new stopping criterion again, but now we have to retain the previous `iterate` in order to have something to compare against.
216+
Similar to the algorithm `State`, we split up the data into a _static_ part, the [`StoppingCriterion`](@ref), and a _dynamic_ part, the [`StoppingCriterionState`](@ref).
151217

152218
```@example Heron
153219
struct StopWhenStable <: StoppingCriterion
@@ -161,40 +227,54 @@ mutable struct StopWhenStableState <: StoppingCriterionState
161227
end
162228
```
163229

164-
Note that our mutable state holds both the `previous_iterate`, which we need to compare to,
165-
as well as the iteration at which the condition was satisfied.
230+
Note that our mutable state holds both the `previous_iterate`, which we need to compare to, as well as the iteration at which the condition was satisfied.
166231
This is not strictly necessary, but can be convenient to have a persistent indication that convergence was reached.
167232

168-
### Initialization
169-
170233
In order to support these _stateful_ criteria, again an initialization phase is needed.
234+
The relevant functions are now:
235+
236+
- [`AlgorithmsInterface.initialize_stopping_state`](@ref)
237+
- [`AlgorithmsInterface.initialize_stopping_state!`](@ref)
238+
171239
This could be implemented as follows:
172240

173241
```@example Heron
174-
function AlgorithmsInterface.initialize_state(::Problem, ::Algorithm, c::StopWhenStable; kwargs...)
242+
function AlgorithmsInterface.initialize_stopping_state(
243+
::Problem, ::Algorithm,
244+
stopping_criterion::StopWhenStable;
245+
kwargs...
246+
)
175247
return StopWhenStableState(NaN, -1, NaN)
176248
end
177249
178-
function AlgorithmsInterface.initialize_state!(
179-
::Problem, ::Algorithm, stop_when::StopWhenStable, st::StopWhenStableState;
250+
function AlgorithmsInterface.initialize_stopping_state!(
251+
::Problem, ::Algorithm, ::State,
252+
stopping_criterion::StopWhenStable,
253+
stopping_criterion_state::StopWhenStableState;
180254
kwargs...
181-
)
182-
st.previous_iterate = NaN
183-
st.at_iteration = -1
184-
st.delta = NaN
185-
return st
255+
)
256+
stopping_criterion_state.previous_iterate = NaN
257+
stopping_criterion_state.at_iteration = -1
258+
stopping_criterion_state.delta = NaN
259+
return stopping_criterion_state
186260
end
187261
```
188262

189-
### Checking for convergence
263+
!!! note
190264

191-
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).
192-
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.
265+
While for this simple case this does not matter, note that there is a subtle detail associated to the initialization order of the `State` and `StoppingCriterionState` respectively.
266+
For the first initialization, [`AlgorithmsInterface.initialize_stopping_state`](@ref) is called _before_ [`initialize_state`](@ref).
267+
This is required since the `State` encapsulates the `StoppingCriterionState`.
268+
On the other hand, during the solver, the [`AlgorithmsInterface.initialize_stopping_state!`](@ref) is called _before_ [`initialize_state`](@ref).
269+
This can be important for example to ensure that the initialization time of the state is taken into account for the stopping criteria.
270+
271+
The remainder of the implementation follows straightforwardly, where we again take care to only mutate the `stopping_criterion_state` in the mutating `is_finished!` implementation.
193272

194273
```@example Heron
195274
function AlgorithmsInterface.is_finished!(
196275
::Problem, ::Algorithm, state::State, c::StopWhenStable, st::StopWhenStableState
197-
)
276+
)
277+
198278
k = state.iteration
199279
if k == 0
200280
st.previous_iterate = state.iterate
@@ -213,21 +293,14 @@ end
213293
214294
function AlgorithmsInterface.is_finished(
215295
::Problem, ::Algorithm, state::State, c::StopWhenStable, st::StopWhenStableState
216-
)
296+
)
217297
k = state.iteration
218298
k == 0 && return false
219299
220300
Δ = abs(state.iterate - st.previous_iterate)
221301
return Δ < c.tol
222302
end
223-
```
224-
225-
### Reason and convergence reporting
226303
227-
Finally, we need to implement [`get_reason`](@ref) and [`indicates_convergence`](@ref).
228-
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.
229-
230-
```@example Heron
231304
function AlgorithmsInterface.get_reason(c::StopWhenStable, st::StopWhenStableState)
232305
(st.at_iteration >= 0 && st.delta < c.tol) || return nothing
233306
return "The algorithm reached an approximate stable point after $(st.at_iteration) iterations; the change $(st.delta) is less than $(c.tol)."
@@ -238,14 +311,14 @@ AlgorithmsInterface.indicates_convergence(c::StopWhenStable, st::StopWhenStableS
238311

239312
### Convergence in action
240313

241-
Then we are finally ready to test out our new stopping criterion.
314+
Again, we can inspect our work:
242315

243316
```@example Heron
244317
criterion = StopWhenStable(1e-8)
245318
heron_sqrt(16.0; stopping_criterion = criterion)
246319
```
247320

248-
Note that our work payed off, as we can still compose this stopping criterion with other criteria as well:
321+
Note that our work to ensure the correct interface payed off, as we can still compose this stopping criterion with other criteria as well:
249322

250323
```@example Heron
251324
criterion = StopWhenStable(1e-8) | StopAfterIteration(5)
@@ -258,7 +331,7 @@ Implementing a criterion usually means defining:
258331

259332
1. A subtype of [`StoppingCriterion`](@ref).
260333
2. A state subtype of [`StoppingCriterionState`](@ref) capturing dynamic fields.
261-
3. `initialize_state` and `initialize_state!` for setup/reset.
334+
3. `initialize_stopping_state` and `initialize_stopping_state!` for setup/reset.
262335
4. `is_finished!` (mutating) and optionally `is_finished` (non‑mutating) variants.
263336
5. `get_reason` (return `nothing` or a string) for user feedback.
264337
6. `indicates_convergence(::YourCriterion)` to mark if meeting it implies convergence.

0 commit comments

Comments
 (0)