Skip to content

Commit b9c9b26

Browse files
committed
various improvements and rework interface.md
1 parent 2e9d3df commit b9c9b26

3 files changed

Lines changed: 203 additions & 46 deletions

File tree

docs/src/interface.md

Lines changed: 137 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,139 @@
1-
# The algorithm interface
1+
# [The algorithm interface](@id sec_interface)
22

3-
## General design ideas
3+
This section starts a single, cohesive story that will weave through all documentation pages.
4+
We will incrementally build an iterative algorithm, enrich it with stopping criteria, and
5+
finally refine how it records (logs) its progress. Instead of presenting the API in the
6+
abstract, we anchor every concept in one concrete, tiny example you can copy & adapt.
47

5-
The interface this package provides is based on three ingredients of running an algorithm
6-
consists of:
8+
Why an “interface” for algorithms? Iterative numerical methods nearly always share the
9+
same moving pieces:
710

8-
* a [`Problem`](@ref) that is to be solved and contains all information that is algorithm independent.
9-
This is _static information_ in the sense that it does not change during the runtime of the algorithm.
10-
* an [`Algorithm`](@ref) that includes all of the _settings_ and _parameters_ that an algorithm.
11-
this is also information that is _static_.
12-
* a [`State`](@ref) that contains all remaining data, especially data that might vary during the iteration,
13-
temporary caches, for example the current iteration the algorithm run is in and the current iterate, respectively.
11+
* immutable input (the mathematical problem you are solving),
12+
* immutable configuration (parameters and knobs of the chosen algorithm), and
13+
* mutable working data (current iterate, caches, diagnostics) that evolves as you step.
1414

15-
The combination of the static information should be enough to initialize the varying data.
15+
Bundling these together loosely—without forcing one giant monolithic type—makes it easier to:
1616

17-
This general scheme is a guiding principle of the package, splitting information into _static_
18-
or _configuration_ types or data that allows to [`initialize_state`](@ref) a corresponding _variable_ data type.
17+
* reason about what is allowed to change and what must remain fixed,
18+
* write generic tooling (e.g. stopping logic, logging, benchmarking) that applies across many algorithms,
19+
* test algorithms in isolation by constructing minimal `Problem`/`Algorithm` pairs, and
20+
* extend behavior (add new stopping criteria, new logging events) without rewriting core loops.
1921

20-
The order of arguments is given by two ideas
22+
The interface in this package formalizes those roles with three abstract types:
23+
* [`Problem`](@ref): immutable, algorithm‑agnostic input data.
24+
* [`Algorithm`](@ref): immutable configuration and parameters deciding how to iterate.
25+
* [`State`](@ref): mutable data that evolves (current iterate, caches, counters, diagnostics).
26+
It provides a framework for decomposing iterative methods into small, composable parts:
27+
concrete `Problem`/`Algorithm`/`State` types have to implement a minimal set of core functionality,
28+
and this package helps to stitch everything together and provide additional helper functionality such as stopping criteria and logging functionality.
2129

