diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..35e80aa --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,12 @@ +name: Check Changelog +on: + pull_request: + +jobs: + Check-Changelog: + name: Check Changelog Action + runs-on: ubuntu-latest + steps: + - uses: tarides/changelog-check-action@v3 + with: + changelog: Changelog.md \ No newline at end of file diff --git a/.github/workflows/spell_check.yml b/.github/workflows/spell_check.yml new file mode 100644 index 0000000..91341d7 --- /dev/null +++ b/.github/workflows/spell_check.yml @@ -0,0 +1,13 @@ +name: Spell Check + +on: [pull_request, workflow_dispatch] + +jobs: + typos-check: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v6 + - name: Check spelling + uses: crate-ci/typos@v1.45.0 diff --git a/.gitignore b/.gitignore index 9ed1b59..51a6c34 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ docs/build docs/Manifest.toml Manifest.toml +docs/src/changelog.md diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..25c41db --- /dev/null +++ b/Changelog.md @@ -0,0 +1,10 @@ +# Changelog + +All notable Changes to the Julia package `AlgorithmsInterface.jl` are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] 2026/05/01 + +Initial release. \ No newline at end of file diff --git a/Project.toml b/Project.toml index 56f3148..3a44ecf 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "AlgorithmsInterface" uuid = "d1e3940c-cd12-4505-8585-b0a4b322527d" -authors = ["Ronny Bergmann "] +authors = ["Ronny Bergmann ", "Lukas Devos "] version = "0.1.0" [deps] @@ -16,4 +16,3 @@ julia = "1.10" [workspace] projects = ["test", "docs"] - diff --git a/Readme.md b/Readme.md index d945b6c..9c3749b 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,7 @@ `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-img]][ci-url] [![runic][runic-img]][runic-url] [![codecov][codecov-img]][codecov-url] +[![docs][docs-dev-img]][docs-dev-url] [![CI][ci-img]][ci-url] [![runic][runic-img]][runic-url] [![codecov][codecov-img]][codecov-url] [![aqua][aqua-img]][aqua-url] [docs-dev-img]: https://img.shields.io/badge/docs-dev-blue.svg [docs-dev-url]: https://JuliaManifolds.github.io/AlgorithmsInterface.jl/dev/ @@ -17,6 +17,9 @@ [runic-img]: https://img.shields.io/badge/code_style-%E1%9A%B1%E1%9A%A2%E1%9A%BE%E1%9B%81%E1%9A%B2-black [runic-url]: https://github.com/fredrikekre/Runic.jl +[aqua-img]: https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg +[aqua-url]: https://github.com/JuliaTesting/Aqua.jl + # Statement of need A first approach to algorithms is a simple for-loop for a maximum number of iterations. @@ -28,7 +31,7 @@ Finally, a common interface also allows to easily combine existing algorithms, h # Main features -See the [intial discussion](https://github.com/JuliaManifolds/AlgorithmsInterface.jl/discussions/1) +See the [initial 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 diff --git a/docs/make.jl b/docs/make.jl index 1481694..eaacd7d 100755 --- a/docs/make.jl +++ b/docs/make.jl @@ -17,17 +17,62 @@ if "--help" ∈ ARGS exit(0) end -using Pkg -Pkg.activate(@__DIR__) -Pkg.develop(PackageSpec(; path = (@__DIR__) * "/../")) -Pkg.resolve() -Pkg.instantiate() +# if docs is not the current active environment, switch to it +# (from https://github.com/JuliaIO/HDF5.jl/pull/1020/)  +if Base.active_project() != joinpath(@__DIR__, "Project.toml") + using Pkg + Pkg.activate(@__DIR__) + Pkg.instantiate() +end +# +# Load packages using Documenter, DocumenterCitations, DocumenterInterLinks using AlgorithmsInterface run_on_CI = (get(ENV, "CI", nothing) == "true") +# +# Copy and reformat changelog + +# (d) add contributing.md and changelog.md to the docs – and link to releases and issues + +function add_links(line::String, url::String = "https://github.com/JuliaManifolds/Manopt.jl") + # replace issues (#XXXX) -> ([#XXXX](url/issue/XXXX)) + while (m = match(r"\(\#([0-9]+)\)", line)) !== nothing + id = m.captures[1] + line = replace(line, m.match => "([#$id]($url/issues/$id))") + end + # replace ## [X.Y.Z] -> with a link to the release [X.Y.Z](url/releases/tag/vX.Y.Z) + while (m = match(r"\#\# \[([0-9]+.[0-9]+.[0-9]+)\] (.*)", line)) !== nothing + tag = m.captures[1] + date = m.captures[2] + line = replace(line, m.match => "## [$tag]($url/releases/tag/v$tag) ($date)") + end + return line +end + +generated_path = joinpath(@__DIR__, "src") +base_url = "https://github.com/JuliaManifolds/Manopt.jl/blob/master/" +isdir(generated_path) || mkdir(generated_path) +for (md_file, doc_file) in [("Changelog.md", "changelog.md")] + open(joinpath(generated_path, doc_file), "w") do io + # Point to source license file + println( + io, + """ + ```@meta + EditURL = "$(base_url)$(md_file)" + ``` + """, + ) + # Write the contents out below the meta block + for line in eachline(joinpath(dirname(@__DIR__), md_file)) + println(io, add_links(line)) + end + end +end + bib = CitationBibliography(joinpath(@__DIR__, "src", "references.bib"); style = :alpha) links = InterLinks() makedocs(; @@ -47,8 +92,12 @@ makedocs(; "Interface" => "interface.md", "Stopping criteria" => "stopping_criterion.md", "Logging" => "logging.md", - "Notation" => "notation.md", - "References" => "references.md", + "Miscellanea" => [ + "Internals" => "internals.md", + "Notation" => "notation.md", + "Changelog" => "changelog.md", + "References" => "references.md", + ], ], expandfirst = ["interface.md", "stopping_criterion.md"], plugins = [bib, links], diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo-manopt.png similarity index 100% rename from docs/src/assets/logo.png rename to docs/src/assets/logo-manopt.png diff --git a/docs/src/assets/logo-text-dark.png b/docs/src/assets/logo-text-dark.png deleted file mode 100644 index bb1a271..0000000 Binary files a/docs/src/assets/logo-text-dark.png and /dev/null differ diff --git a/docs/src/assets/logo-text-readme-dark.png b/docs/src/assets/logo-text-readme-dark.png deleted file mode 100644 index 135a455..0000000 Binary files a/docs/src/assets/logo-text-readme-dark.png and /dev/null differ diff --git a/docs/src/assets/logo-text-readme.png b/docs/src/assets/logo-text-readme.png deleted file mode 100644 index 2fd3466..0000000 Binary files a/docs/src/assets/logo-text-readme.png and /dev/null differ diff --git a/docs/src/assets/logo-text.png b/docs/src/assets/logo-text.png deleted file mode 100644 index 4d40b64..0000000 Binary files a/docs/src/assets/logo-text.png and /dev/null differ diff --git a/docs/src/internals.md b/docs/src/internals.md new file mode 100644 index 0000000..e1aeac4 --- /dev/null +++ b/docs/src/internals.md @@ -0,0 +1,15 @@ +# Internals + +## Test + +```@docs +AlgorithmsInterface.Test +``` + +```@autodocs +Modules = [AlgorithmsInterface] +Pages = ["test_suite.jl"] +Order = [:type, :function] +Private = true +Public = true +``` \ No newline at end of file diff --git a/docs/src/stopping_criterion.md b/docs/src/stopping_criterion.md index a7e2218..7ffc475 100644 --- a/docs/src/stopping_criterion.md +++ b/docs/src/stopping_criterion.md @@ -68,7 +68,7 @@ function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm:: # 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 @@ -244,7 +244,7 @@ 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 payd off, as we can still compose this stopping criterion with other criteria as well: ```@example Heron criterion = StopWhenStable(1e-8) | StopAfterIteration(5) diff --git a/src/AlgorithmsInterface.jl b/src/AlgorithmsInterface.jl index 0907dbd..ae84edb 100644 --- a/src/AlgorithmsInterface.jl +++ b/src/AlgorithmsInterface.jl @@ -20,6 +20,8 @@ include("interface/interface.jl") include("stopping_criterion.jl") include("logging.jl") +include("test_suite.jl") + # general interface export Algorithm, Problem, State export initialize_state, initialize_state! diff --git a/src/stopping_criterion.jl b/src/stopping_criterion.jl index a6da5d0..dd2f13c 100644 --- a/src/stopping_criterion.jl +++ b/src/stopping_criterion.jl @@ -54,18 +54,16 @@ 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). +Return whether or not a [`StoppingCriterion`](@ref) indicates convergence when it is in [`StoppingCriterionState`](@ref), +i.e. also check whether the state indicates that the criterion has been active. -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, + stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState, ) - return isnothing(get_reason(stopping_criterion, stopping_criterion_state)) && - indicates_convergence(stopping_criterion) + return isnothing(get_reason(stopping_criterion, stopping_criterion_state)) && indicates_convergence(stopping_criterion) end _doc_is_finished = """ @@ -86,11 +84,8 @@ once per iteration, the other one merely inspects the current status without mut @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, + problem, algorithm, state, + algorithm.stopping_criterion, state.stopping_criterion_state, ) end @@ -100,11 +95,8 @@ is_finished(::Problem, ::Algorithm, ::State, ::StoppingCriterion, ::StoppingCrit @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, + problem, algorithm, state, + algorithm.stopping_criterion, state.stopping_criterion_state, ) end @@ -113,9 +105,10 @@ is_finished!(::Problem, ::Algorithm, ::State, ::StoppingCriterion, ::StoppingCri @doc """ summary(io::IO, stopping_criterion::StoppingCriterion, stopping_criterion_state::StoppingCriterionState) + summary(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 +it currently indicates to stop.ìo` and generates a string from that. # Example @@ -127,6 +120,15 @@ Max Iterations (15): not reached """ Base.summary(io::IO, ::StoppingCriterion, ::StoppingCriterionState) +function Base.summary( + stopping_criterion::StoppingCriterion, + stopping_criterion_state::StoppingCriterionState + ) + io = IOBuffer() + summary(io, stopping_criterion, stopping_criterion_state) + return String(take!(io)) +end + # # # Meta StoppingCriteria @@ -245,8 +247,6 @@ This is for example used in combination with [`StopWhenAny`](@ref) and [`StopWhe 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 @@ -255,15 +255,13 @@ function get_reason( stopping_criterion_states::GroupStoppingCriterionState, ) stopping_criterion_states.at_iteration < 0 && return nothing - criteria = stop_when.criteriaq + criteria = stop_when.criteria 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}; + problem::Problem, algorithm::Algorithm, stop_when::Union{StopWhenAll, StopWhenAny}; kwargs..., ) return GroupStoppingCriterionState( @@ -274,19 +272,14 @@ function initialize_state( ) end function initialize_state!( - problem::Problem, - algorithm::Algorithm, - stop_when::Union{StopWhenAll, StopWhenAny}, + problem::Problem, algorithm::Algorithm, 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, - stopping_criterion, - stopping_criterion_state; + problem, algorithm, stopping_criterion, stopping_criterion_state; kwargs..., ) end @@ -295,11 +288,8 @@ function initialize_state!( end function is_finished( - problem::Problem, - algorithm::Algorithm, - state::State, - stop_when_all::StopWhenAll, - stopping_criterion_states::GroupStoppingCriterionState, + 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 @@ -312,11 +302,8 @@ function is_finished( return false end function is_finished!( - problem::Problem, - algorithm::Algorithm, - state::State, - stop_when_all::StopWhenAll, - stopping_criterion_states::GroupStoppingCriterionState, + 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 @@ -331,11 +318,8 @@ function is_finished!( end function is_finished( - problem::Problem, - algorithm::Algorithm, - state::State, - stop_when_any::StopWhenAny, - stopping_criterion_states::GroupStoppingCriterionState, + 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 @@ -348,11 +332,8 @@ function is_finished( return false end function is_finished!( - problem::Problem, - algorithm::Algorithm, - state::State, - stop_when_any::StopWhenAny, - stopping_criterion_states::GroupStoppingCriterionState, + 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 @@ -368,33 +349,31 @@ end function Base.summary( io::IO, - stop_when_any::StopWhenAny, - stopping_criterion_states::GroupStoppingCriterionState, + 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" + 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" + t = replace(summary(stopping_criterion, stopping_criterion_state), "\n" => "\n ") + r = "$(r) $(t)\n" end - return print(io, "$(r)Overall: $s") + return print(io, "$(r)Overall: $(s)") end function Base.summary( io::IO, - stop_when_all::StopWhenAll, - stopping_criterion_states::GroupStoppingCriterionState, + 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" + 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" + t = replace(summary(stopping_criterion, stopping_criterion_state), "\n" => "\n ") + r = "$(r) $(t)\n" end - return print(io, "$(r)Overall: $s") + return print(io, "$(r)Overall: $(s)") end # @@ -437,12 +416,9 @@ mutable struct DefaultStoppingCriterionState <: StoppingCriterionState DefaultStoppingCriterionState() = new(-1) end -initialize_state(::Problem, ::Algorithm, ::StopAfterIteration; kwargs...) = - DefaultStoppingCriterionState() +initialize_state(::Problem, ::Algorithm, ::StopAfterIteration; kwargs...) = DefaultStoppingCriterionState() function initialize_state!( - ::Problem, - ::Algorithm, - ::StopAfterIteration, + ::Problem, ::Algorithm, ::StopAfterIteration, stopping_criterion_state::DefaultStoppingCriterionState; kwargs..., ) @@ -452,18 +428,14 @@ end function is_finished( - ::Problem, - ::Algorithm, - state::State, + ::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, + ::Problem, ::Algorithm, state::State, stop_after_iteration::StopAfterIteration, stopping_criterion_state::DefaultStoppingCriterionState, ) @@ -492,7 +464,7 @@ function Base.summary( ) 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") + return print(io, "Max Iteration $(stop_after_iteration.max_iterations): $s") end """ @@ -549,9 +521,7 @@ initialize_state(::Problem, ::Algorithm, ::StopAfter; kwargs...) = StopAfterTimePeriodState() function initialize_state!( - ::Problem, - ::Algorithm, - ::StopAfter, + ::Problem, ::Algorithm, ::StopAfter, stopping_criterion_state::StopAfterTimePeriodState; kwargs..., ) @@ -562,22 +532,16 @@ function initialize_state!( end function is_finished( - ::Problem, - ::Algorithm, - state::State, - stop_after::StopAfter, - stop_after_state::StopAfterTimePeriodState, + ::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, + ::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 @@ -604,11 +568,10 @@ function get_reason( end function Base.summary( io::IO, - stop_after::StopAfter, - stopping_criterion_state::StopAfterTimePeriodState, + 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") + return print(io, "stopped after $(stop_after.threshold): $s") end indicates_convergence(stop_after::StopAfter) = false diff --git a/src/test_suite.jl b/src/test_suite.jl new file mode 100644 index 0000000..4788c6d --- /dev/null +++ b/src/test_suite.jl @@ -0,0 +1,18 @@ +""" + AlgorithmsInterface.Test + +The module `AlgorithmsInterface.Test` contains concrete (dummy) instances +to test prats of the interface. +""" +module Test +using ..AlgorithmsInterface + +struct DummyAlgorithm{S <: AlgorithmsInterface.StoppingCriterion} <: AlgorithmsInterface.Algorithm + stopping_criterion::S +end +struct DummyProblem <: AlgorithmsInterface.Problem end +mutable struct DummyState{S <: AlgorithmsInterface.StoppingCriterionState} <: AlgorithmsInterface.State + stopping_criterion_state::S + iteration::Int +end +end diff --git a/test/newton.jl b/test/newton.jl index a60cb90..fff9886 100644 --- a/test/newton.jl +++ b/test/newton.jl @@ -65,4 +65,7 @@ end solution2 = solve(problem, algorithm2) @test solution2 ≈ sqrt(a) @test abs(solution2 - sqrt(a)) < abs(solution1 - sqrt(a)) + state3 = initialize_state(problem, algorithm2) + solve!(problem, algorithm2, state3) + @test state3.iterate == solution2 end diff --git a/test/runtests.jl b/test/runtests.jl index 18b9b80..3a47f4f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,22 +1,24 @@ -using SafeTestsets +using SafeTestsets, Test # these have to be included here to make show tests behave using AlgorithmsInterface using Dates -@safetestset "Newton" begin - include("newton.jl") -end +@testset "AlgorithmsInterface.jl" begin + @safetestset "Newton" begin + include("newton.jl") + end -@safetestset "Stopping Criteria" begin - include("stopping_criterion.jl") -end + @safetestset "Stopping Criteria" begin + include("stopping_criterion.jl") + end -@safetestset "Logging Infrastructure" begin - include("logging.jl") -end + @safetestset "Logging Infrastructure" begin + include("logging.jl") + end -@safetestset "Aqua" begin - using AlgorithmsInterface, Aqua - Aqua.test_all(AlgorithmsInterface) + @safetestset "Aqua" begin + using AlgorithmsInterface, Aqua + Aqua.test_all(AlgorithmsInterface) + end end diff --git a/test/stopping_criterion.jl b/test/stopping_criterion.jl index 36d5a37..96399de 100644 --- a/test/stopping_criterion.jl +++ b/test/stopping_criterion.jl @@ -1,72 +1,129 @@ using Test using AlgorithmsInterface +using AlgorithmsInterface: Test as AIT 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() +problem = AIT.DummyProblem() @testset "StopAfterIteration" begin s1 = StopAfterIteration(2) @test s1 isa StoppingCriterion - @test string(s1) == "StopAfterIteration(2)" - - algorithm = DummyAlgorithm(s1) + @test repr(s1) == "StopAfterIteration(2)" + @test !indicates_convergence(s1) + algorithm = AIT.DummyAlgorithm(s1) s1_state = initialize_state(problem, algorithm, s1) - state_finished = DummyState(s1_state, 2) - state_not_finished = DummyState(s1_state, 1) + @test !indicates_convergence(s1, s1_state) + state_finished = AIT.DummyState(s1_state, 2) + alg_state = AIT.DummyState(s1_state, 1) @test is_finished(problem, algorithm, state_finished) - @test !is_finished(problem, algorithm, state_not_finished) + @test !is_finished(problem, algorithm, alg_state) + # Fake a stop: + s1_state.at_iteration = 2 + @test startswith(get_reason(s1, s1_state), "At iteration 2") + @test endswith(summary(s1, s1_state), " reached") end @testset "StopAfter" begin - s1 = StopAfter(Second(1)) + s1 = StopAfter(Nanosecond(7)) @test s1 isa StoppingCriterion - @test string(s1) == "StopAfter(Second(1))" + @test string(s1) == "StopAfter(Nanosecond(7))" + @test_throws ArgumentError StopAfter(Second(-1)) - algorithm = DummyAlgorithm(s1) + algorithm = AIT.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) + alg_state = AIT.DummyState(s1_state, 0) + # Iteration 0: Start timer + @test !is_finished!(problem, algorithm, alg_state) + @test !is_finished(problem, algorithm, alg_state) + @test isnothing(get_reason(s1, s1_state)) + # Fake stop + s1_state.time = Nanosecond(9) + alg_state.iteration = 2 + @test is_finished!(problem, algorithm, alg_state) + @test is_finished(problem, algorithm, alg_state) + @test startswith(get_reason(s1, s1_state), "After iteration 2") + @test endswith(summary(s1, s1_state), " reached") end @testset "StopWhenAll" begin - s1 = StopAfterIteration(2) & StopAfter(Second(1)) + c1 = StopAfterIteration(2) + c2 = StopAfter(Nanosecond(2)) + c3 = StopAfterIteration(3) + s1 = c1 & c2 + s1b = StopWhenAll([c1, c2]) + @test s1 == s1b @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) + "StopWhenAll with the Stopping Criteria:\n StopAfterIteration(2)\n StopAfter(Nanosecond(2))" + algorithm = AIT.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) + + s1_str = summary(s1, s1_state) + @test contains(s1_str, "Stop when _all_ ") + @test contains(s1_str, "Overall: not reached") + + @test isnothing(AlgorithmsInterface.get_reason(s1, s1_state)) + alg_state = AIT.DummyState(s1_state, 1) + @test !is_finished(problem, algorithm, alg_state) + # Fake start timer + s1_state.criteria_states[2].start = Nanosecond(time_ns()) + s1_state.criteria_states[2].time = Nanosecond(7) + # just time is not enough + @test !is_finished!(problem, algorithm, alg_state) + @test !is_finished(problem, algorithm, alg_state) + alg_state.iteration = 2 + # but now both are + @test is_finished!(problem, algorithm, alg_state) + @test !indicates_convergence(s1) + # check that reset works (a) check with modification + @test is_finished!(problem, algorithm, alg_state) + @test is_finished(problem, algorithm, alg_state) + @test startswith(get_reason(s1, s1_state), "At iteration 2") + @test alg_state.stopping_criterion_state.at_iteration > 0 + AlgorithmsInterface.initialize_state!(problem, algorithm, s1, s1_state) + @test s1_state.criteria_states[1].at_iteration == -1 + # Different constructors + s2 = c1 & c2 & c3 + @test s1 & c3 == s2 + @test c1 & (c2 & c3) == s2 + @test s1 & s2 isa StopWhenAll end @testset "StopWhenAny" begin - s1 = StopAfterIteration(2) | StopAfter(Second(1)) + c1 = StopAfterIteration(2) + c2 = StopAfter(Second(1)) + c3 = StopAfterIteration(3) + + s1 = c1 | c2 @test s1 isa StoppingCriterion + @test s1 == StopWhenAny([c1, c2]) @test sprint((io, x) -> show(io, MIME"text/plain"(), x), s1) == "StopWhenAny with the Stopping Criteria:\n StopAfterIteration(2)\n StopAfter(Second(1))" + @test !indicates_convergence(s1) - algorithm = DummyAlgorithm(s1) + algorithm = AIT.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_str = summary(s1, s1_state) + @test contains(s1_str, "Stop when _one_ ") + @test contains(s1_str, "Overall: not reached") + + @test isnothing(AlgorithmsInterface.get_reason(s1, s1_state)) + alg_state = AIT.DummyState(s1_state, 1) + @test !is_finished!(problem, algorithm, alg_state) + @test !is_finished(problem, algorithm, alg_state) 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) + @test is_finished(problem, algorithm, alg_state) + alg_state.iteration = 2 + @test is_finished(problem, algorithm, alg_state) + # check that reset works (a) check with modification + @test is_finished!(problem, algorithm, alg_state) + @test alg_state.stopping_criterion_state.at_iteration > 0 + AlgorithmsInterface.initialize_state!(problem, algorithm, s1, s1_state) + @test s1_state.criteria_states[1].at_iteration == -1 + # Different constructors + s2 = c1 | c2 | c3 + @test s1 | c3 == s2 + @test c1 | (c2 | c3) == s2 + @test s1 | s2 isa StopWhenAny end