|
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. |
2 | 267 |
|
3 | 268 | ```@autodocs |
4 | 269 | Modules = [AlgorithmsInterface] |
5 | 270 | Pages = ["stopping_criterion.jl"] |
6 | 271 | Order = [:type, :function] |
7 | 272 | 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. |
0 commit comments