Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `StopReasonAction`, a `LoggingAction` that reports `get_reason` at the `:Stop` context.
- Defaults for `get_reason` (`nothing`) and for the type-domain `indicates_convergence` (`false`), so a criterion that implements neither no longer hits a `MethodError` from the derived convergence reporting.
- Exports for `DefaultStoppingCriterionState`, `StopAfterTimePeriodState` and `GroupStoppingCriterionState`, which a downstream criterion is expected to reuse.
- `DefaultState`, a `State` with the `iterate`, `iteration` and `stopping_criterion_state` fields, and a `data` field for any additional data.
- Default `initialize_state` and `initialize_state!` methods using a `DefaultState`, so an algorithm only has to provide a `Problem`, an `Algorithm` and a `step!`.
- Default `initialize_state` and `initialize_state!` methods for any `StoppingCriterion`, using a `DefaultStoppingCriterionState`.

### Changed

- `indicates_convergence` without a state moved to the type domain: a new criterion implements `indicates_convergence(::Type{YourCriterion})`, and `indicates_convergence(criterion)` forwards to it.
`StopWhenAll` and `StopWhenAny` combine their children in the type domain as well, so a criterion that only implements the variant taking an instance is no longer accounted for in a group.
- `StopAfterIteration` now uses the default `initialize_state` and `initialize_state!`.

### Fixed

Expand Down
110 changes: 79 additions & 31 deletions docs/src/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,50 +58,26 @@ end
struct HeronAlgorithm <: Algorithm
stopping_criterion # will be plugged in later (any StoppingCriterion)
end

mutable struct HeronState <: State
iterate::Float64 # current iterate
iteration::Int # current iteration count
stopping_criterion_state # will be plugged in later (any StoppingCriterionState)
end
```

For the state, we use the [`DefaultState`](@ref), which stores the `iterate`, the `iteration` count and the `stopping_criterion_state`.
If you need more, see [defining your own state](@ref sec_custom_state) below.

### Initialization

In order to start 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
- [`initialize_state!`](@ref) (in-place) reset of an existing state.

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)
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
end
```
Both have default implementations that work with a [`DefaultState`](@ref), so there is nothing to define here.

### Iteration steps

Algorithms define a mutable step via [`step!`](@ref). For Heron's method:

```@example Heron
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::DefaultState)
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
Expand Down Expand Up @@ -129,7 +105,7 @@ With these definitions in place you can already run (assuming you also choose a
function heron_sqrt(x; maxiter = 10)
prob = SqrtProblem(x)
alg = HeronAlgorithm(StopAfterIteration(maxiter))
return solve(prob, alg) # allocates & runs
return solve(prob, alg; iterate = 1.0) # initial guess, allocates & runs
end

println("Approximate sqrt: ", heron_sqrt(16.0))
Expand All @@ -139,6 +115,78 @@ Note that [`solve`](@ref) will default to returning `state.iterate`.
If desired, this can be customized by altering [`finalize_state!`](@ref).
We will refine this example with better halting logic and logging shortly.

### Carrying extra data

Any additional data the algorithm needs can be stored in the `data::D` field of the [`DefaultState{V, S, D}`](@ref).

```@example Heron
struct TrackingHeronAlgorithm <: Algorithm
stopping_criterion
end

function AlgorithmsInterface.step!(problem::SqrtProblem, ::TrackingHeronAlgorithm, state::DefaultState)
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
push!(state.data.residuals, abs(state.iterate^2 - S))
return state
end

problem = SqrtProblem(16.0)
tracking = TrackingHeronAlgorithm(StopAfterIteration(5))
state = initialize_state(problem, tracking, 1.0, (; residuals = Float64[]))
solve!(problem, tracking, state)
state.data.residuals
```

Here the initial iterate and the data are passed positionally, as `initialize_state(problem, algorithm, iterate, state_data)`.

### [Defining your own state](@id sec_custom_state)

If you need named fields or want to dispatch on the state, subtype [`State`](@ref) instead.
Such a state has to provide the same three properties, as well as its own initialization methods:

```@example Heron
mutable struct HeronState <: State
iterate::Float64 # current iterate
iteration::Int # current iteration count
stopping_criterion_state # any StoppingCriterionState
end

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
AlgorithmsInterface.initialize_state!(
problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state
)
return state
end

