Stopping criteria
Continuing the square‑root story from the Interface page, we now decide when the iteration should halt. A stopping criterion encapsulates halting logic separately from the algorithm update rule.
Why separate stopping logic?
Decoupling halting from stepping lets us:
- Reuse generic stopping (iteration caps, time limits) across algorithms.
- Compose multiple conditions (stop after 1 second OR 100 iterations, etc.).
- Query convergence indication vs. mere forced termination.
- Store structured reasons and state (e.g. at which iteration a threshold was met).
Built-in criteria: Heron's method
The package ships several concrete StoppingCriterions:
StopAfterIteration: stop after a maximum number of iterations.StopAfter: stop after a wall‑clock timePeriod(e.g.Second(2),Minute(1)).- Combinations
StopWhenAll(logical AND) andStopWhenAny(logical OR) built via&and|operators.
Each criterion has an associated StoppingCriterionState storing dynamic data (iteration when met, elapsed time, etc.).
Recall our example implementation for Heron's method, where we we added a stopping_criterion to the Algorithm, as well as a stopping_criterion_state to the State.
using AlgorithmsInterface
struct SqrtProblem <: Problem
S::Float64 # number whose square root we seek
end
struct HeronAlgorithm <: Algorithm
stopping_criterion # any StoppingCriterion
end
mutable struct HeronState <: State
iterate::Float64 # current iterate
iteration::Int # current iteration count
stopping_criterion_state # any StoppingCriterionState
endHere, 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.
Initialization
The first core component to enable working with stopping criteria is to extend the initialization step to include initializing a StoppingCriterionState as well. This can conveniently be done through the same initialization functions we used for initializing the state:
initialize_stateconstructs an entirely new stopping state for the algorithminitialize_state!(in-place) reset of an existing stopping state.
function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
x0 = rand() # random initial guess
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion)
return HeronState(x0, 0, stopping_criterion_state)
end
function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
# reset the state for the algorithm
state.iterate = rand()
state.iteration = 0
# reset the state for the stopping criterion
state = AlgorithmsInterface.initialize_state!(
problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state
)
return state
endIteration
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:
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
return state
endWhat is really going on is that behind the scenes, the loop of the iterative solver expands to code that is equivalent to:
while !is_finished!(problem, algorithm, state)
increment!(state)
step!(problem, algorithm, state)
endIn other words, all of the logic is handled by the is_finished! function. The generic stopping criteria provided by this package have default implementations for this function that work out-of-the-box. This is partially because we used conventional names for the fields in the structs. There, Algorithm assumes the existence of stopping_criterion, while State assumes iterate and iteration and stopping_criterion_state to exist.
Running the algorithm
We can again combine everything into a single function, but now make the stopping criterion accessible:
function heron_sqrt(x; stopping_criterion)
prob = SqrtProblem(x)
alg = HeronAlgorithm(stopping_criterion)
state = solve(prob, alg) # allocates & runs
return state.iterate, state.iteration
end
heron_sqrt(2; stopping_criterion = StopAfterIteration(10))(1.414213562373095, 10)With this function, we are now ready to explore different ways of telling the algorithm to stop. For example, using the basic criteria provided by this package, we can alternatively do:
using Dates
criterion = StopAfter(Millisecond(50))
heron_sqrt(2; stopping_criterion = criterion)(1.414213562373095, 180198)We can tighten the condition by combining criteria. Suppose we want to stop after either 25 iterations or 50 milliseconds, whichever comes first:
criterion = StopAfterIteration(25) | StopAfter(Millisecond(50)) # logical OR
heron_sqrt(2; stopping_criterion = criterion)(1.414213562373095, 25)Conversely, to demand both a minimum iteration quality condition and a cap, use & (logical AND).
criterion = StopAfterIteration(25) & StopAfter(Millisecond(50)) # logical AND
heron_sqrt(2; stopping_criterion = criterion)(1.414213562373095, 133875)Implementing a new criterion
It is of course possible that we are not satisfied by the stopping criteria that are provided by default. Suppose we want to stop when successive iterates change by less than ϵ, we could achieve this by implementing our own stopping criterion. In order to do so, we need to define our own structs and implement the required interface. Again, we split up the data into a static part, the StoppingCriterion, and a dynamic part, the StoppingCriterionState.
struct StopWhenStable <: StoppingCriterion
tol::Float64 # when do we consider things converged
end
mutable struct StopWhenStableState <: StoppingCriterionState
previous_iterate::Float64 # previous value to compare to
at_iteration::Int # iteration at which stability was reached
delta::Float64 # difference between the values
endNote 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. This is not strictly necessary, but can be convenient to have a persistent indication that convergence was reached.
Initialization
In order to support these stateful criteria, again an initialization phase is needed. This could be implemented as follows:
function AlgorithmsInterface.initialize_state(::Problem, ::Algorithm, c::StopWhenStable; kwargs...)
return StopWhenStableState(NaN, -1, NaN)
end
function AlgorithmsInterface.initialize_state!(
::Problem, ::Algorithm, stop_when::StopWhenStable, st::StopWhenStableState;
kwargs...
)
st.previous_iterate = NaN
st.at_iteration = -1
st.delta = NaN
return st
endChecking for convergence
Then, we need to implement the logic that checks whether an algorithm has finished, which is achieved through is_finished and is_finished!. 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.
function AlgorithmsInterface.is_finished!(
::Problem, ::Algorithm, state::State, c::StopWhenStable, st::StopWhenStableState
)
k = state.iteration
if k == 0
st.previous_iterate = state.iterate
st.at_iteration = -1
return false
end
st.delta = abs(state.iterate - st.previous_iterate)
st.previous_iterate = state.iterate
if st.delta < c.tol
st.at_iteration = k
return true
end
return false
end
function AlgorithmsInterface.is_finished(
::Problem, ::Algorithm, state::State, c::StopWhenStable, st::StopWhenStableState
)
k = state.iteration
k == 0 && return false
Δ = abs(state.iterate - st.previous_iterate)
return Δ < c.tol
endReason and convergence reporting
Finally, we need to implement get_reason and indicates_convergence. These helper functions are required to interact with the logging system, to distinguish between states that are considered ongoing, stopped and converged, or stopped without convergence.
function AlgorithmsInterface.get_reason(c::StopWhenStable, st::StopWhenStableState)
(st.at_iteration >= 0 && st.delta < c.tol) || return nothing
return "The algorithm reached an approximate stable point after $(st.at_iteration) iterations; the change $(st.delta) is less than $(c.tol)."
end
AlgorithmsInterface.indicates_convergence(c::StopWhenStable, st::StopWhenStableState) = trueConvergence in action
Then we are finally ready to test out our new stopping criterion.
criterion = StopWhenStable(1e-8)
heron_sqrt(16.0; stopping_criterion = criterion)(4.0, 7)Note that our work payed off, as we can still compose this stopping criterion with other criteria as well:
criterion = StopWhenStable(1e-8) | StopAfterIteration(5)
heron_sqrt(16.0; stopping_criterion = criterion)(4.248505646582453, 5)Summary
Implementing a criterion usually means defining:
- A subtype of
StoppingCriterion. - A state subtype of
StoppingCriterionStatecapturing dynamic fields. initialize_stateandinitialize_state!for setup/reset.is_finished!(mutating) and optionallyis_finished(non‑mutating) variants.get_reason(returnnothingor a string) for user feedback.indicates_convergence(::YourCriterion)to mark if meeting it implies convergence.
You may also implement Base.summary(io, criterion, criterion_state) for compact status reports.
Reference API
Below are the auto‑generated docs for all stopping criterion infrastructure.
AlgorithmsInterface.DefaultStoppingCriterionState — TypeDefaultStoppingCriterionState <: StoppingCriterionState
A StoppingCriterionState that does not require any information besides storing the iteration number when it (last) indicated to stop).
Field
at_iteration::Intstore the iteration number this state indicated to stop.0means already at the start it indicated to stop- any negative number means that it did not yet indicate to stop.
AlgorithmsInterface.GroupStoppingCriterionState — TypeGroupStoppingCriterionState <: StoppingCriterionStateA StoppingCriterionState that groups multiple StoppingCriterionStates internally as a tuple. This is for example used in combination with StopWhenAny and StopWhenAll
Constructor
GroupStoppingCriterionState(c::Vector{N,StoppingCriterionState} where N)
GroupStoppingCriterionState(c::StoppingCriterionState...)AlgorithmsInterface.StopAfter — TypeStopAfter <: StoppingCriterionstore a threshold when to stop looking at the complete runtime. It uses time_ns() to measure the time and you provide a Period as a time limit, for example Minute(15).
Fields
thresholdstores thePeriodafter which to stop
Constructor
StopAfter(t)initialize the stopping criterion to a Period t to stop after.
AlgorithmsInterface.StopAfterIteration — TypeStopAfterIteration <: StoppingCriterionA simple stopping criterion to stop after a maximal number of iterations.
Fields
max_iterationsstores the maximal iteration number where to stop at
Constructor
StopAfterIteration(maxIter)initialize the functor to indicate to stop after maxIter iterations.
AlgorithmsInterface.StopAfterTimePeriodState — TypeStopAfterTimePeriodState <: StoppingCriterionStateA state for stopping criteria that are based on time measurements, for example StopAfter.
startstores the starting time when the algorithm is started, that is a call withi=0.timestores the elapsed timeat_iterationindicates at which iteration (includingi=0) the stopping criterion was fulfilled and is-1while it is not fulfilled.
AlgorithmsInterface.StopWhenAll — TypeStopWhenAll <: StoppingCriterionstore a tuple of StoppingCriterions and indicate to stop, when all indicate to stop.
Constructor
StopWhenAll(c::NTuple{N,StoppingCriterion} where N)
StopWhenAll(c::StoppingCriterion,...)AlgorithmsInterface.StopWhenAny — TypeStopWhenAny <: StoppingCriterionstore an array of StoppingCriterion elements and indicates to stop, when any single one indicates to stop. The reason is given by the concatenation of all reasons (assuming that all non-indicating return "").
Constructors
StopWhenAny(c::Vector{N,StoppingCriterion} where N)
StopWhenAny(c::StoppingCriterion...)AlgorithmsInterface.StoppingCriterion — TypeStoppingCriterionAn abstract type to represent a stopping criterion of an Algorithm.
A concrete StoppingCriterion should also implement a initialize_state(problem::Problem, algorithm::Algorithm, stopping_criterion::StoppingCriterion; kwargs...) function to create its accompanying StoppingCriterionState. as well as the corresponding mutating variant to reset such a StoppingCriterionState.
It should usually implement
indicates_convergence(stopping_criterion)indicates_convergence(stopping_criterion, stopping_criterion_state)is_finished!(problem, algorithm, state, stopping_criterion, stopping_criterion_state)is_finished(problem, algorithm, state, stopping_criterion, stopping_criterion_state)
AlgorithmsInterface.StoppingCriterionState — TypeStoppingCriterionStateAn abstract type to represent a stopping criterion state within a State. It represents the concrete state a StoppingCriterion is in.
It should usually implement
get_reason(stopping_criterion, stopping_criterion_state)indicates_convergence(stopping_criterion, stopping_criterion_state)is_finished!(problem, algorithm, state, stopping_criterion, stopping_criterion_state)is_finished(problem, algorithm, state, stopping_criterion, stopping_criterion_state)
AlgorithmsInterface.get_reason — Methodget_reason(stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)Provide a reason in human readable text as to why a StoppingCriterion with StoppingCriterionState indicated to stop. If it does not indicate to stop, this should return nothing.
Providing the iteration at which this indicated to stop in the reason would be preferable.
AlgorithmsInterface.indicates_convergence — Methodindicates_convergence(stopping_criterion::StoppingCriterion, ::StoppingCriterionState)Return whether or not a StoppingCriterion indicates convergence when it is in StoppingCriterionState.
By default this checks whether the StoppingCriterion has actually stopped. If so it returns whether stopping_criterion itself indicates convergence, otherwise it returns false, since the algorithm has then not yet stopped.
AlgorithmsInterface.indicates_convergence — Methodindicates_convergence(stopping_criterion::StoppingCriterion)Return whether or not a StoppingCriterion indicates convergence.
AlgorithmsInterface.is_finished! — Methodis_finished(problem::Problem, algorithm::Algorithm, state::State)
is_finished(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)
is_finished!(problem::Problem, algorithm::Algorithm, state::State)
is_finished!(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)Indicate whether an Algorithm solving Problem is finished having reached a certain State. The variant with three arguments by default extracts the StoppingCriterion and its StoppingCriterionState and their actual checks are performed in the implementation with five arguments.
The mutating variant does alter the stopping_criterion_state and and should only be called once per iteration, the other one merely inspects the current status without mutation.
AlgorithmsInterface.is_finished! — Methodis_finished(problem::Problem, algorithm::Algorithm, state::State)
is_finished(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)
is_finished!(problem::Problem, algorithm::Algorithm, state::State)
is_finished!(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)Indicate whether an Algorithm solving Problem is finished having reached a certain State. The variant with three arguments by default extracts the StoppingCriterion and its StoppingCriterionState and their actual checks are performed in the implementation with five arguments.
The mutating variant does alter the stopping_criterion_state and and should only be called once per iteration, the other one merely inspects the current status without mutation.
AlgorithmsInterface.is_finished — Methodis_finished(problem::Problem, algorithm::Algorithm, state::State)
is_finished(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)
is_finished!(problem::Problem, algorithm::Algorithm, state::State)
is_finished!(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)Indicate whether an Algorithm solving Problem is finished having reached a certain State. The variant with three arguments by default extracts the StoppingCriterion and its StoppingCriterionState and their actual checks are performed in the implementation with five arguments.
The mutating variant does alter the stopping_criterion_state and and should only be called once per iteration, the other one merely inspects the current status without mutation.
AlgorithmsInterface.is_finished — Methodis_finished(problem::Problem, algorithm::Algorithm, state::State)
is_finished(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)
is_finished!(problem::Problem, algorithm::Algorithm, state::State)
is_finished!(problem::Problem, algorithm::Algorithm, state::State, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)Indicate whether an Algorithm solving Problem is finished having reached a certain State. The variant with three arguments by default extracts the StoppingCriterion and its StoppingCriterionState and their actual checks are performed in the implementation with five arguments.
The mutating variant does alter the stopping_criterion_state and and should only be called once per iteration, the other one merely inspects the current status without mutation.
Base.:& — Method&(s1,s2)
s1 & s2Combine two StoppingCriterion within an StopWhenAll. If either s1 (or s2) is already an StopWhenAll, then s2 (or s1) is appended to the list of StoppingCriterion within s1 (or s2).
Example
a = StopAfterIteration(200) & StopAfter(Minute(1))Is the same as
a = StopWhenAll(StopAfterIteration(200), StopAfter(Minute(1))Base.:| — Method|(s1,s2)
s1 | s2Combine two StoppingCriterion within an StopWhenAny. If either s1 (or s2) is already an StopWhenAny, then s2 (or s1) is appended to the list of StoppingCriterion within s1 (or s2)
Example
a = StopAfterIteration(200) | StopAfter(Minute(1))Is the same as
a = StopWhenAny(StopAfterIteration(200), StopAfter(Minute(1)))Base.summary — Methodsummary(io::IO, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState)Provide a summary of the status of a stopping criterion – its parameters and whether it currently indicates to stop. It should not be longer than one line
Example
For the StopAfterIteration criterion, the summary looks like
Max Iterations (15): not reachedNext: Logging
With halting logic done, proceed to the logging section to instrument the same example and capture intermediate diagnostics.