diff --git a/docs/src/interface.md b/docs/src/interface.md index 9d4817a..dccbd76 100644 --- a/docs/src/interface.md +++ b/docs/src/interface.md @@ -67,7 +67,7 @@ end ### Initialization -In order to start implementing the core parts of our algorithm, we start at the very beginning. +In order to begin implementing the core parts of our algorithm, we start at the very beginning. There are two main entry points provided by the interface: - [`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: An example implementation might look like: ```@example Heron -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) +function AlgorithmsInterface.initialize_state( + problem::SqrtProblem, algorithm::HeronAlgorithm, + stopping_criterion_state::StoppingCriterionState; + kwargs... + ) + x0 = rand() + iteration = 0 + return HeronState(x0, iteration, 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 +function AlgorithmsInterface.initialize_state!( + problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; + kwargs... ) + state.iteration = 0 return state end ``` @@ -175,6 +175,15 @@ Order = [:type, :function] Private = true ``` +### Stopping Criteria + +```@autodocs +Modules = [AlgorithmsInterface] +Pages = ["interface/stopping.jl"] +Order = [:type, :function] +Private = true +``` + ### Next: Stopping criteria 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. diff --git a/docs/src/logging.md b/docs/src/logging.md index c167abb..bbc5e1c 100644 --- a/docs/src/logging.md +++ b/docs/src/logging.md @@ -52,16 +52,21 @@ mutable struct HeronState <: State stopping_criterion_state end -function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...) +function AlgorithmsInterface.initialize_state( + problem::SqrtProblem, algorithm::HeronAlgorithm, + stopping_criterion_state::StoppingCriterionState; + kwargs... + ) x0 = rand() - stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion) + iteration = 0 return HeronState(x0, 0, stopping_criterion_state) end -function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...) - state.iterate = rand() +function AlgorithmsInterface.initialize_state!( + problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; + kwargs... + ) state.iteration = 0 - initialize_state!(problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state) return state end diff --git a/docs/src/stopping_criterion.md b/docs/src/stopping_criterion.md index f03c3e4..1f23589 100644 --- a/docs/src/stopping_criterion.md +++ b/docs/src/stopping_criterion.md @@ -52,31 +52,31 @@ Here, we delve a bit deeper into the core components of what made our algorithm ### Initialization The first core component to enable working with stopping criteria is to extend the initialization step to include initializing a [`StoppingCriterionState`](@ref) as well. -This can conveniently be done through the same initialization functions we used for initializing the state: - -- [`initialize_state`](@ref) constructs an entirely new stopping state for the algorithm -- [`initialize_state!`](@ref) (in-place) reset of an existing stopping state. +Since some of these may require _stateful_ implementations, we also keep a `stopping_criterion_state` that captures this, and thus needs to be initialized. +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: ```@example Heron -function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...) - x0 = rand() # random initial guess - stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion) +function AlgorithmsInterface.initialize_state( + problem::SqrtProblem, algorithm::HeronAlgorithm, + stopping_criterion_state::StoppingCriterionState; + kwargs... + ) + x0 = rand() + iteration = 0 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 +function AlgorithmsInterface.initialize_state!( + problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; + kwargs... ) + state.iteration = 0 return state end ``` +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. + ### Iteration 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) ## 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`](@ref), and a _dynamic_ part, the [`StoppingCriterionState`](@ref). +For example, we might check for convergence by squaring our current `iterate` and seeing if it equals the input value. +In order to do so, we need to define our own struct and implement the required interface. + +```@example Heron +struct StopWhenSquared <: StoppingCriterion + tol::Float64 # when do we consider things to be converged +end +``` + +### Checking for convergence + +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). + +```@example Heron +using AlgorithmsInterface: DefaultStoppingCriterionState + +function AlgorithmsInterface.is_finished( + problem::SqrtProblem, ::Algorithm, state::State, + stopping_criterion::StopWhenSquared, ::DefaultStoppingCriterionState + ) + return state.iteration > 0 && isapprox(state.iterate^2, problem.S; atol = stopping_criterion.tol) +end +``` + +Note that we automatically obtain a `DefaultStoppingCriterionState` as the final argument, in which we have to store the iteration at which convergence is reached. +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. + +```@example Heron +function AlgorithmsInterface.is_finished!( + problem::SqrtProblem, ::Algorithm, state::State, + stopping_criterion::StopWhenSquared, stopping_criterion_state::DefaultStoppingCriterionState + ) + if state.iteration > 0 && isapprox(state.iterate^2, problem.S; atol = criterion.tol) + stopping_criterion_state.at_iteration = state.iteration + return true + else + return false + end +end +``` + +### Reason and convergence reporting + +Finally, we need to implement [`get_reason`](@ref) and [`indicates_convergence`](@ref). +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. + +```@example Heron +function AlgorithmsInterface.get_reason(stopping_criterion::StopWhenSquared, stopping_criterion_state::DefaultStoppingCriterionState) + stopping_criterion_state.at_iteration >= 0 || return nothing + return "The algorithm reached a square root after $(stopping_criterion_state.at_iteration) iterations up to a tolerance of $(stopping_criterion.tol)." +end + +AlgorithmsInterface.indicates_convergence(::StopWhenSquared, ::DefaultStoppingCriterionState) = true +``` + +### Convergence in action + +Then we are finally ready to test out our new stopping criteria. + +```@example Heron +criterion = StopWhenSquared(1e-8) +heron_sqrt(16.0; stopping_criterion = criterion) +``` + +### Initialization + +Now suppose we want to stop when successive iterates change by less than `ϵ`. +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. +Similar to the algorithm `State`, we split up the data into a _static_ part, the [`StoppingCriterion`](@ref), and a _dynamic_ part, the [`StoppingCriterionState`](@ref). ```@example Heron struct StopWhenStable <: StoppingCriterion @@ -161,40 +227,54 @@ mutable struct StopWhenStableState <: StoppingCriterionState end ``` -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. +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. 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. +The relevant functions are now: + +- [`AlgorithmsInterface.initialize_stopping_state`](@ref) +- [`AlgorithmsInterface.initialize_stopping_state!`](@ref) + This could be implemented as follows: ```@example Heron -function AlgorithmsInterface.initialize_state(::Problem, ::Algorithm, c::StopWhenStable; kwargs...) +function AlgorithmsInterface.initialize_stopping_state( + ::Problem, ::Algorithm, + stopping_criterion::StopWhenStable; + kwargs... + ) return StopWhenStableState(NaN, -1, NaN) end -function AlgorithmsInterface.initialize_state!( - ::Problem, ::Algorithm, stop_when::StopWhenStable, st::StopWhenStableState; +function AlgorithmsInterface.initialize_stopping_state!( + ::Problem, ::Algorithm, ::State, + stopping_criterion::StopWhenStable, + stopping_criterion_state::StopWhenStableState; kwargs... -) - st.previous_iterate = NaN - st.at_iteration = -1 - st.delta = NaN - return st + ) + stopping_criterion_state.previous_iterate = NaN + stopping_criterion_state.at_iteration = -1 + stopping_criterion_state.delta = NaN + return stopping_criterion_state end ``` -### Checking for convergence +!!! note -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). -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. + 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. + For the first initialization, [`AlgorithmsInterface.initialize_stopping_state`](@ref) is called _before_ [`initialize_state`](@ref). + This is required since the `State` encapsulates the `StoppingCriterionState`. + On the other hand, during the solver, the [`AlgorithmsInterface.initialize_stopping_state!`](@ref) is called _before_ [`initialize_state`](@ref). + This can be important for example to ensure that the initialization time of the state is taken into account for the stopping criteria. + +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. ```@example Heron function AlgorithmsInterface.is_finished!( ::Problem, ::Algorithm, state::State, c::StopWhenStable, st::StopWhenStableState -) + ) + k = state.iteration if k == 0 st.previous_iterate = state.iterate @@ -213,21 +293,14 @@ 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 end -``` - -### Reason and convergence reporting -Finally, we need to implement [`get_reason`](@ref) and [`indicates_convergence`](@ref). -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. - -```@example Heron 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)." @@ -238,14 +311,14 @@ AlgorithmsInterface.indicates_convergence(c::StopWhenStable, st::StopWhenStableS ### Convergence in action -Then we are finally ready to test out our new stopping criterion. +Again, we can inspect our work: ```@example Heron criterion = StopWhenStable(1e-8) heron_sqrt(16.0; stopping_criterion = criterion) ``` -Note that our work payed off, as we can still compose this stopping criterion with other criteria as well: +Note that our work to ensure the correct interface payed off, as we can still compose this stopping criterion with other criteria as well: ```@example Heron criterion = StopWhenStable(1e-8) | StopAfterIteration(5) @@ -258,7 +331,7 @@ Implementing a criterion usually means defining: 1. A subtype of [`StoppingCriterion`](@ref). 2. A state subtype of [`StoppingCriterionState`](@ref) capturing dynamic fields. -3. `initialize_state` and `initialize_state!` for setup/reset. +3. `initialize_stopping_state` and `initialize_stopping_state!` for setup/reset. 4. `is_finished!` (mutating) and optionally `is_finished` (non‑mutating) variants. 5. `get_reason` (return `nothing` or a string) for user feedback. 6. `indicates_convergence(::YourCriterion)` to mark if meeting it implies convergence. diff --git a/src/AlgorithmsInterface.jl b/src/AlgorithmsInterface.jl index 2a9ed68..e3ddd48 100644 --- a/src/AlgorithmsInterface.jl +++ b/src/AlgorithmsInterface.jl @@ -15,6 +15,7 @@ using ScopedValues include("interface/algorithm.jl") include("interface/problem.jl") include("interface/state.jl") +include("interface/stopping.jl") include("interface/interface.jl") include("stopping_criterion.jl") diff --git a/src/interface/interface.jl b/src/interface/interface.jl index 8640795..347838c 100644 --- a/src/interface/interface.jl +++ b/src/interface/interface.jl @@ -1,13 +1,34 @@ _doc_init_state = """ - state = initialize_state(problem::Problem, algorithm::Algorithm; kwargs...) - state = initialize_state!(problem::Problem, algorithm::Algorithm, state::State; kwargs...) + state = initialize_state( + problem::Problem, algorithm::Algorithm, + [stopping_criterion_state::StoppingCriterionState]; + kwargs... + ) + state = initialize_state!( + problem::Problem, algorithm::Algorithm, state::State; + kwargs... + ) Initialize a [`State`](@ref) based on a [`Problem`](@ref) and an [`Algorithm`](@ref). The `kwargs...` should allow to initialize for example the initial point. This can be done in-place for `state`, then only values that did change have to be provided. + +Note that since the returned state should also hold `state.stopping_criterion_state`, +which will be used to keep the internal state of the stopping criterion, the out-of-place +version receives this as one of its arguments. By default, that will be initialized separately +through a call to [`initialize_stopping_state`](@ref) and provided as an argument. + +On the other hand, the in-place version is not responsible for initializing the `stopping_criterion_state`, +as that will be handled separately by [`initialize_stopping_state!`](@ref). + +Users that which to handle the stopping criterion initialization in `initialize_state` manually +should overload the 2-argument version, while by default the 3-argument version should be implemented. """ -function initialize_state end +function initialize_state(problem::Problem, algorithm::Algorithm; kwargs...) + stopping_criterion_state = initialize_stopping_state(problem, algorithm; kwargs...) + return initialize_state(problem, algorithm, stopping_criterion_state; kwargs...) +end @doc "$(_doc_init_state)" initialize_state(::Problem, ::Algorithm; kwargs...) @@ -23,9 +44,10 @@ initialize_state!(::Problem, ::Algorithm, ::State; kwargs...) solve(problem::Problem, algorithm::Algorithm; kwargs...) Solve the [`Problem`](@ref) using an [`Algorithm`](@ref). -The keyword arguments `kwargs...` have to provide enough details such that -the corresponding state initialisation [`initialize_state`](@ref)`(problem, algorithm)` -returns a state. + +The keyword arguments `kwargs...` have to provide enough details such that the corresponding +state and stopping state initialisation [`initialize_state`](@ref)` and [`initialize_stopping_state`](@ref) +can be used to return valid states and stopping states. By default this method continues to call [`solve!`](@ref). """ @@ -40,16 +62,18 @@ end Solve the [`Problem`](@ref) using an [`Algorithm`](@ref), starting from a given [`State`](@ref). The state is modified in-place. -All keyword arguments are passed to the [`initialize_state!`](@ref)`(problem, algorithm, state)` function. +All keyword arguments are passed to the [`initialize_state!`](@ref) and +[`initialize_stopping_state!`](@ref) functions. """ function solve!(problem::Problem, algorithm::Algorithm, state::State; kwargs...) # obtain logger once to minimize overhead from accessing ScopedValue # additionally handle logging initialization to enable stateful LoggingAction logger = algorithm_logger() - # initialize_logger(logger, problem, algorithm, state) # initialize the state and emit message + initialize_stopping_state!(problem, algorithm, state; kwargs...) initialize_state!(problem, algorithm, state; kwargs...) + emit_message(logger, problem, algorithm, state, :Start) # main body of the algorithm @@ -72,6 +96,7 @@ function solve!(problem::Problem, algorithm::Algorithm, state::State; kwargs...) end function step! end + @doc """ step!(problem::Problem, algorithm::Algorithm, state::State) diff --git a/src/interface/stopping.jl b/src/interface/stopping.jl new file mode 100644 index 0000000..82b5bce --- /dev/null +++ b/src/interface/stopping.jl @@ -0,0 +1,168 @@ +@doc """ + StoppingCriterion + +An abstract type to represent a stopping criterion of an [`Algorithm`](@ref). + +A concrete [`StoppingCriterion`](@ref) should also implement a +[`initialize_state(problem::Problem, algorithm::Algorithm, stopping_criterion::StoppingCriterion; kwargs...)`](@ref) function to create its accompanying +[`StoppingCriterionState`](@ref). +as well as the corresponding mutating variant to reset such a [`StoppingCriterionState`](@ref). + +It should usually implement + +* [`indicates_convergence`](@ref)`(stopping_criterion)` +* [`indicates_convergence`](@ref)`(stopping_criterion, stopping_criterion_state)` +* [`is_finished!`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` +* [`is_finished`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` +""" +abstract type StoppingCriterion end + +@doc """ + StoppingCriterionState + +An abstract type to represent a stopping criterion state within a [`State`](@ref). +It represents the concrete state a [`StoppingCriterion`](@ref) is in. + +It should usually implement + +* [`get_reason`](@ref)`(stopping_criterion, stopping_criterion_state)` +* [`indicates_convergence`](@ref)`(stopping_criterion, stopping_criterion_state)` +* [`is_finished!`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` +* [`is_finished`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` +""" +abstract type StoppingCriterionState end + +# Initialization +# -------------- +_doc_init_stopping_state = """ + stopping_criterion_state = initialize_stopping_state( + problem::Problem, algorithm::Algorithm + stopping_criterion::StoppingCriterion = algorithm.stopping_criterion; + kwargs... + ) + stopping_criterion_state = initialize_stopping_state!( + problem::Problem, algorithm::Algorithm, state::State, + stopping_criterion::StoppingCriterion = algorithm.stopping_criterion, + stopping_criterion_state::StoppingCriterionState = state.stopping_criterion_state; + kwargs... + ) + +Initialize a [`StoppingCriterionState`](@ref) based on a [`Problem`](@ref), [`Algorithm`](@ref), +[`State`](@ref) triplet for a given [`StoppingCriterion`](@ref). +By default, the `stopping_criterion` is retrieved from the `Algorithm` via `algorithm.stopping_criterion`. + +The first signature is used for setting up a completely new stopping criterion state, while the second +simply resets a given state in-place. +""" + +initialize_stopping_state(problem::Problem, algorithm::Algorithm; kwargs...) = + initialize_stopping_state(problem, algorithm, algorithm.stopping_criterion; kwargs...) + +@doc "$(_doc_init_stopping_state)" +initialize_stopping_state(::Problem, ::Algorithm, ::StoppingCriterion; kwargs...) + +function initialize_stopping_state!(problem::Problem, algorithm::Algorithm, state::State; kwargs...) + return initialize_stopping_state!( + problem, algorithm, state, algorithm.stopping_criterion, state.stopping_criterion_state; kwargs... + ) +end + +@doc "$(_doc_init_stopping_state)" +initialize_stopping_state!(::Problem, ::Algorithm, ::State, ::StoppingCriterion, ::StoppingCriterionState; kwargs...) + + +# Convergence characterization +# ---------------------------- +function get_reason end + +@doc """ + get_reason(stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState) + +Provide a reason in human readable text as to why a [`StoppingCriterion`](@ref) with [`StoppingCriterionState`](@ref) 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. +""" +get_reason(::StoppingCriterion, ::StoppingCriterionState) + +function indicates_convergence end +@doc """ + indicates_convergence(stopping_criterion::StoppingCriterion) + +Return whether or not a [`StoppingCriterion`](@ref) indicates convergence. +""" +indicates_convergence(stopping_criterion::StoppingCriterion) + +@doc """ + indicates_convergence(stopping_criterion::StoppingCriterion, ::StoppingCriterionState) + +Return whether or not a [`StoppingCriterion`](@ref) indicates convergence when it is in [`StoppingCriterionState`](@ref). + +By default this checks whether the [`StoppingCriterion`](@ref) 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. +""" +function indicates_convergence( + stopping_criterion::StoppingCriterion, + stopping_criterion_state::StoppingCriterionState, + ) + return isnothing(get_reason(stopping_criterion, stopping_criterion_state)) && + indicates_convergence(stopping_criterion) +end + +# Convergence indication +# ---------------------- +_doc_is_finished = """ + is_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`](@ref) solving [`Problem`](@ref) is finished having reached +a certain [`State`](@ref). The variant with three arguments by default extracts the +[`StoppingCriterion`](@ref) and its [`StoppingCriterionState`](@ref) 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. +""" + +@doc "$(_doc_is_finished)" +function is_finished(problem::Problem, algorithm::Algorithm, state::State) + return is_finished( + problem, algorithm, state, + algorithm.stopping_criterion, + state.stopping_criterion_state, + ) +end + +@doc "$(_doc_is_finished)" +is_finished(::Problem, ::Algorithm, ::State, ::StoppingCriterion, ::StoppingCriterionState) + +@doc "$(_doc_is_finished)" +function is_finished!(problem::Problem, algorithm::Algorithm, state::State) + return is_finished!( + problem, algorithm, state, + algorithm.stopping_criterion, + state.stopping_criterion_state, + ) +end + +@doc "$(_doc_is_finished)" +is_finished!(::Problem, ::Algorithm, ::State, ::StoppingCriterion, ::StoppingCriterionState) + +@doc """ + summary(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`](@ref) criterion, the summary looks like + +``` +Max Iterations (15): not reached +``` +""" +Base.summary(io::IO, ::StoppingCriterion, ::StoppingCriterionState) diff --git a/src/stopping_criterion.jl b/src/stopping_criterion.jl index a6da5d0..ef79c3a 100644 --- a/src/stopping_criterion.jl +++ b/src/stopping_criterion.jl @@ -1,135 +1,38 @@ -@doc """ - StoppingCriterion - -An abstract type to represent a stopping criterion of an [`Algorithm`](@ref). - -A concrete [`StoppingCriterion`](@ref) should also implement a -[`initialize_state(problem::Problem, algorithm::Algorithm, stopping_criterion::StoppingCriterion; kwargs...)`](@ref) function to create its accompanying -[`StoppingCriterionState`](@ref). -as well as the corresponding mutating variant to reset such a [`StoppingCriterionState`](@ref). - -It should usually implement - -* [`indicates_convergence`](@ref)`(stopping_criterion)` -* [`indicates_convergence`](@ref)`(stopping_criterion, stopping_criterion_state)` -* [`is_finished!`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` -* [`is_finished`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` -""" -abstract type StoppingCriterion end - -@doc """ - StoppingCriterionState - -An abstract type to represent a stopping criterion state within a [`State`](@ref). -It represents the concrete state a [`StoppingCriterion`](@ref) is in. - -It should usually implement - -* [`get_reason`](@ref)`(stopping_criterion, stopping_criterion_state)` -* [`indicates_convergence`](@ref)`(stopping_criterion, stopping_criterion_state)` -* [`is_finished!`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` -* [`is_finished`](@ref)`(problem, algorithm, state, stopping_criterion, stopping_criterion_state)` +# By default, assume stopping criteria are stateless - they only need information +# in the regular state, and indicate the last iteration they converged on """ -abstract type StoppingCriterionState end - -function get_reason end -@doc """ - get_reason(stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState) - -Provide a reason in human readable text as to why a [`StoppingCriterion`](@ref) with [`StoppingCriterionState`](@ref) 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. -""" -get_reason(::StoppingCriterion, ::StoppingCriterionState) - -function indicates_convergence end -@doc """ - indicates_convergence(stopping_criterion::StoppingCriterion) - -Return whether or not a [`StoppingCriterion`](@ref) indicates convergence. -""" -indicates_convergence(stopping_criterion::StoppingCriterion) - -@doc """ - indicates_convergence(stopping_criterion::StoppingCriterion, ::StoppingCriterionState) + DefaultStoppingCriterionState <: StoppingCriterionState -Return whether or not a [`StoppingCriterion`](@ref) indicates convergence when it is in [`StoppingCriterionState`](@ref). +A [`StoppingCriterionState`](@ref) that does not require any information besides +storing the iteration number when it (last) indicated to stop). -By default this checks whether the [`StoppingCriterion`](@ref) 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. +# Field +* `at_iteration::Int` store the iteration number this state + indicated to stop. + * `0` means already at the start it indicated to stop + * any negative number means that it did not yet indicate to stop. """ -function indicates_convergence( - stopping_criterion::StoppingCriterion, - stopping_criterion_state::StoppingCriterionState, - ) - return isnothing(get_reason(stopping_criterion, stopping_criterion_state)) && - indicates_convergence(stopping_criterion) +mutable struct DefaultStoppingCriterionState <: StoppingCriterionState + at_iteration::Int + DefaultStoppingCriterionState() = new(-1) end -_doc_is_finished = """ - is_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`](@ref) solving [`Problem`](@ref) is finished having reached -a certain [`State`](@ref). The variant with three arguments by default extracts the -[`StoppingCriterion`](@ref) and its [`StoppingCriterionState`](@ref) 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. -""" - -@doc "$(_doc_is_finished)" -function is_finished(problem::Problem, algorithm::Algorithm, state::State) - return is_finished( - problem, - algorithm, - state, - algorithm.stopping_criterion, - state.stopping_criterion_state, - ) -end +initialize_stopping_state(::Problem, ::Algorithm, ::StoppingCriterion; kwargs...) = + DefaultStoppingCriterionState() -@doc "$(_doc_is_finished)" -is_finished(::Problem, ::Algorithm, ::State, ::StoppingCriterion, ::StoppingCriterionState) - -@doc "$(_doc_is_finished)" -function is_finished!(problem::Problem, algorithm::Algorithm, state::State) - return is_finished!( - problem, - algorithm, - state, - algorithm.stopping_criterion, - state.stopping_criterion_state, +function initialize_stopping_state!( + ::Problem, ::Algorithm, ::State, + ::StoppingCriterion, + stopping_criterion_state::DefaultStoppingCriterionState; + kwargs..., ) + stopping_criterion_state.at_iteration = -1 + return stopping_criterion_state end -@doc "$(_doc_is_finished)" -is_finished!(::Problem, ::Algorithm, ::State, ::StoppingCriterion, ::StoppingCriterionState) - -@doc """ - summary(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`](@ref) criterion, the summary looks like - -``` -Max Iterations (15): not reached -``` -""" -Base.summary(io::IO, ::StoppingCriterion, ::StoppingCriterionState) - -# -# # Meta StoppingCriteria +# --------------------- @doc raw""" StopWhenAll <: StoppingCriterion @@ -239,7 +142,7 @@ internally as a tuple. This is for example used in combination with [`StopWhenAny`](@ref) and [`StopWhenAll`](@ref) # Constructor - GroupStoppingCriterionState(c::Vector{N,StoppingCriterionState} where N) + GroupStoppingCriterionState(c::Vector{<:StoppingCriterionState}) GroupStoppingCriterionState(c::StoppingCriterionState...) """ mutable struct GroupStoppingCriterionState{TCriteriaStates <: Tuple} <: StoppingCriterionState @@ -260,31 +163,28 @@ function get_reason( return join(Iterators.map(get_reason, criteria, stopping_criterion_states)) end -function initialize_state( - problem::Problem, - algorithm::Algorithm, +function initialize_stopping_state( + problem::Problem, algorithm::Algorithm, stop_when::Union{StopWhenAll, StopWhenAny}; kwargs..., ) return GroupStoppingCriterionState( ( - initialize_state(problem, algorithm, stopping_criterion; kwargs...) for + initialize_stopping_state(problem, algorithm, stopping_criterion; kwargs...) for stopping_criterion in stop_when.criteria )..., ) end -function initialize_state!( - problem::Problem, - algorithm::Algorithm, +function initialize_stopping_state!( + problem::Problem, algorithm::Algorithm, state::State, stop_when::Union{StopWhenAll, StopWhenAny}, stopping_criterion_states::GroupStoppingCriterionState; kwargs..., ) for (stopping_criterion_state, stopping_criterion) in zip(stopping_criterion_states.criteria_states, stop_when.criteria) - initialize_state!( - problem, - algorithm, + initialize_stopping_state!( + problem, algorithm, state, stopping_criterion, stopping_criterion_state; kwargs..., @@ -420,36 +320,6 @@ struct StopAfterIteration <: StoppingCriterion max_iterations::Int end -""" -DefaultStoppingCriterionState <: StoppingCriterionState - -A [`StoppingCriterionState`](@ref) that does not require any information besides -storing the iteration number when it (last) indicated to stop). - -# Field -* `at_iteration::Int` store the iteration number this state - indicated to stop. - * `0` means already at the start it indicated to stop - * any negative number means that it did not yet indicate to stop. -""" -mutable struct DefaultStoppingCriterionState <: StoppingCriterionState - at_iteration::Int - DefaultStoppingCriterionState() = new(-1) -end - -initialize_state(::Problem, ::Algorithm, ::StopAfterIteration; kwargs...) = - DefaultStoppingCriterionState() -function initialize_state!( - ::Problem, - ::Algorithm, - ::StopAfterIteration, - stopping_criterion_state::DefaultStoppingCriterionState; - kwargs..., - ) - stopping_criterion_state.at_iteration = -1 - return stopping_criterion_state -end - function is_finished( ::Problem, @@ -545,14 +415,12 @@ mutable struct StopAfterTimePeriodState <: StoppingCriterionState end end -initialize_state(::Problem, ::Algorithm, ::StopAfter; kwargs...) = +initialize_stopping_state(::Problem, ::Algorithm, ::StopAfter; kwargs...) = StopAfterTimePeriodState() -function initialize_state!( - ::Problem, - ::Algorithm, - ::StopAfter, - stopping_criterion_state::StopAfterTimePeriodState; +function initialize_stopping_state!( + ::Problem, ::Algorithm, ::State, + ::StopAfter, stopping_criterion_state::StopAfterTimePeriodState; kwargs..., ) stopping_criterion_state.start = Nanosecond(0) diff --git a/test/logging.jl b/test/logging.jl index e31cf1c..cb89fad 100644 --- a/test/logging.jl +++ b/test/logging.jl @@ -13,27 +13,26 @@ mutable struct LogDummyState{S <: StoppingCriterionState} <: State end # State initialization for the dummy algorithm -function AlgorithmsInterface.initialize_state(problem::LogDummyProblem, algorithm::LogDummyAlgorithm; kwargs...) - sc_state = initialize_state(problem, algorithm, algorithm.stopping_criterion; kwargs...) - return LogDummyState(0.0, 0, sc_state) +function AlgorithmsInterface.initialize_state( + problem::LogDummyProblem, algorithm::LogDummyAlgorithm, + stopping_criterion_state::StoppingCriterionState; + kwargs... + ) + iteration = 0 + iterate = 0.0 # hardcode initial guess to 0.0 + return LogDummyState(iterate, iteration, stopping_criterion_state) end function AlgorithmsInterface.initialize_state!( - problem::LogDummyProblem, - algorithm::LogDummyAlgorithm, - state::LogDummyState; + problem::LogDummyProblem, algorithm::LogDummyAlgorithm, state::LogDummyState; kwargs... ) - initialize_state!(problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state; kwargs...) - state.iterate = 0.0 state.iteration = 0 return state end # One trivial step per iteration (not relevant for the logging test) function AlgorithmsInterface.step!( - ::LogDummyProblem, - ::LogDummyAlgorithm, - state::LogDummyState, + ::LogDummyProblem, ::LogDummyAlgorithm, state::LogDummyState, ) state.iterate += 1.0 return state diff --git a/test/newton.jl b/test/newton.jl index 3a038aa..a09f8e1 100644 --- a/test/newton.jl +++ b/test/newton.jl @@ -25,23 +25,21 @@ end # Implementing the algorithm # -------------------------- -function initialize_state(problem::RootFindingProblem, algorithm::NewtonMethod) - scs = initialize_state(problem, algorithm, algorithm.stopping_criterion) - return NewtonState(0, 1.0, scs) # hardcode initial guess to 1.0 +function initialize_state( + problem::RootFindingProblem, algorithm::NewtonMethod, + stopping_criterion_state::StoppingCriterionState; + kwargs... + ) + iteration = 0 + iterate = 1.0 # hardcode initial guess to 1.0 + return NewtonState(iteration, iterate, stopping_criterion_state) end + function initialize_state!( - problem::RootFindingProblem, - algorithm::NewtonMethod, - state::NewtonState, + problem::RootFindingProblem, algorithm::NewtonMethod, state::NewtonState; + kwargs... ) state.iteration = 0 - state.iterate = 1.0 - initialize_state!( - problem, - algorithm, - algorithm.stopping_criterion, - state.stopping_criterion_state, - ) return state end diff --git a/test/stopping_criterion.jl b/test/stopping_criterion.jl index 36d5a37..c0d7461 100644 --- a/test/stopping_criterion.jl +++ b/test/stopping_criterion.jl @@ -5,12 +5,21 @@ using Dates struct DummyAlgorithm <: Algorithm stopping_criterion::StoppingCriterion end + struct DummyProblem <: Problem end + mutable struct DummyState{S <: StoppingCriterionState} <: State stopping_criterion_state::S iteration::Int end +function AlgorithmsInterface.initialize_state( + problem::DummyProblem, algorithm::DummyAlgorithm, stopping_criterion_state::StoppingCriterionState; + kwargs... + ) + return DummyState(stopping_criterion_state, 1) +end + problem = DummyProblem() @testset "StopAfterIteration" begin @@ -19,11 +28,10 @@ problem = DummyProblem() @test string(s1) == "StopAfterIteration(2)" algorithm = DummyAlgorithm(s1) - s1_state = initialize_state(problem, algorithm, s1) - state_finished = DummyState(s1_state, 2) - state_not_finished = DummyState(s1_state, 1) - @test is_finished(problem, algorithm, state_finished) - @test !is_finished(problem, algorithm, state_not_finished) + state = initialize_state(problem, algorithm) + @test !is_finished(problem, algorithm, state) + AlgorithmsInterface.increment!(state) + @test is_finished(problem, algorithm, state) end @testset "StopAfter" begin @@ -32,11 +40,10 @@ end @test string(s1) == "StopAfter(Second(1))" algorithm = DummyAlgorithm(s1) - s1_state = initialize_state(problem, algorithm, s1) - state_not_finished = DummyState(s1_state, 1) - @test !is_finished(problem, algorithm, state_not_finished) - s1_state.time = Second(2) - @test is_finished(problem, algorithm, state_not_finished) + state = initialize_state(problem, algorithm) + @test !is_finished(problem, algorithm, state) + state.stopping_criterion_state.time = Second(2) + @test is_finished(problem, algorithm, state) end @testset "StopWhenAll" begin @@ -46,13 +53,12 @@ end "StopWhenAll with the Stopping Criteria:\n StopAfterIteration(2)\n StopAfter(Second(1))" algorithm = DummyAlgorithm(s1) - s1_state = initialize_state(problem, algorithm, s1) - state_not_finished = DummyState(s1_state, 1) - @test !is_finished(problem, algorithm, state_not_finished) - s1_state.criteria_states[2].time = Second(2) - @test !is_finished(problem, algorithm, state_not_finished) - state_not_finished.iteration = 2 - @test is_finished(problem, algorithm, state_not_finished) + state = initialize_state(problem, algorithm) + @test !is_finished(problem, algorithm, state) + state.stopping_criterion_state.criteria_states[2].time = Second(2) + @test !is_finished(problem, algorithm, state) + AlgorithmsInterface.increment!(state) + @test is_finished(problem, algorithm, state) end @testset "StopWhenAny" begin @@ -62,11 +68,10 @@ end "StopWhenAny with the Stopping Criteria:\n StopAfterIteration(2)\n StopAfter(Second(1))" algorithm = DummyAlgorithm(s1) - s1_state = initialize_state(problem, algorithm, s1) - state_not_finished = DummyState(s1_state, 1) - @test !is_finished(problem, algorithm, state_not_finished) - s1_state.criteria_states[2].time = Second(2) - @test is_finished(problem, algorithm, state_not_finished) - state_not_finished.iteration = 2 - @test is_finished(problem, algorithm, state_not_finished) + state = initialize_state(problem, algorithm) + @test !is_finished(problem, algorithm, state) + state.stopping_criterion_state.criteria_states[2].time = Second(2) + @test is_finished(problem, algorithm, state) + AlgorithmsInterface.increment!(state) + @test is_finished(problem, algorithm, state) end