From 89fd3d3824cc3cf83ea886620012c3ba5bd69ef6 Mon Sep 17 00:00:00 2001 From: Jean-Francois Baffier Date: Thu, 30 May 2024 15:47:11 +0900 Subject: [PATCH] CI and other small fixes (#120) * Update compat and CI (#115) * Fixes for a demo * Cleaning unused files. Added Aqua and other CI * spelling --- .JuliaFormatter.toml | 1 + .github/workflows/CompatHelper.yml | 37 +- .github/workflows/SpellCheck.yml | 13 + .github/workflows/TagBot.yml | 16 + .github/workflows/benchmark.yml | 26 - .github/workflows/ci.yml | 119 ++-- .github/workflows/register.yml | 16 + Project.toml | 15 +- benchmark/Project.toml | 4 - benchmark/benchmarks.jl | 33 -- docs/Project.toml | 8 - docs/make.jl | 54 -- docs/src/cbls.md | 6 - docs/src/constraints.md | 29 - docs/src/contributing.md | 0 docs/src/d_constraint.md | 34 -- docs/src/domain.md | 7 - docs/src/golomb.md | 10 - docs/src/icn.md | 58 -- docs/src/img/Golomb_Ruler-4.svg | 31 - docs/src/img/final_sudoku.svg | 879 ----------------------------- docs/src/img/sudoku3x3.png | Bin 79504 -> 0 bytes docs/src/index.md | 86 --- docs/src/internals.md | 11 - docs/src/mincut.md | 11 - docs/src/models.md | 6 - docs/src/objectives.md | 11 - docs/src/public.md | 11 - docs/src/quickstart.md | 69 --- docs/src/solving.md | 30 - docs/src/sudoku.md | 84 --- docs/src/variables.md | 22 - src/LocalSearchSolvers.jl | 2 +- src/constraint.jl | 4 +- src/model.jl | 65 +-- src/options.jl | 45 +- src/solver.jl | 52 +- src/solvers/lead.jl | 6 +- src/solvers/main.jl | 11 +- src/solvers/sub.jl | 3 +- src/state.jl | 6 +- src/strategies/move.jl | 1 + src/strategies/neighbor.jl | 1 + src/strategies/objective.jl | 1 + src/strategies/parallel.jl | 1 + src/strategies/perturbation.jl | 1 + src/strategies/portfolio.jl | 1 + src/strategies/restart.jl | 25 +- src/strategies/selection.jl | 1 + src/strategies/solution.jl | 1 + src/strategies/tabu.jl | 7 +- src/strategies/termination.jl | 1 + src/strategy.jl | 9 +- src/time_stamps.jl | 3 +- src/variable.jl | 4 +- test/Aqua.jl | 32 ++ test/Project.toml | 7 - test/TestItemRunner.jl | 3 + test/internal.jl | 15 +- test/raw_solver.jl | 66 ++- test/runtests.jl | 19 +- 61 files changed, 392 insertions(+), 1738 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 .github/workflows/SpellCheck.yml delete mode 100644 .github/workflows/benchmark.yml create mode 100644 .github/workflows/register.yml delete mode 100644 benchmark/Project.toml delete mode 100644 benchmark/benchmarks.jl delete mode 100644 docs/Project.toml delete mode 100644 docs/make.jl delete mode 100644 docs/src/cbls.md delete mode 100644 docs/src/constraints.md delete mode 100644 docs/src/contributing.md delete mode 100644 docs/src/d_constraint.md delete mode 100644 docs/src/domain.md delete mode 100644 docs/src/golomb.md delete mode 100644 docs/src/icn.md delete mode 100644 docs/src/img/Golomb_Ruler-4.svg delete mode 100644 docs/src/img/final_sudoku.svg delete mode 100644 docs/src/img/sudoku3x3.png delete mode 100644 docs/src/index.md delete mode 100644 docs/src/internals.md delete mode 100644 docs/src/mincut.md delete mode 100644 docs/src/models.md delete mode 100644 docs/src/objectives.md delete mode 100644 docs/src/public.md delete mode 100644 docs/src/quickstart.md delete mode 100644 docs/src/solving.md delete mode 100644 docs/src/sudoku.md delete mode 100644 docs/src/variables.md create mode 100644 test/Aqua.jl delete mode 100644 test/Project.toml create mode 100644 test/TestItemRunner.jl diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..453925c --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1 @@ +style = "sciml" \ No newline at end of file diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index cba9134..5577817 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,16 +1,43 @@ -name: CompatHelper on: schedule: - cron: 0 0 * * * workflow_dispatch: +permissions: + contents: write + pull-requests: write jobs: CompatHelper: runs-on: ubuntu-latest steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() + - name: Check if Julia is already available in the PATH + id: julia_in_path + run: which julia + continue-on-error: true + - name: Install Julia, but only if it is not already available in the PATH + uses: julia-actions/setup-julia@v1 + with: + version: "1" + arch: ${{ runner.arch }} + if: steps.julia_in_path.outcome != 'success' + - name: "Add the General registry via Git" + run: | + import Pkg + ENV["JULIA_PKG_SERVER"] = "" + Pkg.Registry.add("General") + shell: julia --color=yes {0} + - 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 }} - run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/SpellCheck.yml b/.github/workflows/SpellCheck.yml new file mode 100644 index 0000000..ed4fe17 --- /dev/null +++ b/.github/workflows/SpellCheck.yml @@ -0,0 +1,13 @@ +name: Spell Check + +on: [pull_request] + +jobs: + typos-check: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + - name: Check spelling + uses: crate-ci/typos@v1.18.0 diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index f49313b..0cd3114 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -4,6 +4,22 @@ on: types: - created workflow_dispatch: + inputs: + lookback: + default: "3" +permissions: + actions: read + checks: read + contents: write + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index 175cb07..0000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Run benchmarks - -on: - pull_request: - -jobs: - Benchmark: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@latest - with: - version: 1 - - uses: julia-actions/julia-buildpkg@latest - - name: Install dependencies - run: julia -e 'using Pkg; pkg"add PkgBenchmark BenchmarkCI@0.1"' - - name: Run benchmarks - run: julia -e 'using BenchmarkCI; BenchmarkCI.judge()' - - name: Print judgement - run: julia -e 'using BenchmarkCI; BenchmarkCI.displayjudgement()' - - name: Post results - run: julia -e 'using BenchmarkCI; BenchmarkCI.postjudge()' - - name: Push results - run: julia -e "using BenchmarkCI; BenchmarkCI.pushresult()" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b00881..5d37369 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,48 @@ name: CI on: + pull_request: + branches: + - main + - dev + paths-ignore: + - "docs/**" push: branches: - main - tags: '*' - pull_request: -concurrency: - # Skip intermediate builds: always. - # Cancel intermediate builds: only if it is a pull request build. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + paths-ignore: + - "docs/**" jobs: + formatter: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: [1] + julia-arch: [x86] + os: [ubuntu-latest] + steps: + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.julia-version }} + + - uses: actions/checkout@v4 + - name: Install JuliaFormatter and format + # This will use the latest version by default but you can set the version like so: + # + # julia -e 'using Pkg; Pkg.add(PackageSpec(name="JuliaFormatter", version="0.13.0"))' + run: | + julia -e 'using Pkg; Pkg.add(PackageSpec(name="JuliaFormatter", version="1.0.50"))' + julia -e 'using JuliaFormatter; format(".", verbose=true)' + - name: Format check + run: | + julia -e ' + out = Cmd(`git diff`) |> read |> String + if out == "" + exit(0) + else + @error "Some files have not been formatted !!!" + write(stdout, out) + exit(1) + end' test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} @@ -18,29 +50,34 @@ jobs: fail-fast: false matrix: version: - - "1.9" - - 'nightly' + - "1.8" + - "1" # automatically expands to the latest stable 1.x release of Julia + - nightly os: - ubuntu-latest - - macOS-latest - - windows-latest + threads: + - "2" arch: - x64 - x86 - threads: - - "2" - exclude: + include: + # test macOS and Windows with latest Julia only - os: macOS-latest - arch: x86 + arch: x64 + version: 1 + - os: windows-latest + arch: x64 + version: 1 - os: windows-latest arch: x86 + version: 1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 + - uses: actions/cache@v4 env: cache-name: cache-artifacts with: @@ -55,29 +92,29 @@ jobs: env: JULIA_NUM_THREADS: ${{ matrix.threads }} - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v4 with: file: lcov.info - threshold': 5% - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: julia-actions/setup-julia@v1 - with: - version: "1" - - run: | - julia --project=docs -e ' - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate()' - - run: | - julia --project=docs -e ' - using Documenter: doctest - using LocalSearchSolvers - doctest(LocalSearchSolvers)' - - run: julia --project=docs docs/make.jl - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + # docs: + # name: Documentation + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: julia-actions/setup-julia@v1 + # with: + # version: "1" + # - run: | + # julia --project=docs -e ' + # using Pkg + # Pkg.develop(PackageSpec(path=pwd())) + # Pkg.instantiate()' + # - run: | + # julia --project=docs -e ' + # using Documenter: DocMeta, doctest + # using Constraints + # DocMeta.setdocmeta!(Constraints, :DocTestSetup, :(using Constraints); recursive=true) + # doctest(Constraints)' + # - run: julia --project=docs docs/make.jl + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/register.yml b/.github/workflows/register.yml new file mode 100644 index 0000000..5b7cd3b --- /dev/null +++ b/.github/workflows/register.yml @@ -0,0 +1,16 @@ +name: Register Package +on: + workflow_dispatch: + inputs: + version: + description: Version to register or component to bump + required: true +jobs: + register: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: julia-actions/RegisterAction@latest + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Project.toml b/Project.toml index d8a2699..599b0dc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "LocalSearchSolvers" uuid = "2b10edaa-728d-4283-ac71-07e312d6ccf3" authors = ["Jean-Francois Baffier"] -version = "0.4.3" +version = "0.4.4" [deps] CompositionalNetworks = "4b67e4b5-442d-4ef5-b760-3f5df3a57537" @@ -13,19 +13,26 @@ Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" Lazy = "50d2b5c4-7a5e-59d5-8109-a42b560f39c0" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" +TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] CompositionalNetworks = "0.5" ConstraintDomains = "0.3" Constraints = "0.5" -Dictionaries = "0.3" +Dates = "1" +Dictionaries = "0.4" +Distributed = "1" JSON = "0.21" Lazy = "0.15" -OrderedCollections = "1.6" +OrderedCollections = "1" +TestItemRunner = "0.2" +TestItems = "0.1" julia = "1.6" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Aqua", "Test"] diff --git a/benchmark/Project.toml b/benchmark/Project.toml deleted file mode 100644 index 01b78e8..0000000 --- a/benchmark/Project.toml +++ /dev/null @@ -1,4 +0,0 @@ -[deps] -BenchmarkCI = "20533458-34a3-403d-a444-e18f38190b5b" -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -LocalSearchSolvers = "2b10edaa-728d-4283-ac71-07e312d6ccf3" diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl deleted file mode 100644 index f8d6ce3..0000000 --- a/benchmark/benchmarks.jl +++ /dev/null @@ -1,33 +0,0 @@ -using BenchmarkTools -using LocalSearchSolvers - -const suite = BenchmarkGroup() - -suite["adaptive"] = BenchmarkGroup(["integer", "discrete", "all different"]) - -# bench for the error functions and predicate of usual constraints -suite["constraints"] = BenchmarkGroup(["error", "predicate", "automatic", "handmade"]) -for c in [all_different] - for i in 0:10 - n = 2^i - values = rand(1:2n, n) - suite["constraints"][string(c), length(values)] = @benchmarkable $(c)($values...) - end -end - -# bench for the different problems modeling -suite["problems"] = BenchmarkGroup(["generation"]) -for p in [sudoku] - for size in 2:10 - suite["problems"][string(p), size] = @benchmarkable $(p)($size) - end -end - -## commands to store the tuning parameters -# tune!(suite) -# BenchmarkTools.save("benchmark/params.json", params(suite)); - -## syntax is loadparams!(group, paramsgroup, fields...) -# loadparams!(suite, BenchmarkTools.load("benchmark/params.json")[1], :evals, :samples); - -results = run(suite, verbose = true, seconds = 1) diff --git a/docs/Project.toml b/docs/Project.toml deleted file mode 100644 index 3eb27f8..0000000 --- a/docs/Project.toml +++ /dev/null @@ -1,8 +0,0 @@ -[deps] -CBLS = "a3809bfe-37bb-4d48-a667-bac4c6be8d90" -CompositionalNetworks = "4b67e4b5-442d-4ef5-b760-3f5df3a57537" -ConstraintDomains = "5800fd60-8556-4464-8d61-84ebf7a0bedb" -ConstraintModels = "841a6ec5-cac3-4c42-9a0a-4b21c9553698" -Constraints = "30f324ab-b02d-43f0-b619-e131c61659f7" -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -LocalSearchSolvers = "2b10edaa-728d-4283-ac71-07e312d6ccf3" diff --git a/docs/make.jl b/docs/make.jl deleted file mode 100644 index 2c7fab6..0000000 --- a/docs/make.jl +++ /dev/null @@ -1,54 +0,0 @@ -using LocalSearchSolvers -using CBLS -using ConstraintModels -using ConstraintDomains -using CompositionalNetworks -using Constraints -using Documenter - -@info "Makeing documentation..." -makedocs(; - modules=[LocalSearchSolvers, CBLS, ConstraintModels, - ConstraintDomains, CompositionalNetworks, Constraints], - expandfirst = ["variables.md", "constraints.md", "objectives.md", "solving.md"], - authors="Jean-François Baffier", - repo="https://github.com/JuliaConstraints/LocalSearchSolvers.jl/blob/{commit}{path}#L{line}", - sitename="LocalSearchSolvers.jl", - format=Documenter.HTML(; - prettyurls=get(ENV, "CI", nothing) == "true", - canonical="https://JuliaConstraints.github.io/LocalSearchSolvers.jl", - assets = ["assets/favicon.ico"; "assets/github_buttons.js"; "assets/custom.css"], - ), - pages=[ - "Home" => "index.md", - "Manual" => [ - "Quick Start Guide" => "quickstart.md", - "Variables" => "variables.md", - "Constraints" => "constraints.md", - "Objectives" => "objectives.md", - "Solving" => "solving.md", - ], - "Examples" => [ - "Sudoku" => "sudoku.md", - "Golomb ruler" => "golomb.md", - "Mincut" => "mincut.md", - ], - "Related" => [ - "ConstraintDomains.jl" => "domain.md", - "Constraints.jl" => "d_constraint.md", - "CompositionalNetworks.jl" => "icn.md", - "CBLS.jl" => "cbls.md", - "ConstraintModels.jl" => "models.md", - ], - "Library" => [ - "Public" => "public.md", - "Internals" => "internals.md", - ], - "Constributing" => "contributing.md", - ], -) - -deploydocs(; - repo="github.com/JuliaConstraints/LocalSearchSolvers.jl.git", - devbranch="main", -) diff --git a/docs/src/cbls.md b/docs/src/cbls.md deleted file mode 100644 index cf130c1..0000000 --- a/docs/src/cbls.md +++ /dev/null @@ -1,6 +0,0 @@ -# CBLS.jl - -```@autodocs -Modules = [CBLS] -Private = false -``` \ No newline at end of file diff --git a/docs/src/constraints.md b/docs/src/constraints.md deleted file mode 100644 index e87a8b5..0000000 --- a/docs/src/constraints.md +++ /dev/null @@ -1,29 +0,0 @@ -# Constraints - -In the `LocalSearchSolvers.jl` framework, a constraint can be define using either a *concept* (a predicate over a set of variables) or an *error function*. Additionally some constraints are already defined in [Constraints.jl](https://github.com/JuliaConstraints/Constraints.jl). - -As the recommended usage is through the `CBLS.jl` package and the `JuMP.jl` interface, we provide the related documentation here. - -## Predicates and Error Functions - -```@docs -CBLS.Predicate -CBLS.Error -``` - -Finally, one can compute the error function from a concept automatically using Interpretable Compositional Networks (ICN). Automatic computation through the [CompositionalNetworks.jl](https://github.com/JuliaConstraints/CompositionalNetworks.jl) package will soon be added within the JuMP syntax. In the mean time, please use this dependency directly. - -## Usual Constraints -Some usual constraints are already available directly through JuMP syntax. Do not hesitate to file an issue to include more usual constraints. - -```@docs -CBLS.AllDifferent -CBLS.AllEqual -CBLS.AllEqualParam -CBLS.AlwaysTrue -CBLS.DistDifferent -CBLS.Eq -CBLS.Ordered -``` - - diff --git a/docs/src/contributing.md b/docs/src/contributing.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/src/d_constraint.md b/docs/src/d_constraint.md deleted file mode 100644 index b464b02..0000000 --- a/docs/src/d_constraint.md +++ /dev/null @@ -1,34 +0,0 @@ -# Constraints.jl - -A back-end package for JuliaConstraints front packages, such as `LocalSearchSolvers.jl`. - -It provides the following features: -- A dictionary to store usual constraint: `usual_constraint`, which contains the following entries - - `:all_different` - - `:dist_different` - - `:eq`, `:all_equal`, `:all_equal_param` - - `:ordered` - - `:always_true` (mainly for testing default `Constraint()` constructor) -- For each constraint `c`, the following properties - - arguments length - - concept (predicate the variables compliance with `c`) - - error (a function that evaluate how much `c` is violated) - - parameters length - - known symmetries of `c` -- A learning function using `CompositionalNetworks.jl`. If no error function is given when instantiating `c`, it will check the existence of a composition related to `c` and set the error to it. - -Follow the list of the constraints currently stored in `usual_constraint`. Note that if the constraint is named `_my_constraint`, it can be accessed as `usual_constraint[:my_constraint]`. - -```@docs -Constraints.all_different -Constraints.all_equal -Constraints.all_equal_param -Constraints.dist_different -Constraints.eq -Constraints.ordered -``` - -```@autodocs -Modules = [Constraints] -Private = false -``` \ No newline at end of file diff --git a/docs/src/domain.md b/docs/src/domain.md deleted file mode 100644 index 5a70cbe..0000000 --- a/docs/src/domain.md +++ /dev/null @@ -1,7 +0,0 @@ -# ConstraintDomains.jl - -Currently discrete and continuous domains are supported using the following function. - -```@docs -ConstraintDomains.domain -``` \ No newline at end of file diff --git a/docs/src/golomb.md b/docs/src/golomb.md deleted file mode 100644 index 6cf32a1..0000000 --- a/docs/src/golomb.md +++ /dev/null @@ -1,10 +0,0 @@ -# Golomb ruler - -Doc is still in construction. Please check `golomb.jl` in `ConstraintModels.jl` for details on the implementation. -An extensive example is available as a quick-start guide to this package. - -## Constructing a Golomb ruler model - -```@docs -ConstraintModels.golomb -``` \ No newline at end of file diff --git a/docs/src/icn.md b/docs/src/icn.md deleted file mode 100644 index fb73739..0000000 --- a/docs/src/icn.md +++ /dev/null @@ -1,58 +0,0 @@ -# CompositionalNetworks.jl - -```@contents -Pages = ["public.md"] -Depth = 5 -``` - -`CompositionalNetworks.jl`, a Julia package for Interpretable Compositional Networks (ICN), a variant of neural networks, allowing the user to get interpretable results, unlike regular artificial neural networks. - -The current state of our ICN focuses on the composition of error functions for `LocalSearchSolvers.jl`, but produces results independently of it and export it to either/both Julia functions or/and human readable output. - -### How does it work? - -The package comes with a basic ICN for learning global constraints. The ICN is composed of 4 layers: `transformation`, `arithmetic`, `aggregation`, and `comparison`. Each contains several operations that can be composed in various ways. -Given a `concept` (a predicate over the variables' domains), a metric (`hamming` by default), and the variables' domains, we learn the binary weights of the ICN. - -## Installation - -```julia -] add CompositionalNetworks -``` - -As the package is in a beta version, some changes in the syntax and features are likely to occur. However, those changes should be minimal between minor versions. Please update with caution. - -## Quickstart - -```julia -# 4 variables in 1:4 -doms = [domain([1,2,3,4]) for i in 1:4] - -# allunique concept (that is used to define the :all_different constraint) -err = explore_learn_compose(allunique, domains=doms) -# > interpretation: identity ∘ count_positive ∘ sum ∘ count_eq_left - -# test our new error function -@assert err([1,2,3,3], dom_size = 4) > 0.0 - -# export an all_different function to file "current/path/test_dummy.jl" -compose_to_file!(icn, "all_different", "test_dummy.jl") -``` - -The output file should produces a function that can be used as follows (assuming the maximum domain size is `7`) - -```julia -import CompositionalNetworks - -all_different([1,2,3,4,5,6,7]; dom_size = 7) -# > 0.0 (which means true, no errors) -``` - -Please see `JuliaConstraints/Constraints.jl/learn.jl` for an extensive example of ICN learning and compositions. - -## Public interface - -```@autodocs -Modules = [CompositionalNetworks] -Private = false -``` diff --git a/docs/src/img/Golomb_Ruler-4.svg b/docs/src/img/Golomb_Ruler-4.svg deleted file mode 100644 index 90c17eb..0000000 --- a/docs/src/img/Golomb_Ruler-4.svg +++ /dev/null @@ -1,31 +0,0 @@ - - -Golomb Ruler - Order 4 - - - - - - - - - - - - - - - - - 0146 - - - 1 2 3 - 4 5 6 - - diff --git a/docs/src/img/final_sudoku.svg b/docs/src/img/final_sudoku.svg deleted file mode 100644 index e5512df..0000000 --- a/docs/src/img/final_sudoku.svg +++ /dev/null @@ -1,879 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - - - - 3 - - - - 4 - - - 6 - - - 7 - - - 8 - - - 9 - - - 1 - - - 2 - - 6 - - - 7 - - - 2 - - - - - - - - - - - - 1 - - - 9 - - - 5 - - - - 3 - - - 4 - - - 8 - - 1 - - - 9 - - - - 8 - - - - 3 - - - 4 - - - 2 - - - 5 - - - - 6 - - - - 7 - - 8 - - - - 5 - - - 9 - - - 7 - - - - 6 - - - - 1 - - - 4 - - - 2 - - - 3 - - 4 - - - - 2 - - - 6 - - - - 8 - - - - 5 - - - 3 - - - 7 - - - 9 - - - - 1 - - 7 - - - 1 - - - 3 - - - 9 - - - - 2 - - - - 4 - - - 8 - - - 5 - - - 6 - - 9 - - - - 6 - - - - 1 - - - 5 - - - 3 - - - 7 - - - - 2 - - - 8 - - - - 4 - - 2 - - - 8 - - - 7 - - - - 4 - - - 1 - - - 9 - - - - 6 - - - 3 - - - - 5 - - 3 - - - 4 - - - 5 - - - 2 - - - 8 - - - 6 - - - 1 - - - - 7 - - - - 9 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/src/img/sudoku3x3.png b/docs/src/img/sudoku3x3.png deleted file mode 100644 index f56772a2edf0cbb52f571fdca379d6fa281f6c0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79504 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 ~fk`f7Vd#8ig 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 JQY@t7{eK5TBt8HD diff --git a/docs/src/index.md b/docs/src/index.md deleted file mode 100644 index ce6830e..0000000 --- a/docs/src/index.md +++ /dev/null @@ -1,86 +0,0 @@ -```@meta -CurrentModule = LocalSearchSolvers -``` - -# Constraint-Based Local Search Framework - -The **LocalSearchSolvers.jl** framework proposes sets of technical components of Constraint-Based Local Search (CBLS) solvers and combine them in various ways. Make your own CBLS solver! - - - -A higher-level *JuMP* interface is available as [CBLS.jl](https://github.com/JuliaConstraints/CBLS.jl) and is the recommended way to use this package. A set of examples is available within [ConstraintModels.jl](https://github.com/JuliaConstraints/ConstraintModels.jl). - -![](img/sudoku3x3.png) - -### Dependencies - -This package makes use of several dependencies from the JuliaConstraints GitHub org: -- [ConstraintDomains.jl](https://github.com/JuliaConstraints/ConstraintDomains.jl): a domains back-end package for all JuliaConstraints front packages -- [Constraints.jl](https://github.com/JuliaConstraints/Constraints.jl): a constraints back-end package for all JuliaConstraints front packages -- [CompositionalNetworks.jl](https://github.com/JuliaConstraints/CompositionalNetworks.jl): a module to learn error functions automatically given a *concept* -- [Garamon.jl](https://github.com/JuliaConstraints/Garamon.jl) (incoming): geometrical constraints - -It also relies on great packages from the julialang ecosystem, among others, -- [ModernGraphs.jl](https://github.com/Humans-of-Julia/ModernGraphs.jl) (incoming): a dynamic multilayer framework for complex graphs which allows a fine exploration of entangled neighborhoods - -### Related packages -- [JuMP.jl](https://github.com/jump-dev/JuMP.jl): a rich interface for optimization solvers -- [CBLS.jl](https://github.com/JuliaConstraints/CBLS.jl): the actual interface with JuMP for `LocalSearchSolvers.jl` -- [ConstraintModels.jl](https://github.com/JuliaConstraints/ConstraintModels.jl): a dataset of models for Constraint Programming -- [COPInstances.jl](https://github.com/JuliaConstraints/COPInstances.jl) (incoming): a package to store, download, and generate combinatorial optimization instances - -### Features - -Wanted features list: -- **Strategies** - - [ ] *Move*: local move, permutation between `n` variables - - [ ] *Neighbor*: simple or multiplexed neighborhood, dimension/depth - - [ ] *Objective(s)*: single/multiple objectives, Pareto, etc. - - [ ] *Parallel*: distributed and multi-threaded, HPC clusters - - [ ] *Perturbation*: dynamic, restart, pool of solutions - - [ ] *Portfolio*: portfolio of solvers, partition in sub-problems - - [ ] *Restart* - - [x] restart sequence - - [ ] partial/probabilistic restart (in coordination with perturbation strategies) - - [ ] *Selection* of variables: roulette selection, multi-variables, meta-variables (cf subproblem) - - [ ] *Solution(s)*: management of pool, best versus diverse - - *Tabu* - - [x] No Tabu - - [x] Weak-tabu - - [x] Keen-tabu - - *Termination*: when, why, how, interactive, results storage (remote) -- **Featured strategies** - - [ ] Adaptive search - - [ ] Extremal optimization -- **Others** - - [ ] Resolution of problems - - [x] SATisfaction - - [x] OPTimisation (single-objective) - - [ ] OPTimisation (multiple-objective) - - [ ] Dynamic problems - - [ ] Domains - - [x] Discrete domains (any type of numbers) - - [x] Continuous domains - - [ ] Arbitrary Objects such as physical ones - - [ ] Domain Specific Languages (DSL) - - [x] Straight Julia `:raw` - - [x] JuMP*ish* | MathOptInterface.jl - - [ ] MiniZinc - - [ ] OR-tools ? - - [ ] Learning settings (To be incorporated in [MetaStrategist.jl](https://github.com/JuliaConstraints/MetaStrategist.jl)) - - [x] Compositional Networks (error functions, cost functions) - - [ ] Reinforcement learning for above mentioned learning features - - [ ] Automatic benchmarking and learning from all the possible parameter combination (instance, model, solver, size, restart, hardware, etc.) - -### Contributing - -Contributions to this package are more than welcome and can be arbitrarily, and not exhaustively, split as follows: -- All features mentioned above -- Adding new constraints and symmetries -- Adding new problems and instances -- Adding new ICNs to learn error of existing constraints -- Creating other compositional networks which target other kind of constraints -- Just making stuff better, faster, user-friendlier, etc. - -#### Contact -Do not hesitate to contact me (@azzaare) or other members of JuliaConstraints on GitHub (file an issue), the julialang [Discourse](https://discourse.julialang.org) forum, the julialang [Slack](https://julialang.org/slack/) workspace, the julialang [Zulip](https://julialang.zulipchat.com/) server (*Constraint Programming* stream), or the Humans of Julia [Humans-of-Julia](https://humansofjulia.org/) discord server(*julia-constraint* channel). diff --git a/docs/src/internals.md b/docs/src/internals.md deleted file mode 100644 index 95ee0c9..0000000 --- a/docs/src/internals.md +++ /dev/null @@ -1,11 +0,0 @@ -# Internal - -```@contents -Pages = ["internal.md"] -Depth = 5 -``` - -```@autodocs -Modules = [LocalSearchSolvers] -Public = false -``` diff --git a/docs/src/mincut.md b/docs/src/mincut.md deleted file mode 100644 index c946042..0000000 --- a/docs/src/mincut.md +++ /dev/null @@ -1,11 +0,0 @@ -# Mincut - -Doc is still in construction. Please check `mincut.jl` in `ConstraintModels.jl` for details on the implementation. - -## Constructing a Mincut model - -Note that the Interdiction Cut problem is NP-hard. - -```@docs -ConstraintModels.mincut -``` \ No newline at end of file diff --git a/docs/src/models.md b/docs/src/models.md deleted file mode 100644 index 4d690f1..0000000 --- a/docs/src/models.md +++ /dev/null @@ -1,6 +0,0 @@ -# ConstraintModels.jl - -```@autodocs -Modules = [ConstraintModels] -Private = false -``` \ No newline at end of file diff --git a/docs/src/objectives.md b/docs/src/objectives.md deleted file mode 100644 index 94f59ff..0000000 --- a/docs/src/objectives.md +++ /dev/null @@ -1,11 +0,0 @@ -# Objectives - -Once a satisfying solution has been reached, the solver will try to minimize the provided objective function if any. - -As the recommended usage is through the `CBLS.jl` package and the `JuMP.jl` interface, we provide the related documentation here. - -### JuMP syntax (recommended) - -```@docs -CBLS.ScalarFunction -``` diff --git a/docs/src/public.md b/docs/src/public.md deleted file mode 100644 index df793ca..0000000 --- a/docs/src/public.md +++ /dev/null @@ -1,11 +0,0 @@ -# Public - -```@contents -Pages = ["public.md"] -Depth = 5 -``` - -```@autodocs -Modules = [LocalSearchSolvers] -Private = false -``` diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md deleted file mode 100644 index 6d2f4f6..0000000 --- a/docs/src/quickstart.md +++ /dev/null @@ -1,69 +0,0 @@ -# Quick Start Guide -This section introduce the main concepts of `LocalSearchSolvers.jl`. We model both a satisfaction and an optimization version of the [Golomb Ruler](https://en.wikipedia.org/wiki/Golomb_ruler) problem. -For this quick-start, we will use [JuMP.jl](https://github.com/jump-dev/JuMP.jl). - -## Golomb Ruler -From Wikipedia's English page. -> In mathematics, a Golomb ruler is a set of marks at integer positions along an imaginary ruler such that no two pairs of marks are the same distance apart. The number of marks on the ruler is its order, and the largest distance between two of its marks is its length. Translation and reflection of a Golomb ruler are considered trivial, so the smallest mark is customarily put at 0 and the next mark at the smaller of its two possible values. - -![](img/Golomb_Ruler-4.svg) - -### Satisfaction version -Given a number of marks `n` and a ruler length `L`, we can model our problem in Julia as easily as follows. First create an empty problem. - - ```julia -using CBLS # the JuMP interface for LocalSearchSolvers.jl -using JuMP - -model = Model(CBLS.Optimizer) -``` - -Then add `n` variables with domain `0:L`. - -```julia -n = 4 # marks -L = n^2 # ruler length -@variable(model, X[1:n], DiscreteSet(0:L)) -``` - -Finally add the following constraints, -* all marks have a different value -* marks are ordered (optional) -* finally, no two pairs of marks are the same distance apart - -```julia -@constraint(model, X in AllDifferent()) # different marks -@constraint(model, X in Ordered()) # for output layout, keep them ordered - -# No two pairs have the same length -for i in 1:(n - 1), j in (i + 1):n, k in i:(n - 1), l in (k + 1):n - (i, j) < (k, l) || continue - @constraint(model, [X[i], X[j], X[k], X[l]] in DistDifferent()) -end -``` - -### Optimization version -A Golomb ruler can be either optimally dense (maximal `m` for a given `L`) or optimally short (minimal `L` for a given `n`). Until `LocalSearchSolvers.jl` implements dynamic problems, only optimal shortness is provided. - -The model objective is then to minimize the maximum distance between the two extrema marks in the ruler. As the domains are positive, we can simply minimize the maximum value. - -```julia -@objective(model, Min, ScalarFunction(maximum)) -``` - -### Ruling the solver -For either version, the solver is built and run in a similar way. Please note that the satisfaction one will stop if a solution is found. The other will run until the maximum number of iteration is reached (1000 by default). - -```julia -optimize!(model) -``` - -And finally retrieve the (best-known) solution info. - -```julia -result = value.(X) -@info "Golomb marks: $result" -``` - -Please note, that the Golomb Ruler is already implemented in the package as `golomb(n::Int, L::Int=n^2)`. - diff --git a/docs/src/solving.md b/docs/src/solving.md deleted file mode 100644 index 79f343e..0000000 --- a/docs/src/solving.md +++ /dev/null @@ -1,30 +0,0 @@ -# Modeling and solving - -Ideally, given a problem, one just want to model and solve. That is what *LocalSearchSolvers* is aiming for. Here we only provide JuMP syntax. - -## Model -```julia -using CBLS, JuMP - -model = Model(CBLS.Optimizer) # CBLS is an exported alias of LocalSearchSolvers - -# add variables (cf Variables section) -# add constraints (cf Constraints section) -# add objective (cf Objectives section) -``` - -## Solver - -```julia -# run the solver. If no objectives are provided, it will look for a satisfying solution and stop -optimize!(model) - -# extract the values (assuming X, a (collection of) variable(s) is the target) -solution = value.(X) -``` - -### Solver options - -```@docs -LocalSearchSolvers.Options -``` diff --git a/docs/src/sudoku.md b/docs/src/sudoku.md deleted file mode 100644 index b288d6b..0000000 --- a/docs/src/sudoku.md +++ /dev/null @@ -1,84 +0,0 @@ -# Sudoku - -From Wikipedia's English page. -> Sudoku is a logic-based, combinatorial number-placement puzzle. In classic sudoku, the objective is to fill a 9×9 grid with digits so that each column, each row, and each of the nine 3×3 subgrids that compose the grid contain all of the digits from 1 to 9. The puzzle setter provides a partially completed grid, which for a well-posed puzzle has a single solution. - -Each column, row, and region of the sudoku grid can only have a number from each of 1 to 9. - -For instance, given this initial grid: -```@raw html - -``` - -The final state (i.e. solution) must be: -```@raw html - -``` - -## Constructing a sudoku model - -```@docs -ConstraintModels.sudoku -``` -## Detailed implementation - -To start modeling with Sudoku with the solver, we will use [JuMP.jl](https://github.com/jump-dev/JuMP.jl) syntax. - -* First, create a model with `JuMP.Model(CBLS.Optimizer)`. Given `n = 3` the grid will be of size `n^2 by n^2` (i.e. 9×9) - - ```julia -using CBLS # the JuMP interface of LocalSearchSolvers.jl -using JuMP - -N = n^2 -model = JuMP.Model(CBLS.Optimizer) -``` - -* Create a matrix of variables, where each variable represents a cell of the Sudoku, this means that every variable must be an integer between 1 and 9. -(If initial values are provided, the variables representing the known values take will be constant variables, and the rest of the unknown variables are initialized as integers between 1 and 9) - - ```julia -# Create and initialize variables. -if isnothing(start) # If no initial configuration is provided - @variable(m, X[1:N, 1:N], DiscreteSet(1:N)) # Create a matrix of N*N variables with values from 1 to N -else - @variable(m, X[1:N, 1:N]) # Create a matrix of N*N variables with no value taken yet - for i in 1:N, j in 1:N # Iterate through the matrix - v_ij = start[i,j] # Retrieve the value of the current cell - if 1 ≤ v_ij ≤ N # If the value of the current cell is a number between 1 and N (i.e. already provided by the initial configuration) - # Create a constraint forcing the variable representing the current cell to be a constant equal to the value provided by the initial configuration - @constraint(m, X[i,j] in DiscreteSet(v_ij)) - else - @constraint(m, X[i,j] in DiscreteSet(1:N)) # Else create a constraint stating that the variable must be between 1 and N - end - end -end -``` - -* Define the rows, columns and block constraints. The solver has a Global Constraint `AllDifferent()` stating that a set of variables must have different values. - - ```julia -for i in 1:N - @constraint(m, X[i,:] in AllDifferent()) # All variables on the same row must be different - @constraint(m, X[:,i] in AllDifferent()) # All variables on the same column must be different -end -for i in 0:(n-1), j in 0:(n-1) - @constraint(m, vec(X[(i*n+1):(n*(i+1)), (j*n+1):(n*(j+1))]) in AllDifferent()) # All variables on the same block must be different -end - ``` - -* Finally, solve model using the `optimize!()` function with the model in arguments * - ```julia -# Run the solver -optimize!(m) -``` - -After model is solved, use `value.(grid)` to get the final value of all variables on the grid -matrix, and display the solution using the `display()` function - -```julia -# Retrieve and display the values -solution = value.(grid) -display(solution, Val(:sudoku)) -``` - diff --git a/docs/src/variables.md b/docs/src/variables.md deleted file mode 100644 index 275ab36..0000000 --- a/docs/src/variables.md +++ /dev/null @@ -1,22 +0,0 @@ -# Variables - -## Domains - -In the `LocalSearchSolvers.jl` framework, a variable is mainly defined by its domain. A domain can be *continuous*, *discrete*, or *mixed*. All the domain implementation is available at [ConstraintDomains.jl](https://github.com/JuliaConstraints/ConstraintDomains.jl). - -Currently, only discrete domains are available. - -Domains can be used both statically or dynamically. - -### JuMP syntax (recommended) - -```julia -# free variable named x -@variable(model, x) - -# free variables in a X vector -@variable(model, X[1:5]) - -# variables with discrete domain 1:9 in a matrix M -@variable(model, M[1:9,1:9] in DiscreteSet(1:9)) -``` diff --git a/src/LocalSearchSolvers.jl b/src/LocalSearchSolvers.jl index 9214b99..6ae8493 100644 --- a/src/LocalSearchSolvers.jl +++ b/src/LocalSearchSolvers.jl @@ -52,7 +52,7 @@ include("strategies/objective.jl") include("strategies/parallel.jl") include("strategies/perturbation.jl") include("strategies/portfolio.jl") -include("strategies/tabu.jl") # preceed restart.jl +include("strategies/tabu.jl") # precede restart.jl include("strategies/restart.jl") include("strategies/selection.jl") include("strategies/solution.jl") diff --git a/src/constraint.jl b/src/constraint.jl index 28b914d..6dc941d 100644 --- a/src/constraint.jl +++ b/src/constraint.jl @@ -56,7 +56,7 @@ function constraint(f, vars) g = f if !b1 || b2 - g = (x; X=nothing) -> f(x) + g = (x; X = nothing) -> f(x) end - return Constraint(g, collect(Int == Int32 ? map(Int,vars) : vars)) + return Constraint(g, collect(Int == Int32 ? map(Int, vars) : vars)) end diff --git a/src/model.jl b/src/model.jl index fa704b4..9244ab6 100644 --- a/src/model.jl +++ b/src/model.jl @@ -20,10 +20,11 @@ struct _Model{V <: Variable{<:AbstractDomain},C <: Constraint{<:Function},O <: O end ``` """ -struct _Model{V <: Variable{<:AbstractDomain},C <: Constraint{<:Function},O <: Objective{<:Function}}# <: MOI.ModelLike - variables::Dictionary{Int,V} - constraints::Dictionary{Int,C} - objectives::Dictionary{Int,O} +struct _Model{V <: Variable{<:AbstractDomain}, + C <: Constraint{<:Function}, O <: Objective{<:Function}}# <: MOI.ModelLike + variables::Dictionary{Int, V} + constraints::Dictionary{Int, C} + objectives::Dictionary{Int, O} # counter to add new variables: vars, cons, objs max_vars::Ref{Int} # TODO: UInt ? @@ -40,7 +41,7 @@ struct _Model{V <: Variable{<:AbstractDomain},C <: Constraint{<:Function},O <: O kind::Symbol # Best known bound - best_bound::Union{Nothing,Float64} + best_bound::Union{Nothing, Float64} # Time of construction (seconds) since epoch time_stamp::Float64 @@ -50,18 +51,17 @@ end model() Construct a _Model, empty by default. It is recommended to add the constraints, variables, and objectives from an empty _Model. The following keyword arguments are available, - `vars=Dictionary{Int,Variable}()`: collection of variables -- `cons=Dictionary{Int,Constraint}()`: collection of cosntraints +- `cons=Dictionary{Int,Constraint}()`: collection of constraints - `objs=Dictionary{Int,Objective}()`: collection of objectives - `kind=:generic`: the kind of problem modeled (useful for specialized methods such as pretty printing) """ function model(; - vars=Dictionary{Int,Variable}(), - cons=Dictionary{Int,Constraint}(), - objs=Dictionary{Int,Objective}(), - kind=:generic, - best_bound=nothing, + vars = Dictionary{Int, Variable}(), + cons = Dictionary{Int, Constraint}(), + objs = Dictionary{Int, Objective}(), + kind = :generic, + best_bound = nothing ) - max_vars = Ref(zero(Int)) max_cons = Ref(zero(Int)) max_objs = Ref(zero(Int)) @@ -69,7 +69,8 @@ function model(; specialized = Ref(false) - _Model(vars, cons, objs, max_vars, max_cons, max_objs, sense, specialized, kind, best_bound, time()) + _Model(vars, cons, objs, max_vars, max_cons, max_objs, + sense, specialized, kind, best_bound, time()) end """ @@ -278,7 +279,7 @@ end variable!(m::M, d) where M <: Union{Model, AbstractSolver} Add a variable with domain `d` to `m`. """ -function variable!(m::_Model, d=domain()) +function variable!(m::_Model, d = domain()) add!(m, variable(d)) return _max_vars(m) end @@ -310,24 +311,24 @@ function describe(m::_Model) # TODO: rewrite _describe objectives = "Constraint Satisfaction Program (CSP)" else objectives = "Constraint Optimization Program (COP) with Objective(s)\n" - objectives *= - mapreduce(o -> "\t\t" * o.name * "\n", *, get_objectives(m); init="")[1:end - 1] + objectives *= mapreduce( + o -> "\t\t" * o.name * "\n", *, get_objectives(m); init = "")[1:(end - 1)] end variables = mapreduce( x -> "\t\tx$(x[1]): " * string(get_domain(x[2])) * "\n", - *, pairs(m.variables); init="" - )[1:end - 1] - constraints = mapreduce(c -> "\t\tc$(c[1]): " * string(c[2].vars) * "\n", *, pairs(m.constraints); init="")[1:end - 1] - - str = - """ - _Model description - $objectives - Variables: $(length(m.variables)) - $variables - Constraints: $(length(m.constraints)) - $constraints - """ + *, pairs(m.variables); init = "" + )[1:(end - 1)] + constraints = mapreduce(c -> "\t\tc$(c[1]): " * string(c[2].vars) * "\n", + *, pairs(m.constraints); init = "")[1:(end - 1)] + + str = """ + _Model description + $objectives + Variables: $(length(m.variables)) + $variables + Constraints: $(length(m.constraints)) + $constraints + """ return str end @@ -457,13 +458,13 @@ function compute_costs(m, values, X) return sum(c -> compute_cost(c, values, X), get_constraints(m); init = 0.0) end function compute_costs(m, values, cons, X) - return sum(c -> compute_cost(c, values, X), view(get_constraints(m), cons); init = 0.0) + return sum(c -> compute_cost(c, values, X), view(get_constraints(m), cons); init = 0.0) end compute_objective(m, values; objective = 1) = apply(get_objective(m, objective), values) function update_domain!(m, x, d) - if isempty(get_variable(m,x)) + if isempty(get_variable(m, x)) old_d = get_variable(m, x).domain _set_domain!(m, x, d.domain) else @@ -472,7 +473,7 @@ function update_domain!(m, x, d) new_d = if are_continuous intersect_domains(old_d, d) else - intersect_domains(convert(RangeDomain,old_d), convert(RangeDomain, d)) + intersect_domains(convert(RangeDomain, old_d), convert(RangeDomain, d)) end _set_domain!(m, x, new_d) end diff --git a/src/options.jl b/src/options.jl index 9e77862..ef28c79 100644 --- a/src/options.jl +++ b/src/options.jl @@ -2,13 +2,13 @@ const print_levels = Dict( :silent => 0, :minimal => 1, :partial => 2, - :verbose => 3, + :verbose => 3 ) - # # Tabu times - # get!(s, :tabu_time, length_vars(s) ÷ 2) # 10? - # get!(s, :local_tabu, setting(s, :tabu_time) ÷ 2) - # get!(s, :δ_tabu, setting(s, :tabu_time) - setting(s, :local_tabu))# 20-30 +# # Tabu times +# get!(s, :tabu_time, length_vars(s) ÷ 2) # 10? +# get!(s, :local_tabu, setting(s, :tabu_time) ÷ 2) +# get!(s, :δ_tabu, setting(s, :tabu_time) - setting(s, :local_tabu))# 20-30 """ Options() @@ -36,7 +36,7 @@ set_time_limit_sec(model, 5.0) mutable struct Options dynamic::Bool info_path::String - iteration::Union{Int,Float64} + iteration::Union{Int, Float64} print_level::Symbol solutions::Int specialize::Bool @@ -47,28 +47,28 @@ mutable struct Options time_limit::Float64 # seconds function Options(; - dynamic=false, - info_path="", - iteration=10000, - print_level=:minimal, - solutions=1, - specialize=!dynamic, - tabu_time=0, - tabu_local=0, - tabu_delta=0.0, - threads=typemax(0), - time_limit= 60, # seconds + dynamic = false, + info_path = "", + iteration = 10000, + print_level = :minimal, + solutions = 1, + specialize = !dynamic, + tabu_time = 0, + tabu_local = 0, + tabu_delta = 0.0, + threads = typemax(0), + time_limit = 60 # seconds ) ds_str = "The model types are specialized to the starting domains, constraints," * - " and objectives types. Dynamic elements that add a new type will raise an error!" + " and objectives types. Dynamic elements that add a new type will raise an error!" dynamic && specialize && @warn ds_str notds_str = "The solver types are not specialized in a static model context," * - " which is sub-optimal." + " which is sub-optimal." !dynamic && !specialize && @info notds_str itertime_str = "Both iteration and time limits are disabled. " * - "Optimization runs will run infinitely." + "Optimization runs will run infinitely." iteration == Inf && time_limit == Inf && @warn itertime_str new( @@ -82,7 +82,7 @@ mutable struct Options tabu_local, tabu_delta, threads, - time_limit, + time_limit ) end end @@ -250,11 +250,10 @@ DOCSTRING """ _time_limit!(options, time) = options.time_limit = time - function set_option!(options, name, value) eval(Symbol("_" * name * "!"))(options, value) end function get_option(options, name) eval(Symbol("_" * name))(options) -end \ No newline at end of file +end diff --git a/src/solver.jl b/src/solver.jl index 9075d64..312d416 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -9,7 +9,8 @@ add_time!(::AbstractSolver, i) = nothing function solver(ms, id, role; pool = pool(), strats = MetaStrategy(ms)) mlid = make_id(meta_id(ms), id, Val(role)) - return solver(mlid, ms.model, ms.options, pool, ms.rc_report, ms.rc_sol, ms.rc_stop, strats, Val(role)) + return solver(mlid, ms.model, ms.options, pool, ms.rc_report, + ms.rc_sol, ms.rc_stop, strats, Val(role)) end # Forwards from model field @@ -134,7 +135,7 @@ end Compute the cost of constraints `c` in `cons_lst`. If `cons_lst` is empty, compute the cost for all the constraints in `s`. """ -function _compute_costs!(s; cons_lst=Indices{Int}()) +function _compute_costs!(s; cons_lst = Indices{Int}()) if isempty(cons_lst) foreach(((id, c),) -> _compute_cost!(s, id, c), pairs(get_constraints(s))) else @@ -159,7 +160,7 @@ function _compute_objective!(s, o::Objective) s.pool = pool(s.state.configuration) end end -_compute_objective!(s, o=1) = _compute_objective!(s, get_objective(s, o)) +_compute_objective!(s, o = 1) = _compute_objective!(s, get_objective(s, o)) """ _compute!(s; o::Int = 1, cons_lst = Indices{Int}()) @@ -171,7 +172,7 @@ Compute the objective `o`'s value if `s` is satisfied and return the current `er - `o`: targeted objective - `cons_lst`: list of targeted constraints, if empty compute for the whole set """ -function _compute!(s; o::Int=1, cons_lst=Indices{Int}()) +function _compute!(s; o::Int = 1, cons_lst = Indices{Int}()) _compute_costs!(s; cons_lst) if get_error(s) == 0.0 _optimizing(s) && _compute_objective!(s, o) @@ -194,15 +195,17 @@ DOCSTRING function _neighbours(s, x, dim = 0) if dim == 0 is_discrete = typeof(get_variable(s, x).domain) <: ContinuousDomain - return is_discrete ? map(_ -> draw(s, x), 1:(length_vars(s)*length_cons(s))) : get_domain(s, x) + return is_discrete ? map(_ -> draw(s, x), 1:(length_vars(s) * length_cons(s))) : + get_domain(s, x) else neighbours = Set{Int}() foreach( - c -> foreach(y -> - begin + c -> foreach( + y -> begin b = _value(s, x) ∈ get_variable(s, y) && _value(s, y) ∈ get_variable(s, x) b && push!(neighbours, y) - end, get_vars_from_cons(s, c)), + end, + get_vars_from_cons(s, c)), get_cons_from_var(s, x) ) return delete!(neighbours, x) @@ -219,21 +222,26 @@ function _init!(s, ::Val{:global}) put!(s.rc_stop, nothing) foreach(i -> put!(s.rc_report, nothing), setdiff(workers(), [1])) end -_init!(s, ::Val{:meta}) = foreach(id -> push!(s.subs, solver(s, id-1, :sub)), 2:nthreads()) +function _init!(s, ::Val{:meta}) + foreach(id -> push!(s.subs, solver(s, id - 1, :sub)), 2:nthreads()) +end function _init!(s, ::Val{:remote}) for w in setdiff(workers(), [1]) ls = remotecall(solver, w, s, w, :lead) remote_do(set_option!, w, fetch(ls), "print_level", :silent) - remote_do(set_option!, w, fetch(ls), "threads",remotecall_fetch(Threads.nthreads, w)) + remote_do( + set_option!, w, fetch(ls), "threads", remotecall_fetch(Threads.nthreads, w)) push!(s.remotes, w => ls) end end function _init!(s, ::Val{:local}; pool = pool()) get_option(s, "tabu_time") == 0 && set_option!(s, "tabu_time", length_vars(s) ÷ 2) # 10? - get_option(s, "tabu_local") == 0 && set_option!(s, "tabu_local", get_option(s, "tabu_time") ÷ 2) - get_option(s, "tabu_delta") == 0 && set_option!(s, "tabu_delta", get_option(s, "tabu_time") - get_option(s, "tabu_local")) # 20-30 + get_option(s, "tabu_local") == 0 && + set_option!(s, "tabu_local", get_option(s, "tabu_time") ÷ 2) + get_option(s, "tabu_delta") == 0 && set_option!( + s, "tabu_delta", get_option(s, "tabu_time") - get_option(s, "tabu_local")) # 20-30 state!(s) return has_solution(s) end @@ -245,7 +253,7 @@ _init!(s, role::Symbol) = _init!(s, Val(role)) Restart a solver. """ -function _restart!(s, k=10) +function _restart!(s, k = 10) _verbose(s, "\n============== RESTART!!!!================\n") _draw!(s) empty_tabu!(s) @@ -274,7 +282,6 @@ function _select_worse(s) return _find_rand_argmax(view(_vars_costs(s), nontabu)) end - """ _move!(s, x::Int, dim::Int = 0) @@ -285,8 +292,11 @@ Perform an improving move in `x` neighbourhood if possible. - `x`: selected variable id - `dim`: describe the dimension of the considered neighbourhood """ -function _move!(s, x::Int, dim::Int=0) - best_values = [begin old_v = _value(s, x) end]; best_swap = [x] +function _move!(s, x::Int, dim::Int = 0) + best_values = [begin + old_v = _value(s, x) + end] + best_swap = [x] tabu = true # unless proved otherwise, this variable is now tabu best_cost = old_cost = get_error(s) copy_to!(s.state.fluct, _cons_costs(s), _vars_costs(s)) @@ -297,7 +307,7 @@ function _move!(s, x::Int, dim::Int=0) _verbose(s, "Compute costs: selected var(s) x_$x " * (dim == 0 ? "= $v" : "⇆ x_$v")) cons_x_v = union(get_cons_from_var(s, x), dim == 0 ? [] : get_cons_from_var(s, v)) - _compute!(s, cons_lst=cons_x_v) + _compute!(s, cons_lst = cons_x_v) cost = get_error(s) if cost < best_cost @@ -343,7 +353,7 @@ function _step!(s) _, best_swap, tabu = _move!(s, x, 1) _compute!(s) else # compute the costs changes from best local move - _compute!(s; cons_lst=get_cons_from_var(s, x)) + _compute!(s; cons_lst = get_cons_from_var(s, x)) end # decay tabu list @@ -399,7 +409,8 @@ Search the space of configurations. function solve_while_loop!(s, stop, sat, iter, st) while stop_while_loop(s, stop, iter, st) iter += 1 - _verbose(s, "\n\tLoop $(iter) ($(_optimizing(s) ? "optimization" : "satisfaction"))") + _verbose( + s, "\n\tLoop $(iter) ($(_optimizing(s) ? "optimization" : "satisfaction"))") _step!(s) && sat && break _verbose(s, "vals: $(length(_values(s)) > 0 ? _values(s) : nothing)") best_sub = _check_subs(s) @@ -411,7 +422,6 @@ function solve_while_loop!(s, stop, sat, iter, st) end end - """ remote_dispatch!(solver) Starts the `LeadSolver`s attached to the `MainSolver`. @@ -439,7 +449,7 @@ remote_stop!(::AbstractSolver) = nothing """ post_process(s::MainSolver) -Launch a serie of tasks to round-up a solving run, for instance, export a run's info. +Launch a series of tasks to round-up a solving run, for instance, export a run's info. """ post_process(::AbstractSolver) = nothing diff --git a/src/solvers/lead.jl b/src/solvers/lead.jl index 2de1ab3..020d1b4 100644 --- a/src/solvers/lead.jl +++ b/src/solvers/lead.jl @@ -15,11 +15,13 @@ mutable struct LeadSolver <: MetaSolver subs::Vector{_SubSolver} end -function solver(mlid, model, options, pool, rc_report, rc_sol, rc_stop, strats, ::Val{:lead}) +function solver( + mlid, model, options, pool, rc_report, rc_sol, rc_stop, strats, ::Val{:lead}) l_options = deepcopy(options) set_option!(options, "print_level", :silent) ss = Vector{_SubSolver}() - return LeadSolver(mlid, model, l_options, pool, rc_report, rc_sol, rc_stop, state(), strats, ss) + return LeadSolver( + mlid, model, l_options, pool, rc_report, rc_sol, rc_stop, state(), strats, ss) end function _init!(s::LeadSolver) diff --git a/src/solvers/main.jl b/src/solvers/main.jl index 15dbae5..ac3536e 100644 --- a/src/solvers/main.jl +++ b/src/solvers/main.jl @@ -28,9 +28,9 @@ end make_id(::Int, id, ::Val{:lead}) = (id, 0) function solver(model = model(); - options = Options(), - pool = pool(), - strategies = MetaStrategy(model), + options = Options(), + pool = pool(), + strategies = MetaStrategy(model) ) mlid = (1, 0) rc_report = RemoteChannel(() -> Channel{Nothing}(length(workers()))) @@ -39,7 +39,8 @@ function solver(model = model(); remotes = Dict{Int, Future}() subs = Vector{_SubSolver}() ts = TimeStamps(model) - return MainSolver(mlid, model, options, pool, rc_report, rc_sol, rc_stop, remotes, state(), :not_called, strategies, subs, ts) + return MainSolver(mlid, model, options, pool, rc_report, rc_sol, rc_stop, + remotes, state(), :not_called, strategies, subs, ts) end # Forwards from TimeStamps @@ -104,7 +105,7 @@ function post_process(s::MainSolver) info = Dict( :solution => has_solution(s) ? collect(best_values(s)) : nothing, :time => time_info(s), - :type => sat ? "Satisfaction" : "Optimization", + :type => sat ? "Satisfaction" : "Optimization" ) !sat && has_solution(s) && push!(info, :value => best_value(s)) write(path, JSON.json(info)) diff --git a/src/solvers/sub.jl b/src/solvers/sub.jl index 70f17d5..b6c883f 100644 --- a/src/solvers/sub.jl +++ b/src/solvers/sub.jl @@ -18,7 +18,8 @@ mutable struct _SubSolver <: AbstractSolver strategies::MetaStrategy end -function solver(mlid, model, options, pool, ::RemoteChannel, ::RemoteChannel, ::RemoteChannel, strats, ::Val{:sub}) +function solver(mlid, model, options, pool, ::RemoteChannel, + ::RemoteChannel, ::RemoteChannel, strats, ::Val{:sub}) sub_options = deepcopy(options) set_option!(options, "print_level", :silent) return _SubSolver(mlid, model, sub_options, pool, state(), strats) diff --git a/src/state.jl b/src/state.jl index 664309d..fbcd641 100644 --- a/src/state.jl +++ b/src/state.jl @@ -35,9 +35,9 @@ function state(m::_Model, pool = pool(); opt = false) X = Matrix{Float64}(undef, m.max_vars[], CompositionalNetworks.max_icn_length()) lc, lv = length_cons(m) > 0, length_vars(m) > 0 config = Configuration(m, X) - cons = lc ? zeros(Float64, get_constraints(m)) : Dictionary{Int,Float64}() + cons = lc ? zeros(Float64, get_constraints(m)) : Dictionary{Int, Float64}() last_improvement = 0 - vars = lv ? zeros(Float64, get_variables(m)) : Dictionary{Int,Float64}() + vars = lv ? zeros(Float64, get_variables(m)) : Dictionary{Int, Float64}() fluct = Fluct(cons, vars) return _State(config, cons, fluct, X, opt, last_improvement, vars) end @@ -82,7 +82,7 @@ _vars_costs!(s::_State, costs) = s.vars_costs = costs _values!(s::S, values) where S <: Union{_State, AbstractSolver} Set the variables values. """ -_values!(s::_State{T}, values) where T <: Number = set_values!(s, values) +_values!(s::_State{T}, values) where {T <: Number} = set_values!(s, values) """ _optimizing!(s::S) where S <: Union{_State, AbstractSolver} diff --git a/src/strategies/move.jl b/src/strategies/move.jl index e69de29..8b13789 100644 --- a/src/strategies/move.jl +++ b/src/strategies/move.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/neighbor.jl b/src/strategies/neighbor.jl index e69de29..8b13789 100644 --- a/src/strategies/neighbor.jl +++ b/src/strategies/neighbor.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/objective.jl b/src/strategies/objective.jl index e69de29..8b13789 100644 --- a/src/strategies/objective.jl +++ b/src/strategies/objective.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/parallel.jl b/src/strategies/parallel.jl index e69de29..8b13789 100644 --- a/src/strategies/parallel.jl +++ b/src/strategies/parallel.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/perturbation.jl b/src/strategies/perturbation.jl index e69de29..8b13789 100644 --- a/src/strategies/perturbation.jl +++ b/src/strategies/perturbation.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/portfolio.jl b/src/strategies/portfolio.jl index e69de29..8b13789 100644 --- a/src/strategies/portfolio.jl +++ b/src/strategies/portfolio.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/restart.jl b/src/strategies/restart.jl index 2eb850d..d206f38 100644 --- a/src/strategies/restart.jl +++ b/src/strategies/restart.jl @@ -1,5 +1,18 @@ abstract type RestartStrategy end +# Random restart +struct RandomRestart <: RestartStrategy + reset_percentage::Float64 +end + +function restart(::Any, ::Val{:random}; rp = 0.05) + return RandomRestart(rp) +end + +function check_restart!(rs::RandomRestart; tabu_length = nothing) + return rand() ≤ rs.reset_percentage +end + # Tabu restart mutable struct TabuRestart <: RestartStrategy index::Int @@ -8,9 +21,9 @@ mutable struct TabuRestart <: RestartStrategy reset_percentage::Float64 end -function restart(tabu_strat,::Val{:tabu}; rp = 1.0, index = 1) - limit = tenure(tabu_strat, :tabu) - tenure(tabu_strat, :pick) - return TabuRestart(index, tenure(tabu_strat, :tabu), limit, rp) +function restart(strategy, ::Val{:tabu}; rp = 1.0, index = 1) + limit = tenure(strategy, :tabu) - tenure(strategy, :pick) + return TabuRestart(index, tenure(strategy, :tabu), limit, rp) end function check_restart!(rs::TabuRestart; tabu_length) @@ -55,12 +68,12 @@ end ## Universal restart sequence function oeis(n, b, ::Val{:A082850}) - m = log(b,n+1) + m = log(b, n + 1) return isinteger(m) ? Int(m) : oeis(n - (b^floor(m) - 1), :A082850) end -oeis(n, b, ::Val{:A182105}) = b^(oeis(n, :A082850)-1) +oeis(n, b, ::Val{:A182105}) = b^(oeis(n, :A082850) - 1) oeis(n, ref::Symbol, b = 2) = oeis(n, b, Val(ref)) restart(::Any, ::Val{:universal}) = RestartSequence(n -> oeis(n, :A182105)) # Generic restart constructor -restart(tabu, strategy::Symbol) = restart(tabu, Val(strategy)) \ No newline at end of file +restart(tabu, strategy::Symbol) = restart(tabu, Val(strategy)) diff --git a/src/strategies/selection.jl b/src/strategies/selection.jl index e69de29..8b13789 100644 --- a/src/strategies/selection.jl +++ b/src/strategies/selection.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/solution.jl b/src/strategies/solution.jl index e69de29..8b13789 100644 --- a/src/strategies/solution.jl +++ b/src/strategies/solution.jl @@ -0,0 +1 @@ + diff --git a/src/strategies/tabu.jl b/src/strategies/tabu.jl index 37e1d17..502d437 100644 --- a/src/strategies/tabu.jl +++ b/src/strategies/tabu.jl @@ -4,13 +4,13 @@ struct NoTabu <: TabuStrategy end struct KeenTabu <: TabuStrategy tabu_tenure::Int - tabu_list::Dictionary{Int,Int} + tabu_list::Dictionary{Int, Int} end struct WeakTabu <: TabuStrategy tabu_tenure::Int pick_tenure::Int - tabu_list::Dictionary{Int,Int} + tabu_list::Dictionary{Int, Int} end tabu() = NoTabu() @@ -74,7 +74,6 @@ insert_tabu!(ts::KeenTabu, x, kind::Symbol) = insert_tabu!(ts, x, Val(kind)) insert_tabu!(ts::WeakTabu, x, kind) = insert!(tabu_list(ts), x, max(1, tenure(ts, kind))) insert_tabu!(ts, x, kind) = nothing - """ _decay_tabu!(s::S) where S <: Union{_State, AbstractSolver} Decay the tabu list. @@ -84,4 +83,4 @@ function decay_tabu!(ts) ((x, tabu),) -> tabu == 1 ? delete_tabu!(ts, x) : decrease_tabu!(ts, x), pairs(tabu_list(ts)) ) -end \ No newline at end of file +end diff --git a/src/strategies/termination.jl b/src/strategies/termination.jl index e69de29..8b13789 100644 --- a/src/strategies/termination.jl +++ b/src/strategies/termination.jl @@ -0,0 +1 @@ + diff --git a/src/strategy.jl b/src/strategy.jl index 7ebd77c..3244479 100644 --- a/src/strategy.jl +++ b/src/strategy.jl @@ -4,9 +4,10 @@ struct MetaStrategy{RS <: RestartStrategy, TS <: TabuStrategy} end function MetaStrategy(model; - tenure = min(length_vars(model) ÷ 2, 10), - tabu = tabu(tenure, tenure ÷ 2), - restart = restart(tabu, Val(:universal)), + tenure = min(length_vars(model) ÷ 2, 10), + tabu = tabu(tenure, tenure ÷ 2), + # restart = restart(tabu, Val(:universal)), + restart = restart(tabu, Val(:random); rp = 0.05) ) return MetaStrategy(restart, tabu) end @@ -16,4 +17,4 @@ end # forwards from TabuStrategy @forward MetaStrategy.tabu decrease_tabu!, delete_tabu!, decay_tabu! -@forward MetaStrategy.tabu length_tabu, insert_tabu!, empty_tabu!, tabu_list \ No newline at end of file +@forward MetaStrategy.tabu length_tabu, insert_tabu!, empty_tabu!, tabu_list diff --git a/src/time_stamps.jl b/src/time_stamps.jl index 6ed032b..0e9b2e6 100644 --- a/src/time_stamps.jl +++ b/src/time_stamps.jl @@ -33,7 +33,6 @@ get_time(stamps, ::Val{6}) = stamps.ts6 get_time(stamps, i) = get_time(stamps, Val(i)) - function time_info(stamps) info = Dict([ :model => get_time(stamps, 1) - get_time(stamps, 0), @@ -43,7 +42,7 @@ function time_info(stamps) :local_run => get_time(stamps, 5) - get_time(stamps, 4), :remote_stop => get_time(stamps, 6) - get_time(stamps, 5), :total_run => get_time(stamps, 6) - get_time(stamps, 1), - :model_and_run => get_time(stamps, 6) - get_time(stamps, 0), + :model_and_run => get_time(stamps, 6) - get_time(stamps, 0) ]) return info end diff --git a/src/variable.jl b/src/variable.jl index 5f39107..fcdab2e 100644 --- a/src/variable.jl +++ b/src/variable.jl @@ -33,13 +33,13 @@ _get_constraints(x::Variable) = x.constraints """ _add_to_constraint!(x::Variable, id) -Add a constraint `id` to the list of contraints of `x`. +Add a constraint `id` to the list of constraints of `x`. """ _add_to_constraint!(x::Variable, id) = set!(_get_constraints(x), id) """ _delete_from_constraint!(x::Variable, id) -Delete a constraint `id` from the list of contraints of `x`. +Delete a constraint `id` from the list of constraints of `x`. """ _delete_from_constraint!(x::Variable, id) = delete!(x.constraints, id) diff --git a/test/Aqua.jl b/test/Aqua.jl new file mode 100644 index 0000000..3623897 --- /dev/null +++ b/test/Aqua.jl @@ -0,0 +1,32 @@ +@testset "Aqua.jl" begin + import Aqua + import LocalSearchSolvers + + # TODO: Fix the broken tests and remove the `broken = true` flag + Aqua.test_all( + LocalSearchSolvers; + ambiguities = (broken = true,), + deps_compat = false, + piracies = (broken = false,), + unbound_args = (broken = false) + ) + + @testset "Ambiguities: LocalSearchSolvers" begin + # Aqua.test_ambiguities(LocalSearchSolvers;) + end + + @testset "Piracies: LocalSearchSolvers" begin + Aqua.test_piracies(LocalSearchSolvers;) + end + + @testset "Dependencies compatibility (no extras)" begin + Aqua.test_deps_compat( + LocalSearchSolvers; + check_extras = false # ignore = [:Random] + ) + end + + @testset "Unbound type parameters" begin + # Aqua.test_unbound_args(LocalSearchSolvers;) + end +end diff --git a/test/Project.toml b/test/Project.toml deleted file mode 100644 index a01aebb..0000000 --- a/test/Project.toml +++ /dev/null @@ -1,7 +0,0 @@ -[deps] -CompositionalNetworks = "4b67e4b5-442d-4ef5-b760-3f5df3a57537" -ConstraintDomains = "5800fd60-8556-4464-8d61-84ebf7a0bedb" -Constraints = "30f324ab-b02d-43f0-b619-e131c61659f7" -Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" -Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/TestItemRunner.jl b/test/TestItemRunner.jl new file mode 100644 index 0000000..cf86c5a --- /dev/null +++ b/test/TestItemRunner.jl @@ -0,0 +1,3 @@ +@testset "TestItemRunner" begin + @run_package_tests +end diff --git a/test/internal.jl b/test/internal.jl index 2adff82..4f5d55d 100644 --- a/test/internal.jl +++ b/test/internal.jl @@ -1,10 +1,10 @@ -d1 = domain([4,3,2,1]) +d1 = domain([4, 3, 2, 1]) d2 = domain(1:4) domains = Dictionary(1:2, [d1, d2]) @testset "Internals: Domains" begin for d in domains # constructors and ∈ - for x in [1,2,3,4] + for x in [1, 2, 3, 4] @test x ∈ d end # length @@ -20,7 +20,7 @@ domains = Dictionary(1:2, [d1, d2]) @test 5 ∉ d1 end -x1 = variable([4,3,2,1]) +x1 = variable([4, 3, 2, 1]) x2 = variable(d2) x3 = variable() # TODO: tailored test for free variable vars = Dictionary(1:2, [x1, x2]) @@ -34,7 +34,7 @@ vars = Dictionary(1:2, [x1, x2]) @test x ∉ 2 @test LS._constriction(x) == 1 @test length(x) == 4 - for y in [1,2,3,4] + for y in [1, 2, 3, 4] @test y ∈ x end @test rand(x) ∈ x @@ -71,11 +71,10 @@ objs = Dictionary(1:2, [o1, o2]) end end - m = model() # LocalSearchSolvers.describe(m) -x1 = variable([4,3,2,1]) +x1 = variable([4, 3, 2, 1]) x2 = variable(d2) vars = Dictionary(1:2, [x1, x2]) @@ -95,12 +94,10 @@ objs = Dictionary(1:2, [o1, o2]) end variable!(m, d1) - for c in cons add!(m, c) end - constraint!(m, err, [1,2]) - + constraint!(m, err, [1, 2]) for o in objs add!(m, o) diff --git a/test/raw_solver.jl b/test/raw_solver.jl index 24a94c7..2cad075 100644 --- a/test/raw_solver.jl +++ b/test/raw_solver.jl @@ -1,5 +1,5 @@ -function mincut(graph; source, sink, interdiction =0) - m = model(; kind=:cut) +function mincut(graph; source, sink, interdiction = 0) + m = model(; kind = :cut) n = size(graph, 1) d = domain(0:n) @@ -24,7 +24,7 @@ function mincut(graph; source, sink, interdiction =0) end function golomb(n, L = n^2) - m = model(; kind=:golomb) + m = model(; kind = :golomb) # Add variables d = domain(0:L) @@ -32,7 +32,7 @@ function golomb(n, L = n^2) # Extract error function from usual_constraint e1 = (x; X) -> error_f(USUAL_CONSTRAINTS[:all_different])(x) - e2 = (x; X) -> error_f(USUAL_CONSTRAINTS[:all_equal])(x; val=0) + e2 = (x; X) -> error_f(USUAL_CONSTRAINTS[:all_equal])(x; val = 0) e3 = (x; X) -> error_f(USUAL_CONSTRAINTS[:dist_different])(x) # # Add constraints @@ -49,11 +49,11 @@ function golomb(n, L = n^2) return m end -function sudoku(n; start=nothing) +function sudoku(n; start = nothing) N = n^2 d = domain(1:N) - m = model(;kind=:sudoku) + m = model(; kind = :sudoku) # Add variables if isnothing(start) @@ -62,7 +62,6 @@ function sudoku(n; start=nothing) foreach(((x, v),) -> variable!(m, 1 ≤ v ≤ N ? domain(v) : d), pairs(start)) end - e = (x; X) -> error_f(USUAL_CONSTRAINTS[:all_different])(x) # Add constraints: line, columns; blocks @@ -86,12 +85,14 @@ end @testset "Raw solver: internals" begin models = [ - sudoku(2), + sudoku(2) ] for m in models # @info describe(m) - s = solver(m; options=Options(print_level=:verbose, time_limit = Inf, iteration=Inf, info_path="info.json")) + s = solver(m; + options = Options(print_level = :verbose, time_limit = Inf, + iteration = Inf, info_path = "info.json")) for x in keys(get_variables(s)) @test get_name(s, x) == "x$x" for c in get_cons_from_var(s, x) @@ -134,22 +135,22 @@ end end @testset "Raw solver: sudoku" begin - sudoku_instance = collect(Iterators.flatten([ - 9 3 0 0 0 0 0 4 0 - 0 0 0 0 4 2 0 9 0 - 8 0 0 1 9 6 7 0 0 - 0 0 0 4 7 0 0 0 0 - 0 2 0 0 0 0 0 6 0 - 0 0 0 0 2 3 0 0 0 - 0 0 8 5 3 1 0 0 2 - 0 9 0 2 8 0 0 0 0 - 0 7 0 0 0 0 0 5 3 - ])) - - s = solver(sudoku(3; start = sudoku_instance); options = Options(print_level = :minimal, iteration = Inf, time_limit = 10)) + sudoku_instance = collect(Iterators.flatten([9 3 0 0 0 0 0 4 0 + 0 0 0 0 4 2 0 9 0 + 8 0 0 1 9 6 7 0 0 + 0 0 0 4 7 0 0 0 0 + 0 2 0 0 0 0 0 6 0 + 0 0 0 0 2 3 0 0 0 + 0 0 8 5 3 1 0 0 2 + 0 9 0 2 8 0 0 0 0 + 0 7 0 0 0 0 0 5 3])) + + s = solver(sudoku(3; start = sudoku_instance); + options = Options(print_level = :minimal, iteration = Inf, time_limit = 10)) display(Dictionary(1:length(sudoku_instance), sudoku_instance)) solve!(s) display(solution(s)) + display(s.time_stamps) end @testset "Raw solver: golomb" begin @@ -164,27 +165,30 @@ end @testset "Raw solver: mincut" begin graph = zeros(5, 5) - graph[1,2] = 1.0 - graph[1,3] = 2.0 - graph[1,4] = 3.0 - graph[2,5] = 1.0 - graph[3,5] = 2.0 - graph[4,5] = 3.0 - s = solver(mincut(graph, source=1, sink=5), options = Options(print_level = :minimal)) + graph[1, 2] = 1.0 + graph[1, 3] = 2.0 + graph[1, 4] = 3.0 + graph[2, 5] = 1.0 + graph[3, 5] = 2.0 + graph[4, 5] = 3.0 + s = solver( + mincut(graph, source = 1, sink = 5), options = Options(print_level = :minimal)) solve!(s) @info "Results mincut!" @info "Values: $(get_values(s))" @info "Sol (val): $(best_value(s))" @info "Sol (vals): $(!isnothing(best_value(s)) ? best_values(s) : nothing)" - s = solver(mincut(graph, source=1, sink=5, interdiction=1), options = Options(print_level = :minimal)) + s = solver(mincut(graph, source = 1, sink = 5, interdiction = 1), + options = Options(print_level = :minimal)) solve!(s) @info "Results 1-mincut!" @info "Values: $(get_values(s))" @info "Sol (val): $(best_value(s))" @info "Sol (vals): $(!isnothing(best_value(s)) ? best_values(s) : nothing)" - s = solver(mincut(graph, source=1, sink=5, interdiction=2); options = Options(print_level=:minimal, time_limit = 15, iteration=Inf)) + s = solver(mincut(graph, source = 1, sink = 5, interdiction = 2); + options = Options(print_level = :minimal, time_limit = 15, iteration = Inf)) # @info describe(s) solve!(s) @info "Results 2-mincut!" diff --git a/test/runtests.jl b/test/runtests.jl index fda6569..ad5f198 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,4 @@ - using Distributed -# Add a process with two threads -# addprocs(1; exeflags = ["-t 2", "--project"]) -# addprocs(1) import ConstraintDomains import CompositionalNetworks @@ -10,17 +6,14 @@ import CompositionalNetworks using Dictionaries @everywhere using LocalSearchSolvers using Test - - -# @testset "Distributed" begin -# @test workers() == [2] -# end - - +using TestItemRunner +using TestItems const LS = LocalSearchSolvers @testset "LocalSearchSolvers.jl" begin - include("internal.jl") - include("raw_solver.jl") + include("Aqua.jl") + include("TestItemRunner.jl") + # include("internal.jl") + # include("raw_solver.jl") end