22-
1. for non-mutating functions the order should be from the most fixed data to the most variable one.
23-
For example the three types just mentioned would be ordered like `f(problem, algorithm, state)`
24-
2. For mutating functions the variable that is mutated comes first, for the remainder the guiding principle from 1 continues.
25-
The main case here is `f!(state, problem, algorithm)`.
30+
## [Concrete example: Heron's method](@id sec_heron)
31+
32+
To make everything tangible, we will work through a concrete example to illustrate the library's goals and concepts.
33+
Our running example is Heron's / Babylonian method for estimating $\sqrt{S}$.
34+
(see also the concise background on Wikipedia: [Babylonian method (Heron's method)](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)):
35+
Starting from an initial guess $x_0$, we may converge to the solution by iterating:
36+
37+
$$x_{k+1} = \frac{1}{2}\left(x_k + \frac{S}{x_k}\right)$$
38+
39+
We therefore suggest the following concrete implementations of the abstract types provided by this package:
40+
They are illustrative; various performance and generality questions will be left unaddressed to keep this example simple.
41+
42+
### Algorithm types
43+
44+
```@example Heron
45+
using AlgorithmsInterface
46+
47+
struct SqrtProblem <: Problem
48+
S::Float64 # number whose square root we seek
49+
end
50+
51+
struct HeronAlgorithm <: Algorithm
52+
stopping_criterion # will be plugged in later (any StoppingCriterion)
53+
end
54+
55+
mutable struct HeronState <: State
56+
iterate::Float64 # current iterate
57+
iteration::Int # current iteration count
58+
stopping_criterion_state # will be plugged in later (any StoppingCriterionState)
59+
end
60+
```
61+
62+
### Initialization
63+
64+
In order to start implementing the core parts of our algorithm, we start at the very beginning.
65+
There are two main entry points provided by the interface:
66+
67+
- [`initialize_state`](@ref) constructs an entirely new state for the algorithm
68+
- [`initialize_state!`](@ref) (in-place) reset of an existing state.
69+
70+
An example implementation might look like:
71+
72+
```@example Heron
73+
function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
74+
x0 = rand() # random initial guess
75+
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion)
76+
return HeronState(x0, 0, stopping_criterion_state)
77+
end
78+
79+
function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
80+
# reset the state for the algorithm
81+
state.iterate = rand()
82+
state.iteration = 0
83+
84+
# reset the state for the stopping criterion
85+
state = AlgorithmsInterface.initialize_state!(
86+
problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state
87+
)
88+
return state
89+
end
90+
```
91+
92+
### Iteration steps
93+
94+
Algorithms define a mutable step via [`step!`](@ref). For Heron's method:
95+
96+
```@example Heron
97+
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
98+
S = problem.S
99+
x = state.iterate
100+
state.iterate = 0.5 * (x + S / x)
101+
return state
102+
end
103+
```
104+
105+
Note that we are only focussing on the actual algorithm, and *not* incrementing the iteration counter.
106+
These kinds of bookkeeping should be handled by the [`increment!(state)`](@ref) function, which will by default already increment the iteration counter.
107+
The following generic functionality is therefore enough for our purposes, and does *not* need to be defined.
108+
Nevertheless, if additional bookkeeping would be desired, this can be achieved by overloading that function:
109+
110+
```julia
111+
function AlgorithmsInterface.increment!(state::State)
112+
state.iteration += 1
113+
return state
114+
end
115+
```
116+
117+
### Running the algorithm
118+
119+
With these definitions in place you can already run (assuming you also choose a stopping criterion – added in the next section):
120+
121+
```@example Heron
122+
function heron_sqrt(x; maxiter = 10)
123+
prob = SqrtProblem(x)
124+
alg = HeronAlgorithm(StopAfterIteration(maxiter))
125+
state = solve(prob, alg) # allocates & runs
126+
return state.iterate
127+
end
128+
129+
println("Approximate sqrt: ", heron_sqrt(16.0))
130+
```
131+
132+
We will refine this example with better halting logic and logging shortly.
133+
134+
## Reference: Core interface types & functions
135+
136+
Below are the automatic API docs for the core interface pieces. Read them after grasping the example above – the intent should now be clearer.
26137

27138
```@autodocs
28139
Modules = [AlgorithmsInterface]
@@ -31,7 +142,7 @@ Order = [:type, :function]
31142
Private = true
32143
```
33144

34-
## Algorithm
145+
### Algorithm
35146

36147
```@autodocs
37148
Modules = [AlgorithmsInterface]
@@ -40,7 +151,7 @@ Order = [:type, :function]
40151
Private = true
41152
```
42153

43-
## Problem
154+
### Problem
44155

45156
```@autodocs
46157
Modules = [AlgorithmsInterface]
@@ -49,11 +160,15 @@ Order = [:type, :function]
49160
Private = true
50161
```
51162

52-
## State
163+
### State
53164

54165
```@autodocs
55166
Modules = [AlgorithmsInterface]
56167
Pages = ["interface/state.jl"]
57168
Order = [:type, :function]
58169
Private = true
59-
```
170+
```
171+
172+
### Next: Stopping criteria
173+
174+
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.

src/interface/interface.jl

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,31 @@ The state is modified in-place.
4343
All keyword arguments are passed to the [`initialize_state!`](@ref)`(problem, algorithm, state)` function.
4444
"""
4545
function solve!(problem::Problem, algorithm::Algorithm, state::State; kwargs...)
46+
# obtain logger once to minimize overhead from accessing ScopedValue
47+
# additionally handle logging initialization to enable stateful LoggingAction
4648
logger = algorithm_logger()
49+
# initialize_logger(logger, problem, algorithm, state)
50+
51+
# initialize the state and emit message
4752
initialize_state!(problem, algorithm, state; kwargs...)
48-
isnothing(logger) || handle_message(logger, problem, algorithm, state, :Start)
53+
emit_message(logger, problem, algorithm, state, :Start)
54+
55+
# main body of the algorithm
4956
while !is_finished!(problem, algorithm, state)
50-
isnothing(logger) || handle_message(logger, problem, algorithm, state, :PreStep)
57+
# logging event between convergence check and algorithm step
58+
emit_message(logger, problem, algorithm, state, :PreStep)
59+
60+
# algorithm step
5161
increment!(state)
5262
step!(problem, algorithm, state)
53-
isnothing(logger) || handle_message(logger, problem, algorithm, state, :PostStep)
63+
64+
# logging event between algorithm step and convergence check
65+
emit_message(logger, problem, algorithm, state, :PostStep)
5466
end
55-
isnothing(logger) || handle_message(logger, problem, algorithm, state, :Stop)
67+
68+
# emit message about finished state
69+
emit_message(logger, problem, algorithm, state, :Stop)
70+
5671
return state
5772
end
5873

src/logging.jl

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The callback function must have the following signature:
4646
```julia
4747
callback(algorithm, problem, state; kwargs...) = ...
4848
```
49-
Here `args...` and `kwargs...` are optional and can be filled out with context-specific information.
49+
Here `kwargs...` are optional and can be filled out with context-specific information.
5050
"""
5151
struct CallbackAction{F} <: LoggingAction
5252
callback::F
@@ -59,6 +59,16 @@ function handle_message!(
5959
return nothing
6060
end
6161

62+
"""
63+
IfAction(predicate, action)
64+
65+
Concrete [`LoggingAction`](@ref) that wraps another action and hides it behind a clause, only
66+
emitting logging events whenever the `predicate` evaluates to true. The `predicate` must have
67+
the signature:
68+
```julia
69+
predicate(algorithm, problem, state; kwargs...)::Bool
70+
```
71+
"""
6272
struct IfAction{F, A <: LoggingAction} <: LoggingAction
6373
predicate::F
6474
action::A
@@ -97,18 +107,34 @@ struct AlgorithmLogger
97107
end
98108
AlgorithmLogger(args...) = AlgorithmLogger(Dict{Symbol, LoggingAction}(args...))
99109

100-
"""
101-
const LOGGING_ENABLED = Ref(true)
102110

103-
Global toggle for enabling and disabling all logging features.
104-
"""
111+
@doc """
112+
get_global_logging_state()
113+
set_global_logging_state!(state::Bool) -> previous_state
114+
115+
Retrieve or set the value to globally enable or disable the handling of logging events.
116+
""" get_global_logging_state, set_global_logging_state!
117+
105118
const LOGGING_ENABLED = Ref(true)
106119

107-
"""
108-
const algorithm_logger = ScopedValue(AlgorithmLogger())
120+
get_global_logging_state() = LOGGING_ENABLED[]
121+
function set_global_logging_state(state::Bool)
122+
previous = LOGGING_ENABLED[]
123+
LOGGING_ENABLED[] = state
124+
return previous
125+
end
126+
127+
128+
@doc """
129+
algorithm_logger()::Union{AlgorithmLogger, Nothing}
130+
131+
Retrieve the current logger that is responsible for handling logging events.
132+
The current logger is determined by a `ScopedValue`.
133+
Whenever `nothing` is returned, no logging should happen.
134+
135+
See also [`set_global_logging_state!`](@ref) for globally toggling whether logging should happen.
136+
""" algorithm_logger
109137

110-
Scoped value for handling the logging events of arbitrary algorithms.
111-
"""
112138
const ALGORITHM_LOGGER = ScopedValue(AlgorithmLogger())
113139

114140
function algorithm_logger()
@@ -118,21 +144,22 @@ function algorithm_logger()
118144
return logger
119145
end
120146

121-
# @inline here to enable the cheap global check
122-
@inline function log!(problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...)
123-
if LOGGING_ENABLED[]
124-
logger::AlgorithmLogger = ALGORITHM_LOGGER[]
125-
handle_message(logger, problem, algorithm, state, context; kwargs...)
126-
end
127-
return nothing
128-
end
147+
"""
148+
emit_message(problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) -> nothing
149+
emit_message(algorithm_logger, problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) -> nothing
129150
130-
# @noinline to keep the algorithm function bodies small
131-
@noinline function handle_message(
151+
Use the current or the provided algorithm logger to handle the logging event of the given `context`.
152+
The [`AlgorithmLogger`](@ref) is responsible for dispatching the correct events to the correct [`LoggingAction`](@ref)s.
153+
"""
154+
emit_message(problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) =
155+
emit_message(algorithm_logger(), problem, algorithm, state, context; kwargs...)
156+
emit_message(::Nothing, problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) =
157+
nothing
158+
function emit_message(
132159
alglogger::AlgorithmLogger, problem::Problem, algorithm::Algorithm, state::State, context::Symbol;
133160
kwargs...
134161
)
135-
isempty(alglogger.actions) && return nothing
162+
@noinline; @nospecialize
136163
action::LoggingAction = @something(get(alglogger.actions, context, nothing), return nothing)
137164
try
138165
handle_message!(action, problem, algorithm, state, args...; kwargs...)

0 commit comments

Comments
 (0)