diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 0000000..688c5c4 --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,25 @@ +name: CompatHelper +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "3" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} + - name: "Run CompatHelper" + run: | + import CompatHelper + CompatHelper.main() + shell: julia --color=yes {0} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..f49313b --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,15 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1bd88f9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI +on: + push: + branches: [master] + tags: [v*] + pull_request: + +jobs: + test: + name: Julia ${{ matrix.julia-version }}โ€“${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: ["lts", "1", "pre"] + os: [ubuntu-latest, macOS-latest] + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + arch: x64 + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-runtest@latest + env: + PYTHON: "" + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./lcov.info + name: codecov-umbrella + fail_ci_if_error: false + if: ${{ matrix.os =='ubuntu-latest' }} diff --git a/.github/workflows/clear_preview.yml b/.github/workflows/clear_preview.yml new file mode 100644 index 0000000..0b00303 --- /dev/null +++ b/.github/workflows/clear_preview.yml @@ -0,0 +1,26 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +jobs: + doc-preview-cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + - name: Delete preview and history + push changes + run: | + if [ -d "previews/PR$PRNUM" ]; then + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git rm -rf "previews/PR$PRNUM" + git commit -m "delete preview" + git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) + git push --force origin gh-pages-new:gh-pages + fi + env: + PRNUM: ${{ github.event.number }} \ No newline at end of file diff --git a/.github/workflows/documenter.yml b/.github/workflows/documenter.yml new file mode 100644 index 0000000..37f2d44 --- /dev/null +++ b/.github/workflows/documenter.yml @@ -0,0 +1,65 @@ +name: Documenter +on: + push: + branches: [main] + tags: [v*] + pull_request: + +jobs: + docs: + name: Documentation + runs-on: ubuntu-latest +# if: contains( github.event.pull_request.labels.*.name, 'preview docs') || github.ref == 'refs/heads/master' || contains(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v4 +# - uses: quarto-dev/quarto-actions/setup@v2 +# with: +# version: "1.6.38" + - uses: julia-actions/setup-julia@latest + with: + version: "1.11" + - name: Julia Cache + uses: julia-actions/cache@v2 +# - name: Cache Quarto +# id: cache-quarto +# uses: actions/cache@v4 +# env: +# cache-name: cache-quarto +# with: +# path: tutorials/_freeze +# key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('tutorials/*.qmd') }} +# restore-keys: | +# ${{ runner.os }}-${{ env.cache-name }}- +# - name: Cache Documenter +# id: cache-documenter +# uses: actions/cache@v4 +# env: +# cache-name: cache-documenter +# with: +# path: docs/src/tutorials +# key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('tutorials/*.qmd') }} +# restore-keys: | +# ${{ runner.os }}-${{ env.cache-name }}- +# - name: Cache CondaPkg +# id: cache-condaPkg +# uses: actions/cache@v4 +# env: +# cache-name: cache-condapkg +# with: +# path: docs/.CondaPkg +# key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('docs/CondaPkg.toml') }} +# restore-keys: | +# ${{ runner.os }}-${{ env.cache-name }}- + - name: "Documenter rendering" + run: "docs/make.jl --quarto" + env: + PYTHON: "" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} +# note: +# name: "Documentation deployment note." +# runs-on: ubuntu-latest +# if: "!contains( github.event.pull_request.labels.*.name, 'preview docs')" +# steps: +# - name: echo instructions +# run: echo 'The Documentation is only generated and pushed on a PR if the โ€œpreview docsโ€ label is added.' diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..ce8609b --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,30 @@ +name: Format +on: + push: + branches: [master] + tags: [v*] + pull_request: + +jobs: + format: + name: "Format Check" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: 1 + - uses: julia-actions/cache@v2 + - name: Install JuliaFormatter and format + run: | + using Pkg + Pkg.add(PackageSpec(name="JuliaFormatter", version="1")) + using JuliaFormatter + format("."; verbose=true) + shell: julia --color=yes {0} + - name: Suggest formatting changes + uses: reviewdog/action-suggester@v1 + if: github.event_name == 'pull_request' + with: + tool_name: JuliaFormatter + fail_on_error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ed1b59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +docs/build +docs/Manifest.toml +Manifest.toml diff --git a/Project.toml b/Project.toml index 388c068..6cf9f86 100644 --- a/Project.toml +++ b/Project.toml @@ -1,4 +1,22 @@ -name = "AlgorithmInterface" -uuid = "125df6b2-f2b1-44d3-9e4a-6c50c163a640" +name = "AlgorithmsInterface" +uuid = "d1e3940c-cd12-4505-8585-b0a4b322527d" authors = ["Ronny Bergmann "] version = "0.1.0" + +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[compat] +Aqua = "0.8" +Dates = "1.10" +SafeTestsets = "0.1" +Test = "1.10" +julia = "1.10" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Aqua", "Test", "SafeTestsets"] diff --git a/Readme.md b/Readme.md index 73b1cd0..3f8772d 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,17 @@ -# IterativeAlgorithmsInterface.jl +# ๐Ÿงฎ AlgorithmsInterface.jl -`IterativeAlgorithmsInterface.jl` is a Julia package to provide a common interface to run iterative tasks. **Algorithm** here refers to an iterative sequence of commands, that are run until a certain stopping criterion is met. +`AlgorithmsInterface.jl` is a Julia package to provide a common interface to run iterative tasks. +**Algorithm** here refers to an iterative sequence of commands, that are run until a certain stopping criterion is met. + +[![docs][docs-dev-img]][docs-dev-url] ![CI][ci-url] [![codecov][codecov-img]][codecov-url] + +[docs-dev-img]: https://img.shields.io/badge/docs-dev-blue.svg +[docs-dev-url]: https://JuliaManifolds.github.io/AlgorithmsInterface.jl/dev/ + +[codecov-img]: https://codecov.io/gh/JuliaManifolds/AlgorithmsInterface.jl/graph/badge.svg?token=1OBDY03SUP +[codecov-url]: https://codecov.io/gh/JuliaManifolds/AlgorithmsInterface.jl + +[ci-url]: https://github.com/JuliaManifolds/AlgorithmsInterface.jl/workflows/ci/badge.svg # Statement of need @@ -13,20 +24,10 @@ Finally, a common interface also allows to easily combine existing algorithms, h # Main features -We consider solving _Tasks_, which consist of - -* An `AbstractProblem` to solve, which contains all information that is static to the problem and usually does not change during the iterations, this might for example be a cost function and its gradient in an optimisation problem. -* An `AbstractAlgorithmState` that both specifies which algorithm to use to _solve_ the problem, but also stores all parameters that an algorithm needs as well as everything the algorithm needs to store between two iterations. - -This generic data structures are accompanied by the methods - -* `step!(problem::Problem, state::AlgorithmState, k)` to perform the `k`th iteration of the algorithm. -* `solve!(problem::Problem, state::AlgorithmState)` to solve a problem with a given algorithm, which is identified by the `AlgorithmState`. -* `stop(problem::Problem, state::AlgorithmState)` to check whether the algorithm should stop. - -where the first is the main one to implement for a new algorithm. +See the [intial discussion](https://github.com/JuliaManifolds/AlgorithmsInterface.jl/discussions/1) +as well as the [overview on existing things](https://github.com/JuliaManifolds/AlgorithmsInterface.jl/discussions/2) -# Further ideas +## Further ideas * generic stopping criteria `<:AbstractStoppingCriterion` * `StopAfterIteration(i)` for example @@ -35,7 +36,7 @@ where the first is the main one to implement for a new algorithm. * by default `stop()` from above would check such a stopping criterion * generic debug and record functionality โ€“ together with hooks even -# possible extensions +## Possible extensions * to `LineSearches.jl` * diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..e53d291 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,5 @@ +[deps] +AlgorithmsInterface = "d1e3940c-cd12-4505-8585-b0a4b322527d" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" diff --git a/docs/make.jl b/docs/make.jl new file mode 100755 index 0000000..f72a36d --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,54 @@ +#!/usr/bin/env julia +# +# + +if "--help" โˆˆ ARGS + println(""" + docs/make.jl + + Render the `AlgorithmsInterface.jl` documentation with optional arguments + + Arguments + * `--help` print this help and exit without rendering the documentation + * `--prettyurls` toggle the pretty urls part to true, which is always set on CI + """) + exit(0) +end + +using Pkg +Pkg.activate(@__DIR__) +Pkg.develop(PackageSpec(; path = (@__DIR__) * "/../")) +Pkg.resolve() +Pkg.instantiate() + +using Documenter, DocumenterCitations, DocumenterInterLinks +using AlgorithmsInterface + +run_on_CI = (get(ENV, "CI", nothing) == "true") + +bib = CitationBibliography(joinpath(@__DIR__, "src", "references.bib"); style = :alpha) +links = InterLinks() +makedocs(; + format = Documenter.HTML(; + prettyurls = run_on_CI || ("--prettyurls" โˆˆ ARGS), + assets = [ + # "assets/favicon.ico", + "assets/citations.css", + "assets/link-icons.css", + ], + ), + modules = [AlgorithmsInterface], + authors = "Ronny Bergmann, Lukas Devos, and contributors.", + sitename = "AlgorithmsInterface.jl", + pages = [ + "Home" => "index.md", + "Interface" => "interface.md", + "Stopping criteria" => "stopping_criterion.md", + "Notation" => "notation.md", + "References" => "references.md", + ], + plugins = [bib, links], +) +deploydocs(; repo = "github.com/JuliaManifolds/AlgorithmsInterface.jl", push_preview = true) +#back to main env +Pkg.activate() diff --git a/docs/src/assets/citations.css b/docs/src/assets/citations.css new file mode 100644 index 0000000..5ea6118 --- /dev/null +++ b/docs/src/assets/citations.css @@ -0,0 +1,19 @@ +/* Taken from https://juliadocs.org/DocumenterCitations.jl/v1.2/styling/ */ + +.citation dl { + display: grid; + grid-template-columns: max-content auto; } +.citation dt { + grid-column-start: 1; } +.citation dd { + grid-column-start: 2; + margin-bottom: 0.75em; } +.citation ul { + padding: 0 0 2.25em 0; + margin: 0; + list-style: none;} +.citation ul li { + text-indent: -2.25em; + margin: 0.33em 0.5em 0.5em 2.25em;} +.citation ol li { + padding-left:0.75em;} diff --git a/docs/src/assets/link-icons.css b/docs/src/assets/link-icons.css new file mode 100644 index 0000000..a84137a --- /dev/null +++ b/docs/src/assets/link-icons.css @@ -0,0 +1,42 @@ +a[href^="https://juliamanifolds.github.io/ManifoldsBase.jl/"]::before { + content: ""; + background-image: url('logo-manifoldsbase.png'); + background-size: contain; + background-repeat: no-repeat; + display: inline-block; + height: 1em; + width: 1em; + margin-right: 4px; + vertical-align: middle; +} +a[href^="https://juliamanifolds.github.io/Manifolds.jl/"]::before { + content: ""; + background-image: url('logo-manifolds.png'); + background-size: contain; + background-repeat: no-repeat; + display: inline-block; + height: 1em; + width: 1em; + margin-right: 4px; + vertical-align: middle; +} +a[href^="https://en.wikipedia.org/"]::before { + content: ""; + background-image: url('wikipedia.png'); + background-size: contain; + background-repeat: no-repeat; + display: inline-block; + height: 1em; + width: 1em; + margin-right: 4px; + vertical-align: middle; +} + +@media (prefers-color-scheme: dark) { + a[href^="https://juliamanifolds.github.io/ManifoldsBase.jl/"]::before { + background-image: url('logo-manifoldsbase-dark.png'); + } + a[href^="https://juliamanifolds.github.io/Manifolds.jl/"]::before { + background-image: url('logo-manifolds-dark.png'); + } +} \ No newline at end of file diff --git a/docs/src/assets/logo-manifolds-dark.png b/docs/src/assets/logo-manifolds-dark.png new file mode 100644 index 0000000..5b10586 Binary files /dev/null and b/docs/src/assets/logo-manifolds-dark.png differ diff --git a/docs/src/assets/logo-manifolds.png b/docs/src/assets/logo-manifolds.png new file mode 100644 index 0000000..f942016 Binary files /dev/null and b/docs/src/assets/logo-manifolds.png differ diff --git a/docs/src/assets/logo-manifoldsbase-dark.png b/docs/src/assets/logo-manifoldsbase-dark.png new file mode 100644 index 0000000..ecfa190 Binary files /dev/null and b/docs/src/assets/logo-manifoldsbase-dark.png differ diff --git a/docs/src/assets/logo-manifoldsbase.png b/docs/src/assets/logo-manifoldsbase.png new file mode 100644 index 0000000..a4e1dc5 Binary files /dev/null and b/docs/src/assets/logo-manifoldsbase.png differ diff --git a/docs/src/assets/logo-text-dark.png b/docs/src/assets/logo-text-dark.png new file mode 100644 index 0000000..bb1a271 Binary files /dev/null and b/docs/src/assets/logo-text-dark.png differ diff --git a/docs/src/assets/logo-text-readme-dark.png b/docs/src/assets/logo-text-readme-dark.png new file mode 100644 index 0000000..135a455 Binary files /dev/null and b/docs/src/assets/logo-text-readme-dark.png differ diff --git a/docs/src/assets/logo-text-readme.png b/docs/src/assets/logo-text-readme.png new file mode 100644 index 0000000..2fd3466 Binary files /dev/null and b/docs/src/assets/logo-text-readme.png differ diff --git a/docs/src/assets/logo-text.png b/docs/src/assets/logo-text.png new file mode 100644 index 0000000..4d40b64 Binary files /dev/null and b/docs/src/assets/logo-text.png differ diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png new file mode 100644 index 0000000..e3a3fec Binary files /dev/null and b/docs/src/assets/logo.png differ diff --git a/docs/src/assets/wikipedia.png b/docs/src/assets/wikipedia.png new file mode 100644 index 0000000..4dd2046 Binary files /dev/null and b/docs/src/assets/wikipedia.png differ diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..4f2fe61 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,11 @@ +# AlgorithmsInterface.jl + +Welcome to the Documentation of `AlgorithmsInterface.jl`. + +```@meta +CurrentModule = AlgorithmsInterface +``` + +```@docs +AlgorithmsInterface.AlgorithmsInterface +``` \ No newline at end of file diff --git a/docs/src/interface.md b/docs/src/interface.md new file mode 100644 index 0000000..c6bb211 --- /dev/null +++ b/docs/src/interface.md @@ -0,0 +1,59 @@ +# The algorithm interface + +## General design ideas + +The interface this package provides is based on three ingredients of running an algorithm +consists of: + +* a [`Problem`](@ref) that is to be solved and contains all information that is algorithm independent. + This is _static information_ in the sense that it does not change during the runtime of the algorithm. +* an [`Algorithm`](@ref) that includes all of the _settings_ and _parameters_ that an algorithm. + this is also information that is _static_. +* a [`State`](@ref) that contains all remaining data, especially data that might vary during the iteration, + temporary caches, for example the current iteration the algorithm run is in and the current iterate, respectively. + +The combination of the static information should be enough to initialize the varying data. + +This general scheme is a guiding principle of the package, splitting information into _static_ +or _configuration_ types or data that allows to [`initialize_state`](@ref) a corresponding _variable_ data type. + +The order of arguments is given by two ideas + +1. for non-mutating functions the order should be from the most fixed data to the most variable one. + For example the three types just mentioned would be ordered like `f(problem, algorithm, state)` +2. For mutating functions the variable that is mutated comes first, for the remainder the guiding principle from 1 continues. + The main case here is `f!(state, problem, algorithm)`. + +```@autodocs +Modules = [AlgorithmsInterface] +Pages = ["interface/interface.jl"] +Order = [:type, :function] +Private = true +``` + +## Algorithm + +```@autodocs +Modules = [AlgorithmsInterface] +Pages = ["interface/algorithm.jl"] +Order = [:type, :function] +Private = true +``` + +## Problem + +```@autodocs +Modules = [AlgorithmsInterface] +Pages = ["interface/problem.jl"] +Order = [:type, :function] +Private = true +``` + +## State + +```@autodocs +Modules = [AlgorithmsInterface] +Pages = ["interface/state.jl"] +Order = [:type, :function] +Private = true +``` \ No newline at end of file diff --git a/docs/src/notation.md b/docs/src/notation.md new file mode 100644 index 0000000..411a7f8 --- /dev/null +++ b/docs/src/notation.md @@ -0,0 +1,13 @@ +# Notation + +Throughout the package we use the following abbreviations and variable names, +where the longer names are usually used in the documentation and as keywords. +The shorter ones are often used in code when their name does not cause ambiguities. + +| Name | Variable | Comment | +| ---- | -------- | ------- | +| [`Algorithm`](@ref) | `algorithm`, `a` | | +| [`Problem`](@ref) | `problem`, `p` | | +| [`State`](@ref) | `state`, `s` | | +| [`StoppingCriterion`](@ref) | `stopping_criterion`, `sc` | | +| [`StoppingCriterionState`](@ref) | `stopping_criterion_state`, `scs` | | \ No newline at end of file diff --git a/docs/src/references.bib b/docs/src/references.bib new file mode 100644 index 0000000..e69de29 diff --git a/docs/src/references.md b/docs/src/references.md new file mode 100644 index 0000000..d7fb1e8 --- /dev/null +++ b/docs/src/references.md @@ -0,0 +1,8 @@ +# Literature + +This is all literature mentioned / referenced in the `AlgorithmsInterface.jl` documentation. +Usually you find a small reference section at the end of every documentation page that contains +the corresponding references as well. + +```@bibliography +``` \ No newline at end of file diff --git a/docs/src/stopping_criterion.md b/docs/src/stopping_criterion.md new file mode 100644 index 0000000..6f5917c --- /dev/null +++ b/docs/src/stopping_criterion.md @@ -0,0 +1,8 @@ +# Stopping criterion + +```@autodocs +Modules = [AlgorithmsInterface] +Pages = ["stopping_criterion.jl"] +Order = [:type, :function] +Private = true +``` \ No newline at end of file diff --git a/src/AlgorithmInterface.jl b/src/AlgorithmInterface.jl deleted file mode 100644 index d59ebaf..0000000 --- a/src/AlgorithmInterface.jl +++ /dev/null @@ -1,3 +0,0 @@ -module AlgorithmInterface - -end # module AlgorithmInterface diff --git a/src/AlgorithmsInterface.jl b/src/AlgorithmsInterface.jl new file mode 100644 index 0000000..8dd052a --- /dev/null +++ b/src/AlgorithmsInterface.jl @@ -0,0 +1,27 @@ +@doc raw""" +๐Ÿงฎ AlgorithmsInterface.jl: an interface for iterative algorithms in Julia + +* ๐Ÿ“š Documentation: [juliamanifolds.github.io/AlgorithmsInterface.jl/](https://juliamanifolds.github.io/AlgorithmsInterface.jl/) +* ๐Ÿ“ฆ Repository: [github.com/JuliaManifolds/AlgorithmsInterface.jl](https://github.com/JuliaManifolds/AlgorithmsInterface.jl) +* ๐Ÿ’ฌ Discussions: [github.com/JuliaManifolds/AlgorithmsInterface.jl/discussions](https://github.com/JuliaManifolds/AlgorithmsInterface.jl/discussions) +* ๐ŸŽฏ Issues: [github.com/JuliaManifolds/AlgorithmsInterface.jl/issues](https://github.com/JuliaManifolds/AlgorithmsInterface.jl/issues) +""" +module AlgorithmsInterface + +using Dates: Millisecond, Nanosecond, Period, canonicalize, value + +include("interface/algorithm.jl") +include("interface/problem.jl") +include("interface/state.jl") +include("interface/interface.jl") + +include("stopping_criterion.jl") + +export Algorithm, Problem, State +export StoppingCriterion, StoppingCriterionState +export StopAfter, StopAfterIteration, StopWhenAll, StopWhenAny +export is_finished, is_finished! +export initialize_state, initialize_state! +export step!, solve, solve! + +end # module AlgorithmsInterface diff --git a/src/interface/algorithm.jl b/src/interface/algorithm.jl new file mode 100644 index 0000000..722d054 --- /dev/null +++ b/src/interface/algorithm.jl @@ -0,0 +1,22 @@ +@doc """ + Algorithm + +An abstract type to represent an algorithm. + +A concrete algorithm contains all static parameters that characterise the algorithms. +Together with a [`Problem`](@ref) an `Algorithm` subtype should be able to initialize +or reset a [`State`](@ref). + +## Properties + +Algorithms can contain any number of properties that are needed to define the algorithm, +but should additionally contain the following properties to interact with the stopping criteria. + +* `stopping_criterion::StoppingCriterion` + +## Example + +For a [gradient descent](https://en.wikipedia.org/wiki/Gradient_descent) algorithm +the algorithm would specify which step size selection to use. +""" +abstract type Algorithm end diff --git a/src/interface/interface.jl b/src/interface/interface.jl new file mode 100644 index 0000000..c72c8ca --- /dev/null +++ b/src/interface/interface.jl @@ -0,0 +1,61 @@ +_doc_init_state = """ + state = initialize_state(problem::Problem, algorithm::Algorithm; kwargs...) + state = initialize_state!(state::State, problem::Problem, algorithm::Algorithm; 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. +""" + +function initialize_state end + +@doc "$(_doc_init_state)" +initialize_state(::Problem, ::Algorithm; kwargs...) + +function initialize_state! end + +@doc "$(_doc_init_state)" +initialize_state!(::Problem, ::Algorithm, ::State; kwargs...) + +# has to be defined before used in solve but is documented alphabetically after + +@doc """ + 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. + +By default this method continues to call [`solve!`](@ref). +""" +function solve(problem::Problem, algorithm::Algorithm; kwargs...) + state = initialize_state(problem, algorithm; kwargs...) + return solve!(problem, algorithm, state; kwargs...) +end + +@doc """ + solve!(problem::Problem, algorithm::Algorithm, state::State; kwargs...) + +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. +""" +function solve!(problem::Problem, algorithm::Algorithm, state::State; kwargs...) + initialize_state!(problem, algorithm, state; kwargs...) + while !is_finished!(problem, algorithm, state) + increment!(state) + step!(problem, algorithm, state) + end + return state +end + +function step! end +@doc """ + step!(problem::Problem, algorithm::Algorithm, state::State) + +Perform the current step of an [`Algorithm`](@ref) solving a [`Problem`](@ref) +modifying the algorithm's [`State`](@ref). +""" +step!(problem::Problem, algorithm::Algorithm, state::State) diff --git a/src/interface/problem.jl b/src/interface/problem.jl new file mode 100644 index 0000000..291f875 --- /dev/null +++ b/src/interface/problem.jl @@ -0,0 +1,21 @@ +""" + Problem + +An abstract type to represent a problem to be solved with all its static properties, that do +not change during an algorithm run. + +## Example + +For a [gradient descent](https://en.wikipedia.org/wiki/Gradient_descent) algorithm the problem consists of + +* a `cost` function ``f: C โ†’ โ„`` +* a gradient function ``$(raw"\operatorname{grad}")f`` + +The problem then could that these are given in four different forms + +* a function `c = cost(x)` and a gradient `d = gradient(x)` +* a function `c = cost(x)` and an in-place gradient `gradient!(d,x)` +* a combined cost-grad function `(c,d) = costgrad(x)` +* a combined cost-grad function `(c, d) = costgrad!(d, x)` that computes the gradient in-place. +""" +abstract type Problem end diff --git a/src/interface/state.jl b/src/interface/state.jl new file mode 100644 index 0000000..1cab6f3 --- /dev/null +++ b/src/interface/state.jl @@ -0,0 +1,37 @@ +@doc """ + State + +An abstract type to represent the state an iterative algorithm is in. + +The state consists of any information that describes the current step the algorithm is in +and keeps all information needed from one step to the next. + +## Properties + +In order to interact with the stopping criteria, the state should contain the following properties, +and provide corresponding `getproperty` and `setproperty!` methods. + +* `iteration` โ€“ the current iteration step ``k`` that is is currently performed or was last performed +* `stopping_criterion_state` โ€“ a [`StoppingCriterionState`](@ref) that indicates whether an [`Algorithm`](@ref) + will stop after this iteration or has stopped. +* `iterate` the current iterate ``x^{(k)}```. + +## Methods + +The following methods should be implemented for a state + +* [`increment!](@ref)`(state)` +""" +abstract type State end + +""" + increment!(state::State) + +Increment the current iteration a [`State`](@ref) either is currently performing or was last performed + +The default assumes that the current iteration is stored in `state.iteration`. +""" +function increment!(state::State) + state.iteration += 1 + return state +end diff --git a/src/stopping_criterion.jl b/src/stopping_criterion.jl new file mode 100644 index 0000000..d2398d6 --- /dev/null +++ b/src/stopping_criterion.jl @@ -0,0 +1,615 @@ +@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 + +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 an empty string. + +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 + +_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) + +# +# +# Meta StoppingCriteria +@doc raw""" + StopWhenAll <: StoppingCriterion + +store a tuple of [`StoppingCriterion`](@ref)s and indicate to stop, +when _all_ indicate to stop. + +# Constructor + + StopWhenAll(c::NTuple{N,StoppingCriterion} where N) + StopWhenAll(c::StoppingCriterion,...) +""" +struct StopWhenAll{TCriteria<:Tuple} <: StoppingCriterion + criteria::TCriteria +end +StopWhenAll(c::AbstractVector{<:StoppingCriterion}) = StopWhenAll(Tuple(c)) +StopWhenAll(c...) = StopWhenAll(c) +function indicates_convergence(stop_when_all::StopWhenAll) + return any(indicates_convergence, stop_when_all.criteria) +end + +function Base.show(io::IO, ::MIME"text/plain", stop_when_all::StopWhenAll) + print(io, "StopWhenAll with the Stopping Criteria:") + for stopping_criterion in stop_when_all.criteria + print(io, "\n ") + replace(io, string(stopping_criterion), "\n" => "\n ") #increase indent + end + return nothing +end + +""" + &(s1,s2) + s1 & s2 + +Combine two [`StoppingCriterion`](@ref) within an [`StopWhenAll`](@ref). +If either `s1` (or `s2`) is already an [`StopWhenAll`](@ref), then `s2` (or `s1`) is +appended to the list of [`StoppingCriterion`](@ref) within `s1` (or `s2`). + +# Example + a = StopAfterIteration(200) & StopAfter(Minute(1)) + +Is the same as + + a = StopWhenAll(StopAfterIteration(200), StopAfter(Minute(1)) +""" +Base.:&(s1::StoppingCriterion, s2::StoppingCriterion) = StopWhenAll(s1, s2) +Base.:&(s1::StoppingCriterion, s2::StopWhenAll) = StopWhenAll(s1, s2.criteria...) +Base.:&(s1::StopWhenAll, s2::StoppingCriterion) = StopWhenAll(s1.criteria..., s2) +Base.:&(s1::StopWhenAll, s2::StopWhenAll) = StopWhenAll(s1.criteria..., s2.criteria...) + +@doc raw""" + StopWhenAny <: StoppingCriterion + +store an array of [`StoppingCriterion`](@ref) 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...) +""" +struct StopWhenAny{TCriteria<:Tuple} <: StoppingCriterion + criteria::TCriteria + StopWhenAny(c::Vector{<:StoppingCriterion}) = new{typeof(tuple(c...))}(tuple(c...)) + StopWhenAny(c::StoppingCriterion...) = new{typeof(c)}(c) +end + +function indicates_convergence(stop_when_any::StopWhenAny) + return all(indicates_convergence, stop_when_any.criteria) +end + +function Base.show(io::IO, ::MIME"text/plain", stop_when_any::StopWhenAny) + print(io, "StopWhenAny with the Stopping Criteria:") + for stopping_criterion in stop_when_any.criteria + print(io, "\n ") + replace(io, string(stopping_criterion), "\n" => "\n ") #increase indent + end + return nothing +end + +""" + |(s1,s2) + s1 | s2 + +Combine two [`StoppingCriterion`](@ref) within an [`StopWhenAny`](@ref). +If either `s1` (or `s2`) is already an [`StopWhenAny`](@ref), then `s2` (or `s1`) is +appended to the list of [`StoppingCriterion`](@ref) within `s1` (or `s2`) + +# Example + a = StopAfterIteration(200) | StopAfter(Minute(1)) + +Is the same as + + a = StopWhenAny(StopAfterIteration(200), StopAfter(Minute(1))) +""" +Base.:|(s1::StoppingCriterion, s2::StoppingCriterion) = StopWhenAny(s1, s2) +Base.:|(s1::StoppingCriterion, s2::StopWhenAny) = StopWhenAny(s1, s2.criteria...) +Base.:|(s1::StopWhenAny, s2::StoppingCriterion) = StopWhenAny(s1.criteria..., s2) +Base.:|(s1::StopWhenAny, s2::StopWhenAny) = StopWhenAny(s1.criteria..., s2.criteria...) + +# A common state for stopping criteria working on tuples of stopping criteria +""" + GroupStoppingCriterionState <: StoppingCriterionState + +A [`StoppingCriterionState`](@ref) that groups multiple [`StoppingCriterionState`](@ref)s +internally as a tuple. +This is for example used in combination with [`StopWhenAny`](@ref) and [`StopWhenAny`](@ref) + +# Constructor + GroupStoppingCriterionState(c::Vector{N,StoppingCriterionState} where N) + GroupStoppingCriterionState(c::StoppingCriterionState...) +""" +mutable struct GroupStoppingCriterionState{TCriteriaStates<:Tuple} <: StoppingCriterionState + criteria_states::TCriteriaStates + at_iteration::Int + GroupStoppingCriterionState(c::Vector{<:StoppingCriterionState}) = + new{typeof(tuple(c...))}(tuple(c...), -1) + GroupStoppingCriterionState(c::StoppingCriterionState...) = new{typeof(c)}(c, -1) +end + +function get_reason( + stop_when::Union{StopWhenAll,StopWhenAny}, + stopping_criterion_states::GroupStoppingCriterionState, +) + stopping_criterion_states.at_iteration < 0 && return nothing + criteria = stop_when.criteriaq + stopping_criterion_states = stopping_criterion_states.criteria_states + return join(Iterators.map(get_reason, criteria, stopping_criterion_states)) +end + +function initialize_state( + problem::Problem, + algorithm::Algorithm, + stop_when::Union{StopWhenAll,StopWhenAny}; + kwargs..., +) + return GroupStoppingCriterionState( + ( + initialize_state(problem, algorithm, stopping_criterion; kwargs...) for + stopping_criterion in stop_when.criteria + )..., + ) +end +function initialize_state!( + stopping_criterion_states::GroupStoppingCriterionState, + problem::Problem, + algorithm::Algorithm, + stop_when::Union{StopWhenAll,StopWhenAny}; + kwargs..., +) + for (stopping_criterion_state, stopping_criterion) in + zip(stopping_criterion_states.criteria_states, stop_when.criteria) + initialize_state!( + stopping_criterion_state, + problem, + algorithm, + stopping_criterion; + kwargs..., + ) + end + stopping_criterion_states.at_iteration = -1 + return stopping_criterion_states +end + +function is_finished( + problem::Problem, + algorithm::Algorithm, + state::State, + stop_when_all::StopWhenAll, + stopping_criterion_states::GroupStoppingCriterionState, +) + k = state.iteration + (k == 0) && (stopping_criterion_states.at_iteration = -1) # reset on init + if all( + st -> is_finished(problem, algorithm, state, st[1], st[2]), + zip(stop_when_all.criteria, stopping_criterion_states.criteria_states), + ) + return true + end + return false +end +function is_finished!( + problem::Problem, + algorithm::Algorithm, + state::State, + stop_when_all::StopWhenAll, + stopping_criterion_states::GroupStoppingCriterionState, +) + k = state.iteration + (k == 0) && (stopping_criterion_states.at_iteration = -1) # reset on init + if all( + st -> is_finished!(problem, algorithm, state, st[1], st[2]), + zip(stop_when_all.criteria, stopping_criterion_states.criteria_states), + ) + stopping_criterion_states.at_iteration = k + return true + end + return false +end + +function is_finished( + problem::Problem, + algorithm::Algorithm, + state::State, + stop_when_any::StopWhenAny, + stopping_criterion_states::GroupStoppingCriterionState, +) + k = state.iteration + (k == 0) && (stopping_criterion_states.at_iteration = -1) # reset on init + if any( + st -> is_finished(problem, algorithm, state, st[1], st[2]), + zip(stop_when_any.criteria, stopping_criterion_states.criteria_states), + ) + return true + end + return false +end +function is_finished!( + problem::Problem, + algorithm::Algorithm, + state::State, + stop_when_any::StopWhenAny, + stopping_criterion_states::GroupStoppingCriterionState, +) + k = state.iteration + (k == 0) && (stopping_criterion_states.at_iteration = -1) # reset on init + if any( + st -> is_finished!(problem, algorithm, state, st[1], st[2]), + zip(stop_when_any.criteria, stopping_criterion_states.criteria_states), + ) + stopping_criterion_states.at_iteration = k + return true + end + return false +end + +function Base.summary( + io::IO, + stop_when_any::StopWhenAny, + stopping_criterion_states::GroupStoppingCriterionState, +) + has_stopped = (stopping_criterion_states.at_iteration >= 0) + s = has_stopped ? "reached" : "not reached" + r = "Stop When _one_ of the following are fulfilled:\n" + for (stopping_criterion, stopping_criterion_state) in + zip(stop_when_any.criteria, stopping_criterion_states.criteria_states) + s = replace(summary(stopping_criterion, stopping_criterion_state), "\n" => "\n ") + r = "$r $(s)\n" + end + return print(io, "$(r)Overall: $s") +end +function Base.summary( + io::IO, + stop_when_all::StopWhenAll, + stopping_criterion_states::GroupStoppingCriterionState, +) + has_stopped = (stopping_criterion_states.at_iteration >= 0) + s = has_stopped ? "reached" : "not reached" + r = "Stop When _all_ of the following are fulfilled:\n" + for (stopping_criterion, stopping_criterion_state) in + zip(stop_when_all.criteria, stopping_criterion_states.criteria_states) + s = replace(summary(stopping_criterion, stopping_criterion_state), "\n" => "\n ") + r = "$r $(s)\n" + end + return print(io, "$(r)Overall: $s") +end + +# +# +# Concrete Stopping Criteria + +@doc raw""" + StopAfterIteration <: StoppingCriterion + +A simple stopping criterion to stop after a maximal number of iterations. + +# Fields + +* `max_iterations` stores the maximal iteration number where to stop at + +# Constructor + + StopAfterIteration(maxIter) + +initialize the functor to indicate to stop after `maxIter` iterations. +""" +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!( + stopping_criterion_state::DefaultStoppingCriterionState, + ::Problem, + ::Algorithm, + ::StopAfterIteration; + kwargs..., +) + stopping_criterion_state.at_iteration = -1 + return stopping_criterion_state +end + + +function is_finished( + ::Problem, + ::Algorithm, + state::State, + stop_after_iteration::StopAfterIteration, + stopping_criterion_state::DefaultStoppingCriterionState, +) + return state.iteration >= stop_after_iteration.max_iterations +end +function is_finished!( + ::Problem, + ::Algorithm, + state::State, + stop_after_iteration::StopAfterIteration, + stopping_criterion_state::DefaultStoppingCriterionState, +) + k = state.iteration + (k == 0) && (stopping_criterion_state.at_iteration = -1) + if k >= stop_after_iteration.max_iterations + stopping_criterion_state.at_iteration = k + return true + end + return false +end +function get_reason( + stop_after_iteration::StopAfterIteration, + stopping_criterion_state::DefaultStoppingCriterionState, +) + if stopping_criterion_state.at_iteration >= stop_after_iteration.max_iterations + return "At iteration $(stopping_criterion_state.at_iteration) the algorithm reached its maximal number of iterations ($(stop_after_iteration.max_iterations)).\n" + end + return nothing +end +indicates_convergence(stop_after_iteration::StopAfterIteration) = false +function Base.summary( + io::IO, + stop_after_iteration::StopAfterIteration, + stopping_criterion_state::DefaultStoppingCriterionState, +) + has_stopped = (stopping_criterion_state.at_iteration >= 0) + s = has_stopped ? "reached" : "not reached" + return print(io, "Max Iteration $(stop_after_iteration.max_iterations):\t$s") +end + +""" + StopAfter <: StoppingCriterion + +store 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 + +* `threshold` stores the `Period` after which to stop + +# Constructor + + StopAfter(t) + +initialize the stopping criterion to a `Period t` to stop after. +""" +struct StopAfter <: StoppingCriterion + threshold::Period + function StopAfter(t::Period) + if value(t) < 0 + throw(ArgumentError("You must provide a positive time period")) + else + s = new(t) + end + return s + end +end + +@doc """ + StopAfterTimePeriodState <: StoppingCriterionState + +A state for stopping criteria that are based on time measurements, +for example [`StopAfter`](@ref). + +* `start` stores the starting time when the algorithm is started, that is a call with `i=0`. +* `time` stores the elapsed time +* `at_iteration` indicates at which iteration (including `i=0`) the stopping criterion + was fulfilled and is `-1` while it is not fulfilled. + +""" +mutable struct StopAfterTimePeriodState <: StoppingCriterionState + start::Nanosecond + time::Nanosecond + at_iteration::Int + function StopAfterTimePeriodState() + return new(Nanosecond(0), Nanosecond(0), -1) + end +end + +initialize_state(::Problem, ::Algorithm, ::StopAfter; kwargs...) = + StopAfterTimePeriodState() + +function initialize_state!( + stopping_criterion_state::DefaultStoppingCriterionState, + ::Problem, + ::Algorithm, + ::StopAfter; + kwargs..., +) + stopping_criterion_state.start = Nanosecond(0) + stopping_criterion_state.time = Nanosecond(0) + stopping_criterion_state.at_iteration = -1 + return stopping_criterion_state +end + +function is_finished( + ::Problem, + ::Algorithm, + state::State, + stop_after::StopAfter, + stop_after_state::StopAfterTimePeriodState, +) + k = state.iteration + # Just check whether the (last recorded) time is beyond the threshold + return (k > 0 && (stop_after_state.time > Nanosecond(stop_after.threshold))) +end +function is_finished!( + ::Problem, + ::Algorithm, + state::State, + stop_after::StopAfter, + stop_after_state::StopAfterTimePeriodState, +) + k = state.iteration + if value(stop_after_state.start) == 0 || k <= 0 # (re)start timer + stop_after_state.at_iteration = -1 + stop_after_state.start = Nanosecond(time_ns()) + stop_after_state.time = Nanosecond(0) + else + stop_after_state.time = Nanosecond(time_ns()) - stopping_criterion_state.start + if k > 0 && (stop_after_state.time > Nanosecond(stop_after.threshold)) + stop_after_state.at_iteration = k + return true + end + end + return false +end +function get_reason( + stop_after::StopAfter, + stopping_criterion_state::StopAfterTimePeriodState, +) + if (stopping_criterion_state.at_iteration >= 0) + return "After iteration $(stopping_criterion_state.at_iteration) the algorithm ran for $(floor(stopping_criterion_state.time, typeof(stop_after.threshold))) (threshold: $(stop_after.threshold)).\n" + end + return nothing +end +function Base.summary( + io::IO, + stop_after::StopAfter, + stopping_criterion_state::StopAfterTimePeriodState, +) + has_stopped = (stopping_criterion_state.at_iteration >= 0) + s = has_stopped ? "reached" : "not reached" + return print(io, "stopped after $(stop_after.threshold):\t$s") +end +indicates_convergence(stop_after::StopAfter) = false diff --git a/test/newton.jl b/test/newton.jl new file mode 100644 index 0000000..e83d2da --- /dev/null +++ b/test/newton.jl @@ -0,0 +1,68 @@ +# Newton's method for finding roots of a function +# wrapped as a very simple iterative algorithm + +using AlgorithmsInterface +import AlgorithmsInterface: initialize_state, initialize_state!, is_finished, solve!, step! +using Test + +# Defining the structs +# ------------------ +struct RootFindingProblem <: Problem + f::Function + df::Function +end + +struct NewtonMethod{S} <: Algorithm + stopping_criterion::S + # TODO: logging settings? stopping criterium initialization? +end + +mutable struct NewtonState{S} <: State + iteration::Int + iterate::Float64 + stopping_criterion_state::S +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 +end +function initialize_state!( + problem::RootFindingProblem, + algorithm::NewtonMethod, + state::NewtonState, +) + state.iteration = 0 + state.iterate = 1.0 + initialize_state!( + state.stopping_criterion_state, + problem, + algorithm, + algorithm.stopping_criterion, + ) + return state +end + +function step!(problem::RootFindingProblem, ::NewtonMethod, state::NewtonState) + state.iterate -= problem.f(state.iterate) / problem.df(state.iterate) + return state +end + +# Testing the algorithm +# --------------------- +@testset "Babylonian square roots" begin + f(x, a) = x^2 - a + df(x, a) = 2x + + a = 612.0 + problem = RootFindingProblem(x -> f(x, a), x -> df(x, a)) + algorithm1 = NewtonMethod(StopAfterIteration(8)) + solution1 = solve(problem, algorithm1) + @test solution1.iterate โ‰ˆ sqrt(a) + algorithm2 = NewtonMethod(StopAfterIteration(10)) + solution2 = solve(problem, algorithm2) + @test solution2.iterate โ‰ˆ sqrt(a) + @test abs(solution2.iterate - sqrt(a)) < abs(solution1.iterate - sqrt(a)) +end diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..a6d423f --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,18 @@ +using SafeTestsets + +# these have to be included here to make show tests behave +using AlgorithmsInterface +using Dates + +@safetestset "Newton" begin + include("newton.jl") +end + +@safetestset "Stopping Criteria" begin + include("stopping_criterion.jl") +end + +@safetestset "Aqua" begin + using AlgorithmsInterface, Aqua + Aqua.test_all(AlgorithmsInterface) +end diff --git a/test/stopping_criterion.jl b/test/stopping_criterion.jl new file mode 100644 index 0000000..a51a414 --- /dev/null +++ b/test/stopping_criterion.jl @@ -0,0 +1,72 @@ +using Test +using AlgorithmsInterface +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 + +problem = DummyProblem() + +@testset "StopAfterIteration" begin + s1 = StopAfterIteration(2) + @test s1 isa StoppingCriterion + @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) +end + +@testset "StopAfter" begin + s1 = StopAfter(Second(1)) + @test s1 isa StoppingCriterion + @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) +end + +@testset "StopWhenAll" begin + s1 = StopAfterIteration(2) & StopAfter(Second(1)) + @test s1 isa StoppingCriterion + @test sprint((io, x) -> show(io, MIME"text/plain"(), x), s1) == + "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) +end + +@testset "StopWhenAny" begin + s1 = StopAfterIteration(2) | StopAfter(Second(1)) + @test s1 isa StoppingCriterion + @test sprint((io, x) -> show(io, MIME"text/plain"(), x), s1) == + "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) +end