function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
return state
end
```

Since this state picks its own initial guess, the `iterate` keyword is no longer needed:

```@example Heron
println("Approximate sqrt: ", solve(SqrtProblem(16.0), HeronAlgorithm(StopAfterIteration(10))))
```

Note that the three required *properties* do not enforce a struct with these *fields*, and the properties can be emulated with correct implementations of `Base.getproperty`.

## Reference: Core interface types & functions

Below are the automatic API docs for the core interface pieces. Read them after grasping the example above – the intent should now be clearer.
Expand Down Expand Up @@ -172,7 +220,7 @@ Private = true

```@autodocs
Modules = [AlgorithmsInterface]
Pages = ["interface/state.jl"]
Pages = ["interface/state.jl", "default_state.jl"]
Order = [:type, :function]
Private = true
```
Expand Down
29 changes: 5 additions & 24 deletions docs/src/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,7 @@ struct HeronAlgorithm <: Algorithm
stopping_criterion
end

mutable struct HeronState <: State
iterate::Float64
iteration::Int
stopping_criterion_state
end

function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
x0 = rand()
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...)
state.iterate = rand()
state.iteration = 0
initialize_state!(problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state)
return state
end

function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::DefaultState)
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
Expand All @@ -75,7 +56,7 @@ end
function heron_sqrt(x; stopping_criterion = StopAfterIteration(10))
prob = SqrtProblem(x)
alg = HeronAlgorithm(stopping_criterion)
return solve(prob, alg) # allocates & runs
return solve(prob, alg; iterate = 1.0) # allocates & runs
end
nothing # hide
```
Expand Down Expand Up @@ -211,7 +192,7 @@ function AlgorithmsInterface.handle_message!(
action::CaptureHistory,
problem::SqrtProblem,
algorithm::HeronAlgorithm,
state::HeronState;
state::DefaultState;
kwargs...
)
push!(action.iterates, state.iterate)
Expand Down Expand Up @@ -287,7 +268,7 @@ end
StatsCollector() = StatsCollector(0, 0.0, 0.0)

function AlgorithmsInterface.handle_message!(
action::StatsCollector, problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState;
action::StatsCollector, problem::SqrtProblem, algorithm::HeronAlgorithm, state::DefaultState;
kwargs...
)
action.count += 1
Expand Down Expand Up @@ -415,7 +396,7 @@ Here we will illustrate this by a slight adaptation of our algorithm, which coul
To emit a custom logging event from within your algorithm, call [`emit_message`](@ref):

```@example Heron
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::DefaultState)
# Suppose we check for numerical issues
if !isfinite(state.iterate) || mod(state.iteration, 10) == 0
emit_message(problem, algorithm, state, :Restart)
Expand Down
61 changes: 25 additions & 36 deletions docs/src/stopping_criterion.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The package ships several concrete [`StoppingCriterion`](@ref)s:

Each criterion has an associated [`StoppingCriterionState`](@ref) storing dynamic data (iteration when met, elapsed time, etc.).

Recall our [example implementation](@ref sec_heron) for Heron's method, where we added a `stopping_criterion` to the `Algorithm`, as well as a `stopping_criterion_state` to the `State`.
Recall our [example implementation](@ref sec_heron) for Heron's method, where the `Algorithm` carries a `stopping_criterion` and the `State` a `stopping_criterion_state`.

```@example Heron
using AlgorithmsInterface
Expand All @@ -39,41 +39,24 @@ 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
end
```

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.

### 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:
The first core component to enable working with stopping criteria is to initialize a [`StoppingCriterionState`](@ref) as well.
This is 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.

```@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)
end

function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
# reset the state for the algorithm
state.iterate = rand()
state.iteration = 0
For a [`DefaultState`](@ref) this is already taken care of, by a default that roughly does

# reset the state for the stopping criterion
state = AlgorithmsInterface.initialize_state!(
problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state
)
return state
```julia
function AlgorithmsInterface.initialize_state(problem::Problem, algorithm::Algorithm; iterate, kwargs...)
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion; kwargs...)
return DefaultState(iterate, stopping_criterion_state)
end
```

Expand All @@ -82,7 +65,7 @@ end
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:

```@example Heron
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::DefaultState)
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
Expand Down Expand Up @@ -112,7 +95,7 @@ We can again combine everything into a single function, but now make the stoppin
function heron_sqrt(x; stopping_criterion)
prob = SqrtProblem(x)
alg = HeronAlgorithm(stopping_criterion)
return solve(prob, alg) # allocates & runs
return solve(prob, alg; iterate = 1.0) # allocates & runs
end

heron_sqrt(2; stopping_criterion = StopAfterIteration(10))
Expand Down Expand Up @@ -148,6 +131,9 @@ Suppose we want to stop when successive iterates change by less than `ϵ`, we co
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).

By default, a criterion gets a [`DefaultStoppingCriterionState`](@ref), which only records the iteration at which it triggered.
Ours also has to remember the previous iterate, so it needs its own state.

```@example Heron
struct StopWhenStable <: StoppingCriterion
tol::Float64 # when do we consider things converged
Expand Down Expand Up @@ -253,8 +239,8 @@ The variant taking a criterion simply forwards to the type, and the two-argument

```@example Heron
criterion = StopWhenStable(1e-8)
state = AlgorithmsInterface.initialize_state(SqrtProblem(16.0), HeronAlgorithm(criterion), criterion)
indicates_convergence(criterion), indicates_convergence(criterion, state)
criterion_state = AlgorithmsInterface.initialize_state(SqrtProblem(16.0), HeronAlgorithm(criterion), criterion)
indicates_convergence(criterion), indicates_convergence(criterion, criterion_state)
```

The criterion always *could* indicate convergence, but its fresh state has not yet seen it happen.
Expand All @@ -274,7 +260,7 @@ Since [`solve`](@ref) returns only the iterate, we use [`solve!`](@ref) with a s
function heron_verdict(x, criterion)
problem = SqrtProblem(x)
algorithm = HeronAlgorithm(criterion)
state = AlgorithmsInterface.initialize_state(problem, algorithm)
state = AlgorithmsInterface.initialize_state(problem, algorithm, 1.0)

solve!(problem, algorithm, state)

Expand Down Expand Up @@ -320,16 +306,19 @@ heron_sqrt(16.0; stopping_criterion = criterion)

### Summary

Implementing a criterion usually means defining:
Implementing a criterion means defining:

1. A subtype of [`StoppingCriterion`](@ref).
2. A state subtype of [`StoppingCriterionState`](@ref) capturing dynamic fields, including an `at_iteration` recording when the criterion triggered.
3. `initialize_state` and `initialize_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, gated on `is_active`.
6. `indicates_convergence(::Type{YourCriterion})` to mark if meeting it implies convergence.
2. `is_finished!` (mutating) and optionally `is_finished` (non‑mutating) variants.
3. `get_reason` (return `nothing` or a string) for user feedback, gated on `is_active`.
4. `indicates_convergence(::Type{YourCriterion})` to mark if meeting it implies convergence.
The `(criterion,)` and the `(criterion, criterion_state)` variant are derived from this one and do not need to be defined.

If the criterion needs more state than the [`DefaultStoppingCriterionState`](@ref), it additionally defines:

* A state subtype of [`StoppingCriterionState`](@ref) capturing its dynamic fields, including an `at_iteration` recording when the criterion triggered.
* `initialize_state` and `initialize_state!` for setup/reset.

You may also implement `Base.summary(io, criterion, criterion_state)` for compact status reports,
and `is_active(criterion, criterion_state)` if your state does not record its status in an
`at_iteration` property.
Expand Down
2 changes: 2 additions & 0 deletions src/AlgorithmsInterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ include("interface/state.jl")
include("interface/interface.jl")

include("stopping_criterion.jl")
include("default_state.jl")
include("logging.jl")

include("test_suite.jl")

# general interface
export Algorithm, Problem, State
export DefaultState
export initialize_state, initialize_state!
export finalize_state!

Expand Down
Loading
Loading