├── docs ├── .gitignore ├── src │ ├── assets │ │ └── logo.png │ ├── reference.md │ ├── index.md │ ├── explanation.md │ ├── how_to.md │ └── supported.md ├── make.jl └── Project.toml ├── .github ├── FUNDING.yml └── workflows │ ├── TagBot.yml │ ├── CompatHelper.yml │ └── ci.yml ├── .JuliaFormatter.toml ├── benchmark ├── sudoku │ ├── data │ │ ├── Readme.md │ │ ├── hardest.txt │ │ └── 25x25_gecode.jl │ ├── results │ │ ├── norvig.csv │ │ ├── python-constraint.csv │ │ ├── or-tools.csv │ │ ├── sudoku-bb.csv │ │ └── cs.csv │ ├── sudoku-bb.py │ ├── or-tools.py │ ├── python-constraint.py │ ├── benchmark.jl │ └── cs.jl ├── eternity │ ├── data │ │ ├── eternity_5x5 │ │ ├── eternity_6x5 │ │ ├── eternity_7x5 │ │ ├── eternity_6x6 │ │ └── eternity_7x7 │ ├── run_benchmarks.jl │ ├── cs.jl │ └── benchmark.jl ├── Project.toml ├── sort │ └── benchmark.jl ├── graph_color │ ├── run_benchmarks.jl │ ├── cs.jl │ └── or_tools_file.py ├── lp │ └── benchmark.jl ├── killer_sudoku │ ├── data │ │ ├── niallsudoku_5503 │ │ ├── niallsudoku_5502 │ │ ├── niallsudoku_6249 │ │ └── niallsudoku_6417 │ ├── plot.jl │ ├── or-tools.py │ ├── benchmark.jl │ └── cs.jl ├── results2csvs.jl ├── steiner │ └── benchmark.jl ├── run_benchmarks.jl ├── small │ └── benchmark.jl ├── tune.json └── scheduling │ └── benchmark.jl ├── .gitignore ├── test ├── data │ ├── eternity_5x5 │ ├── eternity_6x5 │ └── killer_niallsudoku_5503 ├── options.jl ├── unit │ ├── constraints │ │ ├── scc.jl │ │ ├── not_equal.jl │ │ ├── svc.jl │ │ ├── geqset.jl │ │ ├── less_than.jl │ │ └── equal.jl │ └── index.jl ├── constraints │ ├── xor.jl │ └── equal_to.jl ├── refs │ ├── hard_fsudoku │ └── niallsudoku_5503_negative ├── docs.jl ├── Project.toml ├── maximum_weight_matching.jl ├── steiner.jl ├── runtests.jl ├── monks_and_doors.jl ├── scheduling.jl ├── small_eq_sum_real.jl └── str8ts.jl ├── src ├── constraints │ ├── complement.jl │ ├── table │ │ ├── residues.jl │ │ ├── support.jl │ │ └── RSparseBitSet.jl │ ├── and.jl │ ├── svc.jl │ ├── or.jl │ ├── all_different │ │ └── scc.jl │ └── not_equal.jl ├── MOI_wrapper │ ├── element.jl │ ├── indicator.jl │ ├── complement.jl │ ├── Bridges │ │ ├── strictly_greater_than.jl │ │ ├── util.jl │ │ ├── reified.jl │ │ ├── complement.jl │ │ └── indicator.jl │ ├── results.jl │ ├── objective.jl │ ├── reified.jl │ └── bool.jl ├── lp_model.jl ├── printing.jl └── options.jl ├── LICENSE ├── Project.toml └── README.md /docs/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | site/ 3 | -------------------------------------------------------------------------------- /docs/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikunia/ConstraintSolver.jl/master/docs/src/assets/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Wikunia] 4 | patreon: opensources 5 | 6 | -------------------------------------------------------------------------------- /docs/src/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ## User interface functions 4 | 5 | ```@docs 6 | ConstraintSolver.values(::Model, ::VariableRef) 7 | ``` 8 | -------------------------------------------------------------------------------- /.JuliaFormatter.toml: -------------------------------------------------------------------------------- 1 | annotate_untyped_fields_with_any = false 2 | always_for_in = true 3 | whitespace_in_kwargs = true 4 | whitespace_ops_in_indices = true 5 | -------------------------------------------------------------------------------- /benchmark/sudoku/data/Readme.md: -------------------------------------------------------------------------------- 1 | # top95, easy50 & hardest 2 | 3 | https://github.com/dimitri/sudoku 4 | 5 | # 25x25 gecode 6 | 7 | http://www.gecode.org/gecode-doc-latest/sudoku08cpp-source.html 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/current/ 2 | gif 3 | /Manifest.toml 4 | test/Manifest.toml 5 | visualizations/images/ 6 | visualizations/videos/ 7 | literature/ 8 | extra/ 9 | test/logs/ 10 | docs/build/ 11 | docs/Manifest.toml 12 | benchmark/**/results 13 | instances 14 | **/nohup.out -------------------------------------------------------------------------------- /test/data/eternity_5x5: -------------------------------------------------------------------------------- 1 | 0 0 1 1 2 | 0 0 2 1 3 | 0 0 2 3 4 | 0 0 3 1 5 | 0 1 4 2 6 | 0 1 5 1 7 | 0 1 6 3 8 | 0 1 7 2 9 | 0 1 7 3 10 | 0 2 4 1 11 | 0 2 5 1 12 | 0 2 7 2 13 | 0 3 4 2 14 | 0 3 5 2 15 | 0 3 5 3 16 | 0 3 7 3 17 | 4 4 7 7 18 | 4 5 4 6 19 | 4 5 4 7 20 | 4 5 6 7 21 | 4 6 6 5 22 | 4 7 6 7 23 | 5 5 6 6 24 | 5 6 6 7 25 | 5 6 7 6 -------------------------------------------------------------------------------- /benchmark/eternity/data/eternity_5x5: -------------------------------------------------------------------------------- 1 | 0 0 1 1 2 | 0 0 2 1 3 | 0 0 2 3 4 | 0 0 3 1 5 | 0 1 4 1 6 | 0 1 4 3 7 | 0 1 5 2 8 | 0 1 5 3 9 | 0 1 6 2 10 | 0 2 5 2 11 | 0 2 6 3 12 | 0 2 7 3 13 | 0 3 4 1 14 | 0 3 6 1 15 | 0 3 6 2 16 | 0 3 7 2 17 | 4 4 5 6 18 | 4 5 4 6 19 | 4 5 5 6 20 | 4 5 7 7 21 | 4 6 5 6 22 | 4 6 7 7 23 | 4 7 5 5 24 | 5 6 7 7 25 | 6 7 7 7 -------------------------------------------------------------------------------- /test/options.jl: -------------------------------------------------------------------------------- 1 | @testset "Options" begin 2 | cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) 3 | @test_logs (:error, r"Possible values are") Model(optimizer_with_attributes( 4 | CS.Optimizer, 5 | "lp_optimizer" => cbc_optimizer, 6 | "logging" => [], 7 | "traverse_strategy" => :KFS, 8 | )) 9 | end 10 | -------------------------------------------------------------------------------- /test/data/eternity_6x5: -------------------------------------------------------------------------------- 1 | 0 0 1 3 2 | 0 0 2 1 3 | 0 0 2 3 4 | 0 0 3 1 5 | 0 1 4 2 6 | 0 1 5 1 7 | 0 1 6 2 8 | 0 1 6 3 9 | 0 1 7 2 10 | 0 2 4 3 11 | 0 2 5 1 12 | 0 2 5 2 13 | 0 2 8 1 14 | 0 3 4 1 15 | 0 3 4 2 16 | 0 3 7 3 17 | 0 3 8 2 18 | 0 3 8 3 19 | 4 5 4 7 20 | 4 5 4 8 21 | 4 5 6 8 22 | 4 5 8 8 23 | 4 6 6 5 24 | 4 7 5 8 25 | 4 7 8 7 26 | 4 8 5 7 27 | 5 6 6 6 28 | 5 6 8 7 29 | 6 7 6 8 30 | 6 7 7 7 -------------------------------------------------------------------------------- /benchmark/eternity/data/eternity_6x5: -------------------------------------------------------------------------------- 1 | 0 0 1 3 2 | 0 0 2 1 3 | 0 0 2 3 4 | 0 0 3 1 5 | 0 1 4 2 6 | 0 1 5 1 7 | 0 1 6 2 8 | 0 1 6 3 9 | 0 1 7 2 10 | 0 2 4 3 11 | 0 2 5 1 12 | 0 2 5 2 13 | 0 2 8 1 14 | 0 3 4 1 15 | 0 3 4 2 16 | 0 3 7 3 17 | 0 3 8 2 18 | 0 3 8 3 19 | 4 5 4 7 20 | 4 5 4 8 21 | 4 5 6 8 22 | 4 5 8 8 23 | 4 6 6 5 24 | 4 7 5 8 25 | 4 7 8 7 26 | 4 8 5 7 27 | 5 6 6 6 28 | 5 6 8 7 29 | 6 7 6 8 30 | 6 7 7 7 -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | workflow_dispatch: 7 | jobs: 8 | TagBot: 9 | if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: JuliaRegistries/TagBot@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | ssh: ${{ secrets.DOCUMENTER_KEY }} -------------------------------------------------------------------------------- /src/constraints/complement.jl: -------------------------------------------------------------------------------- 1 | function init_constraint_struct(com, set::ComplementSet{F,S}, internals) where {F,S} 2 | inner_set = set.set 3 | fct = internals.fct 4 | if !(internals.fct isa SAF) && F <: SAF 5 | fct = get_saf(internals.fct) 6 | end 7 | 8 | constraint = get_constraint(com, fct, inner_set) 9 | complement_constraint = get_complement_constraint(com, constraint) 10 | return complement_constraint 11 | end -------------------------------------------------------------------------------- /benchmark/eternity/data/eternity_7x5: -------------------------------------------------------------------------------- 1 | 0 0 1 1 2 | 0 0 1 2 3 | 0 0 2 2 4 | 0 0 3 2 5 | 0 1 4 1 6 | 0 1 5 2 7 | 0 1 6 2 8 | 0 1 7 1 9 | 0 1 7 3 10 | 0 2 4 1 11 | 0 2 4 3 12 | 0 2 5 2 13 | 0 2 6 3 14 | 0 2 7 1 15 | 0 2 7 3 16 | 0 3 5 1 17 | 0 3 5 3 18 | 0 3 6 2 19 | 0 3 7 1 20 | 0 3 7 3 21 | 4 4 5 5 22 | 4 4 6 8 23 | 4 4 8 5 24 | 4 5 8 5 25 | 4 6 5 7 26 | 4 6 6 8 27 | 4 6 7 5 28 | 4 7 5 5 29 | 4 7 8 7 30 | 4 8 5 8 31 | 5 6 6 6 32 | 5 7 8 8 33 | 6 6 8 7 34 | 6 6 8 8 35 | 6 7 8 8 -------------------------------------------------------------------------------- /benchmark/eternity/data/eternity_6x6: -------------------------------------------------------------------------------- 1 | 0 0 1 1 2 | 0 0 1 3 3 | 0 0 2 2 4 | 0 0 2 3 5 | 0 1 6 1 6 | 0 1 6 2 7 | 0 1 8 1 8 | 0 1 8 2 9 | 0 1 8 3 10 | 0 2 4 1 11 | 0 2 5 1 12 | 0 2 6 2 13 | 0 2 6 3 14 | 0 2 7 3 15 | 0 3 4 2 16 | 0 3 4 3 17 | 0 3 5 1 18 | 0 3 6 2 19 | 0 3 7 1 20 | 0 3 8 2 21 | 4 4 7 5 22 | 4 5 5 5 23 | 4 5 8 8 24 | 4 6 6 7 25 | 4 6 7 8 26 | 4 7 6 5 27 | 4 7 7 5 28 | 4 7 7 7 29 | 4 8 5 6 30 | 4 8 5 7 31 | 4 8 7 6 32 | 4 8 7 8 33 | 5 6 5 7 34 | 5 6 6 8 35 | 5 6 7 8 36 | 5 8 6 8 -------------------------------------------------------------------------------- /.github/workflows/CompatHelper.yml: -------------------------------------------------------------------------------- 1 | name: CompatHelper 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * 5 | workflow_dispatch: 6 | jobs: 7 | CompatHelper: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pkg.add("CompatHelper") 11 | run: julia -e 'using Pkg; Pkg.add("CompatHelper")' 12 | - name: CompatHelper.main() 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} 16 | run: julia -e 'using CompatHelper; CompatHelper.main()' -------------------------------------------------------------------------------- /benchmark/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Arpack = "7d9fca2a-8960-54d3-9f78-7d1dccf2cb97" 3 | BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" 4 | Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" 5 | ConstraintSolver = "e0e52ebd-5523-408d-9ca3-7641f1cd1405" 6 | GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" 7 | GitHub = "bc5e4493-9b4d-5f90-b8aa-2b2bcaad7a26" 8 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 9 | JuMP = "4076af6c-e467-56ae-b986-b466b2749572" 10 | MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" 11 | PkgBenchmark = "32113eaa-f34f-5b0d-bd6c-c81e245fc73d" 12 | Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 13 | -------------------------------------------------------------------------------- /benchmark/eternity/data/eternity_7x7: -------------------------------------------------------------------------------- 1 | 0 0 1 2 2 | 0 0 1 3 3 | 0 0 2 1 4 | 0 0 3 2 5 | 0 1 5 1 6 | 0 1 6 1 7 | 0 1 6 3 8 | 0 1 7 2 9 | 0 1 8 2 10 | 0 1 8 3 11 | 0 2 4 2 12 | 0 2 5 3 13 | 0 2 7 2 14 | 0 2 8 2 15 | 0 2 8 3 16 | 0 2 9 1 17 | 0 2 9 3 18 | 0 3 4 1 19 | 0 3 4 2 20 | 0 3 5 1 21 | 0 3 6 1 22 | 0 3 6 3 23 | 0 3 8 3 24 | 0 3 9 1 25 | 4 4 5 8 26 | 4 4 7 5 27 | 4 4 7 7 28 | 4 5 5 7 29 | 4 5 8 5 30 | 4 6 5 7 31 | 4 6 7 6 32 | 4 8 6 9 33 | 4 8 7 7 34 | 4 8 9 5 35 | 4 8 9 6 36 | 4 9 6 8 37 | 4 9 8 5 38 | 4 9 9 6 39 | 5 5 8 7 40 | 5 5 9 7 41 | 5 6 7 7 42 | 5 6 8 6 43 | 5 6 9 9 44 | 5 9 7 9 45 | 6 7 9 8 46 | 6 8 7 7 47 | 6 8 8 7 48 | 6 9 8 9 49 | 6 9 9 7 -------------------------------------------------------------------------------- /src/MOI_wrapper/element.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Support for nice element 1D const constraint 3 | """ 4 | function Base.getindex(v::AbstractVector{<:Integer}, i::VariableRef) 5 | m = JuMP.owner_model(i) 6 | # check if the AbstractVector has standard indexing 7 | if !checkbounds(Bool, v, 1:length(v)) 8 | throw(ArgumentError("Currently the specified vector needs to be using standard indexing 1:... so OffsetArrays are not possible.")) 9 | end 10 | v = collect(v) 11 | min_val, max_val = extrema(v) 12 | x = @variable(m, integer=true, lower_bound = min_val, upper_bound = max_val) 13 | @constraint(m, [x, i] in CS.Element1DConst(v)) 14 | return x 15 | end -------------------------------------------------------------------------------- /benchmark/sort/benchmark.jl: -------------------------------------------------------------------------------- 1 | function sort_element_constr(n) 2 | m = Model(optimizer_with_attributes(CS.Optimizer, 3 | "logging" => [], 4 | "seed" => 4, 5 | "traverse_strategy" => :BFS, 6 | )) 7 | seed = 1337 8 | Random.seed!(seed) 9 | c = rand(1:1000, n) 10 | @variable(m, 1 <= idx[1:length(c)] <= length(c), Int) 11 | @variable(m, minimum(c) <= val[1:length(c)] <= maximum(c), Int) 12 | for i in 1:length(c)-1 13 | @constraint(m, val[i] <= val[i+1]) 14 | end 15 | for i in 1:length(c) 16 | @constraint(m, c[idx[i]] == val[i]) 17 | end 18 | @constraint(m, idx in CS.AllDifferent()) 19 | optimize!(m) 20 | end -------------------------------------------------------------------------------- /test/unit/constraints/scc.jl: -------------------------------------------------------------------------------- 1 | @testset "scc graph wikipedia" begin 2 | n = 8 3 | scc_init = CS.SCCInit( 4 | zeros(Int, n + 1), 5 | zeros(Int, n), 6 | zeros(Int, n), 7 | zeros(Bool, n), 8 | zeros(Int, n), 9 | ) 10 | di_ei = [1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 8] 11 | di_ej = [2, 2, 3, 5, 7, 4, 3, 8, 6, 1, 7, 6, 4, 7] 12 | scc_map = CS.scc(di_ei, di_ej, scc_init) 13 | @test scc_map[1] == scc_map[2] == scc_map[5] 14 | @test scc_map[3] == scc_map[4] == scc_map[8] 15 | @test scc_map[6] == scc_map[7] 16 | @test scc_map[1] != scc_map[3] 17 | @test scc_map[1] != scc_map[6] 18 | @test scc_map[3] != scc_map[6] 19 | end 20 | -------------------------------------------------------------------------------- /test/constraints/xor.jl: -------------------------------------------------------------------------------- 1 | @testset "Xor" begin 2 | @testset "Xor basic all solutions" begin 3 | m = Model(optimizer_with_attributes( 4 | CS.Optimizer, 5 | "all_solutions" => true, 6 | "logging" => [], 7 | )) 8 | @variable(m, 1 <= x <= 5, Int) 9 | @variable(m, 1 <= y <= 5, Int) 10 | @constraint(m, (x <= 2) ⊻ (y <= 2)) 11 | optimize!(m) 12 | 13 | num_sols = MOI.get(m, MOI.ResultCount()) 14 | @test num_sols == count((i <= 2) ⊻ (j <= 2) for i=1:5, j=1:5) 15 | for i in 1:num_sols 16 | xval = convert(Int, JuMP.value(x; result=i)) 17 | yval = convert(Int, JuMP.value(y; result=i)) 18 | @test (xval <= 2) ⊻ (yval <= 2) 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /src/MOI_wrapper/indicator.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Support for indicator constraints with a set constraint as the right hand side 3 | """ 4 | function JuMP._build_indicator_constraint( 5 | _error::Function, 6 | variable::JuMP.AbstractVariableRef, 7 | constraint::JuMP.VectorConstraint, 8 | ::Type{MOI.Indicator{A}}, 9 | ) where {A} 10 | S = typeof(constraint.set) 11 | F = typeof(JuMP.moi_function(constraint)) 12 | set = CS.Indicator{A,F,S}(constraint.set, 1 + length(constraint.func)) 13 | if constraint.func isa Vector{VariableRef} 14 | vov = JuMP.VariableRef[variable] 15 | else 16 | vov = JuMP.AffExpr[variable] 17 | end 18 | append!(vov, constraint.func) 19 | return JuMP.VectorConstraint(vov, set) 20 | end -------------------------------------------------------------------------------- /test/refs/hard_fsudoku: -------------------------------------------------------------------------------- 1 | [[1,2,3,4,5,6,7,8,9],[10,11,12,13,14,15,16,17,18],[19,20,21,22,23,24,25,26,27],[28,29,30,31,32,33,34,35,36],[37,38,39,40,41,42,43,44,45],[46,47,48,49,50,51,52,53,54],[55,56,57,58,59,60,61,62,63],[64,65,66,67,68,69,70,71,72],[73,74,75,76,77,78,79,80,81],[1,10,19,28,37,46,55,64,73],[2,11,20,29,38,47,56,65,74],[3,12,21,30,39,48,57,66,75],[4,13,22,31,40,49,58,67,76],[5,14,23,32,41,50,59,68,77],[6,15,24,33,42,51,60,69,78],[7,16,25,34,43,52,61,70,79],[8,17,26,35,44,53,62,71,80],[9,18,27,36,45,54,63,72,81],[1,10,19,2,11,20,3,12,21],[4,13,22,5,14,23,6,15,24],[7,16,25,8,17,26,9,18,27],[28,37,46,29,38,47,30,39,48],[31,40,49,32,41,50,33,42,51],[34,43,52,35,44,53,36,45,54],[55,64,73,56,65,74,57,66,75],[58,67,76,59,68,77,60,69,78],[61,70,79,62,71,80,63,72,81]] -------------------------------------------------------------------------------- /src/constraints/table/residues.jl: -------------------------------------------------------------------------------- 1 | function Base.getindex( 2 | tr::TableResidues, 3 | com::ConstraintSolverModel, 4 | vidx::Int, 5 | local_vidx::Int, 6 | val::Int, 7 | ) 8 | val_idx = com.search_space[vidx].init_val_to_index[val + com.search_space[vidx].offset] 9 | index_shift = tr.var_start[local_vidx] - 1 + val_idx 10 | return tr.values[index_shift] 11 | end 12 | 13 | function Base.setindex!( 14 | tr::TableResidues, 15 | residue::Int, 16 | com::ConstraintSolverModel, 17 | vidx::Int, 18 | local_vidx::Int, 19 | val::Int, 20 | ) 21 | val_idx = com.search_space[vidx].init_val_to_index[val + com.search_space[vidx].offset] 22 | index_shift = tr.var_start[local_vidx] - 1 + val_idx 23 | tr.values[index_shift] = residue 24 | end 25 | -------------------------------------------------------------------------------- /src/constraints/table/support.jl: -------------------------------------------------------------------------------- 1 | function get_view( 2 | ts::TableSupport, 3 | com::ConstraintSolverModel, 4 | vidx::Int, 5 | local_vidx::Int, 6 | val::Int, 7 | ) 8 | val_idx = com.search_space[vidx].init_val_to_index[val + com.search_space[vidx].offset] 9 | index_shift = ts.var_start[local_vidx] - 1 + val_idx 10 | return @view ts.values[:, index_shift] 11 | end 12 | 13 | function Base.getindex( 14 | ts::TableSupport, 15 | com::ConstraintSolverModel, 16 | vidx::Int, 17 | local_vidx::Int, 18 | val::Int, 19 | row_idx::Int, 20 | ) 21 | val_idx = com.search_space[vidx].init_val_to_index[val + com.search_space[vidx].offset] 22 | index_shift = ts.var_start[local_vidx] - 1 + val_idx 23 | return ts.values[row_idx, index_shift] 24 | end 25 | -------------------------------------------------------------------------------- /docs/make.jl: -------------------------------------------------------------------------------- 1 | using Documenter 2 | using ConstraintSolver 3 | using JuMP 4 | 5 | makedocs( 6 | # See https://github.com/JuliaDocs/Documenter.jl/issues/868 7 | format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"), 8 | strict = true, 9 | sitename = "ConstraintSolver", 10 | pages = [ 11 | "Home" => "index.md", 12 | "Tutorial" => "tutorial.md", 13 | "How-To" => "how_to.md", 14 | "Solver options" => "options.md", 15 | "Supported/Planned" => "supported.md", 16 | "Explanation" => "explanation.md", 17 | "Reference" => "reference.md", 18 | # "Developer" => [], 19 | # "Library" => "library.md" 20 | ], 21 | ) 22 | 23 | deploydocs(repo = "github.com/Wikunia/ConstraintSolver.jl.git") 24 | -------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | ConstraintProgrammingExtensions = "b65d079e-ed98-51d9-b0db-edee61a5c5f8" 3 | ConstraintSolver = "e0e52ebd-5523-408d-9ca3-7641f1cd1405" 4 | Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" 5 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 6 | Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" 7 | JuMP = "4076af6c-e467-56ae-b986-b466b2749572" 8 | LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d" 9 | MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" 10 | MatrixNetworks = "4f449596-a032-5618-b826-5a251cb6dc11" 11 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 12 | Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 13 | StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" 14 | 15 | [compat] 16 | JuMP = "^0.21.3" 17 | MathOptInterface = "^0.9.11" 18 | julia = "1" 19 | -------------------------------------------------------------------------------- /benchmark/graph_color/run_benchmarks.jl: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env julia 3 | 4 | using ArgParse 5 | 6 | function parse_commandline() 7 | s = ArgParseSettings() 8 | 9 | @add_arg_table! s begin 10 | "--file", "-f" 11 | help = "col file to run" 12 | arg_type = String 13 | required = true 14 | "--time_limit", "-t" 15 | help = "Target commit id or branch" 16 | arg_type = Int 17 | default = 1800 18 | end 19 | 20 | return parse_args(s) 21 | end 22 | 23 | 24 | if isinteractive() == false 25 | args = parse_commandline() 26 | 27 | include("cs.jl") 28 | # for compiling 29 | main("/home/ole/Julia/ConstraintSolver/instances/graph_coloring/in_seconds/queen5_5.col") 30 | println("") 31 | println("======= ACTUAL RUN =======") 32 | println("") 33 | # actual run 34 | main(args["file"]; time_limit = args["time_limit"]) 35 | end 36 | -------------------------------------------------------------------------------- /benchmark/eternity/run_benchmarks.jl: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env julia 3 | 4 | using ArgParse 5 | 6 | function parse_commandline() 7 | s = ArgParseSettings() 8 | 9 | @add_arg_table! s begin 10 | "--file", "-f" 11 | help = "col file to run" 12 | arg_type = String 13 | required = true 14 | "--time_limit", "-t" 15 | help = "Target commit id or branch" 16 | arg_type = Int 17 | default = 1800 18 | end 19 | 20 | return parse_args(s) 21 | end 22 | 23 | 24 | if isinteractive() == false 25 | args = parse_commandline() 26 | 27 | include("cs.jl") 28 | # for compiling 29 | main("/home/ole/Julia/ConstraintSolver/instances/eternity/pieces_05x05.txt") 30 | println("") 31 | println("======= ACTUAL RUN =======") 32 | println("") 33 | flush(stdout) 34 | # actual run 35 | main(args["file"]; time_limit = args["time_limit"]) 36 | end 37 | -------------------------------------------------------------------------------- /benchmark/sudoku/data/hardest.txt: -------------------------------------------------------------------------------- 1 | 85...24..72......9..4.........1.7..23.5...9...4...........8..7..17..........36.4. 2 | ..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97.. 3 | 12..4......5.69.1...9...5.........7.7...52.9..3......2.9.6...5.4..9..8.1..3...9.4 4 | ...57..3.1......2.7...234......8...4..7..4...49....6.5.42...3.....7..9....18..... 5 | 7..1523........92....3.....1....47.8.......6............9...5.6.4.9.7...8....6.1. 6 | 1....7.9..3..2...8..96..5....53..9...1..8...26....4...3......1..4......7..7...3.. 7 | 1...34.8....8..5....4.6..21.18......3..1.2..6......81.52..7.9....6..9....9.64...2 8 | ...92......68.3...19..7...623..4.1....1...7....8.3..297...8..91...5.72......64... 9 | .6.5.4.3.1...9...8.........9...5...6.4.6.2.7.7...4...5.........4...8...1.5.2.3.4. 10 | 7.....4...2..7..8...3..8.799..5..3...6..2..9...1.97..6...3..9...3..4..6...9..1.35 11 | ....7..2.8.......6.1.2.5...9.54....8.........3....85.1...3.2.8.4.......9.7..6.... 12 | -------------------------------------------------------------------------------- /test/docs.jl: -------------------------------------------------------------------------------- 1 | # Tests that every option is documented 2 | @testset "Documentation" begin 3 | @testset "Options" begin 4 | cwd = pwd() 5 | dir = pathof(ConstraintSolver)[1:(end - 20)] 6 | cd(dir) 7 | cd("../docs/src") 8 | options_md = readlines("options.md") 9 | all_options = fieldnames(CS.SolverOptions) 10 | found_all = true 11 | for option in all_options 12 | found = false 13 | option_str = String(option) 14 | for line in options_md 15 | if startswith(line, "## `$option_str`") 16 | found = true 17 | break 18 | end 19 | end 20 | if !found && option != :no_prune 21 | found_all = false 22 | @error "Option $option is not documented" 23 | end 24 | end 25 | @test found_all 26 | 27 | cd(cwd) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/MOI_wrapper/complement.jl: -------------------------------------------------------------------------------- 1 | function _build_complement_constraint( 2 | _error::Function, 3 | constraint, 4 | ) 5 | set = JuMP.moi_set(constraint) 6 | 7 | jump_fct = JuMP.jump_function(constraint) 8 | moi_fct = JuMP.moi_function(constraint) 9 | 10 | return JuMP.VectorConstraint( 11 | [jump_fct...], ComplementSet{typeof(moi_fct)}(set) 12 | ) 13 | end 14 | 15 | 16 | function JuMP.parse_constraint_call(_error::Function, vectorized::Bool, ::Val{:!}, constraint) 17 | _error1 = deepcopy(_error) 18 | if vectorized 19 | _error("`$(constraint)` should be non vectorized. There is currently no vectorized support for `!` constraints. Please open an issue at ConstraintSolver.jl") 20 | end 21 | 22 | vectorized, inner_parsecode, inner_buildcall = JuMP.parse_constraint(_error1, constraint) 23 | 24 | buildcall = :($(esc(:(CS._build_complement_constraint)))( 25 | $_error, 26 | $inner_buildcall, 27 | )) 28 | 29 | return inner_parsecode,buildcall 30 | end -------------------------------------------------------------------------------- /test/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" 3 | Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" 4 | ConstraintProgrammingExtensions = "b65d079e-ed98-51d9-b0db-edee61a5c5f8" 5 | DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" 6 | Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" 7 | ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" 8 | GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" 9 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 10 | JuMP = "4076af6c-e467-56ae-b986-b466b2749572" 11 | LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d" 12 | LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" 13 | MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" 14 | MatrixNetworks = "4f449596-a032-5618-b826-5a251cb6dc11" 15 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 16 | ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" 17 | Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 18 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 19 | 20 | [compat] 21 | ReferenceTests = "=0.9.0" 22 | -------------------------------------------------------------------------------- /src/MOI_wrapper/Bridges/strictly_greater_than.jl: -------------------------------------------------------------------------------- 1 | struct StrictlyGreaterToStrictlyLessBridge{ 2 | T, 3 | F<:MOI.AbstractScalarFunction, 4 | G<:MOI.AbstractScalarFunction, 5 | } <: MOIBC.FlipSignBridge{T,CPE.Strictly{MOI.GreaterThan{T},T},CPE.Strictly{MOI.LessThan{T}, T},F,G} 6 | constraint::CI{F,CPE.Strictly{MOI.LessThan{T}, T}} 7 | end 8 | function MOIB.map_set(::Type{<:StrictlyGreaterToStrictlyLessBridge}, set::CPE.Strictly{MOI.GreaterThan{T}, T}) where T 9 | return CPE.Strictly(MOI.LessThan(-set.set.lower)) 10 | end 11 | function MOIB.inverse_map_set(::Type{<:StrictlyGreaterToStrictlyLessBridge}, set::CPE.Strictly{MOI.LessThan{T}, T}) where T 12 | return CPE.Strictly(MOI.GreaterThan(-set.set.upper)) 13 | end 14 | function MOIBC.concrete_bridge_type( 15 | ::Type{<:StrictlyGreaterToStrictlyLessBridge{T}}, 16 | G::Type{<:MOI.AbstractScalarFunction}, 17 | ::Type{CPE.Strictly{MOI.GreaterThan{T}, T}}, 18 | ) where {T} 19 | F = MOIU.promote_operation(-, T, G) 20 | return StrictlyGreaterToStrictlyLessBridge{T,F,G} 21 | end 22 | -------------------------------------------------------------------------------- /test/unit/index.jl: -------------------------------------------------------------------------------- 1 | function get_constraints_by_type(com, t) 2 | constraints = CS.Constraint[] 3 | for constraint in com.constraints 4 | if constraint isa t 5 | push!(constraints, constraint) 6 | end 7 | end 8 | return constraints 9 | end 10 | 11 | @testset "Unit Tests" begin 12 | include("constraints/bool.jl") 13 | include("constraints/alldifferent.jl") 14 | include("constraints/and.jl") 15 | include("constraints/or.jl") 16 | include("constraints/xor.jl") 17 | include("constraints/xnor.jl") 18 | include("constraints/complement.jl") 19 | include("constraints/scc.jl") 20 | include("constraints/equal_to.jl") 21 | include("constraints/equal.jl") 22 | include("constraints/less_than.jl") 23 | include("constraints/strictly_less_than.jl") 24 | include("constraints/not_equal.jl") 25 | include("constraints/svc.jl") 26 | include("constraints/table.jl") 27 | include("constraints/indicator.jl") 28 | include("constraints/reified.jl") 29 | include("constraints/geqset.jl") 30 | end 31 | -------------------------------------------------------------------------------- /test/refs/niallsudoku_5503_negative: -------------------------------------------------------------------------------- 1 | [[1,2,3,4,5,6,7,8,9],[10,11,12,13,14,15,16,17,18],[19,20,21,22,23,24,25,26,27],[28,29,30,31,32,33,34,35,36],[37,38,39,40,41,42,43,44,45],[46,47,48,49,50,51,52,53,54],[55,56,57,58,59,60,61,62,63],[64,65,66,67,68,69,70,71,72],[73,74,75,76,77,78,79,80,81],[1,10,19,28,37,46,55,64,73],[2,11,20,29,38,47,56,65,74],[3,12,21,30,39,48,57,66,75],[4,13,22,31,40,49,58,67,76],[5,14,23,32,41,50,59,68,77],[6,15,24,33,42,51,60,69,78],[7,16,25,34,43,52,61,70,79],[8,17,26,35,44,53,62,71,80],[9,18,27,36,45,54,63,72,81],[1,10,19,2,11,20,3,12,21],[4,13,22,5,14,23,6,15,24],[7,16,25,8,17,26,9,18,27],[28,37,46,29,38,47,30,39,48],[31,40,49,32,41,50,33,42,51],[34,43,52,35,44,53,36,45,54],[55,64,73,56,65,74,57,66,75],[58,67,76,59,68,77,60,69,78],[61,70,79,62,71,80,63,72,81],[1,2,10,11,20,29],[3,4,5,6],[7,8,17],[9,18,27],[12,21,30],[19,28],[22,31,39,40],[13,14,15,16],[23,32,41,50,59],[24,25,26,33],[34,35,36,44,45],[37,38,46,47,48],[42,43,51,60],[49,56,57,58],[52,61,70],[53,62,71,72,80,81],[54,63],[55,64,73],[65,74,75],[66,67,68,69],[76,77,78,79],[1,10,37,46],[5,14,68,77],[36,45,72,81],[16,25,26],[28,29,30,39],[43,52,53,54],[56,57,66]] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ole Kröger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | # ConstraintSolver.jl 2 | 3 | Thanks for checking out the documentation of this constraint solver. 4 | The documentation is written in four different sections based on [this post about how to write documentation](https://www.divio.com/blog/documentation/). 5 | 6 | - If you want to get a quick overview and just have a look at examples check out the [tutorial](tutorial.md). 7 | - You just have some `How to` questions? -> [How to guide](how_to.md) 8 | - Which constraints and objectives are supported? -> [Supported constraints/objectives](supported.md) 9 | - What solver options do exist? -> [Solver options](options.md) 10 | - You want to understand how it works deep down? Maybe improve it ;) -> [Explanation](explanation.md) 11 | - Gimme the code documentation directly! The [reference](reference.md) section got you covered (It's not much currently) 12 | 13 | If you have some questions please feel free to ask me by making an [issue](https://github.com/Wikunia/ConstraintSolver.jl/issues). 14 | 15 | You might be interested in the process of how I coded this: Checkout the full process on my blog [opensourc.es](https://opensourc.es/blog/constraint-solver-1). 16 | 17 | -------------------------------------------------------------------------------- /src/MOI_wrapper/results.jl: -------------------------------------------------------------------------------- 1 | function MOI.get(model::Optimizer, ::MOI.TerminationStatus) 2 | return model.status 3 | end 4 | 5 | function MOI.get(model::Optimizer, ov::MOI.ObjectiveValue) 6 | return model.inner.solutions[ov.result_index].incumbent 7 | end 8 | 9 | function MOI.get(model::Optimizer, ::MOI.ObjectiveBound) 10 | return model.inner.best_bound 11 | end 12 | 13 | function MOI.get(model::Optimizer, ::MOI.ResultCount) 14 | return length(model.inner.solutions) 15 | end 16 | 17 | function MOI.get(model::Optimizer, vp::MOI.VariablePrimal, vi::MOI.VariableIndex) 18 | check_inbounds(model, vi) 19 | return model.inner.solutions[vp.result_index].values[vi.value] 20 | end 21 | 22 | function set_status!(model::Optimizer, status::Symbol) 23 | if status == :Solved 24 | model.status = MOI.OPTIMAL 25 | elseif status == :Infeasible 26 | model.status = MOI.INFEASIBLE 27 | elseif status == :Time 28 | model.status = MOI.TIME_LIMIT 29 | else 30 | model.status = MOI.OTHER_LIMIT 31 | end 32 | end 33 | 34 | function MOI.get(model::Optimizer, ::MOI.SolveTimeSec) 35 | return model.inner.solve_time 36 | end 37 | -------------------------------------------------------------------------------- /benchmark/lp/benchmark.jl: -------------------------------------------------------------------------------- 1 | function solve_lp() 2 | glpk_optimizer = 3 | optimizer_with_attributes(GLPK.Optimizer, "msg_lev" => GLPK.GLP_MSG_OFF) 4 | model = Model(optimizer_with_attributes( 5 | CS.Optimizer, 6 | "lp_optimizer" => glpk_optimizer, 7 | "logging" => [], 8 | "seed" => 1, 9 | )) 10 | 11 | # Variables 12 | @variable(model, inclusion[h = 1:3], Bin) 13 | @variable(model, 0 <= allocations[h = 1:3, a = 1:3] <= 1, Int) 14 | @variable(model, 0 <= days[h = 1:3, a = 1:3] <= 5, Int) 15 | 16 | # Constraints 17 | @constraint( 18 | model, 19 | must_include[h = 1:3], 20 | sum(allocations[h, a] for a in 1:3) <= inclusion[h] 21 | ) 22 | # at least n 23 | @constraint(model, min_hospitals, sum(inclusion[h] for h in 1:3) >= 3) 24 | # every h must be allocated at most one a 25 | @constraint(model, must_visit[h = 1:3], sum(allocations[h, a] for a in 1:3) <= 1) 26 | # every allocated h must have fewer than 5 days of visits per week 27 | @constraint( 28 | model, 29 | max_visits[h = 1:3], 30 | sum(days[h, a] for a in 1:3) <= 5 * inclusion[h] 31 | ) 32 | 33 | @objective(model, Max, sum(days[h, a] * 5 for h in 1:3, a in 1:3)) 34 | optimize!(model) 35 | end 36 | -------------------------------------------------------------------------------- /src/MOI_wrapper/objective.jl: -------------------------------------------------------------------------------- 1 | MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true 2 | MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{VI}) = true 3 | MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{SAF{T}}) where {T<:Real} = true 4 | 5 | """ 6 | set and get function overloads 7 | """ 8 | MOI.get(model::Optimizer, ::MOI.ObjectiveSense) = model.inner.sense 9 | 10 | function MOI.set(model::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense) 11 | model.inner.sense = sense 12 | return 13 | end 14 | 15 | function MOI.set(model::Optimizer, ::MOI.ObjectiveFunction, func::VI) 16 | check_inbounds(model, func) 17 | model.inner.var_in_obj[func.value] = true 18 | model.inner.objective = 19 | SingleVariableObjective(func, func.value, [func.value]) 20 | return 21 | end 22 | 23 | function MOI.set(model::Optimizer, ::MOI.ObjectiveFunction, func::SAF{T}) where {T<:Real} 24 | check_inbounds(model, func) 25 | indices = [func.terms[i].variable.value for i in 1:length(func.terms)] 26 | coeffs = [func.terms[i].coefficient for i in 1:length(func.terms)] 27 | lc = LinearCombination(indices, coeffs) 28 | model.inner.var_in_obj[indices] .= true 29 | model.inner.objective = LinearCombinationObjective(func, lc, func.constant, indices) 30 | return 31 | end 32 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "ConstraintSolver" 2 | uuid = "e0e52ebd-5523-408d-9ca3-7641f1cd1405" 3 | authors = ["Ole Kröger "] 4 | version = "0.9.2" 5 | 6 | [deps] 7 | ConstraintProgrammingExtensions = "b65d079e-ed98-51d9-b0db-edee61a5c5f8" 8 | DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" 9 | Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" 10 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 11 | JuMP = "4076af6c-e467-56ae-b986-b466b2749572" 12 | LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d" 13 | MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" 14 | MatrixNetworks = "4f449596-a032-5618-b826-5a251cb6dc11" 15 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 16 | Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 17 | StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" 18 | StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" 19 | TableLogger = "72b659bb-f61b-4d0d-9dbb-0f81f57d8545" 20 | 21 | [compat] 22 | ConstraintProgrammingExtensions = "^0.6" 23 | DataStructures = "~0.11, ~0.12, ~0.13, ~0.14, ~0.15, ~0.16, ~0.17, ~0.18" 24 | Formatting = "^0.4.1" 25 | JSON = "~0.18, ~0.19, ~0.20, ~0.21" 26 | JuMP = "^0.22, 0.23, 1" 27 | LightGraphs = "1" 28 | MathOptInterface = "^0.10, 1" 29 | MatrixNetworks = "^1" 30 | StatsBase = "^0.33" 31 | StatsFuns = "^0.9.5" 32 | TableLogger = "^0.1" 33 | julia = "^1.6" 34 | -------------------------------------------------------------------------------- /test/data/killer_niallsudoku_5503: -------------------------------------------------------------------------------- 1 | [{"color":"blue","result":28,"indices":[[1,1],[1,2],[2,1],[2,2],[3,2],[4,2]]},{"color":"yellow","result":14,"indices":[[1,3],[1,4],[1,5],[1,6]]},{"color":"green","result":13,"indices":[[1,7],[1,8],[2,8]]},{"color":"yellow","result":21,"indices":[[1,9],[2,9],[3,9]]},{"color":"green","result":22,"indices":[[2,3],[3,3],[4,3]]},{"color":"yellow","result":11,"indices":[[3,1],[4,1]]},{"color":"yellow","result":27,"indices":[[3,4],[4,4],[5,3],[5,4]]},{"color":"blue","result":14,"indices":[[2,4],[2,5],[2,6],[2,7]]},{"color":"green","result":34,"indices":[[3,5],[4,5],[5,5],[6,5],[7,5]]},{"color":"pink","result":19,"indices":[[3,6],[3,7],[3,8],[4,6]]},{"color":"blue","result":16,"indices":[[4,7],[4,8],[4,9],[5,8],[5,9]]},{"color":"pink","result":25,"indices":[[5,1],[5,2],[6,1],[6,2],[6,3]]},{"color":"yellow","result":11,"indices":[[5,6],[5,7],[6,6],[7,6]]},{"color":"blue","result":17,"indices":[[6,4],[7,2],[7,3],[7,4]]},{"color":"green","result":18,"indices":[[6,7],[7,7],[8,7]]},{"color":"pink","result":32,"indices":[[6,8],[7,8],[8,8],[8,9],[9,8],[9,9]]},{"color":"yellow","result":12,"indices":[[6,9],[7,9]]},{"color":"yellow","result":8,"indices":[[7,1],[8,1],[9,1]]},{"color":"green","result":17,"indices":[[8,2],[9,2],[9,3]]},{"color":"pink","result":24,"indices":[[8,3],[8,4],[8,5],[8,6]]},{"color":"blue","result":22,"indices":[[9,4],[9,5],[9,6],[9,7]]}] -------------------------------------------------------------------------------- /benchmark/killer_sudoku/data/niallsudoku_5503: -------------------------------------------------------------------------------- 1 | [{"color":"blue","result":28,"indices":[[1,1],[1,2],[2,1],[2,2],[3,2],[4,2]]},{"color":"yellow","result":14,"indices":[[1,3],[1,4],[1,5],[1,6]]},{"color":"green","result":13,"indices":[[1,7],[1,8],[2,8]]},{"color":"yellow","result":21,"indices":[[1,9],[2,9],[3,9]]},{"color":"green","result":22,"indices":[[2,3],[3,3],[4,3]]},{"color":"yellow","result":11,"indices":[[3,1],[4,1]]},{"color":"yellow","result":27,"indices":[[3,4],[4,4],[5,3],[5,4]]},{"color":"blue","result":14,"indices":[[2,4],[2,5],[2,6],[2,7]]},{"color":"green","result":34,"indices":[[3,5],[4,5],[5,5],[6,5],[7,5]]},{"color":"pink","result":19,"indices":[[3,6],[3,7],[3,8],[4,6]]},{"color":"blue","result":16,"indices":[[4,7],[4,8],[4,9],[5,8],[5,9]]},{"color":"pink","result":25,"indices":[[5,1],[5,2],[6,1],[6,2],[6,3]]},{"color":"yellow","result":11,"indices":[[5,6],[5,7],[6,6],[7,6]]},{"color":"blue","result":17,"indices":[[6,4],[7,2],[7,3],[7,4]]},{"color":"green","result":18,"indices":[[6,7],[7,7],[8,7]]},{"color":"pink","result":32,"indices":[[6,8],[7,8],[8,8],[8,9],[9,8],[9,9]]},{"color":"yellow","result":12,"indices":[[6,9],[7,9]]},{"color":"yellow","result":8,"indices":[[7,1],[8,1],[9,1]]},{"color":"green","result":17,"indices":[[8,2],[9,2],[9,3]]},{"color":"pink","result":24,"indices":[[8,3],[8,4],[8,5],[8,6]]},{"color":"blue","result":22,"indices":[[9,4],[9,5],[9,6],[9,7]]}] -------------------------------------------------------------------------------- /benchmark/killer_sudoku/plot.jl: -------------------------------------------------------------------------------- 1 | using Plots 2 | using ConstraintSolver 3 | dir = pkgdir(ConstraintSolver) 4 | 5 | x = [ 6 | "niallsudoku_5500", 7 | "niallsudoku_5501", 8 | "niallsudoku_5502", 9 | "niallsudoku_5503", 10 | "niallsudoku_6417", 11 | "niallsudoku_6249", 12 | ]; 13 | 14 | plot(; xaxis = ("Problem"), yaxis = ("Time in s"), title = "Killer sudoku special") 15 | cs_010 = [0.73, 3.69, 0.16, 0.066, 0.13, 0.06] 16 | plot!(x, cs_010, label = "CS v0.1.0", color = :red, seriestype = :scatter) 17 | cs_017 = [0.607, 1.602, 0.173, 0.08, 0.27, 0.09] 18 | plot!(x, cs_017, label = "CS v0.1.7", color = :orange, seriestype = :scatter) 19 | or = [2.82, 1.67, 0.46, 0.74, 0.17, 0.17]; 20 | plot!(x, or, label = "OR-Tools", color = :black, seriestype = :scatter) 21 | 22 | savefig(joinpath(dir, "benchmark/results/killer_sudoku_special/plots/current.png")) 23 | 24 | 25 | plot(; xaxis = ("Problem"), yaxis = ("Time in s"), title = "Killer sudoku normal rules") 26 | cs_010 = [0.16, 0.39, 0.09, 0.39, 0.30, 0.05] 27 | plot!(x, cs_010, label = "CS v0.1.0", color = :red, seriestype = :scatter) 28 | cs_017 = [0.096, 0.50, 0.095, 0.50, 0.48, 0.06] 29 | plot!(x, cs_017, label = "CS v0.1.7", color = :orange, seriestype = :scatter) 30 | or = [1.18, 1.64, 0.37, 1.07, 0.04, 0.04]; 31 | plot!(x, or, label = "OR-Tools", color = :black, seriestype = :scatter) 32 | 33 | savefig(joinpath(dir, "benchmark/results/killer_sudoku/plots/current.png")) 34 | -------------------------------------------------------------------------------- /benchmark/killer_sudoku/data/niallsudoku_5502: -------------------------------------------------------------------------------- 1 | [{"color":"blue","result":5,"indices":[[1,1],[2,1]]},{"color":"yellow","result":18,"indices":[[1,2],[2,2],[3,2],[4,2]]},{"color":"blue","result":29,"indices":[[1,3],[1,4],[1,5],[1,6],[1,7],[2,5]]},{"color":"yellow","result":23,"indices":[[1,8],[2,8],[3,8],[4,8]]},{"color":"blue","result":17,"indices":[[1,9],[2,9]]},{"color":"green","result":9,"indices":[[2,3],[3,3]]},{"color":"yellow","result":14,"indices":[[2,4],[3,4],[4,4]]},{"color":"yellow","result":16,"indices":[[2,6],[3,6],[4,6]]},{"color":"green","result":5,"indices":[[2,7],[3,7]]},{"color":"green","result":29,"indices":[[3,1],[4,1],[5,1],[5,2]]},{"color":"green","result":14,"indices":[[3,5],[4,5]]},{"color":"green","result":14,"indices":[[3,9],[4,9],[5,8],[5,9]]},{"color":"pink","result":15,"indices":[[4,3],[5,3]]},{"color":"pink","result":5,"indices":[[4,7],[5,7]]},{"color":"blue","result":22,"indices":[[5,4],[6,3],[6,4],[7,3],[7,4]]},{"color":"pink","result":31,"indices":[[5,5],[6,5],[7,5],[8,4],[8,5],[8,6]]},{"color":"blue","result":30,"indices":[[5,6],[6,6],[6,7],[7,6],[7,7]]},{"color":"yellow","result":21,"indices":[[6,1],[6,2],[7,1],[7,2]]},{"color":"yellow","result":20,"indices":[[6,8],[6,9],[7,8],[7,9]]},{"color":"blue","result":9,"indices":[[8,1],[9,1]]},{"color":"green","result":17,"indices":[[8,2],[8,3],[9,2],[9,3]]},{"color":"blue","result":9,"indices":[[9,4],[9,5],[9,6]]},{"color":"green","result":27,"indices":[[8,7],[8,8],[9,7],[9,8]]},{"color":"blue","result":6,"indices":[[8,9],[9,9]]}] -------------------------------------------------------------------------------- /benchmark/sudoku/results/norvig.csv: -------------------------------------------------------------------------------- 1 | 0, 0.0072 2 | 1, 0.0299 3 | 2, 0.0128 4 | 3, 0.0454 5 | 4, 0.0169 6 | 5, 0.0515 7 | 6, 0.0065 8 | 7, 0.0042 9 | 8, 0.0077 10 | 9, 0.0399 11 | 10, 0.0141 12 | 11, 0.0243 13 | 12, 0.0252 14 | 13, 0.0123 15 | 14, 0.0178 16 | 15, 0.0046 17 | 16, 0.0056 18 | 17, 0.0041 19 | 18, 0.0051 20 | 19, 0.0146 21 | 20, 0.0738 22 | 21, 0.0041 23 | 22, 0.0109 24 | 23, 0.0088 25 | 24, 0.0083 26 | 25, 0.0189 27 | 26, 0.0076 28 | 27, 0.0080 29 | 28, 0.0040 30 | 29, 0.0188 31 | 30, 0.0115 32 | 31, 0.0061 33 | 32, 0.0056 34 | 33, 0.0120 35 | 34, 0.0083 36 | 35, 0.0064 37 | 36, 0.0235 38 | 37, 0.0056 39 | 38, 0.0079 40 | 39, 0.0087 41 | 40, 0.0063 42 | 41, 0.0255 43 | 42, 0.0084 44 | 43, 0.0094 45 | 44, 0.0107 46 | 45, 0.0081 47 | 46, 0.0136 48 | 47, 0.0064 49 | 48, 0.0585 50 | 49, 0.0132 51 | 50, 0.0068 52 | 51, 0.0039 53 | 52, 0.0054 54 | 53, 0.0058 55 | 54, 0.0100 56 | 55, 0.0096 57 | 56, 0.0079 58 | 57, 0.0036 59 | 58, 0.0138 60 | 59, 0.0062 61 | 60, 0.0146 62 | 61, 0.0161 63 | 62, 0.0049 64 | 63, 0.0133 65 | 64, 0.0035 66 | 65, 0.0110 67 | 66, 0.0193 68 | 67, 0.0197 69 | 68, 0.0144 70 | 69, 0.0093 71 | 70, 0.0388 72 | 71, 0.0055 73 | 72, 0.0212 74 | 73, 0.0054 75 | 74, 0.0100 76 | 75, 0.0154 77 | 76, 0.0060 78 | 77, 0.0038 79 | 78, 0.0247 80 | 79, 0.0064 81 | 80, 0.0073 82 | 81, 0.0063 83 | 82, 0.0051 84 | 83, 0.0070 85 | 84, 0.0059 86 | 85, 0.0036 87 | 86, 0.0227 88 | 87, 0.0044 89 | 88, 0.0153 90 | 89, 0.0042 91 | 90, 0.0063 92 | 91, 0.0115 93 | 92, 0.0071 94 | 93, 0.0070 95 | 94, 0.0284 -------------------------------------------------------------------------------- /benchmark/sudoku/results/python-constraint.csv: -------------------------------------------------------------------------------- 1 | 0, 0.1777 2 | 1, 0.0503 3 | 2, 0.0496 4 | 3, 2.1337 5 | 4, 3.4785 6 | 5, 1.7738 7 | 6, 9.7647 8 | 7, 8.2095 9 | 8, 8.0460 10 | 9, 0.9260 11 | 10, 5.6032 12 | 11, 0.2793 13 | 12, 0.2917 14 | 13, 0.0374 15 | 14, 0.2318 16 | 15, 7.0538 17 | 16, 0.0135 18 | 17, 21.8093 19 | 18, 0.0555 20 | 19, 0.0436 21 | 20, 1.1214 22 | 21, 0.1089 23 | 22, 0.1506 24 | 23, 0.2848 25 | 24, 0.3419 26 | 25, 0.1800 27 | 26, 0.7029 28 | 27, 16.2563 29 | 28, 0.0547 30 | 29, 1.3863 31 | 30, 0.0679 32 | 31, 0.0461 33 | 32, 0.0099 34 | 33, 1.6513 35 | 34, 0.0335 36 | 35, 0.0878 37 | 36, 0.3857 38 | 37, 0.1345 39 | 38, 0.0317 40 | 39, 0.0808 41 | 40, 9.6367 42 | 41, 1.1709 43 | 42, 0.0350 44 | 43, 0.2527 45 | 44, 0.1014 46 | 45, 0.3533 47 | 46, 0.3524 48 | 47, 0.2646 49 | 48, 0.0156 50 | 49, 1.5874 51 | 50, 0.1008 52 | 51, 0.7063 53 | 52, 0.2325 54 | 53, 0.1235 55 | 54, 0.2198 56 | 55, 0.0428 57 | 56, 1.4222 58 | 57, 0.2239 59 | 58, 0.1231 60 | 59, 0.3332 61 | 60, 0.1092 62 | 61, 0.2849 63 | 62, 0.2275 64 | 63, 0.2458 65 | 64, 0.0951 66 | 65, 0.5382 67 | 66, 1.3849 68 | 67, 0.9488 69 | 68, 0.0481 70 | 69, 0.2706 71 | 70, 2.5113 72 | 71, 0.1672 73 | 72, 1.2528 74 | 73, 0.4949 75 | 74, 0.0810 76 | 75, 0.3745 77 | 76, 0.1254 78 | 77, 0.3838 79 | 78, 1.1446 80 | 79, 0.0207 81 | 80, 0.2253 82 | 81, 0.0778 83 | 82, 0.9070 84 | 83, 0.0735 85 | 84, 0.0285 86 | 85, 0.0933 87 | 86, 0.3980 88 | 87, 0.1267 89 | 88, 0.0396 90 | 89, 0.0848 91 | 90, 0.0315 92 | 91, 0.0895 93 | 92, 0.0523 94 | 93, 0.0878 95 | 94, 0.0869 -------------------------------------------------------------------------------- /src/MOI_wrapper/Bridges/util.jl: -------------------------------------------------------------------------------- 1 | #= 2 | Support for >= and > 3 | =# 4 | const UnionGT{T} = Union{CPE.Strictly{MOI.GreaterThan{T}, T}, MOI.GreaterThan{T}} 5 | 6 | function supports_concreteB(concrete_B) 7 | added_constraints = MOIB.added_constraint_types(concrete_B) 8 | length(added_constraints) > 1 && return false 9 | # The inner constraint should not create any variable (might have unexpected consequences) 10 | return isempty(MOIB.added_constrained_variable_types(concrete_B)) 11 | end 12 | 13 | function get_num_vars(fct::SAF) 14 | return length(fct.terms) 15 | end 16 | 17 | function get_num_vars(fct::VAF) 18 | return length(fct.terms) 19 | end 20 | 21 | function get_num_vars(fct::MOI.VectorOfVariables) 22 | return length(fct.variables) 23 | end 24 | 25 | """ 26 | map_function(bridge, fct, set) 27 | 28 | Default for when set is not needed so return `MOIB.map_function(bridge, fct)` 29 | If the set is needed a specific method should be implemented 30 | """ 31 | function map_function( 32 | bridge::Type{<:MOIBC.AbstractBridge}, 33 | fct, 34 | set 35 | ) 36 | MOIB.map_function(bridge, fct) 37 | end 38 | 39 | """ 40 | added_constraint_types(bridge, set) 41 | 42 | Default for when set is not needed so return `MOIBC.added_constraint_types(bridge)` 43 | If the set is needed a specific method should be implemented 44 | """ 45 | function added_constraint_types( 46 | B::Type{<:MOIBC.AbstractBridge}, 47 | S, 48 | ) 49 | return MOIB.added_constraint_types(B) 50 | end -------------------------------------------------------------------------------- /benchmark/results2csvs.jl: -------------------------------------------------------------------------------- 1 | # convert folders in results into single csvs 2 | # if the last line is not a result line => ignored 3 | using DataFrames, CSV 4 | 5 | function create_csv() 6 | root_dir = "graph_color/results" 7 | max_time = 1800 8 | 9 | 10 | for dir in readdir(root_dir) 11 | !isdir(joinpath(root_dir, dir)) && continue 12 | df = DataFrame( 13 | "instance" => String[], 14 | "status" => String[], 15 | "result" => Float64[], 16 | "time" => Float64[], 17 | ) 18 | for (root, dirs, files) in walkdir(joinpath(root_dir, dir)) 19 | pnames = joinpath.(root, files) # files is a Vector{String}, can be empty 20 | for pname in pnames 21 | parts = split(pname, "/") 22 | if parts[end] == "stdout" 23 | instance = parts[end - 1] 24 | lines = readlines(pname) 25 | isempty(lines) && continue 26 | result = lines[end] 27 | result_parts = split(result, ", ") 28 | length(result_parts) != 3 && continue 29 | status, result_str, time_str = result_parts 30 | result = parse(Float64, result_str) 31 | time = parse(Float64, time_str) 32 | push!(df, (instance, status, result, time)) 33 | end 34 | end 35 | CSV.write(joinpath(root_dir, "$dir.csv"), df) 36 | end 37 | end 38 | end 39 | 40 | create_csv() 41 | -------------------------------------------------------------------------------- /test/constraints/equal_to.jl: -------------------------------------------------------------------------------- 1 | @testset "EqualTo Constraint" begin 2 | @testset "digits form a number (Issue 200)" begin 3 | use_diff = false 4 | model = Model(optimizer_with_attributes( 5 | CS.Optimizer, 6 | "traverse_strategy" => :DBFS, 7 | "branch_split" => :InHalf, 8 | "logging" => [], 9 | )) 10 | 11 | @variable(model, 0 <= x[1:10] <= 9, Int) 12 | @variable(model, 10000 <= v[1:2] <= 99999, Int) # ABCDE and FGHIJ 13 | if use_diff 14 | @variable(model, 0 <= diff <= 999, Int) # adding this is much slower than using v[1:2] only 15 | end 16 | 17 | a, b, c, d, e, f, g, h, i, j = x 18 | @constraint(model, x in CS.AllDifferent()) 19 | @constraint(model, v[1] == 10000 * a + 1000 * b + 100 * c + 10 * d + e) # ABCDE 20 | @constraint(model, v[2] == 10000 * f + 1000 * g + 100 * h + 10 * i + j) # FGHIJ 21 | 22 | # Using diff is slower 23 | if use_diff 24 | @constraint(model, diff == v[1] - v[2]) # much slower 25 | @constraint(model, diff >= 1) 26 | else 27 | @constraint(model, v[1] - v[2] >= 1) 28 | end 29 | 30 | if use_diff 31 | @objective(model, Min, diff) # slower 32 | else 33 | @objective(model, Min, v[1] - v[2]) 34 | end 35 | 36 | # Solve the problem 37 | optimize!(model) 38 | 39 | status = JuMP.termination_status(model) 40 | @test status == MOI.OPTIMAL 41 | x_vals = convert.(Int, JuMP.value.(x)) 42 | @test x_vals == [5, 0, 1, 2, 3, 4, 9, 8, 7, 6] 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /benchmark/graph_color/cs.jl: -------------------------------------------------------------------------------- 1 | using ConstraintSolver, JuMP, GLPK 2 | CS = ConstraintSolver 3 | 4 | function main(filename; benchmark = false, time_limit = 100) 5 | lp_optimizer = optimizer_with_attributes(GLPK.Optimizer, "msg_lev" => GLPK.GLP_MSG_OFF) 6 | m = Model(optimizer_with_attributes( 7 | CS.Optimizer, 8 | "time_limit" => time_limit, 9 | "lp_optimizer" => lp_optimizer, 10 | )) 11 | 12 | lines = readlines(filename) 13 | num_colors = 0 14 | x = nothing 15 | max_color = nothing 16 | degrees = nothing 17 | for line in lines 18 | isempty(line) && continue 19 | parts = split(line) 20 | if parts[1] == "p" 21 | num_colors = parse(Int, parts[3]) 22 | @variable(m, 1 <= max_color <= num_colors, Int) 23 | @variable(m, 1 <= x[1:num_colors] <= num_colors, Int) 24 | degrees = zeros(Int, num_colors) 25 | elseif parts[1] == "e" 26 | f = parse(Int, parts[2]) 27 | t = parse(Int, parts[3]) 28 | @constraint(m, x[f] != x[t]) 29 | degrees[f] += 1 30 | degrees[t] += 1 31 | end 32 | end 33 | max_degree = maximum(degrees) 34 | 35 | println("max_degree: ", max_degree) 36 | println("num_colors: ", num_colors) 37 | @constraint(m, max_color <= max_degree) 38 | 39 | @constraint(m, max_color .>= x) 40 | @objective(m, Min, max_color) 41 | 42 | optimize!(m) 43 | 44 | status = JuMP.termination_status(m) 45 | if status == MOI.OPTIMAL 46 | print("$status, $(JuMP.objective_value(m)), $(JuMP.solve_time(m))") 47 | else 48 | print("$status, NaN, $(time_limit)") 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /benchmark/sudoku/sudoku-bb.py: -------------------------------------------------------------------------------- 1 | # Boris Borcic 2006 2 | # Quick and concise Python 2.5 sudoku solver 3 | # https://raw.githubusercontent.com/attractivechaos/plb/master/sudoku/incoming/sudoku-bb.py 4 | 5 | import time 6 | 7 | w2q = [[n/9,n/81*9+n%9+81,n%81+162,n%9*9+n/243*3+n/27%3+243] for n in range(729)] 8 | q2w = (z[1] for z in sorted((x,y) for y,s in enumerate(w2q) for x in s)) 9 | q2w = map(set,zip(*9*[q2w])) 10 | w2q2w = [set(w for q in qL for w in q2w[q]) for qL in w2q] 11 | 12 | class Completed(Exception) : pass 13 | 14 | def sudoku99(problem) : 15 | givens = list(9*j+int(k)-1 for j,k in enumerate(problem[:81]) if '0'80 : 35 | raise Completed(ws) 36 | w1,w0 = q2w[q2nw.index(2)]-takens 37 | try : search([w1],q2nw[:],takens.copy(),ws.copy()) 38 | except KeyError : 39 | w0s.append(w0) 40 | 41 | def from_file(filename, sep='\n'): 42 | "Parse a file into a list of strings, separated by sep." 43 | return open(filename).read().strip().split(sep) 44 | 45 | import sys 46 | if __name__ == '__main__' : 47 | grids = from_file("top95.txt") 48 | i = 0 49 | for grid in grids: 50 | t = time.time() 51 | sudoku99(grid) 52 | t = time.time()-t 53 | print i,", ", t 54 | i += 1 -------------------------------------------------------------------------------- /test/maximum_weight_matching.jl: -------------------------------------------------------------------------------- 1 | @testset "Maximum weighted matching" begin 2 | function neighbors(sym_matrix, i) 3 | return findall(v -> v != 0, sym_matrix[:, i]) 4 | end 5 | 6 | @testset "Bipartite example" begin 7 | 8 | # from slide 65 of https://www.slideshare.net/KuoE0/acmicpc-bipartite-matching 9 | # but set 1 <-> 4 to 4.9 to avoid matching 1 <-> 4 and 3 <-> 5 which also sums to 10 10 | weight_matrix = [ 11 | 0 0 0 4.9 0 2 12 | 0 0 0 3 1 -2 13 | 0 0 0 0 5 0 14 | 4.9 3 5 0 0 0 15 | 0 1 0 0 0 0 16 | 2 -2 0 0 0 0 17 | ] 18 | n = 6 19 | 20 | m = Model(CSJuMPTestOptimizer()) 21 | @variable(m, x[1:n, 1:n], Bin) 22 | for i in 1:n, j in 1:n 23 | if weight_matrix[i, j] == 0 || i > j 24 | @constraint(m, x[i, j] == 0) 25 | end 26 | end 27 | for i in 1:n 28 | @constraint( 29 | m, 30 | sum(x[min(i, j), max(i, j)] for j in neighbors(weight_matrix, i)) <= 1 31 | ) 32 | end 33 | 34 | @objective(m, Max, sum(weight_matrix .* x)) 35 | optimize!(m) 36 | @test JuMP.objective_value(m) ≈ 10 37 | @test JuMP.value(x[1, 6]) == 1 38 | @test JuMP.value(x[2, 4]) == 1 39 | @test JuMP.value(x[3, 5]) == 1 40 | @test sum(JuMP.value.(x[1, 1:6])) == 1 41 | @test sum(JuMP.value.(x[2, 1:6])) == 1 42 | @test sum(JuMP.value.(x[3, 1:6])) == 1 43 | @test sum(JuMP.value.(x[4, 1:6])) == 0 44 | @test sum(JuMP.value.(x[5, 1:6])) == 0 45 | @test sum(JuMP.value.(x[6, 1:6])) == 0 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /benchmark/killer_sudoku/or-tools.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import numpy as np 4 | import json 5 | 6 | from ortools.sat.python import cp_model 7 | 8 | def parse_problem(filename): 9 | json_txt = open("data/"+filename).read() 10 | problem = json.loads(json_txt) 11 | return problem 12 | 13 | def solve(pidx, filename): 14 | problem = parse_problem(filename) 15 | 16 | n = 9 17 | 18 | # Create model 19 | model = cp_model.CpModel() 20 | 21 | # variables 22 | x = {} 23 | for i in range(n): 24 | for j in range(n): 25 | x[i, j] = model.NewIntVar(1, n, "x[%i,%i]" % (i, j)) 26 | 27 | x_flat = [x[i, j] for i in range(n) for j in range(n)] 28 | 29 | # all rows and columns must be unique 30 | for i in range(n): 31 | row = [x[i, j] for j in range(n)] 32 | model.AddAllDifferent(row) 33 | 34 | col = [x[j, i] for j in range(n)] 35 | model.AddAllDifferent(col) 36 | 37 | # cells 38 | for i in range(2): 39 | for j in range(2): 40 | cell = [x[r, c] 41 | for r in range(i * 3, i * 3 + 3) 42 | for c in range(j * 3, j * 3 + 3)] 43 | model.AddAllDifferent(cell) 44 | 45 | for s in problem: 46 | model.Add(sum(x[i[0]-1,i[1]-1] for i in s["indices"]) == s["result"]) 47 | model.AddAllDifferent([x[i[0]-1,i[1]-1] for i in s["indices"]]) 48 | 49 | 50 | 51 | # search and solution 52 | solver = cp_model.CpSolver() 53 | status = solver.Solve(model) 54 | print(str(pidx)+",",solver.WallTime()) 55 | 56 | if __name__ == "__main__": 57 | i = 0 58 | for filename in ["niallsudoku_5500", "niallsudoku_5501", "niallsudoku_5502", "niallsudoku_5503", "niallsudoku_6417", 59 | "niallsudoku_6249"]: 60 | solve(i, filename) 61 | i += 1 -------------------------------------------------------------------------------- /benchmark/graph_color/or_tools_file.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import numpy as np 4 | import json 5 | 6 | from ortools.sat.python import cp_model 7 | 8 | def solve(pidx, filename): 9 | # Open the file with read only permit 10 | f = open(filename, "r") 11 | # use readlines to read all lines in the file 12 | # The variable "lines" is a list containing all lines in the file 13 | lines = f.readlines() 14 | # close the file after reading the lines. 15 | f.close() 16 | 17 | 18 | # Create model 19 | model = cp_model.CpModel() 20 | 21 | x =[] 22 | num_colors = 0 23 | for line in lines: 24 | parts = line.strip().split(" ") 25 | if parts[0] == 'p': 26 | num_colors = int(parts[2]) 27 | for i in range(num_colors): 28 | x.append(model.NewIntVar(1, num_colors, str(i))) 29 | elif parts[0] == 'e': 30 | f = int(parts[1])-1 31 | t = int(parts[2])-1 32 | model.Add(x[f] != x[t]) 33 | 34 | max_color = model.NewIntVar(1, num_colors, "max_color") 35 | 36 | model.AddMaxEquality(max_color, x) 37 | 38 | model.Minimize(max_color) 39 | 40 | # search and solution 41 | solver = cp_model.CpSolver() 42 | status = solver.Solve(model) 43 | print(str(pidx)+",",solver.WallTime()) 44 | print("Status: ", status) 45 | print("Opt status: ", cp_model.OPTIMAL) 46 | print('Minimum of objective function: %i' % solver.ObjectiveValue()) 47 | print() 48 | # for state in x: 49 | # print(solver.Value(state)) 50 | 51 | """ 52 | if status == cp_model.OPTIMAL: 53 | print('Minimum of objective function: %i' % solver.ObjectiveValue()) 54 | print() 55 | for state in states: 56 | print(solver.Value(state)) 57 | """ 58 | 59 | if __name__ == "__main__": 60 | i = 0 61 | solve(i, "data/fpsol2.i.1.col") -------------------------------------------------------------------------------- /benchmark/sudoku/or-tools.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import numpy as np 4 | 5 | from ortools.sat.python import cp_model 6 | 7 | def from_file(filename, sep='\n'): 8 | "Parse a file into a list of strings, separated by sep." 9 | lines = open(filename).read().strip().split(sep) 10 | grids = [] 11 | for line in lines: 12 | line = line.replace(".","0") 13 | grid = list(line) 14 | grid = list(map(int, grid)) 15 | grid = np.reshape(grid, (9,9)) 16 | grids.append(grid.tolist()) 17 | return grids 18 | 19 | def solve(pidx, problem): 20 | n = 9 21 | 22 | # Create model 23 | model = cp_model.CpModel() 24 | 25 | # variables 26 | x = {} 27 | for i in range(n): 28 | for j in range(n): 29 | x[i, j] = model.NewIntVar(1, n, "x[%i,%i]" % (i, j)) 30 | 31 | x_flat = [x[i, j] for i in range(n) for j in range(n)] 32 | 33 | # all rows and columns must be unique 34 | for i in range(n): 35 | row = [x[i, j] for j in range(n)] 36 | model.AddAllDifferent(row) 37 | 38 | col = [x[j, i] for j in range(n)] 39 | model.AddAllDifferent(col) 40 | 41 | # cells 42 | for i in range(2): 43 | for j in range(2): 44 | cell = [x[r, c] 45 | for r in range(i * 3, i * 3 + 3) 46 | for c in range(j * 3, j * 3 + 3)] 47 | model.AddAllDifferent(cell) 48 | 49 | for i in range(n): 50 | for j in range(n): 51 | if problem[i][j]: 52 | model.Add(x[i, j] == problem[i][j]) 53 | 54 | # search and solution 55 | solver = cp_model.CpSolver() 56 | solver.parameters.log_search_progress = True 57 | status = solver.Solve(model) 58 | print(str(pidx)+",",solver.WallTime()) 59 | 60 | if __name__ == "__main__": 61 | grids = from_file("top95.txt") 62 | i = 0 63 | for grid in grids: 64 | solve(i, grid) 65 | i += 1 -------------------------------------------------------------------------------- /benchmark/steiner/benchmark.jl: -------------------------------------------------------------------------------- 1 | function steiner(n) 2 | model = Model(optimizer_with_attributes(CS.Optimizer, 3 | "logging" => [], 4 | "seed" => 4, 5 | 6 | "traverse_strategy" => :BFS, 7 | # "traverse_strategy"=>:DFS, 8 | # "traverse_strategy"=>:DBFS, 9 | 10 | # "branch_split"=>:Smallest, 11 | # "branch_split"=>:Biggest, 12 | "branch_split" => :InHalf, 13 | 14 | # https://wikunia.github.io/ConstraintSolver.jl/stable/options/#branch_strategy-(:Auto) 15 | # "branch_strategy" => :IMPS, # default 16 | "branch_strategy" => :ABS, # Activity Based Search 17 | "activity.decay" => 0.999, # default 0.999 18 | "activity.max_probes" => 1, # default, 10 19 | "activity.max_confidence_deviation" => 20, # default 20 20 | 21 | # "simplify"=>false, 22 | # "simplify"=>true, # default 23 | 24 | # "backtrack" => false, # default true 25 | # "backtrack_sorting" => false, # default true 26 | 27 | # "lp_optimizer" => cbc_optimizer, 28 | # "lp_optimizer" => glpk_optimizer, 29 | # "lp_optimizer" => ipopt_optimizer, 30 | )) 31 | 32 | @assert (n % 6 == 1 || n % 6 == 3) 33 | 34 | nb = round(Int, (n * (n - 1)) / 6) # number of sets 35 | 36 | @variable(model, x[1:nb,1:n], Bin) 37 | @constraint(model, x[1,1] == 1) # symmetry breaking 38 | 39 | # atmost 1 element in common 40 | for i in 1:nb 41 | @constraint(model,sum(x[i,:]) == 3) 42 | 43 | for j in i + 1:nb 44 | b = @variable(model, [1:n], Bin) 45 | for k in 1:n 46 | @constraint(model, b[k] := { x[i,k] && x[j,k] }) 47 | end 48 | @constraint(model, sum(b) <= 1) 49 | end 50 | end 51 | 52 | 53 | # Solve the problem 54 | optimize!(model) 55 | 56 | status = JuMP.termination_status(model) 57 | @assert status == MOI.OPTIMAL 58 | end -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: '*' 7 | pull_request: 8 | jobs: 9 | test: 10 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: 16 | - '1.6' 17 | - '1' 18 | os: 19 | - ubuntu-latest 20 | - macOS-latest 21 | - windows-latest 22 | arch: 23 | - x64 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: julia-actions/setup-julia@v1 27 | with: 28 | version: ${{ matrix.version }} 29 | arch: ${{ matrix.arch }} 30 | - uses: actions/cache@v1 31 | env: 32 | cache-name: cache-artifacts 33 | with: 34 | path: ~/.julia/artifacts 35 | key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 36 | restore-keys: | 37 | ${{ runner.os }}-test-${{ env.cache-name }}- 38 | ${{ runner.os }}-test- 39 | ${{ runner.os }}- 40 | - uses: julia-actions/julia-buildpkg@v1 41 | - uses: julia-actions/julia-runtest@v1 42 | - uses: julia-actions/julia-processcoverage@v1 43 | - uses: codecov/codecov-action@v1 44 | with: 45 | file: lcov.info 46 | docs: 47 | name: Documentation 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v2 51 | - uses: julia-actions/setup-julia@v1 52 | with: 53 | version: '1' 54 | - name: Install dependencies 55 | run: julia --project=docs/ -e 'using Pkg; Pkg.instantiate()' 56 | - name: Build and deploy 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key 60 | run: julia --project=docs/ docs/make.jl -------------------------------------------------------------------------------- /benchmark/sudoku/python-constraint.py: -------------------------------------------------------------------------------- 1 | # implementation found here: https://gist.github.com/lucaswiman/f6769d2e866407dd784d1f29d3556771 2 | 3 | from constraint import * 4 | import time 5 | ROWS = 'abcdefghi' 6 | COLS = '123456789' 7 | DIGITS = range(1, 10) 8 | VARS = [row + col for row in ROWS for col in COLS] 9 | ROWGROUPS = [[row + col for col in COLS] for row in ROWS] 10 | COLGROUPS = [[row + col for row in ROWS] for col in COLS] 11 | SQUAREGROUPS = [ 12 | [ROWS[3 * rowgroup + k] + COLS[3 * colgroup + j] 13 | for j in range(3) for k in range(3)] 14 | for colgroup in range(3) for rowgroup in range(3) 15 | ] 16 | 17 | def solve(prob_num, hints): 18 | problem = Problem() 19 | for var, hint in zip(VARS, hints): 20 | problem.addVariables([var], [hint] if hint in DIGITS else DIGITS) 21 | for vargroups in [ROWGROUPS, COLGROUPS, SQUAREGROUPS]: 22 | for vargroup in vargroups: 23 | problem.addConstraint(AllDifferentConstraint(), vargroup) 24 | start = time.perf_counter() 25 | sol = problem.getSolution() 26 | t = time.perf_counter()-start 27 | print('%d, %.4f' % (prob_num,t)) 28 | return sol 29 | 30 | def pretty(var_to_value): 31 | board = '' 32 | for rownum, row in enumerate('abcdefghi'): 33 | for colnum, col in enumerate('123456789'): 34 | board += str(var_to_value[row+col]) 35 | if colnum % 3 == 2: 36 | board += ' ' 37 | board += '\n' 38 | if rownum % 3 == 2: 39 | board += '\n' 40 | return board 41 | 42 | def from_file(filename, sep='\n'): 43 | "Parse a file into a list of strings, separated by sep." 44 | return open(filename).read().strip().split(sep) 45 | 46 | def solve_all(grids): 47 | for i in range(len(grids)): 48 | str_grid = grids[i].replace(".", "0") 49 | grid = list(str_grid) 50 | grid = tuple([ int(x) for x in grid ]) 51 | solve(i, grid) 52 | 53 | if __name__ == '__main__': 54 | # solve_all(from_file("easy50.txt", '========'), "easy", None) 55 | solve_all(from_file("top95.txt")) -------------------------------------------------------------------------------- /src/MOI_wrapper/Bridges/reified.jl: -------------------------------------------------------------------------------- 1 | struct ReifiedBridge{T, B<:MOIBC.SetMapBridge{T}, A, F, S} <: MOIBC.AbstractBridge 2 | con_idx::CI 3 | end 4 | 5 | function MOI.supports_constraint( 6 | ::Type{<:ReifiedBridge{T, B}}, 7 | ::Type{F}, 8 | ::Type{<:CS.Reified{A,RF,S}} 9 | ) where {T, B, F<:MOI.VectorAffineFunction, A, RF, S} 10 | is_supported = MOI.supports_constraint(B, RF, S) 11 | !is_supported && return false 12 | S <: AbstractBoolSet && return true 13 | 14 | concrete_B = MOIBC.concrete_bridge_type(B, MOI.ScalarAffineFunction{T}, S) 15 | supported = supports_concreteB(concrete_B) 16 | return supported 17 | end 18 | 19 | function MOIBC.concrete_bridge_type( 20 | ::Type{<:ReifiedBridge{T,B}}, 21 | G::Type{<:MOI.VectorAffineFunction}, 22 | ::Type{IS}, 23 | ) where {T,B,A,F,S,IS<:CS.Reified{A,F,S}} 24 | if S <: AbstractBoolSet 25 | concrete_B = B 26 | else 27 | concrete_B = MOIBC.concrete_bridge_type(B, MOI.ScalarAffineFunction{T}, S) 28 | end 29 | return ReifiedBridge{T,concrete_B,A,F,S} 30 | end 31 | 32 | function MOIB.added_constraint_types( 33 | ::Type{<:ReifiedBridge{T,B,A,F,S}} 34 | ) where {T,B,A,F,S} 35 | if S <: AbstractBoolSet 36 | added_constraints = added_constraint_types(B, S) 37 | else 38 | added_constraints = MOIB.added_constraint_types(B) 39 | end 40 | return [(MOI.VectorAffineFunction{T}, CS.Reified{A,F,added_constraints[1][2]})] 41 | end 42 | 43 | function MOIB.added_constrained_variable_types(::Type{<:ReifiedBridge{T,B}}) where {T,B} 44 | return MOIB.added_constrained_variable_types(B) 45 | end 46 | 47 | function MOIBC.bridge_constraint(::Type{<:ReifiedBridge{T, B, A, F, S}}, model, func, set) where {T, B, A, F, S} 48 | f = MOIU.eachscalar(func) 49 | new_func = MOIU.operate(vcat, T, f[1], map_function(B, f[2:end], set.set)) 50 | new_inner_set = MOIB.map_set(B, set.set) 51 | new_set = CS.Reified{A,F,typeof(new_inner_set)}(new_inner_set, 1+MOI.dimension(new_inner_set)) 52 | return ReifiedBridge{T,B,A,F,S}(MOI.add_constraint(model, new_func, new_set)) 53 | end 54 | -------------------------------------------------------------------------------- /benchmark/run_benchmarks.jl: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env julia 3 | 4 | using Pkg 5 | using ArgParse 6 | 7 | function parse_commandline() 8 | s = ArgParseSettings() 9 | 10 | @add_arg_table! s begin 11 | "--base", "-b" 12 | help = "Baseline commit id or branch" 13 | arg_type = String 14 | default = "master" 15 | "--target", "-t" 16 | help = "Target commit id or branch" 17 | arg_type = String 18 | required = true 19 | "--pr" 20 | help = "ID of the PR to comment on" 21 | arg_type = Int 22 | "--comment", "-c" 23 | help = "ID of the comment you want to update" 24 | arg_type = Int 25 | "--file", "-f" 26 | help = "Markdown filename" 27 | arg_type = String 28 | end 29 | 30 | return parse_args(s) 31 | end 32 | 33 | 34 | if isinteractive() == false 35 | Pkg.activate(".") 36 | Pkg.instantiate() 37 | 38 | args = parse_commandline() 39 | using PkgBenchmark 40 | using ConstraintSolver, Cbc 41 | using GitHub, JSON, Statistics 42 | 43 | github_auth = GitHub.authenticate(ENV["GITHUB_AUTH"]) 44 | target = args["target"] 45 | base = args["base"] 46 | 47 | baseline_config = BenchmarkConfig(id = base, juliacmd = `julia -O3`) 48 | target_config = BenchmarkConfig(id = target, juliacmd = `julia -O3`) 49 | 50 | judged = judge("ConstraintSolver", target_config, baseline_config; f = median) 51 | 52 | markdown = sprint(export_markdown, judged) 53 | if args["file"] !== nothing 54 | export_markdown(args["file"], judged) 55 | end 56 | if args["comment"] !== nothing 57 | comment = edit_comment( 58 | "Wikunia/ConstraintSolver.jl", 59 | Comment(args["comment"]), 60 | :pr; 61 | params = Dict(:body => markdown), 62 | auth = github_auth, 63 | ) 64 | println("Updated comment: $(comment.html_url)") 65 | elseif args["pr"] !== nothing 66 | comment = create_comment( 67 | "Wikunia/ConstraintSolver.jl", 68 | PullRequest(args["pr"]), 69 | markdown; 70 | auth = github_auth, 71 | ) 72 | println("New comment: $(comment.html_url)") 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /benchmark/sudoku/results/or-tools.csv: -------------------------------------------------------------------------------- 1 | 0, 0.021009970000000003 2 | 1, 0.00816103 3 | 2, 0.015305240000000001 4 | 3, 0.173581218 5 | 4, 1.246698789 6 | 5, 0.09686302200000001 7 | 6, 0.141791464 8 | 7, 0.015385814000000001 9 | 8, 0.013505684 10 | 9, 0.012200245 11 | 10, 0.018776884 12 | 11, 0.015209442 13 | 12, 0.0035975950000000003 14 | 13, 0.004528882000000001 15 | 14, 0.010929739 16 | 15, 0.006290645 17 | 16, 0.005650691 18 | 17, 0.09300929000000001 19 | 18, 0.003661624 20 | 19, 0.005959731 21 | 20, 0.06322841800000001 22 | 21, 0.006851491 23 | 22, 0.013589690000000001 24 | 23, 0.013449602000000001 25 | 24, 0.00854995 26 | 25, 0.005214214 27 | 26, 0.004436194 28 | 27, 0.057048494000000005 29 | 28, 0.01039517 30 | 29, 0.007878324 31 | 30, 0.039061797 32 | 31, 0.003638529 33 | 32, 0.003664964 34 | 33, 0.14622558000000002 35 | 34, 0.006807697000000001 36 | 35, 0.009148527 37 | 36, 0.009739097 38 | 37, 0.0075763210000000004 39 | 38, 0.024193254 40 | 39, 0.005211674 41 | 40, 0.231065454 42 | 41, 0.006229923 43 | 42, 0.0070046250000000004 44 | 43, 0.007625034 45 | 44, 0.016112237 46 | 45, 0.07955408 47 | 46, 0.002540226 48 | 47, 0.003928817 49 | 48, 0.0041580490000000005 50 | 49, 0.095817804 51 | 50, 0.011964234 52 | 51, 0.012846286 53 | 52, 0.01244453 54 | 53, 0.0065466280000000005 55 | 54, 0.0039865230000000005 56 | 55, 0.004485399 57 | 56, 0.046770553000000006 58 | 57, 0.013408681 59 | 58, 0.009198570000000001 60 | 59, 0.010376503 61 | 60, 0.005598408 62 | 61, 0.008672764000000001 63 | 62, 0.007323882 64 | 63, 0.023551086000000002 65 | 64, 0.002029714 66 | 65, 0.033316851 67 | 66, 0.003944963 68 | 67, 0.019401655 69 | 68, 0.011966186 70 | 69, 0.112729327 71 | 70, 0.040886465000000004 72 | 71, 0.012022046000000002 73 | 72, 0.020595964 74 | 73, 0.031586766 75 | 74, 0.003316986 76 | 75, 0.020512013000000003 77 | 76, 0.013100990000000002 78 | 77, 0.015194064 79 | 78, 0.012467876000000001 80 | 79, 0.004069977000000001 81 | 80, 0.003253906 82 | 81, 0.004872851 83 | 82, 0.023129478000000002 84 | 83, 0.0030973700000000003 85 | 84, 0.003791743 86 | 85, 0.010929643000000001 87 | 86, 0.016593896 88 | 87, 0.013068459000000001 89 | 88, 0.06404707500000001 90 | 89, 0.004015424 91 | 90, 0.004635314000000001 92 | 91, 0.004748204000000001 93 | 92, 0.005127325 94 | 93, 0.009027083 95 | 94, 0.08403764200000001 -------------------------------------------------------------------------------- /benchmark/small/benchmark.jl: -------------------------------------------------------------------------------- 1 | #= 2 | The problems are taken from http://www.hakank.org/julia/constraints/ 3 | 4 | Thanks a lot to Håkan Kjellerstrand 5 | =# 6 | 7 | 8 | 9 | # 10 | # all_different_except_c 11 | # 12 | # Ensure that all values (except c) are distinct 13 | # Thanks to Ole who fixed some initial problems I had. 14 | # (See https://github.com/Wikunia/ConstraintSolver.jl/issues/202 for 15 | # details.) 16 | # 17 | function all_different_except_c(model, x, c=0) 18 | n = length(x) 19 | 20 | for i in 2:n, j in 1:i-1 21 | b = @variable(model, binary=true) 22 | @constraint(model, b := {x[i] != c && x[j] != c}) 23 | @constraint(model, b => {x[i] != x[j]}) 24 | end 25 | end 26 | 27 | # 28 | # increasing(model, x) 29 | # 30 | # Ensure that array x in increasing order 31 | # 32 | function increasing(model, x) 33 | len = length(x) 34 | for i in 2:len 35 | @constraint(model, x[i-1] <= x[i]) 36 | end 37 | end 38 | 39 | #= 40 | Decomposition of global constraint alldifferent_except_0 in Julia + ConstraintSolver. 41 | From Global constraint catalogue: 42 | http://www.emn.fr/x-info/sdemasse/gccat/Calldifferent_except_0.html 43 | """ 44 | Enforce all variables of the collection VARIABLES to take distinct 45 | values, except those variables that are assigned to 0. 46 | Example 47 | (<5, 0, 1, 9, 0, 3>) 48 | The alldifferent_except_0 constraint holds since all the values 49 | (that are different from 0) 5, 1, 9 and 3 are distinct. 50 | """ 51 | Model created by Hakan Kjellerstrand, hakank@gmail.com 52 | See also my Julia page: http://www.hakank.org/julia/ 53 | =# 54 | 55 | function all_different_except_0(n=10) 56 | model = Model(optimizer_with_attributes(CS.Optimizer, 57 | "all_solutions"=>true, 58 | "logging"=>[], 59 | ) 60 | ) 61 | @variable(model, 0 <= x[1:n] <= n, Int) 62 | 63 | all_different_except_c(model,x,0) 64 | increasing(model, x) 65 | 66 | optimize!(model) 67 | 68 | status = JuMP.termination_status(model) 69 | @assert status == MOI.OPTIMAL 70 | num_sols = MOI.get(model, MOI.ResultCount()) 71 | @assert num_sols == 2^n 72 | end 73 | -------------------------------------------------------------------------------- /src/constraints/and.jl: -------------------------------------------------------------------------------- 1 | """ 2 | function is_constraint_violated( 3 | com::CoM, 4 | constraint::BoolConstraint, 5 | fct, 6 | set::AndSet, 7 | ) 8 | 9 | Check if one of the inner constraints is violated 10 | """ 11 | function is_constraint_violated( 12 | com::CoM, 13 | constraint::BoolConstraint, 14 | fct, 15 | set::AndSet, 16 | ) 17 | return is_lhs_constraint_violated(com, constraint) || is_rhs_constraint_violated(com, constraint) 18 | end 19 | 20 | """ 21 | still_feasible(com::CoM, constraint::AndConstraint, fct, set::AndSet, vidx::Int, value::Int) 22 | 23 | Return whether the constraint can be still fulfilled when setting a variable with index `vidx` to `value`. 24 | **Attention:** This assumes that it isn't violated before. 25 | """ 26 | function still_feasible( 27 | com::CoM, 28 | constraint::AndConstraint, 29 | fct, 30 | set::AndSet, 31 | vidx::Int, 32 | value::Int, 33 | ) 34 | lhs_indices = constraint.lhs.indices 35 | for i in 1:length(lhs_indices) 36 | if lhs_indices[i] == vidx 37 | if !still_feasible(com, constraint.lhs, constraint.lhs.fct, constraint.lhs.set, vidx, value) 38 | return false 39 | end 40 | end 41 | end 42 | rhs_indices = constraint.rhs.indices 43 | for i in 1:length(rhs_indices) 44 | if rhs_indices[i] == vidx 45 | if !still_feasible(com, constraint.rhs, constraint.rhs.fct, constraint.rhs.set, vidx, value) 46 | return false 47 | end 48 | end 49 | end 50 | return true 51 | end 52 | 53 | """ 54 | prune_constraint!(com::CS.CoM, constraint::AndConstraint, fct, set::AndSet; logs = true) 55 | 56 | Reduce the number of possibilities given the `AndConstraint` by pruning both parts 57 | Return whether still feasible 58 | """ 59 | function prune_constraint!( 60 | com::CS.CoM, 61 | constraint::AndConstraint, 62 | fct, 63 | set::AndSet; 64 | logs = true, 65 | ) 66 | !activate_lhs!(com, constraint) && return false 67 | !prune_constraint!(com, constraint.lhs, constraint.lhs.fct, constraint.lhs.set; logs=logs) && return false 68 | !activate_rhs!(com, constraint) && return false 69 | feasible = prune_constraint!(com, constraint.rhs, constraint.rhs.fct, constraint.rhs.set; logs=logs) 70 | return feasible 71 | end -------------------------------------------------------------------------------- /benchmark/killer_sudoku/benchmark.jl: -------------------------------------------------------------------------------- 1 | function parseKillerJSON(json_sums) 2 | sums = [] 3 | for s in json_sums 4 | indices = Tuple[] 5 | for ind in s["indices"] 6 | push!(indices, tuple(ind...)) 7 | end 8 | 9 | push!(sums, (result = s["result"], indices = indices, color = s["color"])) 10 | end 11 | return sums 12 | end 13 | 14 | function solve_killer_sudoku(filename; special = false, branch_strategy=:Auto) 15 | json_file = joinpath(dir, "benchmark/killer_sudoku/data/$filename") 16 | sums = parseKillerJSON(JSON.parsefile(json_file)) 17 | 18 | m = CS.Optimizer(logging = [], time_limit = 20, 19 | branch_strategy= branch_strategy) 20 | 21 | x = [[MOI.add_constrained_variable(m, MOI.Integer()) for i in 1:9] for j in 1:9] 22 | for r in 1:9, c in 1:9 23 | MOI.add_constraint(m, x[r][c][1], MOI.GreaterThan(1.0)) 24 | MOI.add_constraint(m, x[r][c][1], MOI.LessThan(9.0)) 25 | end 26 | 27 | for s in sums 28 | saf = MOI.ScalarAffineFunction{Float64}( 29 | [MOI.ScalarAffineTerm(1.0, x[ind[1]][ind[2]][1]) for ind in s.indices], 30 | 0.0, 31 | ) 32 | MOI.add_constraint(m, saf, MOI.EqualTo(convert(Float64, s.result))) 33 | if !special 34 | MOI.add_constraint( 35 | m, 36 | [x[ind[1]][ind[2]][1] for ind in s.indices], 37 | CS.CPE.AllDifferent(length(s.indices)), 38 | ) 39 | end 40 | end 41 | 42 | # sudoku constraints 43 | for r in 1:9 44 | MOI.add_constraint( 45 | m, 46 | MOI.VectorOfVariables([x[r][c][1] for c in 1:9]), 47 | CS.CPE.AllDifferent(9), 48 | ) 49 | end 50 | for c in 1:9 51 | MOI.add_constraint( 52 | m, 53 | MOI.VectorOfVariables([x[r][c][1] for r in 1:9]), 54 | CS.CPE.AllDifferent(9), 55 | ) 56 | end 57 | variables = [MOI.VariableIndex(0) for _ in 1:9] 58 | for br in 0:2 59 | for bc in 0:2 60 | variables_i = 1 61 | for i in (br * 3 + 1):((br + 1) * 3), j in (bc * 3 + 1):((bc + 1) * 3) 62 | variables[variables_i] = x[i][j][1] 63 | variables_i += 1 64 | end 65 | MOI.add_constraint(m, variables, CS.CPE.AllDifferent(9)) 66 | end 67 | end 68 | 69 | MOI.optimize!(m) 70 | end 71 | -------------------------------------------------------------------------------- /benchmark/sudoku/results/sudoku-bb.csv: -------------------------------------------------------------------------------- 1 | 0, 0.00297093391418 2 | 1, 0.000929117202759 3 | 2, 0.00180196762085 4 | 3, 0.00439810752869 5 | 4, 0.00484991073608 6 | 5, 0.00375890731812 7 | 6, 0.00904393196106 8 | 7, 0.00317692756653 9 | 8, 0.00436305999756 10 | 9, 0.00416493415833 11 | 10, 0.000957012176514 12 | 11, 0.0057110786438 13 | 12, 0.00551009178162 14 | 13, 0.00241804122925 15 | 14, 0.00259590148926 16 | 15, 0.000813007354736 17 | 16, 0.000833034515381 18 | 17, 0.00482702255249 19 | 18, 0.00127100944519 20 | 19, 0.00281691551208 21 | 20, 0.000836133956909 22 | 21, 0.00118923187256 23 | 22, 0.00316381454468 24 | 23, 0.00259280204773 25 | 24, 0.00288796424866 26 | 25, 0.00559401512146 27 | 26, 0.00564098358154 28 | 27, 0.00182795524597 29 | 28, 0.00105690956116 30 | 29, 0.0015881061554 31 | 30, 0.000813007354736 32 | 31, 0.00157189369202 33 | 32, 0.00177097320557 34 | 33, 0.00250697135925 35 | 34, 0.00222682952881 36 | 35, 0.00181818008423 37 | 36, 0.00851798057556 38 | 37, 0.00134611129761 39 | 38, 0.00184392929077 40 | 39, 0.00117206573486 41 | 40, 0.00442504882812 42 | 41, 0.00315284729004 43 | 42, 0.00179696083069 44 | 43, 0.00273299217224 45 | 44, 0.00319004058838 46 | 45, 0.000974893569946 47 | 46, 0.00347709655762 48 | 47, 0.00147294998169 49 | 48, 0.00319695472717 50 | 49, 0.00358200073242 51 | 50, 0.000790119171143 52 | 51, 0.00250291824341 53 | 52, 0.00137805938721 54 | 53, 0.0046558380127 55 | 54, 0.00916290283203 56 | 55, 0.00363993644714 57 | 56, 0.00168108940125 58 | 57, 0.00303602218628 59 | 58, 0.00417995452881 60 | 59, 0.00135588645935 61 | 60, 0.00133919715881 62 | 61, 0.00269293785095 63 | 62, 0.00196599960327 64 | 63, 0.00353598594666 65 | 64, 0.000787019729614 66 | 65, 0.00558519363403 67 | 66, 0.00292086601257 68 | 67, 0.0116679668427 69 | 68, 0.00341606140137 70 | 69, 0.00200200080872 71 | 70, 0.00698089599609 72 | 71, 0.00346088409424 73 | 72, 0.0022029876709 74 | 73, 0.00372505187988 75 | 74, 0.000921964645386 76 | 75, 0.00499796867371 77 | 76, 0.00079083442688 78 | 77, 0.00692701339722 79 | 78, 0.00235605239868 80 | 79, 0.00132918357849 81 | 80, 0.00180006027222 82 | 81, 0.00109791755676 83 | 82, 0.00166893005371 84 | 83, 0.00261116027832 85 | 84, 0.0011830329895 86 | 85, 0.00127291679382 87 | 86, 0.00664710998535 88 | 87, 0.0024950504303 89 | 88, 0.00424098968506 90 | 89, 0.00202417373657 91 | 90, 0.000893831253052 92 | 91, 0.0043580532074 93 | 92, 0.00163698196411 94 | 93, 0.00162720680237 95 | 94, 0.00796508789062 -------------------------------------------------------------------------------- /benchmark/sudoku/benchmark.jl: -------------------------------------------------------------------------------- 1 | function from_file(filename, sep = '\n') 2 | s = open(filename) do file 3 | read(file, String) 4 | end 5 | str_sudokus = split(strip(s), sep) 6 | grids = AbstractArray[] 7 | for str_sudoku in str_sudokus 8 | str_sudoku = replace(str_sudoku, "." => "0") 9 | one_line_grid = parse.(Int, split(str_sudoku, "")) 10 | nvals = length(one_line_grid) 11 | side_len = isqrt(nvals) 12 | grid = reshape(one_line_grid, side_len, side_len) 13 | push!(grids, grid) 14 | end 15 | return grids 16 | end 17 | 18 | function solve_sudoku(grid) 19 | m = CS.Optimizer(logging = []) 20 | side_len = size(grid, 1) 21 | block_size = isqrt(side_len) 22 | 23 | x = [[MOI.add_constrained_variable(m, MOI.Integer()) for i in 1:side_len] for j in 1:side_len] 24 | for r in 1:side_len, c in 1:side_len 25 | MOI.add_constraint(m, x[r][c][1], MOI.GreaterThan(1.0)) 26 | MOI.add_constraint(m, x[r][c][1], MOI.LessThan(convert(Float64, side_len))) 27 | end 28 | 29 | # set variables 30 | for r in 1:side_len, c in 1:side_len 31 | if grid[r, c] != 0 32 | sat = [MOI.ScalarAffineTerm(1.0, x[r][c][1])] 33 | MOI.add_constraint( 34 | m, 35 | MOI.ScalarAffineFunction{Float64}(sat, 0.0), 36 | MOI.EqualTo(convert(Float64, grid[r, c])), 37 | ) 38 | end 39 | end 40 | # sudoku constraints 41 | for r in 1:side_len 42 | MOI.add_constraint( 43 | m, 44 | MOI.VectorOfVariables([x[r][c][1] for c in 1:side_len]), 45 | CS.CPE.AllDifferent(side_len), 46 | ) 47 | end 48 | for c in 1:side_len 49 | MOI.add_constraint( 50 | m, 51 | MOI.VectorOfVariables([x[r][c][1] for r in 1:side_len]), 52 | CS.CPE.AllDifferent(side_len), 53 | ) 54 | end 55 | variables = [MOI.VariableIndex(0) for _ in 1:side_len] 56 | for br in 0:block_size-1 57 | for bc in 0:block_size-1 58 | variables_i = 1 59 | for i in (br * block_size + 1):((br + 1) * block_size), j in (bc * block_size + 1):((bc + 1) * block_size) 60 | variables[variables_i] = x[i][j][1] 61 | variables_i += 1 62 | end 63 | MOI.add_constraint(m, variables, CS.CPE.AllDifferent(side_len)) 64 | end 65 | end 66 | 67 | MOI.optimize!(m) 68 | @assert MOI.get(m, MOI.TerminationStatus()) == MOI.OPTIMAL 69 | end 70 | -------------------------------------------------------------------------------- /test/steiner.jl: -------------------------------------------------------------------------------- 1 | function check_steiner(n::Integer, nb::Integer, x; result=1) 2 | x_val = convert.(Integer,JuMP.value.(x; result=result)) 3 | # Convert to ints 4 | solution = [ [j for j in 1:n if x_val[i,j] == 1] for i in 1:nb ] 5 | 6 | for i = 1:length(solution) 7 | for j = i+1:length(solution) 8 | if length(intersect(solution[i], solution[j])) > 1 9 | @show solution 10 | @show solution[i] 11 | @show solution[j] 12 | return false 13 | end 14 | end 15 | end 16 | return true 17 | end 18 | 19 | # taken from http://hakank.org/julia/constraints/steiner_and.jl 20 | @testset "Steiner tests" begin 21 | @testset "Steiner test for && and || complement constraints with !b" begin 22 | model = Model(CSJuMPTestOptimizer()) 23 | 24 | n = 7 25 | 26 | nb = round(Int,(n * (n-1)) / 6) # number of sets 27 | 28 | @variable(model, x[1:nb,1:n], Bin) 29 | @constraint(model, x[1,1] == 1) # symmetry breaking 30 | 31 | # atmost 1 element in common 32 | for i in 1:nb 33 | @constraint(model,sum(x[i,:]) == 3) 34 | for j in i+1:nb 35 | b = @variable(model, [1:n], Bin) 36 | for k in 1:n 37 | @constraint(model, !b[k] := { x[i,k] != 1 || x[j,k] != 1 }) 38 | end 39 | @constraint(model, sum(b) <= 1) 40 | end 41 | end 42 | 43 | 44 | # Solve the problem 45 | optimize!(model) 46 | 47 | status = JuMP.termination_status(model) 48 | @test status == MOI.OPTIMAL 49 | @test check_steiner(n, nb, x) 50 | end 51 | 52 | @testset "Steiner test for && and || complement constraints with b" begin 53 | model = Model(CSJuMPTestOptimizer()) 54 | 55 | n = 7 56 | 57 | nb = round(Int,(n * (n-1)) / 6) # number of sets 58 | 59 | @variable(model, x[1:nb,1:n], Bin) 60 | @constraint(model, x[1,1] == 1) # symmetry breaking 61 | 62 | # atmost 1 element in common 63 | for i in 1:nb 64 | @constraint(model,sum(x[i,:]) == 3) 65 | for j in i+1:nb 66 | b = @variable(model, [1:n], Bin) 67 | for k in 1:n 68 | @constraint(model, b[k] := { x[i,k] && x[j,k] }) 69 | end 70 | @constraint(model, sum(b) <= 1) 71 | end 72 | end 73 | 74 | 75 | # Solve the problem 76 | optimize!(model) 77 | 78 | status = JuMP.termination_status(model) 79 | @test status == MOI.OPTIMAL 80 | @test check_steiner(n, nb, x) 81 | end 82 | end -------------------------------------------------------------------------------- /src/MOI_wrapper/reified.jl: -------------------------------------------------------------------------------- 1 | function _build_reified_constraint( 2 | _error::Function, 3 | variable::JuMP.AbstractVariableRef, 4 | constraint::JuMP.ScalarConstraint, 5 | ::Type{<:CS.Reified{A}}, 6 | ) where {A} 7 | S = typeof(JuMP.moi_set(constraint)) 8 | F = typeof(JuMP.moi_function(constraint)) 9 | set = Reified{A,F,S}(JuMP.moi_set(constraint), 2) 10 | return JuMP.VectorConstraint([variable, JuMP.jump_function(constraint)], set) 11 | end 12 | 13 | function _build_reified_constraint( 14 | _error::Function, 15 | variable::JuMP.AbstractVariableRef, 16 | constraint::JuMP.VectorConstraint, 17 | ::Type{<:CS.Reified{A}}, 18 | ) where {A} 19 | S = typeof(constraint.set) 20 | F = typeof(JuMP.moi_function(constraint)) 21 | set = CS.Reified{A,F,S}(constraint.set, 1 + length(constraint.func)) 22 | if constraint.func isa Vector{VariableRef} 23 | vov = JuMP.VariableRef[variable] 24 | else 25 | vov = JuMP.AffExpr[variable] 26 | end 27 | append!(vov, constraint.func) 28 | return JuMP.VectorConstraint(vov, set) 29 | end 30 | 31 | function _reified_variable_set(::Function, variable::Symbol) 32 | return variable, Reified{MOI.ACTIVATE_ON_ONE} 33 | end 34 | 35 | function _reified_variable_set(_error::Function, expr::Expr) 36 | if expr.args[1] == :¬ || expr.args[1] == :! 37 | if length(expr.args) != 2 38 | _error("Invalid binary variable expression `$(expr)` for reified constraint.") 39 | end 40 | return expr.args[2], Reified{MOI.ACTIVATE_ON_ZERO} 41 | else 42 | return expr, Reified{MOI.ACTIVATE_ON_ONE} 43 | end 44 | end 45 | 46 | function JuMP.parse_constraint_head(_error::Function, ::Val{:(:=)}, lhs, rhs) 47 | variable, S = _reified_variable_set(_error, lhs) 48 | if !JuMP.isexpr(rhs, :braces) || length(rhs.args) != 1 49 | _error("Invalid right-hand side `$(rhs)` of reified constraint. Expected constraint surrounded by `{` and `}`.") 50 | end 51 | rhs_con = rhs.args[1] 52 | rhs_vectorized, rhs_parsecode, rhs_buildcall = 53 | JuMP.parse_constraint(_error, rhs_con) 54 | 55 | # TODO implement vectorized version 56 | vectorized = false 57 | if rhs_vectorized 58 | _error("`$(rhs)` should be non vectorized. There is currently no vectorized support for reified constraints. Please open an issue at ConstraintSolver.jl") 59 | end 60 | 61 | buildcall = :($(esc(:(CS._build_reified_constraint)))( 62 | $_error, 63 | $(esc(variable)), 64 | $rhs_buildcall, 65 | $S, 66 | )) 67 | return vectorized, rhs_parsecode, buildcall 68 | end 69 | -------------------------------------------------------------------------------- /src/lp_model.jl: -------------------------------------------------------------------------------- 1 | function create_lp_model!(model) 2 | model.options.lp_optimizer === nothing && return 3 | com = model.inner 4 | com.sense == MOI.FEASIBILITY_SENSE && return 5 | lp_model = Model() 6 | 7 | set_optimizer(lp_model, model.options.lp_optimizer) 8 | lp_x = Vector{VariableRef}(undef, length(com.search_space)) 9 | for variable in com.search_space 10 | lp_x[variable.idx] = @variable( 11 | lp_model, 12 | lower_bound = variable.lower_bound, 13 | upper_bound = variable.upper_bound 14 | ) 15 | end 16 | lp_backend = backend(lp_model) 17 | # iterate through all constraints and add all supported constraints 18 | for constraint in com.constraints 19 | if MOI.supports_constraint( 20 | model.options.lp_optimizer.optimizer_constructor(), 21 | typeof(constraint.fct), 22 | typeof(constraint.set), 23 | ) 24 | MOI.add_constraint(lp_backend, constraint.fct, constraint.set) 25 | elseif constraint.set isa CPE.Strictly # transform a < into a <= by changing the rhs 26 | if MOI.supports_constraint( 27 | model.options.lp_optimizer.optimizer_constructor(), 28 | typeof(constraint.fct), 29 | typeof(constraint.set.set), 30 | ) 31 | rhs = get_rhs_from_strictly(model.inner, constraint, constraint.fct, constraint.set) 32 | computed_set = typeof(constraint.set.set)(rhs) 33 | MOI.add_constraint(lp_backend, constraint.fct, computed_set) 34 | end 35 | end 36 | end 37 | # add objective 38 | !MOI.supports(lp_backend, MOI.ObjectiveSense()) && 39 | @error "The given lp solver doesn't allow objective functions" 40 | typeof_objective = typeof(com.objective.fct) 41 | if MOI.supports(lp_backend, MOI.ObjectiveFunction{typeof_objective}()) 42 | MOI.set(lp_backend, MOI.ObjectiveFunction{typeof_objective}(), com.objective.fct) 43 | else 44 | @error "The given `lp_optimizer` doesn't support the objective function $(typeof_objective)" 45 | end 46 | if MOI.supports(lp_backend, MOI.ObjectiveSense()) 47 | MOI.set(lp_backend, MOI.ObjectiveSense(), com.sense) 48 | else 49 | @error "The given `lp_optimizer` doesn't support setting `ObjectiveSense`" 50 | end 51 | com.lp_x = lp_x 52 | com.lp_model = lp_model 53 | end 54 | 55 | function create_lp_variable!(lp_model, lp_x; lb = typemin(Int64), ub = typemax(Int64)) 56 | v = @variable(lp_model, lower_bound = lb, upper_bound = ub) 57 | push!(lp_x, v) 58 | return length(lp_x) 59 | end 60 | -------------------------------------------------------------------------------- /src/constraints/table/RSparseBitSet.jl: -------------------------------------------------------------------------------- 1 | is_empty(bitset::RSparseBitSet) = bitset.last_ptr == 0 2 | 3 | function clear_mask(bitset::RSparseBitSet) 4 | indices = bitset.indices 5 | mask = bitset.mask 6 | @inbounds for i in 1:(bitset.last_ptr) 7 | idx = indices[i] 8 | mask[idx] = UInt64(0) 9 | end 10 | end 11 | 12 | function full_mask(bitset::RSparseBitSet) 13 | indices = bitset.indices 14 | mask = bitset.mask 15 | @inbounds for i in 1:length(indices) 16 | idx = indices[i] 17 | mask[idx] = typemax(UInt64) 18 | end 19 | end 20 | 21 | function invert_mask(bitset::RSparseBitSet) 22 | indices = bitset.indices 23 | mask = bitset.mask 24 | @inbounds for i in 1:(bitset.last_ptr) 25 | idx = indices[i] 26 | mask[idx] = ~mask[idx] 27 | end 28 | end 29 | 30 | function add_to_mask(bitset::RSparseBitSet, add) 31 | mask = bitset.mask 32 | indices = bitset.indices 33 | @inbounds for i in 1:(bitset.last_ptr) 34 | idx = indices[i] 35 | mask[idx] |= add[idx] 36 | end 37 | end 38 | 39 | function intersect_mask_with_mask(bitset::RSparseBitSet, intersect_mask) 40 | indices = bitset.indices 41 | mask = bitset.mask 42 | @inbounds for i in 1:(bitset.last_ptr) 43 | idx = indices[i] 44 | mask[idx] &= intersect_mask[idx] 45 | end 46 | end 47 | 48 | function intersect_with_mask_feasible(bitset::RSparseBitSet) 49 | words = bitset.words 50 | mask = bitset.mask 51 | indices = bitset.indices 52 | @inbounds for i in (bitset.last_ptr):-1:1 53 | idx = indices[i] 54 | w = words[idx] & mask[idx] 55 | if w != UInt64(0) 56 | return true # is feasible 57 | end 58 | end 59 | return false 60 | end 61 | 62 | function intersect_with_mask(bitset::RSparseBitSet) 63 | words = bitset.words 64 | mask = bitset.mask 65 | indices = bitset.indices 66 | @inbounds for i in (bitset.last_ptr):-1:1 67 | idx = indices[i] 68 | w = words[idx] & mask[idx] 69 | if w != words[idx] 70 | words[idx] = w 71 | if w == zero(UInt64) 72 | indices[i] = indices[bitset.last_ptr] 73 | indices[bitset.last_ptr] = idx 74 | bitset.last_ptr -= 1 75 | end 76 | end 77 | end 78 | end 79 | 80 | function intersect_index(bitset::RSparseBitSet, mask) 81 | words = bitset.words 82 | indices = bitset.indices 83 | @inbounds for i in 1:(bitset.last_ptr) 84 | idx = indices[i] 85 | w = words[idx] & mask[idx] 86 | if w != zero(UInt64) 87 | return idx 88 | end 89 | end 90 | return 0 91 | end 92 | -------------------------------------------------------------------------------- /docs/src/explanation.md: -------------------------------------------------------------------------------- 1 | # Explanation 2 | 3 | In this part I'll explain how the constraint solver works. You might want to read this either because you're just interested or because you might want to contribute to this project. 4 | 5 | This project evolved during a couple of months and is more or less fully documented on my blog: [Constraint Solver Series](https://opensourc.es/blog/constraint-solver-1). 6 | 7 | That is an ongoing project and there were a lot of changes especially at the beginning. Therefore here you can read just the current state in a shorter format. 8 | 9 | ## General concept 10 | 11 | The constraint solver works on a set of discrete bounded variables. In the solving process the first step is to go through all constraints and remove values which aren't possible i.e if we have a `all_different([x,y])` constraint and `x` is fixed to 3 it can be removed from the possible set of values for `y` directly. 12 | 13 | Now that `y` changed this might lead to further improvements by calling constraints where `y` is involved. By improvement I mean that the search space gets smaller. 14 | 15 | After this step it might turn out that the problem is infeasible or solved but most of the time it's not yet known. That is when backtracking comes in to play. 16 | 17 | ### Backtracking 18 | 19 | In backtracking we split the current model into several models in each of them we fix a variable to one particular value. This creates a tree structure. 20 | The constraint solver decides how to split the model into several parts. Most often it is useful to split it into a few parts rather than many parts. That means if we have two variables `x` and `y` and `x` has 3 possible values after the first step and `y` has 9 possible values we rather choose `x` to create three new branches in our tree than 9. This is useful as we get more information per solving step this way. 21 | 22 | After we fix a value we go into one of the open nodes. An open node is a node in the tree which we didn't split yet (it's a leaf node) and is neither infeasible nor is a fixed solution. 23 | 24 | There are two kind of problems which have a different backtracking strategy. One of them is a feasibility problem like solving sudokus and the other one is an optimization problem like graph coloring. 25 | 26 | In the first way we try one branch until we reach a leaf node and then backtrack until we prove that the problem is infeasible or stop when we found a feasible solution. 27 | 28 | For optimization problems a node is chosen which has the best bound (best possible objective) and if there are several ones the one with the highest depth is chosen. 29 | 30 | In general the solver saves what changed in each step to be able to update the current search space when jumping to a different open node in the tree. 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /benchmark/sudoku/results/cs.csv: -------------------------------------------------------------------------------- 1 | 0, 0.005831003189086914 2 | 1, 0.002382993698120117 3 | 2, 0.002232074737548828 4 | 3, 0.0028429031372070312 5 | 4, 0.004648923873901367 6 | 5, 0.0024139881134033203 7 | 6, 0.005847930908203125 8 | 7, 0.005038022994995117 9 | 8, 0.00595402717590332 10 | 9, 0.005113840103149414 11 | 10, 0.01015615463256836 12 | 11, 0.010463953018188477 13 | 12, 0.003880023956298828 14 | 13, 0.0028018951416015625 15 | 14, 0.0024518966674804688 16 | 15, 0.0028569698333740234 17 | 16, 0.0029418468475341797 18 | 17, 0.0024878978729248047 19 | 18, 0.0023970603942871094 20 | 19, 0.0023431777954101562 21 | 20, 0.0023131370544433594 22 | 21, 0.005335092544555664 23 | 22, 0.0023598670959472656 24 | 23, 0.0029048919677734375 25 | 24, 0.0024290084838867188 26 | 25, 0.002479076385498047 27 | 26, 0.002410888671875 28 | 27, 0.006915092468261719 29 | 28, 0.003609180450439453 30 | 29, 0.0053980350494384766 31 | 30, 0.0030939579010009766 32 | 31, 0.00400090217590332 33 | 32, 0.002599954605102539 34 | 33, 0.002415895462036133 35 | 34, 0.002482891082763672 36 | 35, 0.0022699832916259766 37 | 36, 0.0023758411407470703 38 | 37, 0.004746913909912109 39 | 38, 0.006963014602661133 40 | 39, 0.0027189254760742188 41 | 40, 0.002635955810546875 42 | 41, 0.003114938735961914 43 | 42, 0.008036136627197266 44 | 43, 0.0037500858306884766 45 | 44, 0.012634992599487305 46 | 45, 0.009229898452758789 47 | 46, 0.009695053100585938 48 | 47, 0.0029878616333007812 49 | 48, 0.0022499561309814453 50 | 49, 0.0066699981689453125 51 | 50, 0.009200811386108398 52 | 51, 0.003880023956298828 53 | 52, 0.002508878707885742 54 | 53, 0.00985097885131836 55 | 54, 0.0044100284576416016 56 | 55, 0.013289213180541992 57 | 56, 0.005026817321777344 58 | 57, 0.012823104858398438 59 | 58, 0.006267070770263672 60 | 59, 0.008234024047851562 61 | 60, 0.002991914749145508 62 | 61, 0.002518177032470703 63 | 62, 0.00910496711730957 64 | 63, 0.009068965911865234 65 | 64, 0.0029959678649902344 66 | 65, 0.005750894546508789 67 | 66, 0.006524085998535156 68 | 67, 0.005177021026611328 69 | 68, 0.01028585433959961 70 | 69, 0.005278110504150391 71 | 70, 0.002855062484741211 72 | 71, 0.0031468868255615234 73 | 72, 0.003144979476928711 74 | 73, 0.0038352012634277344 75 | 74, 0.007091999053955078 76 | 75, 0.003942966461181641 77 | 76, 0.002563953399658203 78 | 77, 0.002980947494506836 79 | 78, 0.005793094635009766 80 | 79, 0.0032591819763183594 81 | 80, 0.003078937530517578 82 | 81, 0.0073969364166259766 83 | 82, 0.0050427913665771484 84 | 83, 0.002443075180053711 85 | 84, 0.005383014678955078 86 | 85, 0.003016948699951172 87 | 86, 0.004518985748291016 88 | 87, 0.005201101303100586 89 | 88, 0.010212898254394531 90 | 89, 0.008750200271606445 91 | 90, 0.0030260086059570312 92 | 91, 0.0055119991302490234 93 | 92, 0.006297111511230469 94 | 93, 0.005209922790527344 95 | 94, 0.0032110214233398438 -------------------------------------------------------------------------------- /src/printing.jl: -------------------------------------------------------------------------------- 1 | function Base.show(io::IO, csinfo::CSInfo) 2 | println("Info: ") 3 | for name in fieldnames(CSInfo) 4 | println(io, "$name = $(getfield(csinfo, name))") 5 | end 6 | end 7 | 8 | function compress_var_string(variable::CS.Variable) 9 | if CS.isfixed(variable) 10 | return string(CS.value(variable)) 11 | end 12 | 13 | sorted_vals = sort(CS.values(variable)) 14 | if sorted_vals[1] + length(CS.values(variable)) - 1 == sorted_vals[end] 15 | return "$(sorted_vals[1]):$(sorted_vals[end])" 16 | end 17 | return string(sort(CS.values(variable))) 18 | end 19 | 20 | function get_str_repr(variables::Array{CS.Variable}) 21 | if length(size(variables)) == 1 22 | output = "" 23 | for i in 1:length(variables) 24 | if !CS.isfixed(variables[i]) 25 | output *= "$(compress_var_string(variables[i])), " 26 | else 27 | output *= "$(CS.value(variables[i])), " 28 | end 29 | end 30 | return [output[1:(end - 2)]] 31 | elseif length(size(variables)) == 2 32 | max_length = 1 33 | for j in 1:size(variables)[2] 34 | for i in 1:size(variables)[1] 35 | if !CS.isfixed(variables[i, j]) 36 | len = length(compress_var_string(variables[i, j])) 37 | if len > max_length 38 | max_length = len 39 | end 40 | else 41 | len = length(string(CS.value(variables[i, j]))) 42 | if len > max_length 43 | max_length = len 44 | end 45 | end 46 | end 47 | end 48 | lines = String[] 49 | for j in 1:size(variables)[2] 50 | line = "" 51 | for i in 1:size(variables)[1] 52 | pstr = "" 53 | if !CS.isfixed(variables[i, j]) 54 | pstr = compress_var_string(variables[i, j]) 55 | else 56 | pstr = string(CS.value(variables[i, j])) 57 | end 58 | space_left = floor(Int, (max_length - length(pstr)) / 2) 59 | space_right = ceil(Int, (max_length - length(pstr)) / 2) 60 | line *= repeat(" ", space_left) * pstr * repeat(" ", space_right) * " " 61 | end 62 | push!(lines, line) 63 | end 64 | return lines 65 | else 66 | @warn "Currently not supported to print more than 2 dimensions. Maybe file an issue with your problem and desired output" 67 | end 68 | end 69 | 70 | function Base.show(io::IO, variables::Array{CS.Variable}) 71 | lines = get_str_repr(variables) 72 | for line in lines 73 | println(line) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Test 2 | using ConstraintProgrammingExtensions 3 | using ConstraintSolver 4 | using JSON 5 | using Random 6 | using MathOptInterface, JuMP, Cbc, GLPK, Combinatorics 7 | using ReferenceTests 8 | using LinearAlgebra 9 | 10 | const CPE = ConstraintProgrammingExtensions 11 | const MOI = MathOptInterface 12 | const CS = ConstraintSolver 13 | const MOIU = MOI.Utilities 14 | 15 | function CSTestOptimizer(; branch_strategy = :Auto) 16 | CS.Optimizer(logging = [], seed = 1, branch_strategy = branch_strategy) 17 | end 18 | function CSJuMPTestOptimizer(; branch_strategy = :Auto) 19 | JuMP.optimizer_with_attributes( 20 | CS.Optimizer, 21 | "logging" => [], 22 | "seed" => 4, 23 | "branch_strategy" => branch_strategy, 24 | ) 25 | end 26 | cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) 27 | function CSCbcJuMPTestOptimizer(; branch_strategy = :Auto) 28 | JuMP.optimizer_with_attributes( 29 | CS.Optimizer, 30 | "logging" => [], 31 | "lp_optimizer" => cbc_optimizer, 32 | "seed" => 2, 33 | "branch_strategy" => branch_strategy, 34 | ) 35 | end 36 | 37 | macro test_macro_throws(errortype, m) 38 | # See https://discourse.julialang.org/t/test-throws-with-macros-after-pr-23533/5878 39 | :(@test_throws $(esc(errortype)) try 40 | @eval $m 41 | catch err 42 | throw(err.error) 43 | end) 44 | end 45 | 46 | """ 47 | test_string(arr::AbstractArray) 48 | 49 | In Julia 1.0 string(arr) starts with Array{Int, 1} or something. In 1.5 it doesn't. 50 | This function removes Array{...} and starts with `[`. Additionally all white spaces are removed. 51 | """ 52 | function test_string(arr::AbstractArray) 53 | s = string(arr) 54 | if startswith(s, "Array") 55 | p = first(findfirst("[", s)) 56 | s = s[p:end] 57 | end 58 | s = replace(s, " " => "") 59 | return s 60 | end 61 | 62 | test_stime = time() 63 | 64 | include("general.jl") 65 | include("sudoku_fcts.jl") 66 | 67 | include("docs.jl") 68 | include("fcts.jl") 69 | include("unit/index.jl") 70 | include("options.jl") 71 | include("moi.jl") 72 | include("scheduling.jl") 73 | include("constraints/table.jl") 74 | include("constraints/xor.jl") 75 | include("constraints/indicator.jl") 76 | include("constraints/reified.jl") 77 | include("constraints/equal_to.jl") 78 | include("constraints/element1Dconst.jl") 79 | 80 | include("lp_solver.jl") 81 | 82 | include("steiner.jl") 83 | include("monks_and_doors.jl") 84 | include("stable_set.jl") 85 | include("small_special.jl") 86 | include("maximum_weight_matching.jl") 87 | include("small_eq_sum_real.jl") 88 | include("sudoku.jl") 89 | include("str8ts.jl") 90 | include("eternity.jl") 91 | include("killer_sudoku.jl") 92 | include("graph_color.jl") 93 | println("Time for all tests $(time()-test_stime)") 94 | -------------------------------------------------------------------------------- /src/MOI_wrapper/Bridges/complement.jl: -------------------------------------------------------------------------------- 1 | struct ComplementBridge{T, B<:MOIBC.SetMapBridge{T}, F, S} <: MOIBC.SetMapBridge{T, MOI.AbstractVectorSet, S, MOI.AbstractFunction, MOI.AbstractFunction} 2 | con_idx::CI 3 | end 4 | 5 | function MOI.supports_constraint( 6 | ::Type{<:ComplementBridge{T, B}}, 7 | ::Type{F}, 8 | ::Type{<:CS.ComplementSet{CF,S}} 9 | ) where {T, B, F<:MOI.AbstractFunction, CF, S} 10 | if CF <: MOI.ScalarAffineFunction 11 | is_supported = MOI.supports_constraint(B, CF, S) 12 | else 13 | is_supported = MOI.supports_constraint(B, MOIU.scalar_type(CF), S) 14 | end 15 | !is_supported && return false 16 | S <: AbstractBoolSet && return true 17 | 18 | concrete_B = MOIBC.concrete_bridge_type(B, MOI.ScalarAffineFunction{T}, S) 19 | supported = supports_concreteB(concrete_B) 20 | return supported 21 | end 22 | 23 | function MOIBC.concrete_bridge_type( 24 | ::Type{<:ComplementBridge{T,B}}, 25 | G::Type{<:MOI.AbstractFunction}, 26 | ::Type{<:CS.ComplementSet{F,S}}, 27 | ) where {T,B,F,S} 28 | if S <: AbstractBoolSet 29 | concrete_B = B 30 | else 31 | concrete_B = MOIBC.concrete_bridge_type(B, MOI.ScalarAffineFunction{T}, S) 32 | end 33 | return ComplementBridge{T,concrete_B,F,S} 34 | end 35 | 36 | function MOIB.added_constraint_types( 37 | ::Type{<:ComplementBridge{T,B,F,S}} 38 | ) where {T,B,F,S} 39 | if S <: AbstractBoolSet 40 | added_constraints = added_constraint_types(B, S) 41 | else 42 | added_constraints = MOIB.added_constraint_types(B) 43 | end 44 | return [(F, CS.ComplementSet{F,added_constraints[1][2]})] 45 | end 46 | 47 | function MOIB.added_constrained_variable_types(::Type{<:ComplementBridge{T,B}}) where {T,B} 48 | return MOIB.added_constrained_variable_types(B) 49 | end 50 | 51 | function MOIBC.bridge_constraint(::Type{<:ComplementBridge{T, B, F, S}}, model, fct, set) where {T, B, F, S} 52 | new_fct = map_function(B, fct, set.set) 53 | new_inner_set = MOIB.map_set(B, set.set) 54 | new_set = CS.ComplementSet{F}(new_inner_set) 55 | if (new_fct isa SAF) && F <: SAF 56 | new_fct = get_saf(new_fct) 57 | end 58 | return ComplementBridge{T,B,F,S}(MOI.add_constraint(model, new_fct, new_set)) 59 | end 60 | 61 | function map_function( 62 | bridge::Type{<:ComplementBridge{T, B}}, 63 | fct, 64 | set::ComplementSet{F,S} 65 | ) where {T,B,F,S<:AbstractBoolSet} 66 | return map_function(B, fct, set.set) 67 | end 68 | 69 | function MOIB.map_function( 70 | bridge::Type{<:ComplementBridge{T, B}}, 71 | fct, 72 | ) where {T,B} 73 | return MOIB.map_function(B, fct) 74 | end 75 | 76 | function MOIB.map_set( 77 | bridge::Type{<:ComplementBridge{T, B}}, 78 | set::ComplementSet{F}, 79 | ) where {T,B,F} 80 | mapped_set = MOIB.map_set(B, set.set) 81 | return ComplementSet{F}(mapped_set) 82 | end -------------------------------------------------------------------------------- /src/constraints/svc.jl: -------------------------------------------------------------------------------- 1 | #= 2 | Support for single variable functions i.e a <= b 3 | =# 4 | 5 | """ 6 | prune_constraint!(com::CS.CoM, constraint::CS.SingleVariableConstraint, fct::MOI.ScalarAffineFunction{T}, set::LessThan{T}; logs = true) where T <: Real 7 | 8 | Support for constraints of the form a <= b where a and b are single variables. 9 | This function removes values which aren't possible based on this constraint. 10 | """ 11 | function prune_constraint!( 12 | com::CS.CoM, 13 | constraint::CS.SingleVariableConstraint, 14 | fct::SAF{T}, 15 | set::MOI.LessThan; 16 | logs = true, 17 | ) where {T<:Real} 18 | lhs = constraint.lhs 19 | rhs = constraint.rhs 20 | search_space = com.search_space 21 | !remove_above!(com, search_space[lhs], search_space[rhs].max) && return false 22 | !remove_below!(com, search_space[rhs], search_space[lhs].min) && return false 23 | return true 24 | end 25 | 26 | """ 27 | less_than(com::CoM, constraint::CS.SingleVariableConstraint, vidx::Int, val::Int) 28 | 29 | Checks whether setting an `vidx` to `val` fulfills `constraint` 30 | """ 31 | function still_feasible( 32 | com::CoM, 33 | constraint::CS.SingleVariableConstraint, 34 | fct::SAF{T}, 35 | set::MOI.LessThan, 36 | vidx::Int, 37 | val::Int, 38 | ) where {T<:Real} 39 | if constraint.lhs == vidx 40 | # if a > maximum possible value of rhs => Infeasible 41 | if val > com.search_space[constraint.rhs].max 42 | return false 43 | else 44 | return true 45 | end 46 | elseif constraint.rhs == vidx 47 | if val < com.search_space[constraint.lhs].min 48 | return false 49 | else 50 | return true 51 | end 52 | else 53 | error("This should not happen but if it does please open an issue with the information: SingleVariableConstraint index is neither lhs nor rhs and your model.") 54 | end 55 | end 56 | 57 | function is_constraint_solved( 58 | constraint::CS.SingleVariableConstraint, 59 | fct::SAF{T}, 60 | set::MOI.LessThan, 61 | values::Vector{Int}, 62 | ) where {T<:Real} 63 | return values[1] <= values[2] 64 | end 65 | 66 | 67 | """ 68 | is_constraint_violated( 69 | com::CoM, 70 | constraint::CS.SingleVariableConstraint, 71 | fct::SAF{T}, 72 | set::Union{MOI.LessThan{T}, Union{MOI.LessThan{T},MOI.LessThan{T}}{T}}, 73 | ) where {T<:Real} 74 | 75 | Checks if the constraint is violated as it is currently set. This can happen inside an 76 | inactive reified or indicator constraint. 77 | """ 78 | function is_constraint_violated( 79 | com::CoM, 80 | constraint::CS.SingleVariableConstraint, 81 | fct::SAF{T}, 82 | set::MOI.LessThan, 83 | ) where {T<:Real} 84 | return com.search_space[constraint.indices[1]].min > 85 | com.search_space[constraint.indices[2]].max 86 | end 87 | -------------------------------------------------------------------------------- /benchmark/sudoku/cs.jl: -------------------------------------------------------------------------------- 1 | using ConstraintSolver, JuMP, MathOptInterface 2 | 3 | if !@isdefined CS 4 | const CS = ConstraintSolver 5 | end 6 | const MOI = MathOptInterface 7 | const MOIU = MOI.Utilities 8 | include("../../test/sudoku_fcts.jl") 9 | 10 | function from_file(filename, sep = '\n') 11 | s = open(filename) do file 12 | read(file, String) 13 | end 14 | str_sudokus = split(strip(s), sep) 15 | grids = AbstractArray[] 16 | for str_sudoku in str_sudokus 17 | str_sudoku = replace(str_sudoku, "." => "0") 18 | one_line_grid = parse.(Int, split(str_sudoku, "")) 19 | grid = reshape(one_line_grid, 9, 9) 20 | push!(grids, grid) 21 | end 22 | return grids 23 | end 24 | 25 | function solve_all(grids; benchmark = false, single_times = true) 26 | ct = time() 27 | grids = grids 28 | for (i, grid) in enumerate(grids) 29 | m = CS.Optimizer(logging = []) 30 | 31 | x = [[MOI.add_constrained_variable(m, MOI.Integer()) for i in 1:9] for j in 1:9] 32 | for r in 1:9, c in 1:9 33 | MOI.add_constraint(m, x[r][c][1], MOI.GreaterThan(1.0)) 34 | MOI.add_constraint(m, x[r][c][1], MOI.LessThan(9.0)) 35 | end 36 | 37 | # set variables 38 | for r in 1:9, c in 1:9 39 | if grid[r, c] != 0 40 | sat = [MOI.ScalarAffineTerm(1.0, x[r][c][1])] 41 | MOI.add_constraint( 42 | m, 43 | MOI.ScalarAffineFunction{Float64}(sat, 0.0), 44 | MOI.EqualTo(convert(Float64, grid[r, c])), 45 | ) 46 | end 47 | end 48 | # sudoku constraints 49 | moi_add_sudoku_constr!(m, x) 50 | 51 | if single_times 52 | GC.enable(false) 53 | MOI.optimize!(m) 54 | status = MOI.get(m, MOI.TerminationStatus()) 55 | GC.enable(true) 56 | # println(i - 1, ", ", MOI.get(m, MOI.SolveTimeSec())) 57 | else 58 | GC.enable(false) 59 | MOI.optimize!(m) 60 | status = MOI.get(m, MOI.TerminationStatus()) 61 | GC.enable(true) 62 | end 63 | if !benchmark 64 | println("Status: ", status) 65 | solution = zeros(Int, 9, 9) 66 | for r in 1:9 67 | solution[r, :] = [MOI.get(m, MOI.VariablePrimal(), x[r][c][1]) for c in 1:9] 68 | end 69 | @assert jump_fulfills_sudoku_constr(solution) 70 | end 71 | end 72 | # println("") 73 | tt = time() - ct 74 | # println("total time: ", tt) 75 | # println("avg: ", tt / length(grids)) 76 | end 77 | 78 | function main(; benchmark = false, single_times = true) 79 | solve_all( 80 | from_file("data/top95.txt"); 81 | benchmark = benchmark, 82 | single_times = single_times, 83 | ) 84 | # solve_all(from_file("hardest.txt"), "hardest") 85 | end 86 | -------------------------------------------------------------------------------- /test/monks_and_doors.jl: -------------------------------------------------------------------------------- 1 | # This test case is copied from http://hakank.org/julia/constraints/monks_and_doors.jl 2 | # which is a website by Håkan Kjellerstrand 3 | # please check it out :) 4 | 5 | @testset "Monks & Doors" begin 6 | all_solutions = true 7 | model = Model(optimizer_with_attributes(CS.Optimizer, 8 | "all_solutions"=> all_solutions, 9 | # "all_optimal_solutions"=>all_solutions, 10 | "logging"=>[], 11 | 12 | "traverse_strategy"=>:BFS, 13 | "branch_split"=>:InHalf, 14 | 15 | "branch_strategy" => :IMPS, # default 16 | )) 17 | 18 | num_doors = 4 19 | num_monks = 8 20 | @variable(model, doors[1:num_doors], Bin) 21 | da,db,dc,dd = doors 22 | door_names = ["A","B","C","D"] 23 | 24 | @variable(model, monks[1:num_monks], Bin) 25 | m1,m2,m3,m4,m5,m6,m7,m8 = monks 26 | 27 | # Monk 1: Door A is the exit. 28 | # M1 #= A (Picat constraint) 29 | @constraint(model, m1 == da) 30 | 31 | # Monk 2: At least one of the doors B and C is the exit. 32 | # M2 #= 1 #<=> (B #\/ C) 33 | @constraint(model, m2 := { db == 1 || dc == 1}) 34 | 35 | # Monk 3: Monk 1 and Monk 2 are telling the truth. 36 | # M3 #= 1 #<=> (M1 #/\ M2) 37 | @constraint(model, m3 := { m1 == 1 && m2 == 1}) 38 | 39 | # Monk 4: Doors A and B are both exits. 40 | # M4 #= 1 #<=> (A #/\ B) 41 | @constraint(model, m4 := { da == 1 && db == 1}) 42 | 43 | # Monk 5: Doors A and C are both exits. 44 | # M5 #= 1 #<=> (A #/\ C) 45 | @constraint(model, m5 := { da == 1 && dc == 1}) 46 | 47 | # Monk 6: Either Monk 4 or Monk 5 is telling the truth. 48 | # M6 #= 1 #<=> (M4 #\/ M5) 49 | @constraint(model, m6 := { m4 == 1|| m5 == 1}) 50 | 51 | # Monk 7: If Monk 3 is telling the truth, so is Monk 6. 52 | # M7 #= 1 #<=> (M3 #=> M6) 53 | @constraint(model, m7 := { m3 => {m6 == 1}}) 54 | 55 | # Monk 8: If Monk 7 and Monk 8 are telling the truth, so is Monk 1. 56 | # M8 #= 1 #<=> ((M7 #/\ M8) #=> (M1)) 57 | b1 = @variable(model, binary=true) 58 | @constraint(model, b1 := {m7 == 1 && m8 == 1}) 59 | @constraint(model, m8 := {b1 => {m1 == 1}}) 60 | 61 | # Exactly one door is an exit. 62 | # (A + B + C + D) #= 1 63 | @constraint(model, da + db + dc + dd == 1) 64 | 65 | # Solve the problem 66 | optimize!(model) 67 | 68 | status = JuMP.termination_status(model) 69 | # println("status:$status") 70 | num_sols = 0 71 | @test status == MOI.OPTIMAL 72 | num_sols = MOI.get(model, MOI.ResultCount()) 73 | @test num_sols == 1 74 | doors_val = convert.(Integer,JuMP.value.(doors)) 75 | monks_val = convert.(Integer,JuMP.value.(monks)) 76 | # exit door is A 77 | @test doors_val[1] == 1 78 | @test sum(doors_val) == 1 79 | @test sum(monks_val) == 3 80 | # monks 1,7,8 are the only ones who tell the truth 81 | @test monks_val[1] == monks_val[7] == monks_val[8] == 1 82 | end -------------------------------------------------------------------------------- /src/constraints/or.jl: -------------------------------------------------------------------------------- 1 | """ 2 | function is_constraint_violated( 3 | com::CoM, 4 | constraint::BoolConstraint, 5 | fct, 6 | set::OrSet, 7 | ) 8 | 9 | Check if both of the inner constraints are violated 10 | """ 11 | function is_constraint_violated( 12 | com::CoM, 13 | constraint::BoolConstraint, 14 | fct, 15 | set::OrSet, 16 | ) 17 | return is_lhs_constraint_violated(com, constraint) && is_rhs_constraint_violated(com, constraint) 18 | end 19 | 20 | """ 21 | still_feasible(com::CoM, constraint::OrConstraint, fct, set::OrSet, vidx::Int, value::Int) 22 | 23 | Return whether the constraint can be still fulfilled when setting a variable with index `vidx` to `value`. 24 | **Attention:** This assumes that it isn't violated before. 25 | """ 26 | function still_feasible( 27 | com::CoM, 28 | constraint::OrConstraint, 29 | fct, 30 | set::OrSet, 31 | vidx::Int, 32 | value::Int, 33 | ) 34 | lhs_feasible = !is_constraint_violated(com, constraint.lhs, constraint.lhs.fct, constraint.lhs.set) 35 | if lhs_feasible 36 | lhs_indices = constraint.lhs.indices 37 | for i in 1:length(lhs_indices) 38 | if lhs_indices[i] == vidx 39 | lhs_feasible = still_feasible(com, constraint.lhs, constraint.lhs.fct, constraint.lhs.set, vidx, value) 40 | lhs_feasible && return true 41 | break 42 | end 43 | end 44 | end 45 | rhs_feasible = !is_constraint_violated(com, constraint.rhs, constraint.rhs.fct, constraint.rhs.set) 46 | if rhs_feasible 47 | rhs_indices = constraint.rhs.indices 48 | for i in 1:length(rhs_indices) 49 | if rhs_indices[i] == vidx 50 | rhs_feasible = still_feasible(com, constraint.rhs, constraint.rhs.fct, constraint.rhs.set, vidx, value) 51 | rhs_feasible && return true 52 | break 53 | end 54 | end 55 | end 56 | return rhs_feasible || lhs_feasible 57 | end 58 | 59 | """ 60 | prune_constraint!(com::CS.CoM, constraint::OrConstraint, fct, set::OrSet; logs = true) 61 | 62 | Reduce the number of possibilities given the `OrConstraint` by pruning both parts 63 | Return whether still feasible 64 | """ 65 | function prune_constraint!( 66 | com::CS.CoM, 67 | constraint::OrConstraint, 68 | fct, 69 | set::OrSet; 70 | logs = true, 71 | ) 72 | lhs_violated = is_constraint_violated(com, constraint.lhs, constraint.lhs.fct, constraint.lhs.set) 73 | rhs_violated = is_constraint_violated(com, constraint.rhs, constraint.rhs.fct, constraint.rhs.set) 74 | if lhs_violated && rhs_violated 75 | return false 76 | end 77 | if lhs_violated 78 | activate_rhs!(com, constraint) 79 | return prune_constraint!(com, constraint.rhs, constraint.rhs.fct, constraint.rhs.set; logs=logs) 80 | end 81 | if rhs_violated 82 | activate_lhs!(com, constraint) 83 | return prune_constraint!(com, constraint.lhs, constraint.lhs.fct, constraint.lhs.set; logs=logs) 84 | end 85 | return true 86 | end -------------------------------------------------------------------------------- /src/MOI_wrapper/bool.jl: -------------------------------------------------------------------------------- 1 | const BOOL_VALS = Union{Val{:(&&)}, Val{:(||)}, Val{:(⊻)}} 2 | 3 | bool_val_to_set(::Val{:(&&)}) = AndSet 4 | bool_val_to_set(::Val{:(||)}) = OrSet 5 | bool_val_to_set(::Val{:(⊻)}) = XorSet 6 | 7 | function _build_bool_constraint( 8 | _error::Function, 9 | lhs, 10 | rhs, 11 | set_type 12 | ) 13 | lhs_set = JuMP.moi_set(lhs) 14 | rhs_set = JuMP.moi_set(rhs) 15 | 16 | lhs_jump_func = JuMP.jump_function(lhs) 17 | rhs_jump_func = JuMP.jump_function(rhs) 18 | 19 | lhs_func = JuMP.moi_function(lhs) 20 | rhs_func = JuMP.moi_function(rhs) 21 | 22 | func = [lhs_jump_func..., rhs_jump_func...] 23 | return JuMP.VectorConstraint( 24 | func, set_type{typeof(lhs_func), typeof(rhs_func)}(lhs_set, rhs_set) 25 | ) 26 | end 27 | 28 | """ 29 | transform_binary_expr(sym::Symbol) 30 | 31 | Transform a symbol to a constraint of the form Symbol == 1 32 | """ 33 | function transform_binary_expr(sym::Symbol) 34 | return :($sym == 1) 35 | end 36 | 37 | """ 38 | transform_binary_expr(expr::Expr) 39 | 40 | Transform a ! (symbol) to a constraint of the form Symbol == 0 41 | or x[...] to x[...] == 1 42 | """ 43 | function transform_binary_expr(expr::Expr) 44 | if expr.head == :ref 45 | expr = :($expr == 1) 46 | elseif expr.head == :call && expr.args[1] == :! && (expr.args[2] isa Symbol || expr.args[2].head == :ref) 47 | expr = :($(expr.args[2]) == 0) 48 | end 49 | return expr 50 | end 51 | 52 | function parse_bool_constraint(_error, bool_val::BOOL_VALS, lhs, rhs) 53 | _error1 = deepcopy(_error) 54 | # allow a || b instead of a == 1 || b == 1 55 | lhs = transform_binary_expr(lhs) 56 | 57 | lhs_vectorized, lhs_parsecode, lhs_buildcall = 58 | JuMP.parse_constraint(_error, lhs) 59 | 60 | if lhs_vectorized 61 | _error("`$(lhs)` should be non vectorized. There is currently no vectorized support for `and` constraints. Please open an issue at ConstraintSolver.jl") 62 | end 63 | 64 | rhs = transform_binary_expr(rhs) 65 | rhs_vectorized, rhs_parsecode, rhs_buildcall = 66 | JuMP.parse_constraint(_error1, rhs) 67 | 68 | if rhs_vectorized 69 | _error("`$(rhs)` should be non vectorized. There is currently no vectorized support for `and` constraints. Please open an issue at ConstraintSolver.jl") 70 | end 71 | 72 | # TODO implement vectorized version 73 | vectorized = false 74 | complete_parsecode = quote 75 | $lhs_parsecode 76 | $rhs_parsecode 77 | end 78 | 79 | bool_set = bool_val_to_set(bool_val) 80 | 81 | buildcall = :($(esc(:(CS._build_bool_constraint)))( 82 | $_error, 83 | $lhs_buildcall, 84 | $rhs_buildcall, 85 | $bool_set 86 | )) 87 | return vectorized, complete_parsecode, buildcall 88 | end 89 | 90 | function JuMP.parse_constraint_head(_error::Function, bool_val::BOOL_VALS, lhs, rhs) 91 | return parse_bool_constraint(_error, bool_val, lhs, rhs) 92 | end 93 | 94 | function JuMP.parse_constraint_call(_error::Function, vectorized::Bool, bool_val::BOOL_VALS, lhs, rhs) 95 | @assert !vectorized 96 | _, parse_code, build_code = parse_bool_constraint(_error, bool_val, lhs, rhs) 97 | return parse_code, build_code 98 | end -------------------------------------------------------------------------------- /benchmark/eternity/cs.jl: -------------------------------------------------------------------------------- 1 | using ConstraintSolver, JuMP 2 | CS = ConstraintSolver 3 | 4 | function read_puzzle(pname) 5 | dir = pkgdir(ConstraintSolver) 6 | lines = readlines(pname) 7 | width, height = parse.(Int, split(lines[1])) 8 | lines = lines[2:end] 9 | npieces = length(lines) 10 | puzzle = zeros(Int, (npieces, 5)) 11 | for i in 1:npieces 12 | puzzle[i, 1] = i 13 | parts = split(lines[i], " ") 14 | puzzle[i, 2:end] = parse.(Int, parts) 15 | end 16 | return puzzle, width, height 17 | end 18 | 19 | function get_rotations(puzzle) 20 | npieces = size(puzzle)[1] 21 | rotations = zeros(Int, (npieces * 4, 5)) 22 | rotation_indices = [[2, 3, 4, 5], [3, 4, 5, 2], [4, 5, 2, 3], [5, 2, 3, 4]] 23 | for i in 1:npieces 24 | j = 1 25 | for rotation in rotation_indices 26 | rotations[(i - 1) * 4 + j, 1] = i 27 | rotations[(i - 1) * 4 + j, 2:end] = puzzle[i, rotation] 28 | j += 1 29 | end 30 | end 31 | 32 | return rotations 33 | end 34 | 35 | function main(pname; time_limit = 1800) 36 | puzzle, width, height = read_puzzle(pname) 37 | rotations = get_rotations(puzzle) 38 | npieces = size(puzzle)[1] 39 | ncolors = maximum(puzzle[:, 2:end]) 40 | 41 | m = Model(optimizer_with_attributes(CS.Optimizer, "time_limit" => time_limit)) 42 | 43 | @variable(m, 1 <= p[1:height, 1:width] <= npieces, Int) 44 | @variable(m, 0 <= pu[1:height, 1:width] <= ncolors, Int) 45 | @variable(m, 0 <= pr[1:height, 1:width] <= ncolors, Int) 46 | @variable(m, 0 <= pd[1:height, 1:width] <= ncolors, Int) 47 | @variable(m, 0 <= pl[1:height, 1:width] <= ncolors, Int) 48 | 49 | @constraint(m, p[:] in CS.AllDifferent()) 50 | for i in 1:height, j in 1:width 51 | @constraint( 52 | m, 53 | [p[i, j], pu[i, j], pr[i, j], pd[i, j], pl[i, j]] in CS.TableSet(rotations) 54 | ) 55 | end 56 | 57 | # borders 58 | # up and down 59 | for j in 1:width 60 | @constraint(m, pu[1, j] == 0) 61 | @constraint(m, pd[height, j] == 0) 62 | 63 | if j != width 64 | @constraint(m, pr[1, j] == pl[1, j + 1]) 65 | @constraint(m, pr[height, j] == pl[height, j + 1]) 66 | end 67 | end 68 | 69 | # right and left 70 | for i in 1:height 71 | @constraint(m, pl[i, 1] == 0) 72 | @constraint(m, pr[i, width] == 0) 73 | 74 | if i != height 75 | @constraint(m, pd[i, 1] == pu[i + 1, 1]) 76 | @constraint(m, pd[i, width] == pu[i + 1, width]) 77 | end 78 | end 79 | 80 | for i in 1:(height - 1), j in 1:(width - 1) 81 | @constraint(m, pd[i, j] == pu[i + 1, j]) 82 | @constraint(m, pr[i, j] == pl[i, j + 1]) 83 | end 84 | 85 | 86 | if width == height 87 | start_piece = findfirst(i -> count(c -> c == 0, puzzle[i, :]) == 2, 1:npieces) 88 | @constraint(m, p[1, 1] == start_piece) 89 | end 90 | 91 | optimize!(m) 92 | 93 | status = JuMP.termination_status(m) 94 | if status == MOI.OPTIMAL 95 | print("$status, $(JuMP.objective_value(m)), $(JuMP.solve_time(m))") 96 | else 97 | print("$status, NaN, $(time_limit)") 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/scheduling.jl: -------------------------------------------------------------------------------- 1 | # Scheduling isn't really possible directly with the solver yet :/ 2 | # Most of this file is based on http://www.hakank.org/julia/constraints/ 3 | # which is a website by Håkan Kjellerstrand 4 | # please check it out :) 5 | 6 | # by Håkan Kjellerstrand 7 | function cumulative(model, start, duration, resource, limit; times_max = nothing) 8 | tasks = [i for i in 1:length(start) if resource[i] > 0 && duration[i] > 0] 9 | num_tasks = length(tasks) 10 | 11 | times_min = minimum(round.(Int,[JuMP.lower_bound(start[i]) for i in tasks])) 12 | if times_max === nothing 13 | times_max = maximum(round.(Int,[JuMP.upper_bound(start[i])+duration[i] for i in tasks])) 14 | end 15 | for t in times_min:times_max 16 | b = @variable(model, [1:num_tasks], Bin) 17 | for i in tasks 18 | # The following don't work since ConstraintSolver don't 19 | # support nonlinear constraints 20 | # @constraint(model,sum([(start[i] <= t) * (t <= start[i] + duration[i])*resource[i] for i in tasks]) <= b) 21 | 22 | # is this task active during this time t? 23 | @constraint(model, b[i] := { start[i] <= t && t < start[i]+duration[i] }) # is this task active in time t ? 24 | end 25 | # Check that there's no conflicts in time t 26 | @constraint(model,sum([b[i]*resource[i] for i in tasks]) <= limit) 27 | end 28 | end 29 | 30 | #= 31 | Furniture moving (scheduling) in Julia ConstraintSolver.jl 32 | From Marriott & Stuckey: "Programming with constraints", page 112f 33 | Changed for this test case to be easier ;) 34 | 35 | Model created by Hakan Kjellerstrand, hakank@gmail.com 36 | See also his Julia page: http://www.hakank.org/julia/ 37 | =# 38 | function furniture_moving() 39 | model = Model(optimizer_with_attributes(CS.Optimizer, 40 | "logging"=>[], 41 | "branch_split"=>:InHalf, 42 | "time_limit"=>50 43 | )) 44 | 45 | 46 | 47 | # Furniture moving 48 | n = 4 49 | # [piano, chair, bed, table] 50 | durations = [30,10,15,15] 51 | # resource needed per task 52 | resources = [1,1,1,1] # <- changed for this test case to solve it faster 53 | max_end_time = 45 54 | @variable(model, 0 <= start_times[1:n] <= max_end_time, Int) 55 | @variable(model, minimum(durations) <= end_times[1:n] <= max_end_time, Int) 56 | @variable(model, 1 <= limit <= 3, Int) 57 | @variable(model, 0 <= max_time <= max_end_time,Int) 58 | 59 | for i in 1:n 60 | @constraint(model,end_times[i] == start_times[i] + durations[i]) 61 | end 62 | @constraint(model, end_times .<= max_time) 63 | cumulative(model, start_times, durations, resources, limit; times_max = max_end_time) 64 | 65 | # Solve the problem 66 | @objective(model, Min, max_time) 67 | optimize!(model) 68 | 69 | status = JuMP.termination_status(model) 70 | @test status == MOI.OPTIMAL 71 | @test JuMP.value(limit) ≈ 3 72 | @test JuMP.value(max_time) ≈ 30 73 | @test JuMP.value(start_times[1]) ≈ 0 74 | for i = 1:n 75 | @test JuMP.value(start_times[i])+durations[i] ≈ JuMP.value(end_times[i]) 76 | end 77 | end 78 | 79 | @testset "Scheduling" begin 80 | @testset "Furniture" begin 81 | furniture_moving() 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /benchmark/tune.json: -------------------------------------------------------------------------------- 1 | [{"Julia":"1.4.1","BenchmarkTools":"0.4.3"},[["BenchmarkGroup",{"data":{"sudoku":["BenchmarkGroup",{"data":{"top95_71":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_26":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_76":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_41":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_16":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_51":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_46":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_36":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_61":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_11":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_81":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_91":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_21":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_1":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_56":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_6":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_66":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_31":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}],"top95_86":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":2.0,"overhead":0.0,"memory_tolerance":0.01}]},"tags":["alldifferent"]}],"eternity":["BenchmarkGroup",{"data":{"6x5":["BenchmarkTools.Parameters",{"gctrial":true,"time_tolerance":0.05,"samples":10000,"evals":1,"gcsample":false,"seconds":5.0,"overhead":0.0,"memory_tolerance":0.01}]},"tags":["alldifferent","table","equal"]}]},"tags":[]}]]] -------------------------------------------------------------------------------- /test/unit/constraints/not_equal.jl: -------------------------------------------------------------------------------- 1 | @testset "not equal" begin 2 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 3 | @variable(m, y[1:3], CS.Integers([-3, 1, 2, 3])) 4 | @constraint(m, sum(y) + 1 != 5) 5 | optimize!(m) 6 | com = CS.get_inner_model(m) 7 | constraint = get_constraints_by_type(com, CS.LinearConstraint)[1] 8 | 9 | # doesn't check the length 10 | # 1+2+1 + constant (1) == 5 11 | @test !CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2, 1]) 12 | @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2, 2]) 13 | 14 | constr_indices = constraint.indices 15 | @test CS.still_feasible( 16 | com, 17 | constraint, 18 | constraint.fct, 19 | constraint.set, 20 | constr_indices[1], 21 | -3, 22 | ) 23 | @test CS.still_feasible( 24 | com, 25 | constraint, 26 | constraint.fct, 27 | constraint.set, 28 | constr_indices[1], 29 | 1, 30 | ) 31 | CS.fix!(com, com.search_space[constr_indices[1]], 2) 32 | CS.fix!(com, com.search_space[constr_indices[2]], 1) 33 | @test !CS.still_feasible( 34 | com, 35 | constraint, 36 | constraint.fct, 37 | constraint.set, 38 | constr_indices[3], 39 | 1, 40 | ) 41 | 42 | # need to create a backtrack_vec to reverse pruning 43 | dummy_backtrack_obj = CS.BacktrackObj(com) 44 | dummy_backtrack_obj.step_nr = 1 45 | push!(com.backtrack_vec, dummy_backtrack_obj) 46 | # reverse previous fix 47 | CS.reverse_pruning!(com, 1) 48 | com.c_backtrack_idx = 1 49 | # now setting it to 1 should be feasible 50 | @test CS.still_feasible( 51 | com, 52 | constraint, 53 | constraint.fct, 54 | constraint.set, 55 | constr_indices[3], 56 | 1, 57 | ) 58 | 59 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 60 | @variable(m, y[1:3], CS.Integers([-3, 1, 2, 3])) 61 | @constraint(m, sum(y) + 1 != 5) 62 | optimize!(m) 63 | 64 | com = CS.get_inner_model(m) 65 | constraint = get_constraints_by_type(com, CS.LinearConstraint)[1] 66 | 67 | CS.fix!(com, com.search_space[constr_indices[1]], 2) 68 | CS.fix!(com, com.search_space[constr_indices[2]], 1) 69 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 70 | @test sort(CS.values(com.search_space[constr_indices[3]])) == [-3, 2, 3] 71 | end 72 | 73 | @testset "not equal is_constraint_violated test" begin 74 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 75 | @variable(m, -5 <= x[1:2] <= 5, Int) 76 | @constraint(m, sum(x) != 7) 77 | optimize!(m) 78 | com = CS.get_inner_model(m) 79 | 80 | constraint = com.constraints[1] 81 | 82 | variables = com.search_space 83 | @test CS.fix!(com, variables[constraint.indices[1]], 5; check_feasibility = false) 84 | @test CS.fix!(com, variables[constraint.indices[2]], 2; check_feasibility = false) 85 | @test CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 86 | 87 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 88 | @variable(m, -5 <= x[1:2] <= 5, Int) 89 | @constraint(m, sum(x) != 7) 90 | optimize!(m) 91 | com = CS.get_inner_model(m) 92 | 93 | constraint = com.constraints[1] 94 | 95 | variables = com.search_space 96 | @test CS.fix!(com, variables[constraint.indices[1]], 5; check_feasibility = false) 97 | @test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 98 | end 99 | -------------------------------------------------------------------------------- /benchmark/killer_sudoku/cs.jl: -------------------------------------------------------------------------------- 1 | using ConstraintSolver, JuMP, MathOptInterface, JSON 2 | 3 | if !@isdefined CS 4 | const CS = ConstraintSolver 5 | end 6 | const MOI = MathOptInterface 7 | const MOIU = MOI.Utilities 8 | include("../../test/sudoku_fcts.jl") 9 | # include("../../visualizations/plot_search_space.jl") 10 | 11 | function parseJSON(json_sums) 12 | sums = [] 13 | for s in json_sums 14 | indices = Tuple[] 15 | for ind in s["indices"] 16 | push!(indices, tuple(ind...)) 17 | end 18 | 19 | push!(sums, (result = s["result"], indices = indices, color = s["color"])) 20 | end 21 | return sums 22 | end 23 | 24 | function solve_all(filenames; benchmark = false, single_times = true) 25 | ct = time() 26 | for (i, filename) in enumerate(filenames) 27 | sums = parseJSON(JSON.parsefile("data/$(filename)")) 28 | 29 | # plot_killer(zeros(Int, (9,9)), sums, filename; fill=false) 30 | # continue 31 | 32 | m = CS.Optimizer(logging = [], time_limit = 20) 33 | 34 | x = [[MOI.add_constrained_variable(m, MOI.Integer()) for i in 1:9] for j in 1:9] 35 | for r in 1:9, c in 1:9 36 | MOI.add_constraint(m, x[r][c][1], MOI.GreaterThan(1.0)) 37 | MOI.add_constraint(m, x[r][c][1], MOI.LessThan(9.0)) 38 | end 39 | 40 | for s in sums 41 | saf = MOI.ScalarAffineFunction{Float64}( 42 | [MOI.ScalarAffineTerm(1.0, x[ind[1]][ind[2]][1]) for ind in s.indices], 43 | 0.0, 44 | ) 45 | MOI.add_constraint(m, saf, MOI.EqualTo(convert(Float64, s.result))) 46 | # MOI.add_constraint(m, [x[ind[1]][ind[2]][1] for ind in s.indices], CS.CPE.AllDifferent(length(s.indices))) 47 | end 48 | 49 | # sudoku constraints 50 | moi_add_sudoku_constr!(m, x) 51 | 52 | 53 | if single_times 54 | GC.enable(false) 55 | MOI.optimize!(m) 56 | status = MOI.get(m, MOI.TerminationStatus()) 57 | GC.enable(true) 58 | println(i - 1, ", ", MOI.get(m, MOI.SolveTimeSec())) 59 | else 60 | GC.enable(false) 61 | MOI.optimize!(m) 62 | status = MOI.get(m, MOI.TerminationStatus()) 63 | GC.enable(true) 64 | end 65 | if !benchmark 66 | println("Status: ", status) 67 | var_x = fill(MOI.VariableIndex(0), (9, 9)) 68 | for r in 1:9 69 | var_x[r, :] = [x[r][c][1] for c in 1:9] 70 | end 71 | if status == MOI.OPTIMAL 72 | solution = zeros(Int, 9, 9) 73 | for r in 1:9 74 | solution[r, :] = 75 | [MOI.get(m, MOI.VariablePrimal(), x[r][c][1]) for c in 1:9] 76 | end 77 | @assert jump_fulfills_sudoku_constr(solution) 78 | else 79 | println("NOT SOLVED TO OPTIMALITY") 80 | end 81 | end 82 | com = nothing 83 | GC.gc() 84 | end 85 | println("") 86 | tt = time() - ct 87 | println("total time: ", tt) 88 | println("avg: ", tt / length(filenames)) 89 | end 90 | 91 | function main(; benchmark = false, single_times = true) 92 | solve_all( 93 | [ 94 | "niallsudoku_5500", 95 | # "niallsudoku_5501", 96 | # "niallsudoku_5502", 97 | # "niallsudoku_5503", 98 | # "niallsudoku_6417", 99 | # "niallsudoku_6249", 100 | ]; 101 | benchmark = benchmark, 102 | single_times = single_times, 103 | ) 104 | # solve_all(from_file("hardest.txt"), "hardest") 105 | end 106 | -------------------------------------------------------------------------------- /src/MOI_wrapper/Bridges/indicator.jl: -------------------------------------------------------------------------------- 1 | struct IndicatorBridge{T, B<:MOIBC.SetMapBridge{T}, A, IS_MOI_OR_CS, F, S} <: MOIBC.AbstractBridge 2 | con_idx::CI 3 | end 4 | 5 | function MOI.supports_constraint( 6 | ::Type{<:IndicatorBridge{T, B}}, 7 | ::Type{F}, 8 | ::Type{MOI.Indicator{A,S}} 9 | ) where {T, B, F<:MOI.VectorAffineFunction, A, S} 10 | is_supported = MOI.supports_constraint(B, MOIU.scalar_type(F), S) 11 | !is_supported && return false 12 | S <: AbstractBoolSet && return true 13 | 14 | concrete_B = MOIBC.concrete_bridge_type(B, MOI.ScalarAffineFunction{T}, S) 15 | return supports_concreteB(concrete_B) 16 | end 17 | 18 | function MOI.supports_constraint( 19 | ::Type{<:IndicatorBridge{T, B}}, 20 | ::Type{F}, 21 | ::Type{CS.Indicator{A,IF,S}} 22 | ) where {T, B, F<:MOI.VectorAffineFunction, A, IF, S} 23 | is_supported = MOI.supports_constraint(B, IF, S) 24 | !is_supported && return false 25 | S <: AbstractBoolSet && return true 26 | 27 | concrete_B = MOIBC.concrete_bridge_type(B, MOI.ScalarAffineFunction{T}, S) 28 | return supports_concreteB(concrete_B) 29 | end 30 | 31 | function MOIBC.concrete_bridge_type( 32 | IB::Type{<:IndicatorBridge{T,B}}, 33 | G::Type{<:MOI.VectorAffineFunction}, 34 | ::Type{IS}, 35 | ) where {T,B,A,S,IS<:MOI.Indicator{A,S}} 36 | concrete_B = get_concrete_B(IB,S) 37 | return IndicatorBridge{T,concrete_B,A,MOI.Indicator,MOI.ScalarAffineFunction,S} 38 | end 39 | 40 | function MOIBC.concrete_bridge_type( 41 | IB::Type{<:IndicatorBridge{T,B}}, 42 | G::Type{<:MOI.VectorAffineFunction}, 43 | ::Type{IS}, 44 | ) where {T,B,A,S,IF,IS<:CS.Indicator{A,IF,S}} 45 | concrete_B = get_concrete_B(IB,S) 46 | return IndicatorBridge{T,concrete_B,A,CS.Indicator,IF,S} 47 | end 48 | 49 | function get_concrete_B( 50 | ::Type{<:IndicatorBridge{T,B}}, 51 | S 52 | ) where {T,B} 53 | if S <: AbstractBoolSet 54 | return B 55 | else 56 | return MOIBC.concrete_bridge_type(B, MOI.ScalarAffineFunction{T}, S) 57 | end 58 | end 59 | 60 | function MOIB.added_constraint_types( 61 | ::Type{<:IndicatorBridge{T,B,A,IS_MOI_OR_CS,F,S}} 62 | ) where {T,B,A,IS_MOI_OR_CS<:MOI.Indicator,F,S} 63 | added_constraints = added_constraint_types(B, S) 64 | return [(MOI.VectorAffineFunction{T}, MOI.Indicator{A,added_constraints[1][2]})] 65 | end 66 | 67 | function MOIB.added_constraint_types( 68 | ::Type{<:IndicatorBridge{T,B,A,IS_MOI_OR_CS,F,S}} 69 | ) where {T,B,A,IS_MOI_OR_CS<:CS.Indicator,F,S} 70 | added_constraints = added_constraint_types(B, S) 71 | return [(MOI.VectorAffineFunction{T}, CS.Indicator{A,F,added_constraints[1][2]})] 72 | end 73 | 74 | 75 | function MOIB.added_constrained_variable_types(::Type{<:IndicatorBridge{T,B}}) where {T,B} 76 | return MOIB.added_constrained_variable_types(B) 77 | end 78 | 79 | function MOIBC.bridge_constraint(::Type{<:IndicatorBridge{T, B, A, IS_MOI_OR_CS, F, S}}, model, func, set) where {T, B, A, IS_MOI_OR_CS<:MOI.Indicator, F, S} 80 | f = MOIU.eachscalar(func) 81 | new_func = MOIU.operate(vcat, T, f[1], map_function(B, f[2:end], set.set)) 82 | new_inner_set = MOIB.map_set(B, set.set) 83 | new_set = MOI.Indicator{A}(new_inner_set) 84 | return IndicatorBridge{T,B,A,IS_MOI_OR_CS,F,S}(MOI.add_constraint(model, new_func, new_set)) 85 | end 86 | 87 | function MOIBC.bridge_constraint(::Type{<:IndicatorBridge{T, B, A, IS_MOI_OR_CS, F, S}}, model, func, set) where {T, B, A, IS_MOI_OR_CS<:CS.Indicator, F, S} 88 | f = MOIU.eachscalar(func) 89 | new_func = MOIU.operate(vcat, T, f[1], map_function(B, f[2:end], set.set)) 90 | new_inner_set = MOIB.map_set(B, set.set) 91 | new_set = CS.Indicator{A,F}(new_inner_set) 92 | return IndicatorBridge{T,B,A,IS_MOI_OR_CS,F,S}(MOI.add_constraint(model, new_func, new_set)) 93 | end 94 | 95 | -------------------------------------------------------------------------------- /src/constraints/all_different/scc.jl: -------------------------------------------------------------------------------- 1 | """ 2 | scc(di_ei, di_ej, scc_init::SCCInit) 3 | 4 | Compute to which strongly connected component each vertex belongs. 5 | 6 | `di_ei` and `di_ej` describe the edges such that the k-th component of di_ei and k-th component 7 | of `di_ej` form an edge i.e `di_ei = [1, 1, 2]` and `di_ej = [3, 4, 3]` => 8 | those three edges (1, 3), (1, 4), (2, 3) 9 | 10 | `di_ei` must be sorted asc to make this function work. 11 | 12 | The last argument is a `SCCInit` struct which itself provides the memory for the functionality. 13 | This speeds up the process as no new memory needs to be allocated. 14 | """ 15 | function scc(di_ei, di_ej, scc_init::SCCInit) 16 | len = length(di_ei) 17 | index_ei = scc_init.index_ei 18 | n = length(index_ei) - 1 19 | # compute the starting index for each vertex 20 | # bascially knowing where to start/end when looking for neighbors 21 | last = di_ei[1] 22 | prev_last = 1 23 | c = 2 24 | last_i = 0 25 | @inbounds for i in 2:len 26 | di_ei[i] == 0 && break 27 | if di_ei[i] > last 28 | j = last 29 | index_ei[(last + 1):di_ei[i]] .= c 30 | last = di_ei[i] 31 | end 32 | c += 1 33 | last_i = i 34 | end 35 | index_ei[(di_ei[last_i] + 1):end] .= c 36 | 37 | index_ei[1] = 1 38 | 39 | id = 0 40 | sccCount = 0 41 | 42 | ids = scc_init.ids 43 | low = scc_init.low 44 | on_stack = scc_init.on_stack 45 | group_id = scc_init.group_id 46 | 47 | ids .= -1 48 | low .= 0 49 | on_stack .= false 50 | stack = Int[] 51 | group_id .= 0 52 | c_group_id = 1 53 | 54 | # start dfs from each vertex if unconnected graph 55 | @inbounds for s in 1:n 56 | ids[s] != -1 && continue # if visited already continue 57 | dfs_work = Vector{Tuple{Int,Int}}() 58 | # the 0 in dfs_work represents whether it's the first time calling that vertex 59 | push!(dfs_work, (s, 0)) 60 | dfs_stack = Int[] 61 | 62 | while !isempty(dfs_work) 63 | at, i = pop!(dfs_work) 64 | if i == 0 65 | on_stack[at] = true 66 | id += 1 67 | ids[at] = id 68 | low[at] = id 69 | push!(dfs_stack, at) 70 | end 71 | recurse = false 72 | # only works because `di_ei` is sorted 73 | for j in (index_ei[at] + i):(index_ei[at + 1] - 1) 74 | to = di_ej[j] 75 | # println("$to is successor of $at") 76 | if ids[to] == -1 77 | push!(dfs_work, (at, j - index_ei[at] + 1)) 78 | push!(dfs_work, (to, 0)) 79 | recurse = true 80 | break 81 | elseif on_stack[to] 82 | low[at] = min(low[at], ids[to]) 83 | end 84 | end 85 | recurse && continue 86 | # if at is the representative of the group 87 | if ids[at] == low[at] 88 | # take from stack as long as the representative doesn't appear 89 | # and put all of them in the same scc 90 | while true 91 | w = pop!(dfs_stack) 92 | on_stack[w] = false 93 | group_id[w] = c_group_id 94 | w == at && break 95 | end 96 | c_group_id += 1 97 | end 98 | if !isempty(dfs_work) 99 | w = at 100 | at, _ = dfs_work[end] 101 | low[at] = min(low[at], low[w]) 102 | end 103 | end 104 | end 105 | return group_id 106 | end 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build status](https://github.com/Wikunia/ConstraintSolver.jl/workflows/Run%20tests/badge.svg) [![codecov](https://codecov.io/gh/Wikunia/ConstraintSolver.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/Wikunia/ConstraintSolver.jl) 2 | [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://wikunia.github.io/ConstraintSolver.jl/dev) 3 | [![Docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://wikunia.github.io/ConstraintSolver.jl/stable) 4 | 5 | # ConstraintSolver.jl 6 | 7 | ![Logo](https://user-images.githubusercontent.com/4931746/83681097-2c247480-a5e2-11ea-9301-0c46726dea25.png) 8 | 9 | This package aims to be a constraint solver completely written in Julia. The concepts are more or less fully described on my blog [OpenSourc.es](https://opensourc.es/blog/constraint-solver-1). 10 | There is of course also the general user manual [here](https://wikunia.github.io/ConstraintSolver.jl/stable) which explains how to solve your model. 11 | 12 | 13 | ## Goals 14 | - Easily extendable 15 | - Teaching/Learning about constraint programming 16 | 17 | ## Installation 18 | You can install this julia package using 19 | `] add ConstraintSolver` or if you want to change code you might want to use 20 | `] dev ConstraintSolver`. 21 | 22 | ## Example 23 | 24 | You can easily use this package with the same modeling package as you might be used to for solving (non)linear problems in Julia: [JuMP.jl](https://github.com/JuliaOpt/JuMP.jl). 25 | 26 | ### Sudoku 27 | ```julia 28 | using JuMP 29 | 30 | grid = [6 0 2 0 5 0 0 0 0; 31 | 0 0 0 0 0 3 0 4 0; 32 | 0 0 0 0 0 0 0 0 0; 33 | 4 3 0 0 0 8 0 0 0; 34 | 0 1 0 0 0 0 2 0 0; 35 | 0 0 0 0 0 0 7 0 0; 36 | 5 0 0 2 7 0 0 0 0; 37 | 0 0 0 0 0 0 0 8 1; 38 | 0 0 0 6 0 0 0 0 0] 39 | 40 | using ConstraintSolver 41 | # define a shorter name ;) 42 | const CS = ConstraintSolver 43 | 44 | # creating a constraint solver model and setting ConstraintSolver as the optimizer. 45 | m = Model(CS.Optimizer) 46 | # define the 81 variables 47 | @variable(m, 1 <= x[1:9,1:9] <= 9, Int) 48 | # set variables if fixed 49 | for r=1:9, c=1:9 50 | if grid[r,c] != 0 51 | @constraint(m, x[r,c] == grid[r,c]) 52 | end 53 | end 54 | 55 | for rc = 1:9 56 | @constraint(m, x[rc,:] in CS.AllDifferent()) 57 | @constraint(m, x[:,rc] in CS.AllDifferent()) 58 | end 59 | 60 | for br=0:2 61 | for bc=0:2 62 | @constraint(m, vec(x[br*3+1:(br+1)*3,bc*3+1:(bc+1)*3]) in CS.AllDifferent()) 63 | end 64 | end 65 | 66 | optimize!(m) 67 | 68 | # retrieve grid 69 | grid = convert.(Int, JuMP.value.(x)) 70 | ``` 71 | 72 | ## Supported variables and constraints 73 | You can see a list of currently supported constraints [in the docs](https://wikunia.github.io/ConstraintSolver.jl/stable/supported/). 74 | In general the solver works only with bounded discrete variables and supports these constraints 75 | - linear constraints 76 | - all different 77 | - table 78 | - indictoar 79 | - reified 80 | - boolean 81 | 82 | ## Examples 83 | 84 | A list of example problems can be found on the website by [Håkan Kjellerstrand](http://hakank.org/julia/constraints/). 85 | 86 | 87 | ## Blog posts 88 | If you're interested in how the solver works you can checkout my blog [opensourc.es](https://opensourc.es). There are currently around 30 blog posts about the constraint solver and a new one is added about once per month. 89 | 90 | ## Notice 91 | I'm a MSc student in computer science so I don't have much knowledge on how constraint programming works but I'm keen to find out ;) 92 | 93 | ## Support 94 | If you find a bug or improvement please open an issue or make a pull request. 95 | Additionally if you use the solver regularly or are interested in further development please checkout my [Patreon](https://www.patreon.com/opensources) page or click on the support button at the top of this website. ;) 96 | -------------------------------------------------------------------------------- /src/options.jl: -------------------------------------------------------------------------------- 1 | function get_auto_traverse_strategy(com::CS.CoM) 2 | return com.sense == MOI.FEASIBILITY_SENSE ? :DFS : :BFS 3 | end 4 | 5 | function get_traverse_strategy(com; options = SolverOptions()) 6 | if options.traverse_strategy == :DBFS 7 | return isempty(com.solutions) ? Val(:DFS) : Val(:BFS) 8 | end 9 | return Val(options.traverse_strategy) 10 | end 11 | 12 | function get_auto_branch_strategy(com::CS.CoM) 13 | return :IMPS # Infeasible and Minimum Possiblity Search 14 | end 15 | 16 | function get_branch_strategy(; options = SolverOptions()) 17 | strategy = options.branch_strategy 18 | return Val(strategy) 19 | end 20 | 21 | function get_branch_split(; options = SolverOptions()) 22 | strategy = options.branch_split 23 | return Val(strategy) 24 | end 25 | 26 | const POSSIBLE_OPTIONS = Dict( 27 | :traverse_strategy => [:Auto, :BFS, :DFS, :DBFS], 28 | :branch_strategy => [:Auto, :ABS, :IMPS], 29 | :branch_split => [:Auto, :Smallest, :Biggest, :InHalf], 30 | ) 31 | 32 | function SolverOptions() 33 | logging = [:Info, :Table] 34 | 35 | table = init_log_table( 36 | (id=:open_nodes, name="#Open", width=10), 37 | (id=:closed_nodes, name="#Closed", width=10), 38 | (id=:incumbent, name="Incumbent", width=20), 39 | (id=:best_bound, name="Best Bound", width=20), 40 | (id=:duration, name="Time [s]", width=10); 41 | alignment=:center 42 | ) 43 | seed = 1 44 | traverse_strategy = :Auto 45 | branch_strategy = :Auto 46 | branch_split = :Auto 47 | backtrack = true 48 | max_bt_steps = typemax(Int) 49 | backtrack_sorting = true 50 | keep_logs = false 51 | rtol = 1e-6 52 | atol = 1e-6 53 | solution_type = Float64 54 | all_solutions = false 55 | all_optimal_solutions = false 56 | lp_optimizer = nothing 57 | time_limit = Inf 58 | no_prune = false 59 | decay = 0.999 60 | max_probes = 10 61 | max_confidence_deviation = 20 62 | simplify = true 63 | 64 | return SolverOptions( 65 | logging, 66 | table, 67 | time_limit, 68 | seed, 69 | traverse_strategy, 70 | branch_strategy, 71 | branch_split, 72 | backtrack, 73 | max_bt_steps, 74 | backtrack_sorting, 75 | keep_logs, 76 | rtol, 77 | atol, 78 | solution_type, 79 | all_solutions, 80 | all_optimal_solutions, 81 | lp_optimizer, 82 | no_prune, 83 | ActivityOptions(decay, max_probes, max_confidence_deviation), 84 | simplify, 85 | ) 86 | end 87 | 88 | function combine_options(options) 89 | defaults = SolverOptions() 90 | options_dict = Dict{Symbol,Any}() 91 | for kv in options 92 | if !in(kv[1], fieldnames(SolverOptions)) 93 | @error "The option $(kv[1]) doesn't exist." 94 | else 95 | moi_key = MOI.RawOptimizerAttribute(string(kv[1])) 96 | if is_possible_option_value(moi_key, kv[2]) 97 | options_dict[kv[1]] = kv[2] 98 | else 99 | @error "The option $(kv[1]) doesn't have $(kv[2]) as a possible value. Possible values are: $(POSSIBLE_OPTIONS[moi_key])" 100 | end 101 | end 102 | end 103 | 104 | for fname in fieldnames(SolverOptions) 105 | if haskey(options_dict, fname) 106 | setfield!( 107 | defaults, 108 | fname, 109 | convert(fieldtype(SolverOptions, fname), options_dict[fname]), 110 | ) 111 | end 112 | end 113 | return defaults 114 | end 115 | 116 | function is_possible_option_value(option_param::MOI.RawOptimizerAttribute, value) 117 | option = Symbol(option_param.name) 118 | if haskey(POSSIBLE_OPTIONS, option) 119 | return value in POSSIBLE_OPTIONS[option] 120 | end 121 | return true 122 | end 123 | -------------------------------------------------------------------------------- /test/small_eq_sum_real.jl: -------------------------------------------------------------------------------- 1 | @testset "Real coefficients" begin 2 | @testset "Basic all true" begin 3 | m = Model(CSJuMPTestOptimizer(; branch_strategy = :ABS)) 4 | @variable(m, x[1:4], Bin) 5 | weights = [1.7, 0.7, 0.3, 1.3] 6 | @variable(m, 0 <= max_val <= 10, Int) 7 | @constraint(m, sum(weights .* x) == max_val) 8 | @objective(m, Max, max_val) 9 | optimize!(m) 10 | com = CS.get_inner_model(m) 11 | @test is_solved(com) 12 | @test JuMP.termination_status(m) == MOI.OPTIMAL 13 | @test JuMP.objective_value(m) == 4 14 | @test JuMP.value.(x) == [1, 1, 1, 1] 15 | end 16 | 17 | @testset "Some true some false" begin 18 | # disallow that x1 and x2 are both allowed 19 | m = Model(CSJuMPTestOptimizer()) 20 | @variable(m, x[1:4], Bin) 21 | @variable(m, z, Bin) 22 | # x[1]+x[2] <= 1 23 | @constraint(m, x[1] + x[2] + z == 1) 24 | 25 | weights = [1.7, 0.7, 0.3, 1.3] 26 | @variable(m, 0 <= max_val <= 10, Int) 27 | @constraint(m, sum(weights .* x) == max_val) 28 | @objective(m, Max, max_val) 29 | optimize!(m) 30 | @test JuMP.termination_status(m) == MOI.OPTIMAL 31 | @test JuMP.objective_value(m) == 3 32 | @test JuMP.value.(x) == [1, 0, 0, 1] 33 | end 34 | 35 | @testset "Negative coefficients" begin 36 | # must use negative coefficient for optimum 37 | m = Model(CSJuMPTestOptimizer()) 38 | @variable(m, x[1:4], Bin) 39 | @variable(m, z, Bin) 40 | # x[1]+x[2] <= 1 41 | @constraint(m, x[1] + x[2] + z == 1) 42 | 43 | weights = [1.7, 0.7, -0.3, 1.6] 44 | @variable(m, 0 <= max_val <= 10, Int) 45 | @constraint(m, sum(weights .* x) == max_val) 46 | @objective(m, Max, max_val) 47 | optimize!(m) 48 | @test JuMP.termination_status(m) == MOI.OPTIMAL 49 | @test JuMP.objective_value(m) == 3 50 | @test JuMP.value.(x) == [1, 0, 1, 1] 51 | end 52 | 53 | @testset "Minimization negative coefficients" begin 54 | # must use negative coefficient for optimum 55 | m = Model(CSJuMPTestOptimizer()) 56 | @variable(m, x[1:4], Bin) 57 | @variable(m, z, Bin) 58 | # x[1]+x[2] <= 1 59 | @constraint(m, x[1] + x[2] + z == 1) 60 | 61 | weights = [0.3, 0.7, -0.3, 1.6] 62 | @variable(m, 1 <= max_val <= 10, Int) 63 | @constraint(m, sum(weights .* x) == max_val) 64 | @objective(m, Min, max_val) 65 | optimize!(m) 66 | @test JuMP.termination_status(m) == MOI.OPTIMAL 67 | @test JuMP.objective_value(m) == 2 68 | @test JuMP.value.(x) == [0, 1, 1, 1] 69 | end 70 | 71 | @testset "Getting safe upper/lower bounds" begin 72 | # must use negative coefficient for optimum 73 | m = Model(CSJuMPTestOptimizer()) 74 | @variable(m, x[1:4], Bin) 75 | @variable(m, z, Bin) 76 | 77 | weights = [0.1, 0.2, 0.4, -1.3] 78 | @variable(m, 0 <= max_val <= 10, Int) 79 | @constraint(m, sum(weights .* x) == 0.3 * max_val) 80 | @objective(m, Max, max_val) 81 | optimize!(m) 82 | @test JuMP.termination_status(m) == MOI.OPTIMAL 83 | @test JuMP.objective_value(m) == 2 84 | @test JuMP.value.(x) == [0, 1, 1, 0] 85 | end 86 | 87 | @testset "Test where min sum is a bit bigger than 0" begin 88 | m = Model(CSJuMPTestOptimizer()) 89 | @variable(m, 1 <= x[1:2] <= 3, Int) 90 | weights = [0.2, 0.1] 91 | @variable(m, 1 <= min_val <= 2, Int) 92 | @constraint(m, sum(weights .* x) == 0.15 * min_val) 93 | @objective(m, Min, min_val) 94 | optimize!(m) 95 | @test JuMP.termination_status(m) == MOI.OPTIMAL 96 | @test JuMP.objective_value(m) == 2 97 | @test JuMP.value.(x) == [1, 1] 98 | @test JuMP.value.(min_val) == 2 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /docs/src/how_to.md: -------------------------------------------------------------------------------- 1 | # How-To Guide 2 | 3 | It seems like you have some specific questions about how to use the constraint solver. 4 | 5 | ## How to create a simple model? 6 | 7 | ``` 8 | using JuMP, ConstraintSolver 9 | const CS = ConstraintSolver 10 | 11 | m = Model(CS.Optimizer) 12 | @variable(m, 1 <= x <= 9, Int) 13 | @variable(m, 1 <= y <= 5, Int) 14 | 15 | @constraint(m, x + y == 14) 16 | 17 | optimize!(m) 18 | status = JuMP.termination_status(m) 19 | ``` 20 | 21 | ## How to add a uniqueness/all_different constraint? 22 | 23 | If you want that the values are all different for some variables you can use: 24 | 25 | ``` 26 | @constraint(m, vars in CS.AllDifferent() 27 | ``` 28 | 29 | where `vars` is an array of variables of the constraint solver i.e `[x,y]`. 30 | 31 | 32 | ## How to add an optimization function / objective? 33 | 34 | Besides specifying the model you need to specify whether it's a minimization `Min` or maximization `Max` objective. 35 | 36 | ``` 37 | @objective(m, Min, x) 38 | ``` 39 | or for linear functions you would have something like: 40 | ``` 41 | @variable(m, x[1:4], Bin) 42 | weights = [0.2, -0.1, 0.4, -0.8] 43 | @objective(m, Min, sum(weights.*x)) 44 | ``` 45 | 46 | Currently the only objective is to minimize or maximize a single variable or linear function. 47 | 48 | More will come in the future ;) 49 | 50 | ## How to get the solution? 51 | 52 | If you define your variables `x,y` like shown in the [simple model example](#how-to-create-a-simple-model-1) you can get the value 53 | after solving with: 54 | 55 | ``` 56 | val_x = JuMP.value(x) 57 | val_y = JuMP.value(y) 58 | ``` 59 | 60 | or: 61 | 62 | ``` 63 | val_x, val_y = JuMP.value.([x,y]) 64 | ``` 65 | 66 | ## How to get the state before backtracking? 67 | 68 | For the explanation of the question look [here](explanation.html#Backtracking-1). 69 | 70 | Instead of solving the model directly you can have a look at the state before backtracking by setting an option of the ConstraintSolver: 71 | 72 | ``` 73 | m = Model(optimizer_with_attributes(CS.Optimizer, "backtrack"=>false)) 74 | ``` 75 | 76 | and then check the variables using `CS.values(m, x)` or `CS.values(m, y)` this returns an array of possible values. 77 | 78 | 79 | ## How to improve the bound computation? 80 | 81 | You might have encountered that the bound computation is not good. If you haven't already you should check out the tutorial on bound computation. 82 | It is definitely advised that you use an LP solver for computing bounds. 83 | 84 | ## How to define variables by a set of integers? 85 | 86 | Instead of `@variable(m, 1 <= x <= 10, Int)` and then remove values with `@constraint(m, x != 3)`. 87 | You can directly write: 88 | 89 | ``` 90 | @variable(m, CS.Integers([i for i=1:10 if i != 3])) 91 | ``` 92 | 93 | this removes unnecessary constraints. 94 | 95 | ## How to define a set of possibilities for more than one variable? 96 | 97 | In some cases it is useful to define that some variables can only have a fixed number of combinations 98 | together which can't be easily specified by any other constraint. 99 | 100 | Then you can use the table constraint. 101 | 102 | ``` 103 | cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) 104 | model = Model(optimizer_with_attributes( 105 | CS.Optimizer, 106 | "lp_optimizer" => cbc_optimizer, 107 | )) 108 | 109 | # Variables 110 | @variable(model, 1 <= x[1:5] <= 5, Int) 111 | 112 | #= 113 | Specify that only the following 5 options are possible. 114 | First row means: 115 | x[1] = 1, x[2] = 2, x[3] = 3, x[4] = 1, x[5] = 1 is one possible combination. 116 | The last row shows that when x[1] = 4 all other variables are fixed as well. 117 | For x[1]={2,3} there is no solution 118 | =# 119 | table = [ 120 | 1 2 3 1 1; 121 | 1 3 3 2 1; 122 | 1 1 3 2 1; 123 | 1 1 1 2 4; 124 | 4 5 5 3 4; 125 | ] 126 | 127 | @constraint(model, x in CS.TableSet(table)) 128 | 129 | @objective(model, Max, sum(x)) 130 | optimize!(model) 131 | ``` 132 | 133 | Table constraints can represent a lot of constraints including alldifferent but it's always reasonable to use 134 | one of the other constraints if it directly represents the problem. -------------------------------------------------------------------------------- /benchmark/sudoku/data/25x25_gecode.jl: -------------------------------------------------------------------------------- 1 | sudoku25 = Dict( 2 | :89 => 3 | [ 4 | 11 23 13 10 19 16 6 2 24 7 5 9 1 20 17 15 8 18 25 3 4 12 21 22 14; 5 | 15 16 0 22 0 11 8 0 0 0 25 0 14 0 0 0 12 19 0 0 17 0 0 0 0; 6 | 0 0 0 0 0 0 0 0 0 0 0 16 0 4 0 17 0 13 0 24 0 23 19 10 2; 7 | 0 0 0 0 0 19 0 14 23 4 0 21 6 22 10 0 11 0 2 0 0 0 0 0 0; 8 | 17 14 0 0 2 0 0 13 12 0 0 0 0 0 15 4 20 22 10 0 11 0 9 24 8; 9 | 22 0 0 0 0 6 2 0 0 0 4 7 12 1 9 0 0 0 0 0 0 14 5 0 0; 10 | 0 18 2 0 8 22 0 19 16 21 0 0 0 10 13 23 0 0 20 0 0 3 0 15 7; 11 | 0 0 17 3 0 5 0 0 8 9 0 0 0 0 18 0 19 0 0 0 0 0 23 21 0; 12 | 1 11 0 0 9 0 15 10 25 0 6 0 23 0 0 0 0 5 3 7 0 17 0 0 24; 13 | 0 0 0 0 0 0 1 0 0 23 0 0 0 24 0 0 0 21 12 0 6 8 0 25 16; 14 | 20 24 10 0 15 23 11 17 0 0 0 0 0 7 0 12 0 0 0 0 0 22 0 0 6; 15 | 4 5 0 14 12 25 0 18 0 0 23 0 15 0 19 1 0 0 0 22 20 0 7 9 0; 16 | 18 0 21 0 0 8 0 24 0 0 9 0 25 0 0 0 10 0 0 0 2 0 1 19 0; 17 | 0 0 6 2 1 0 13 0 22 0 0 0 0 0 11 8 21 16 0 0 25 0 0 12 17; 18 | 0 17 25 0 23 7 14 0 21 1 0 0 0 0 3 0 0 11 0 0 24 0 16 4 5; 19 | 0 0 0 0 11 18 24 0 0 0 0 5 0 12 0 25 0 0 0 15 23 4 8 14 0; 20 | 0 0 0 15 21 0 0 0 0 0 2 0 13 17 0 0 1 7 0 0 5 9 24 0 0; 21 | 0 0 18 0 22 15 0 0 2 16 0 23 0 0 0 10 6 24 0 17 12 0 25 11 0; 22 | 7 2 0 1 0 0 21 0 0 0 18 22 0 9 6 14 0 4 5 16 0 0 0 0 0; 23 | 0 0 9 0 0 0 7 22 0 0 10 0 24 0 0 0 18 0 0 0 21 0 0 0 0; 24 | 0 12 0 19 10 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 14 0 4 8 0; 25 | 24 0 11 18 0 0 0 0 0 0 0 25 17 21 0 6 0 0 1 0 0 0 0 5 12; 26 | 16 6 22 0 0 0 23 4 15 18 8 0 0 0 20 0 0 17 0 14 0 0 0 0 0; 27 | 0 21 0 0 4 0 9 1 7 0 0 0 0 11 14 0 16 8 15 0 22 0 18 0 0; 28 | 8 15 0 0 0 0 0 0 5 0 24 3 0 0 4 0 0 0 9 0 0 0 0 0 20; 29 | ], 30 | 31 | :90 => 32 | [ 33 | 0 23 13 0 19 16 6 0 24 7 5 9 1 0 0 15 8 18 25 0 4 0 21 22 0; 34 | 15 16 0 22 0 11 8 0 0 0 25 0 14 0 0 0 12 19 0 0 17 0 0 0 0; 35 | 0 0 0 0 0 0 0 0 0 0 0 16 0 4 0 17 0 13 0 24 0 23 19 10 2; 36 | 0 0 0 0 0 19 0 14 23 4 0 21 6 22 10 0 11 0 2 0 0 0 0 0 0; 37 | 17 14 0 0 2 0 0 13 12 0 0 0 0 0 15 4 20 22 10 0 11 0 9 24 8; 38 | 22 0 0 0 0 6 2 0 0 0 4 7 12 1 9 0 0 0 0 0 0 14 5 0 0; 39 | 0 18 2 0 8 22 0 19 16 21 0 0 0 10 13 23 0 0 20 0 0 3 0 15 7; 40 | 0 0 17 3 0 5 0 0 8 9 0 0 0 0 18 0 19 0 0 0 0 0 23 21 0; 41 | 1 11 0 0 9 0 15 10 25 0 6 0 23 0 0 0 0 5 3 7 0 17 0 0 24; 42 | 0 0 0 0 0 0 1 0 0 23 0 0 0 24 0 0 0 21 12 0 6 8 0 25 16; 43 | 20 24 10 0 15 23 11 17 0 0 0 0 0 7 0 12 0 0 0 0 0 22 0 0 6; 44 | 4 5 0 14 12 25 0 18 0 0 23 0 15 0 19 1 0 0 0 22 20 0 7 9 0; 45 | 18 0 21 0 0 8 0 24 0 0 9 0 25 0 0 0 10 0 0 0 2 0 1 19 0; 46 | 0 0 6 2 1 0 13 0 22 0 0 0 0 0 11 8 21 16 0 0 25 0 0 12 17; 47 | 0 17 25 0 23 7 14 0 21 1 0 0 0 0 3 0 0 11 0 0 24 0 16 4 5; 48 | 0 0 0 0 11 18 24 0 0 0 0 5 0 12 0 25 0 0 0 15 23 4 8 14 0; 49 | 0 0 0 15 21 0 0 0 0 0 2 0 13 17 0 0 1 7 0 0 5 9 24 0 0; 50 | 0 0 18 0 22 15 0 0 2 16 0 23 0 0 0 10 6 24 0 17 12 0 25 11 0; 51 | 7 2 0 1 0 0 21 0 0 0 18 22 0 9 6 14 0 4 5 16 0 0 0 0 0; 52 | 0 0 9 0 0 0 7 22 0 0 10 0 24 0 0 0 18 0 0 0 21 0 0 0 0; 53 | 0 12 0 19 10 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 14 0 4 8 0; 54 | 24 0 11 18 0 0 0 0 0 0 0 25 17 21 0 6 0 0 1 0 0 0 0 5 12; 55 | 16 6 22 0 0 0 23 4 15 18 8 0 0 0 20 0 0 17 0 14 0 0 0 0 0; 56 | 0 21 0 0 4 0 9 1 7 0 0 0 0 11 14 0 16 8 15 0 22 0 18 0 0; 57 | 8 15 0 0 0 0 0 0 5 0 24 3 0 0 4 0 0 0 9 0 0 0 0 0 20; 58 | ] 59 | ) -------------------------------------------------------------------------------- /test/unit/constraints/svc.jl: -------------------------------------------------------------------------------- 1 | @testset "SVC" begin 2 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 3 | @variable(m, -5 <= x <= 5, Int) 4 | @variable(m, -5 <= y <= 5, Int) 5 | @constraint(m, x <= y) 6 | optimize!(m) 7 | com = CS.get_inner_model(m) 8 | 9 | constraint = com.constraints[1] 10 | @test constraint isa CS.SingleVariableConstraint 11 | 12 | # doesn't check the length 13 | @test !CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [3, 2]) 14 | @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [2, 2]) 15 | @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2]) 16 | 17 | constr_indices = constraint.indices 18 | @test CS.still_feasible( 19 | com, 20 | constraint, 21 | constraint.fct, 22 | constraint.set, 23 | constr_indices[2], 24 | -5, 25 | ) 26 | @test CS.fix!(com, com.search_space[constr_indices[2]], 3) 27 | @test !CS.still_feasible( 28 | com, 29 | constraint, 30 | constraint.fct, 31 | constraint.set, 32 | constr_indices[1], 33 | 4, 34 | ) 35 | @test CS.still_feasible( 36 | com, 37 | constraint, 38 | constraint.fct, 39 | constraint.set, 40 | constr_indices[1], 41 | 3, 42 | ) 43 | 44 | # need to create a backtrack_vec to reverse pruning 45 | dummy_backtrack_obj = CS.BacktrackObj(com) 46 | dummy_backtrack_obj.step_nr = 1 47 | push!(com.backtrack_vec, dummy_backtrack_obj) 48 | # reverse previous fix 49 | CS.reverse_pruning!(com, 1) 50 | com.c_backtrack_idx = 1 51 | 52 | @test CS.still_feasible( 53 | com, 54 | constraint, 55 | constraint.fct, 56 | constraint.set, 57 | constr_indices[1], 58 | 4, 59 | ) 60 | 61 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 62 | @variable(m, -5 <= x <= 5, Int) 63 | @variable(m, -5 <= y <= 5, Int) 64 | @constraint(m, x <= y) 65 | optimize!(m) 66 | com = CS.get_inner_model(m) 67 | constraint = com.constraints[1] 68 | 69 | # feasible and no changes 70 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 71 | for ind in constr_indices 72 | @test sort(CS.values(com.search_space[ind])) == -5:5 73 | end 74 | @test CS.fix!(com, com.search_space[constr_indices[2]], 4) 75 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 76 | @test sort(CS.values(com.search_space[1])) == -5:4 77 | 78 | 79 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 80 | @variable(m, -5 <= x <= 5, Int) 81 | @variable(m, -5 <= y <= 5, Int) 82 | @constraint(m, x <= y) 83 | optimize!(m) 84 | com = CS.get_inner_model(m) 85 | constraint = com.constraints[1] 86 | 87 | # Should be synced to the other variables 88 | @test CS.remove_above!(com, com.search_space[constr_indices[2]], 1) 89 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 90 | for ind in constr_indices 91 | @test sort(CS.values(com.search_space[ind])) == -5:1 92 | end 93 | end 94 | 95 | @testset "svc is_constraint_violated test" begin 96 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 97 | @variable(m, 0 <= x <= 5, Int) 98 | @variable(m, 0 <= y <= 3, Int) 99 | @constraint(m, x <= y) 100 | optimize!(m) 101 | com = CS.get_inner_model(m) 102 | 103 | constraint = com.constraints[1] 104 | 105 | variables = com.search_space 106 | @test CS.fix!(com, variables[constraint.indices[1]], 5; check_feasibility = false) 107 | @test CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 108 | 109 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 110 | @variable(m, 0 <= x <= 5, Int) 111 | @variable(m, 0 <= y <= 3, Int) 112 | @constraint(m, x <= y) 113 | optimize!(m) 114 | com = CS.get_inner_model(m) 115 | 116 | constraint = com.constraints[1] 117 | 118 | variables = com.search_space 119 | @test CS.fix!(com, variables[constraint.indices[1]], 3; check_feasibility = false) 120 | @test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 121 | end 122 | -------------------------------------------------------------------------------- /src/constraints/not_equal.jl: -------------------------------------------------------------------------------- 1 | """ 2 | prune_constraint!(com::CS.CoM, constraint::BasicConstraint, fct::SAF{T}, set::CPE.DifferentFrom{T}; logs = true) where T <: Real 3 | 4 | Reduce the number of possibilities given the not equal constraint. 5 | Return if still feasible and throw a warning if infeasible and `logs` is set to `true` 6 | """ 7 | function prune_constraint!( 8 | com::CS.CoM, 9 | constraint::LinearConstraint, 10 | fct::SAF{T}, 11 | set::CPE.DifferentFrom{T}; 12 | logs = true, 13 | ) where {T<:Real} 14 | indices = constraint.indices 15 | 16 | # check if only one variable is variable 17 | nfixed = count(v -> isfixed(v), com.search_space[constraint.indices]) 18 | if nfixed >= length(constraint.indices) - 1 19 | search_space = com.search_space 20 | sum = -set.value + fct.constant 21 | unfixed_i = 0 22 | for (i, vidx) in enumerate(indices) 23 | if isfixed(search_space[vidx]) 24 | sum += CS.value(search_space[vidx]) * fct.terms[i].coefficient 25 | else 26 | unfixed_i = i 27 | end 28 | end 29 | # all fixed 30 | if unfixed_i == 0 31 | return get_approx_discrete(sum) != zero(T) 32 | end 33 | not_val = -sum 34 | not_val /= fct.terms[unfixed_i].coefficient 35 | # if not integer 36 | if !isapprox_discrete(com, not_val) 37 | return true 38 | end 39 | not_val = get_approx_discrete(not_val) 40 | # if can be removed => is removed and is feasible otherwise not feasible 41 | if has(search_space[indices[unfixed_i]], not_val) 42 | return rm!(com, search_space[indices[unfixed_i]], not_val) 43 | else 44 | return true 45 | end 46 | end 47 | return true 48 | end 49 | 50 | """ 51 | still_feasible(com::CoM, constraint::LinearConstraint, fct::MOI.ScalarAffineFunction{T}, set::CPE.DifferentFrom{T}, vidx::Int, value::Int) where T <: Real 52 | 53 | Return whether the `not_equal` constraint can be still fulfilled. 54 | """ 55 | function still_feasible( 56 | com::CoM, 57 | constraint::LinearConstraint, 58 | fct::SAF{T}, 59 | set::CPE.DifferentFrom{T}, 60 | vidx::Int, 61 | value::Int, 62 | ) where {T<:Real} 63 | indices = constraint.indices 64 | # check if only one variable is variable 65 | nfixed = count(v -> isfixed(v), com.search_space[indices]) 66 | if nfixed >= length(indices) - 1 67 | search_space = com.search_space 68 | sum = -set.value + fct.constant 69 | unfixed_i = 0 70 | for (i, cvidx) in enumerate(indices) 71 | if isfixed(search_space[cvidx]) 72 | sum += CS.value(search_space[cvidx]) * fct.terms[i].coefficient 73 | elseif vidx == cvidx 74 | sum += value * fct.terms[i].coefficient 75 | else 76 | unfixed_i = i 77 | end 78 | end 79 | # all fixed => must be != 0 80 | if unfixed_i == 0 81 | # not discrete => not 0 => feasible 82 | if !isapprox_discrete(com, sum) 83 | return true 84 | end 85 | return get_approx_discrete(sum) != zero(T) 86 | end 87 | # if not fixed there is a value which fulfills the != constraint 88 | return true 89 | end 90 | return true 91 | end 92 | 93 | function is_constraint_solved( 94 | constraint::LinearConstraint, 95 | fct::SAF{T}, 96 | set::CPE.DifferentFrom{T}, 97 | values::Vector{Int}, 98 | ) where {T<:Real} 99 | 100 | indices = [t.variable.value for t in fct.terms] 101 | coeffs = [t.coefficient for t in fct.terms] 102 | return get_approx_discrete(sum(values .* coeffs) + fct.constant) != set.value 103 | end 104 | 105 | """ 106 | is_constraint_violated( 107 | com::CoM, 108 | constraint::LinearConstraint, 109 | fct::SAF{T}, 110 | set::CPE.DifferentFrom{T}, 111 | ) where {T<:Real} 112 | 113 | Checks if the constraint is violated as it is currently set. This can happen inside an 114 | inactive reified or indicator constraint. 115 | """ 116 | function is_constraint_violated( 117 | com::CoM, 118 | constraint::LinearConstraint, 119 | fct::SAF{T}, 120 | set::CPE.DifferentFrom{T}, 121 | ) where {T<:Real} 122 | if all(isfixed(var) for var in com.search_space[constraint.indices]) 123 | return !is_constraint_solved( 124 | constraint, 125 | fct, 126 | set, 127 | [CS.value(var) for var in com.search_space[constraint.indices]], 128 | ) 129 | end 130 | return false 131 | end 132 | -------------------------------------------------------------------------------- /benchmark/killer_sudoku/data/niallsudoku_6249: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "color": "red", 4 | "result": 21, 5 | "indices": [ 6 | [1,1], 7 | [1,2], 8 | [1,3], 9 | [2,1], 10 | [2,2] 11 | ] 12 | }, 13 | { 14 | "color": "blue", 15 | "result": 18, 16 | "indices": [ 17 | [1,4], 18 | [1,5], 19 | [1,6] 20 | ] 21 | }, 22 | { 23 | "color": "red", 24 | "result": 19, 25 | "indices": [ 26 | [1,7], 27 | [1,8], 28 | [1,9], 29 | [2,8], 30 | [2,9] 31 | ] 32 | } , 33 | { 34 | "color": "green", 35 | "result": 14, 36 | "indices": [ 37 | [2,3], 38 | [2,4], 39 | [3,4] 40 | ] 41 | } , 42 | { 43 | "color": "red", 44 | "result": 9, 45 | "indices": [ 46 | [2,5], 47 | [3,5], 48 | [4,5] 49 | ] 50 | } , 51 | { 52 | "color": "green", 53 | "result": 23, 54 | "indices": [ 55 | [2,6], 56 | [2,7], 57 | [3,6] 58 | ] 59 | } , 60 | { 61 | "color": "blue", 62 | "result": 13, 63 | "indices": [ 64 | [3,1], 65 | [3,2] 66 | ] 67 | }, 68 | { 69 | "color": "yellow", 70 | "result": 24, 71 | "indices": [ 72 | [3,3], 73 | [4,3], 74 | [5,2], 75 | [5,3], 76 | [6,2] 77 | ] 78 | }, 79 | { 80 | "color": "blue", 81 | "result": 19, 82 | "indices": [ 83 | [3,7], 84 | [4,7], 85 | [5,7], 86 | [5,8], 87 | [6,8] 88 | ] 89 | }, 90 | { 91 | "color": "yellow", 92 | "result": 9, 93 | "indices": [ 94 | [3,8], 95 | [3,9] 96 | ] 97 | }, 98 | { 99 | "color": "red", 100 | "result": 19, 101 | "indices": [ 102 | [4,1], 103 | [4,2], 104 | [5,1] 105 | ] 106 | }, 107 | { 108 | "color": "blue", 109 | "result": 8, 110 | "indices": [ 111 | [4,4], 112 | [5,4] 113 | ] 114 | }, 115 | { 116 | "color": "yellow", 117 | "result": 16, 118 | "indices": [ 119 | [4,6], 120 | [5,6] 121 | ] 122 | }, 123 | { 124 | "color": "red", 125 | "result": 21, 126 | "indices": [ 127 | [4,8], 128 | [4,9], 129 | [5,9] 130 | ] 131 | }, 132 | { 133 | "color": "green", 134 | "result": 38, 135 | "indices": [ 136 | [5,5], 137 | [6,5], 138 | [7,4], 139 | [7,5], 140 | [7,6], 141 | [8,5], 142 | [9,5] 143 | ] 144 | }, 145 | { 146 | "color": "blue", 147 | "result": 11, 148 | "indices": [ 149 | [6,1], 150 | [7,1], 151 | [8,1] 152 | ] 153 | }, 154 | { 155 | "color": "red", 156 | "result": 19, 157 | "indices": [ 158 | [6,3], 159 | [6,4], 160 | [7,3] 161 | ] 162 | }, 163 | { 164 | "color": "red", 165 | "result": 13, 166 | "indices": [ 167 | [6,6], 168 | [6,7], 169 | [7,7] 170 | ] 171 | }, 172 | { 173 | "color": "green", 174 | "result": 14, 175 | "indices": [ 176 | [6,9], 177 | [7,9], 178 | [8,9] 179 | ] 180 | }, 181 | { 182 | "color": "green", 183 | "result": 18, 184 | "indices": [ 185 | [7,2], 186 | [8,2], 187 | [9,1], 188 | [9,2] 189 | ] 190 | }, 191 | { 192 | "color": "blue", 193 | "result": 22, 194 | "indices": [ 195 | [8,3], 196 | [8,4], 197 | [9,3], 198 | [9,4] 199 | ] 200 | }, 201 | { 202 | "color": "blue", 203 | "result": 15, 204 | "indices": [ 205 | [8,6], 206 | [8,7], 207 | [9,6], 208 | [9,7] 209 | ] 210 | }, 211 | { 212 | "color": "yellow", 213 | "result": 22, 214 | "indices": [ 215 | [7,8], 216 | [8,8], 217 | [9,8], 218 | [9,9] 219 | ] 220 | } 221 | ] -------------------------------------------------------------------------------- /test/unit/constraints/geqset.jl: -------------------------------------------------------------------------------- 1 | @testset "AllEqual" begin 2 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 3 | @variable(m, -5 <= y <= 5, Int) 4 | @variable(m, -5 <= x[1:2] <= 5, Int) 5 | @constraint(m, [y, x...] in CS.GeqSet()) 6 | optimize!(m) 7 | com = CS.get_inner_model(m) 8 | 9 | constraint = com.constraints[1] 10 | 11 | # doesn't check the length 12 | @test !CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2, 3]) 13 | @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [3, 2, 1]) 14 | 15 | constr_indices = constraint.indices 16 | @test CS.still_feasible( 17 | com, 18 | constraint, 19 | constraint.fct, 20 | constraint.set, 21 | constr_indices[1], 22 | 3, 23 | ) 24 | @test CS.fix!(com, com.search_space[constr_indices[1]], 3) 25 | @test !CS.still_feasible( 26 | com, 27 | constraint, 28 | constraint.fct, 29 | constraint.set, 30 | constr_indices[2], 31 | 4, 32 | ) 33 | @test CS.still_feasible( 34 | com, 35 | constraint, 36 | constraint.fct, 37 | constraint.set, 38 | constr_indices[3], 39 | 2, 40 | ) 41 | 42 | # need to create a backtrack_vec to reverse pruning 43 | dummy_backtrack_obj = CS.BacktrackObj(com) 44 | dummy_backtrack_obj.step_nr = 1 45 | push!(com.backtrack_vec, dummy_backtrack_obj) 46 | # reverse previous fix 47 | CS.reverse_pruning!(com, 1) 48 | com.c_backtrack_idx = 1 49 | 50 | # now setting it to 4 should be feasible 51 | @test CS.still_feasible( 52 | com, 53 | constraint, 54 | constraint.fct, 55 | constraint.set, 56 | constr_indices[2], 57 | 4, 58 | ) 59 | 60 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 61 | @variable(m, -5 <= y <= 5, Int) 62 | @variable(m, -5 <= x[1:2] <= 5, Int) 63 | @constraint(m, [y, x...] in CS.GeqSet()) 64 | optimize!(m) 65 | com = CS.get_inner_model(m) 66 | constraint = com.constraints[1] 67 | constr_indices = constraint.indices 68 | 69 | # feasible and no changes 70 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 71 | for ind in constr_indices 72 | @test sort(CS.values(com.search_space[ind])) == -5:5 73 | end 74 | @test CS.fix!(com, com.search_space[constr_indices[2]], 5) 75 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 76 | @test CS.value(com.search_space[constr_indices[1]]) == 5 77 | @test CS.isfixed(com.search_space[constr_indices[1]]) 78 | 79 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 80 | @variable(m, -5 <= y <= 5, Int) 81 | @variable(m, -5 <= x[1:2] <= 5, Int) 82 | @constraint(m, [y, x...] in CS.GeqSet()) 83 | optimize!(m) 84 | com = CS.get_inner_model(m) 85 | 86 | constraint = com.constraints[1] 87 | constr_indices = constraint.indices 88 | 89 | # Should be synced to the first variable 90 | @test CS.remove_below!(com, com.search_space[constr_indices[3]], 4) 91 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 92 | @test sort(CS.values(com.search_space[constr_indices[1]])) == 4:5 93 | 94 | @test CS.rm!(com, com.search_space[constr_indices[1]], 5) 95 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 96 | @test sort(CS.values(com.search_space[constr_indices[2]])) == -5:4 97 | @test sort(CS.values(com.search_space[constr_indices[3]])) == 4:4 98 | end 99 | 100 | @testset "geqset is_constraint_violated test" begin 101 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 102 | @variable(m, -5 <= x[1:5] <= 5, Int) 103 | @constraint(m, x in CS.GeqSet()) 104 | optimize!(m) 105 | com = CS.get_inner_model(m) 106 | 107 | constraint = com.constraints[1] 108 | 109 | variables = com.search_space 110 | @test CS.fix!(com, variables[1], 3; check_feasibility = false) 111 | @test CS.remove_below!(com, variables[2], 4; check_feasibility = false) 112 | @test CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 113 | 114 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 115 | @variable(m, -5 <= x[1:5] <= 5, Int) 116 | @constraint(m, x in CS.GeqSet()) 117 | optimize!(m) 118 | com = CS.get_inner_model(m) 119 | 120 | constraint = com.constraints[1] 121 | 122 | variables = com.search_space 123 | @test CS.fix!(com, variables[1], -5; check_feasibility = false) 124 | @test CS.fix!(com, variables[2], -5; check_feasibility = false) 125 | @test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 126 | end 127 | -------------------------------------------------------------------------------- /benchmark/killer_sudoku/data/niallsudoku_6417: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "color": "red", 4 | "result": 22, 5 | "indices": [ 6 | [1,1], 7 | [1,2], 8 | [1,3], 9 | [1,4] 10 | ] 11 | }, 12 | { 13 | "color": "blue", 14 | "result": 7, 15 | "indices": [ 16 | [1,5], 17 | [2,5] 18 | ] 19 | }, 20 | { 21 | "color": "red", 22 | "result": 24, 23 | "indices": [ 24 | [1,6], 25 | [1,7], 26 | [2,6], 27 | [2,7] 28 | ] 29 | }, 30 | { 31 | "color": "blue", 32 | "result": 4, 33 | "indices": [ 34 | [1,8], 35 | [2,8] 36 | ] 37 | }, 38 | { 39 | "color": "yellow", 40 | "result": 30, 41 | "indices": [ 42 | [1,9], 43 | [2,9], 44 | [3,8], 45 | [3,9] 46 | ] 47 | }, 48 | { 49 | "color": "yellow", 50 | "result": 20, 51 | "indices": [ 52 | [2,1], 53 | [2,2], 54 | [2,3], 55 | [2,4] 56 | ] 57 | }, 58 | { 59 | "color": "red", 60 | "result": 13, 61 | "indices": [ 62 | [3,1], 63 | [4,1] 64 | ] 65 | }, 66 | { 67 | "color": "blue", 68 | "result": 33, 69 | "indices": [ 70 | [3,2], 71 | [4,2], 72 | [5,1], 73 | [5,2], 74 | [6,2], 75 | [7,2] 76 | ] 77 | }, 78 | { 79 | "color": "green", 80 | "result": 9, 81 | "indices": [ 82 | [3,3], 83 | [3,4], 84 | [3,5] 85 | ] 86 | }, 87 | { 88 | "color": "yellow", 89 | "result": 6, 90 | "indices": [ 91 | [3,6], 92 | [4,6] 93 | ] 94 | }, 95 | { 96 | "color": "pink", 97 | "result": 26, 98 | "indices": [ 99 | [3,7], 100 | [4,7], 101 | [4,8], 102 | [4,9] 103 | ] 104 | }, 105 | { 106 | "color": "red", 107 | "result": 12, 108 | "indices": [ 109 | [4,3], 110 | [5,3], 111 | [6,3] 112 | ] 113 | }, 114 | { 115 | "color": "blue", 116 | "result": 9, 117 | "indices": [ 118 | [4,4], 119 | [4,5] 120 | ] 121 | }, 122 | { 123 | "color": "green", 124 | "result": 29, 125 | "indices": [ 126 | [5,4], 127 | [5,5], 128 | [5,6], 129 | [5,7], 130 | [5,8], 131 | [5,9] 132 | ] 133 | }, 134 | { 135 | "color": "red", 136 | "result": 10, 137 | "indices": [ 138 | [6,1], 139 | [7,1] 140 | ] 141 | }, 142 | { 143 | "color": "blue", 144 | "result": 16, 145 | "indices": [ 146 | [6,4], 147 | [6,5] 148 | ] 149 | }, 150 | { 151 | "color": "red", 152 | "result": 4, 153 | "indices": [ 154 | [6,6], 155 | [7,6] 156 | ] 157 | }, 158 | { 159 | "color": "blue", 160 | "result": 16, 161 | "indices": [ 162 | [6,7], 163 | [6,8], 164 | [6,9], 165 | [7,7] 166 | ] 167 | }, 168 | { 169 | "color": "yellow", 170 | "result": 14, 171 | "indices": [ 172 | [7,3], 173 | [7,4], 174 | [7,5] 175 | ] 176 | }, 177 | { 178 | "color": "yellow", 179 | "result": 16, 180 | "indices": [ 181 | [7,8], 182 | [7,9], 183 | [8,9], 184 | [9,9] 185 | ] 186 | }, 187 | { 188 | "color": "green", 189 | "result": 15, 190 | "indices": [ 191 | [8,1], 192 | [8,2], 193 | [8,3], 194 | [8,4] 195 | ] 196 | }, 197 | { 198 | "color": "blue", 199 | "result": 13, 200 | "indices": [ 201 | [8,5], 202 | [9,5] 203 | ] 204 | }, 205 | { 206 | "color": "green", 207 | "result": 23, 208 | "indices": [ 209 | [8,6], 210 | [8,7], 211 | [9,6], 212 | [9,7] 213 | ] 214 | }, 215 | { 216 | "color": "red", 217 | "result": 12, 218 | "indices": [ 219 | [8,8], 220 | [9,8] 221 | ] 222 | }, 223 | { 224 | "color": "red", 225 | "result": 22, 226 | "indices": [ 227 | [9,1], 228 | [9,2], 229 | [9,3], 230 | [9,4] 231 | ] 232 | } 233 | ] -------------------------------------------------------------------------------- /benchmark/eternity/benchmark.jl: -------------------------------------------------------------------------------- 1 | function read_puzzle(fname) 2 | dir = pkgdir(ConstraintSolver) 3 | lines = readlines(joinpath(dir, "benchmark/eternity/data/$fname")) 4 | npieces = length(lines) 5 | puzzle = zeros(Int, (npieces, 5)) 6 | for i in 1:npieces 7 | puzzle[i, 1] = i 8 | parts = split(lines[i], " ") 9 | puzzle[i, 2:end] = parse.(Int, parts) 10 | end 11 | return puzzle 12 | end 13 | 14 | function get_rotations(puzzle) 15 | npieces = size(puzzle)[1] 16 | rotations = zeros(Int, (npieces * 4, 5)) 17 | rotation_indices = [[2, 3, 4, 5], [3, 4, 5, 2], [4, 5, 2, 3], [5, 2, 3, 4]] 18 | for i in 1:npieces 19 | j = 1 20 | for rotation in rotation_indices 21 | rotations[(i - 1) * 4 + j, 1] = i 22 | rotations[(i - 1) * 4 + j, 2:end] = puzzle[i, rotation] 23 | j += 1 24 | end 25 | end 26 | 27 | return rotations 28 | end 29 | 30 | function solve_eternity( 31 | fname = "eternity_6x6"; 32 | height = nothing, 33 | width = nothing, 34 | all_solutions = false, 35 | optimize = false, 36 | indicator = false, 37 | reified = false, 38 | branch_strategy = :Auto, 39 | ) 40 | puzzle = read_puzzle(fname) 41 | rotations = get_rotations(puzzle) 42 | npieces = size(puzzle)[1] 43 | width === nothing && (width = convert(Int, sqrt(npieces))) 44 | height === nothing && (height = convert(Int, sqrt(npieces))) 45 | ncolors = maximum(puzzle[:, 2:end]) 46 | 47 | m = Model(optimizer_with_attributes( 48 | CS.Optimizer, 49 | "logging" => [], 50 | "all_solutions" => all_solutions, 51 | "seed" => 1, 52 | "branch_strategy" => branch_strategy, 53 | )) 54 | if optimize 55 | glpk_optimizer = optimizer_with_attributes(GLPK.Optimizer, "msg_lev" => GLPK.GLP_MSG_OFF) 56 | m = Model(optimizer_with_attributes( 57 | CS.Optimizer, 58 | "logging" => [], 59 | "all_solutions" => all_solutions, 60 | "lp_optimizer" => glpk_optimizer, 61 | "seed" => 1, 62 | )) 63 | end 64 | 65 | @variable(m, 1 <= p[1:height, 1:width] <= npieces, Int) 66 | @variable(m, 0 <= pu[1:height, 1:width] <= ncolors, Int) 67 | @variable(m, 0 <= pr[1:height, 1:width] <= ncolors, Int) 68 | @variable(m, 0 <= pd[1:height, 1:width] <= ncolors, Int) 69 | @variable(m, 0 <= pl[1:height, 1:width] <= ncolors, Int) 70 | if indicator 71 | @variable(m, b, Bin) 72 | elseif reified 73 | @variable(m, b[1:height, 1:width], Bin) 74 | end 75 | 76 | @constraint(m, p[:] in CS.AllDifferent()) 77 | for i in 1:height, j in 1:width 78 | if indicator 79 | @constraint( 80 | m, 81 | b => { 82 | [p[i, j], pu[i, j], pr[i, j], pd[i, j], pl[i, j]] in 83 | CS.TableSet(rotations), 84 | } 85 | ) 86 | elseif reified 87 | @constraint( 88 | m, 89 | b[i, j] := { 90 | [p[i, j], pu[i, j], pr[i, j], pd[i, j], pl[i, j]] in 91 | CS.TableSet(rotations), 92 | } 93 | ) 94 | else 95 | @constraint( 96 | m, 97 | [p[i, j], pu[i, j], pr[i, j], pd[i, j], pl[i, j]] in CS.TableSet(rotations) 98 | ) 99 | end 100 | end 101 | 102 | # borders 103 | # up and down 104 | for j in 1:width 105 | @constraint(m, pu[1, j] == 0) 106 | @constraint(m, pd[height, j] == 0) 107 | 108 | if j != width 109 | @constraint(m, pr[1, j] == pl[1, j + 1]) 110 | @constraint(m, pr[height, j] == pl[height, j + 1]) 111 | end 112 | end 113 | 114 | # right and left 115 | for i in 1:height 116 | @constraint(m, pl[i, 1] == 0) 117 | @constraint(m, pr[i, width] == 0) 118 | 119 | if i != height 120 | @constraint(m, pd[i, 1] == pu[i + 1, 1]) 121 | @constraint(m, pd[i, width] == pu[i + 1, width]) 122 | end 123 | end 124 | 125 | for i in 1:(height - 1), j in 1:(width - 1) 126 | @constraint(m, pd[i, j] == pu[i + 1, j]) 127 | @constraint(m, pr[i, j] == pl[i, j + 1]) 128 | end 129 | 130 | if !optimize && indicator 131 | @constraint(m, b == 1) 132 | end 133 | 134 | if !optimize && width == height 135 | start_piece = findfirst(i -> count(c -> c == 0, puzzle[i, :]) == 2, 1:npieces) 136 | @constraint(m, p[1, 1] == start_piece) 137 | elseif optimize 138 | if indicator 139 | @objective(m, Max, 1000 * b + p[1, 1] + p[1, 2]) 140 | elseif reified 141 | @objective(m, Max, sum(b)) 142 | else 143 | @objective(m, Max, p[1, 1] + p[1, 2]) 144 | end 145 | end 146 | 147 | optimize!(m) 148 | 149 | status = JuMP.termination_status(m) 150 | @assert status == MOI.OPTIMAL 151 | end 152 | -------------------------------------------------------------------------------- /test/unit/constraints/less_than.jl: -------------------------------------------------------------------------------- 1 | @testset "LessThan" begin 2 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 3 | @variable(m, -1 <= x <= 5, Int) 4 | @variable(m, -1 <= y <= 5, Int) 5 | @variable(m, -5 <= z <= 5, Int) 6 | @constraint(m, 1.2x + π * y - 2z <= 4.71) 7 | optimize!(m) 8 | com = CS.get_inner_model(m) 9 | 10 | constraint = com.constraints[1] 11 | @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2, 3]) 12 | @test !CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [3, 2, 1]) 13 | 14 | constr_indices = constraint.indices 15 | @test !CS.still_feasible( 16 | com, 17 | constraint, 18 | constraint.fct, 19 | constraint.set, 20 | constr_indices[3], 21 | -5, 22 | ) 23 | @test CS.still_feasible( 24 | com, 25 | constraint, 26 | constraint.fct, 27 | constraint.set, 28 | constr_indices[3], 29 | -4, 30 | ) 31 | 32 | @test CS.fix!(com, com.search_space[constr_indices[2]], 0) 33 | @test !CS.still_feasible( 34 | com, 35 | constraint, 36 | constraint.fct, 37 | constraint.set, 38 | constr_indices[3], 39 | -4, 40 | ) 41 | 42 | # need to create a backtrack_vec to reverse pruning 43 | dummy_backtrack_obj = CS.BacktrackObj(com) 44 | dummy_backtrack_obj.step_nr = 1 45 | push!(com.backtrack_vec, dummy_backtrack_obj) 46 | # reverse previous fix 47 | CS.reverse_pruning!(com, 1) 48 | com.c_backtrack_idx = 1 49 | 50 | # now setting it to -4 should be feasible 51 | @test CS.still_feasible( 52 | com, 53 | constraint, 54 | constraint.fct, 55 | constraint.set, 56 | constr_indices[3], 57 | -4, 58 | ) 59 | 60 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 61 | @variable(m, -1 <= x <= 5, Int) 62 | @variable(m, -1 <= y <= 5, Int) 63 | @variable(m, -5 <= z <= 5, Int) 64 | @constraint(m, 1.2x + π * y - 2z <= 4.71) 65 | optimize!(m) 66 | com = CS.get_inner_model(m) 67 | constraint = com.constraints[1] 68 | constr_indices = constraint.indices 69 | 70 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 71 | @test sort(CS.values(com.search_space[1])) == -1:5 72 | @test sort(CS.values(com.search_space[2])) == -1:5 73 | @test sort(CS.values(com.search_space[3])) == -4:5 74 | 75 | @test CS.fix!(com, com.search_space[constr_indices[3]], -4) 76 | CS.changed!(com, constraint, constraint.fct, constraint.set) 77 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 78 | @test CS.value(com.search_space[1]) == -1 79 | @test CS.value(com.search_space[2]) == -1 80 | @test CS.value(com.search_space[3]) == -4 81 | @test CS.isfixed(com.search_space[1]) 82 | @test CS.isfixed(com.search_space[2]) 83 | @test CS.isfixed(com.search_space[3]) 84 | end 85 | 86 | @testset "less than is_constraint_violated test" begin 87 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 88 | @variable(m, -5 <= x[1:2] <= 5, Int) 89 | @constraint(m, sum(x) <= 7) 90 | optimize!(m) 91 | com = CS.get_inner_model(m) 92 | 93 | constraint = com.constraints[1] 94 | 95 | variables = com.search_space 96 | @test CS.fix!(com, variables[constraint.indices[1]], 5; check_feasibility = false) 97 | @test CS.fix!(com, variables[constraint.indices[2]], 3; check_feasibility = false) 98 | @test CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 99 | 100 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 101 | @variable(m, -5 <= x[1:2] <= 5, Int) 102 | @constraint(m, sum(x) <= 7) 103 | optimize!(m) 104 | com = CS.get_inner_model(m) 105 | 106 | constraint = com.constraints[1] 107 | 108 | variables = com.search_space 109 | @test CS.fix!(com, variables[constraint.indices[1]], 5; check_feasibility = false) 110 | @test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 111 | end 112 | 113 | @testset "constraint without variables" begin 114 | m = Model(optimizer_with_attributes(CS.Optimizer, "logging" => [])) 115 | @variable(m, -5 <= x[1:2] <= 5, Int) 116 | @constraint(m, x[2] - x[1] >= x[2]-x[1] + 10) 117 | optimize!(m) 118 | @test JuMP.termination_status(m) == MOI.INFEASIBLE 119 | 120 | m = Model(optimizer_with_attributes(CS.Optimizer, "logging" => [])) 121 | @variable(m, -5 <= x[1:2] <= 5, Int) 122 | @constraint(m, x[2] - x[1] + (-x[2]) + x[1] <= 0) 123 | optimize!(m) 124 | @test JuMP.termination_status(m) == MOI.OPTIMAL 125 | 126 | m = Model(optimizer_with_attributes(CS.Optimizer, "logging" => [])) 127 | @variable(m, -5 <= x[1:2] <= 5, Int) 128 | @constraint(m, sum(0 .* x) <= 1) 129 | optimize!(m) 130 | @test JuMP.termination_status(m) == MOI.OPTIMAL 131 | end 132 | -------------------------------------------------------------------------------- /test/str8ts.jl: -------------------------------------------------------------------------------- 1 | function solve_str8ts( 2 | grid, 3 | white; 4 | backtrack = true, 5 | all_solutions = false, 6 | keep_logs = false, 7 | logging = [:Info, :Table], 8 | ) 9 | straight_tables = Vector{Array{Int,2}}(undef, 9) 10 | for i in 1:9 11 | straight_tables[i] = get_straights(collect(1:9), i) 12 | end 13 | 14 | m = Model(optimizer_with_attributes( 15 | CS.Optimizer, 16 | "backtrack" => backtrack, 17 | "keep_logs" => keep_logs, 18 | "all_solutions" => all_solutions, 19 | "logging" => logging, 20 | )) 21 | @variable(m, 0 <= x[1:9, 1:9] <= 9, Int) 22 | 23 | # set variables 24 | for r in 1:9, c in 1:9 25 | if grid[r, c] != 0 26 | @constraint(m, x[r, c] == grid[r, c]) 27 | elseif white[r, c] == 1 28 | @constraint(m, x[r, c] >= 1) 29 | else # black ones without a number 30 | @constraint(m, x[r, c] == 0) 31 | end 32 | end 33 | 34 | straights = [] 35 | for r in 1:9 36 | found_straight = false 37 | vec = Vector{Tuple{Int,Int}}() 38 | for c in 1:9 39 | if white[r, c] == 1 40 | found_straight = true 41 | push!(vec, (r, c)) 42 | elseif found_straight 43 | push!(straights, copy(vec)) 44 | empty!(vec) 45 | found_straight = false 46 | end 47 | end 48 | if found_straight 49 | push!(straights, copy(vec)) 50 | end 51 | end 52 | 53 | for c in 1:9 54 | found_straight = false 55 | vec = Vector{Tuple{Int,Int}}() 56 | for r in 1:9 57 | if white[r, c] == 1 58 | found_straight = true 59 | push!(vec, (r, c)) 60 | elseif found_straight 61 | push!(straights, copy(vec)) 62 | empty!(vec) 63 | found_straight = false 64 | end 65 | end 66 | if found_straight 67 | push!(straights, copy(vec)) 68 | end 69 | end 70 | 71 | for r in 1:9 72 | variables = [x[r, c] for c = 1:9 if white[r, c] == 1 || grid[r, c] != 0] 73 | @constraint(m, variables in CS.AllDifferent()) 74 | end 75 | for c in 1:9 76 | variables = [x[r, c] for r = 1:9 if white[r, c] == 1 || grid[r, c] != 0] 77 | @constraint(m, variables in CS.AllDifferent()) 78 | end 79 | 80 | for straight in straights 81 | len = length(straight) 82 | variables = [x[s[1], s[2]] for s in straight] 83 | @constraint(m, variables in CS.TableSet(straight_tables[len])) 84 | end 85 | 86 | optimize!(m) 87 | status = JuMP.termination_status(m) 88 | return status, m, x 89 | end 90 | 91 | function get_straights(numbers, len) 92 | sort!(numbers) 93 | nrows = (length(numbers) - len + 1) * factorial(len) 94 | table = Array{Int64}(undef, (nrows, len)) 95 | i = 1 96 | for j in 1:(length(numbers) - len + 1) 97 | l = numbers[j:(j + len - 1)] 98 | for row in permutations(l, len) 99 | table[i, :] = row 100 | i += 1 101 | end 102 | end 103 | return table 104 | end 105 | 106 | @testset "Str8ts no backtrack" begin 107 | grid = zeros(Int, (9, 9)) 108 | grid[1, :] = [0 0 0 0 0 0 0 3 0] 109 | grid[2, :] = [0 0 0 0 8 0 0 0 0] 110 | grid[3, :] = [0 0 0 0 0 0 1 0 0] 111 | grid[4, :] = [0 0 0 0 0 6 0 0 0] 112 | grid[5, :] = [0 0 1 0 0 0 0 0 8] 113 | grid[6, :] = [0 0 0 4 0 0 9 0 0] 114 | grid[7, :] = [0 0 3 5 0 0 0 0 0] 115 | grid[8, :] = [0 0 0 0 0 0 0 0 2] 116 | grid[9, :] = [5 0 9 0 0 0 0 0 0] 117 | 118 | white = zeros(Int, (9, 9)) 119 | white[1, :] = [1 1 1 1 0 0 1 1 0] 120 | white[2, :] = [0 1 1 1 0 1 1 1 0] 121 | white[3, :] = [0 1 1 0 1 1 0 1 1] 122 | white[4, :] = [1 1 0 1 1 0 1 1 1] 123 | white[5, :] = [1 1 1 1 1 1 1 1 1] 124 | white[6, :] = [1 1 1 0 1 1 0 1 1] 125 | white[7, :] = [1 1 0 1 1 0 1 1 0] 126 | white[8, :] = [0 1 1 1 0 1 1 1 0] 127 | white[9, :] = [0 1 1 0 0 1 1 1 1] 128 | 129 | status, m, x = solve_str8ts(grid, white; all_solutions = true, logging = []) 130 | com = CS.get_inner_model(m) 131 | @test JuMP.termination_status(m) == MOI.OPTIMAL 132 | @test MOI.get(m, MOI.ResultCount()) == 1 133 | @test convert.(Int, JuMP.value.(x[1, :])) == [7, 9, 6, 8, 0, 0, 2, 3, 0] 134 | @test convert.(Int, JuMP.value.(x[2, :])) == [0, 6, 5, 7, 8, 4, 3, 2, 0] 135 | @test convert.(Int, JuMP.value.(x[3, :])) == [0, 3, 4, 0, 6, 5, 1, 8, 9] 136 | @test convert.(Int, JuMP.value.(x[4, :])) == [4, 5, 0, 2, 3, 6, 8, 9, 7] 137 | @test convert.(Int, JuMP.value.(x[5, :])) == [2, 4, 1, 3, 5, 9, 7, 6, 8] 138 | @test convert.(Int, JuMP.value.(x[6, :])) == [3, 1, 2, 4, 7, 8, 9, 5, 6] 139 | @test convert.(Int, JuMP.value.(x[7, :])) == [1, 2, 3, 5, 4, 0, 6, 7, 0] 140 | @test convert.(Int, JuMP.value.(x[8, :])) == [0, 7, 8, 6, 0, 3, 5, 4, 2] 141 | @test convert.(Int, JuMP.value.(x[9, :])) == [5, 8, 9, 0, 0, 2, 4, 1, 3] 142 | end 143 | -------------------------------------------------------------------------------- /test/unit/constraints/equal.jl: -------------------------------------------------------------------------------- 1 | @testset "AllEqual" begin 2 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 3 | @variable(m, -5 <= x[1:3] <= 5, Int) 4 | @constraint(m, x in CS.AllEqual()) 5 | optimize!(m) 6 | com = CS.get_inner_model(m) 7 | 8 | constraint = com.constraints[1] 9 | 10 | # doesn't check the length 11 | @test !CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2, 3]) 12 | @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [2, 2, 2]) 13 | 14 | constr_indices = constraint.indices 15 | @test CS.still_feasible( 16 | com, 17 | constraint, 18 | constraint.fct, 19 | constraint.set, 20 | constr_indices[2], 21 | 5, 22 | ) 23 | @test CS.fix!(com, com.search_space[constr_indices[2]], 5) 24 | @test !CS.still_feasible( 25 | com, 26 | constraint, 27 | constraint.fct, 28 | constraint.set, 29 | constr_indices[3], 30 | 4, 31 | ) 32 | @test CS.still_feasible( 33 | com, 34 | constraint, 35 | constraint.fct, 36 | constraint.set, 37 | constr_indices[3], 38 | 5, 39 | ) 40 | 41 | # need to create a backtrack_vec to reverse pruning 42 | dummy_backtrack_obj = CS.BacktrackObj(com) 43 | dummy_backtrack_obj.step_nr = 1 44 | push!(com.backtrack_vec, dummy_backtrack_obj) 45 | # reverse previous fix 46 | CS.reverse_pruning!(com, 1) 47 | com.c_backtrack_idx = 1 48 | 49 | # now setting it to 5 should be feasible 50 | @test CS.still_feasible( 51 | com, 52 | constraint, 53 | constraint.fct, 54 | constraint.set, 55 | constr_indices[3], 56 | 4, 57 | ) 58 | 59 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 60 | @variable(m, -5 <= x[1:3] <= 5, Int) 61 | @constraint(m, x in CS.AllEqual()) 62 | optimize!(m) 63 | com = CS.get_inner_model(m) 64 | constraint = com.constraints[1] 65 | constr_indices = constraint.indices 66 | 67 | # feasible and no changes 68 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 69 | for ind in constr_indices 70 | @test sort(CS.values(com.search_space[ind])) == -5:5 71 | end 72 | @test CS.fix!(com, com.search_space[constr_indices[2]], 5) 73 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 74 | for ind in constr_indices 75 | @test CS.value(com.search_space[ind]) == 5 76 | @test CS.isfixed(com.search_space[ind]) 77 | end 78 | 79 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 80 | @variable(m, -5 <= x[1:3] <= 5, Int) 81 | @constraint(m, x in CS.AllEqual()) 82 | optimize!(m) 83 | com = CS.get_inner_model(m) 84 | 85 | constraint = com.constraints[1] 86 | constr_indices = constraint.indices 87 | 88 | # Should be synced to the other variables 89 | @test CS.remove_below!(com, com.search_space[constr_indices[3]], 3) 90 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 91 | for ind in constr_indices 92 | @test sort(CS.values(com.search_space[ind])) == 3:5 93 | end 94 | 95 | @test CS.rm!(com, com.search_space[constr_indices[1]], 5) 96 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 97 | for ind in constr_indices 98 | @test sort(CS.values(com.search_space[ind])) == 3:4 99 | end 100 | 101 | @test CS.rm!(com, com.search_space[constr_indices[2]], 3) 102 | @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set) 103 | for ind in constr_indices 104 | @test CS.value(com.search_space[ind]) == 4 105 | @test CS.isfixed(com.search_space[ind]) 106 | end 107 | end 108 | 109 | @testset "equalset is_constraint_violated test" begin 110 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 111 | @variable(m, -5 <= x[1:5] <= 5, Int) 112 | @constraint(m, x in CS.AllEqual()) 113 | optimize!(m) 114 | com = CS.get_inner_model(m) 115 | 116 | constraint = com.constraints[1] 117 | 118 | variables = com.search_space 119 | @test CS.fix!(com, variables[1], 3; check_feasibility = false) 120 | @test CS.fix!(com, variables[2], 5; check_feasibility = false) 121 | @test CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 122 | 123 | m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => [])) 124 | @variable(m, -5 <= x[1:5] <= 5, Int) 125 | @constraint(m, x in CS.AllEqual()) 126 | optimize!(m) 127 | com = CS.get_inner_model(m) 128 | 129 | constraint = com.constraints[1] 130 | 131 | variables = com.search_space 132 | @test CS.fix!(com, variables[1], 5; check_feasibility = false) 133 | @test CS.fix!(com, variables[2], 5; check_feasibility = false) 134 | @test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set) 135 | end 136 | -------------------------------------------------------------------------------- /docs/src/supported.md: -------------------------------------------------------------------------------- 1 | # Supported variables, constraints and objectives 2 | 3 | This solver is in a pre-release phase right now and not a lot of constraints or objectives are supported. 4 | If you want to be up to date you might want to check this page every couple of months. 5 | 6 | You can also watch the project to be informed of every change but this might spam you ;) 7 | 8 | ## Supported objectives 9 | 10 | Currently the only objective supported is the linear objective i.e 11 | 12 | ``` 13 | @objective(m, Min, 2x+3y) 14 | ``` 15 | 16 | ## Supported variables 17 | 18 | All variables need to be bounded and discrete. 19 | 20 | ``` 21 | @variable(m, x) # does not work 22 | @variable(m, x, Int) # doesn't work because it isn't bounded 23 | @variable(m, x, Bin) # does work because it is discrete and bounded 24 | @variable(m, 1 <= x, Int) # doesn't work because it isn't bounded from above 25 | @variable(m, 1 <= x <= 7, Int) # does work 26 | ``` 27 | 28 | Additionally you can specify a set of allowed integers: 29 | 30 | ``` 31 | @variable(m, x, CS.Integers([1,3,5,7])) 32 | ``` 33 | 34 | ### Anonymous variables 35 | 36 | Besides the named way of defining variables it's also possible to have anonymous variables as provided by [JuMP.jl](https://github.com/jump-dev/JuMP.jl). 37 | 38 | This can be useful when one needs to create temporary variables for reformulations of the problem. The values of these variables can be accessed by the name as well as named variables under the condition that the given name is not overwritten and available in the current scope. Anonymous variables are mostly needed to avoid the problem of needing a new name for each temporary variables. 39 | 40 | **An example:** 41 | ```julia 42 | function does_equal(x, y) 43 | model = owner_model(x) 44 | b = @variable(model, binary=true) 45 | @constraint(model, b := { x == y }) 46 | return b 47 | end 48 | model = Model(optimizer_with_attributes(CS.Optimizer)) 49 | @variable(model, x[1:4], CS.Integers([0,2,3,4,5])) 50 | first_equal = does_equal(x[1], x[2]) 51 | @objective(model, Max, sum(x)+5*first_equal) 52 | optimize!(model) 53 | @show JuMP.value.(x) 54 | @show JuMP.value(first_equal) 55 | ``` 56 | 57 | The general usage is described in the [JuMP docs](https://jump.dev/JuMP.jl/stable/variables/#Anonymous-JuMP-variables-1) but the following gives an idea on how to use them for the most common use-cases in combination with ConstraintSolver.jl . 58 | 59 | ```julia 60 | # create an anonymous array of 5 integer variables with the domain [0,2,3,4,5] 61 | x = @variable(model, [1:5], variable_type=CS.Integers([0,2,3,4,5])) 62 | # create a single anonymous binary variable 63 | b = @variable(model, binary=true) 64 | # create a single anonymous integer variable **Important:** Needs bounds 65 | y = @variable(model, integer=true, lower_bound=0, upper_bound=10) 66 | ``` 67 | 68 | ### Missing 69 | - Interval variables for scheduling 70 | 71 | ## Supported constraints 72 | 73 | The following list shows constraints that are implemented and those which are planned. 74 | 75 | - [X] Linear constraints 76 | - At the moment this is kind of partially supported as they are not really good at giving bounds yet (See [better bound computation](tutorial.md#Bound-computation-1)) 77 | - [X] `==` 78 | - [X] `<=` 79 | - [X] `>=` 80 | - [X] `!=` 81 | - [X] `<` 82 | - [X] `>` 83 | - [X] All different 84 | - `@constraint(m, [x,y,z] in CS.AllDifferent())` 85 | - `@constraint(m, [x,y+2,x+y] in CS.AllDifferent())` 86 | - [X] `TableSet` constraint [#130](https://github.com/Wikunia/ConstraintSolver.jl/pull/130) 87 | - also with `VectorAffineFunction` i.e `[x,y+2,x+y] in CS.TableSet(...)` 88 | - Indicator constraints [#167](https://github.com/Wikunia/ConstraintSolver.jl/pull/167) 89 | - i.e `@constraint(m, b => {x + y >= 12})` 90 | - [X] for affine inner constraints 91 | - [X] for all types of inner constraints 92 | - [X] Allow `&&` inside the inner constraint i.e `@constraint(m, b => {x + y >= 12 && 2x + y <= 7})` 93 | - [X] Allow `||` inside the inner constraint i.e `@constraint(m, b => {x + y >= 12 || 2x + y <= 7})` 94 | - [-] Have `Indicator` and `Reified` inside one another 95 | - this is only supported for simple cases i.e bridge support is missing 96 | - [ ] `VectorAffineFunction` in `AllDifferentSet` or `TableSet` 97 | - Please open an issue if needed for a problem. Probably not too hard to add 98 | - Reified constraints [#171](https://github.com/Wikunia/ConstraintSolver.jl/pull/171) 99 | - i.e `@constraint(m, b := {x + y >= 12})` 100 | - [X] for everything that is supported by indicator constraints 101 | - Boolean constraints 102 | - i.e `@constraint(m, x + y >= 12 || 2x + y <= 7)` 103 | - allows binary variables without writing ` == 1` or ` == 0` 104 | - one can write something like `a || !b` 105 | - **Attention:** Does not check whether `a` and `b` are actually binary variables 106 | - Element constraints 107 | - [X] 1D array with constant values 108 | - i.e `T = [12,87,42,1337]` `T[y] == z` with `y` and `z` being variables [#213](https://github.com/Wikunia/ConstraintSolver.jl/pull/213) 109 | - [ ] 2D array with constant values 110 | - where T is an array 111 | - [ ] 1D array with variables 112 | - where T is a vector of variables 113 | - [ ] Scheduling constraints 114 | - [ ] Cycle constraints 115 | 116 | If I miss something which would be helpful for your needs please open an issue. 117 | 118 | ## Additionally 119 | - [ ] adding new constraints after `optimize!` got called [#72](https://github.com/Wikunia/ConstraintSolver.jl/issues/72) 120 | -------------------------------------------------------------------------------- /benchmark/scheduling/benchmark.jl: -------------------------------------------------------------------------------- 1 | #= 2 | The problems are taken from http://www.hakank.org/julia/constraints/ 3 | 4 | Thanks a lot to Håkan Kjellerstrand 5 | =# 6 | 7 | function cumulative(model, start, duration, resource, limit) 8 | tasks = [i for i in 1:length(start) if resource[i] > 0 && duration[i] > 0] 9 | num_tasks = length(tasks) 10 | times_min_a = round.(Int,[JuMP.lower_bound(start[i]) for i in tasks]) 11 | times_min = minimum(times_min_a) 12 | times_max_a = round.(Int,[JuMP.upper_bound(start[i])+duration[i] for i in tasks]) 13 | times_max = maximum(times_max_a) 14 | for t in times_min:times_max 15 | b = @variable(model, [1:num_tasks], Bin) 16 | for i in tasks 17 | # is this task active during this time t? 18 | @constraint(model, b[i] := { start[i] <= t && t < start[i]+duration[i]}) 19 | end 20 | # Check that there's no conflicts in time t 21 | @constraint(model,sum(b[i]*resource[i] for i in tasks) <= limit) 22 | end 23 | end 24 | 25 | # 26 | # no_overlap(model, begins,durations) 27 | # 28 | # Ensure that there is no overlap between the tasks. 29 | # 30 | function no_overlap(model, begins,durations) 31 | n = length(begins) 32 | for i in 1:n, j in i+1:n 33 | b = @variable(model,[1:2], Bin) 34 | @constraint(model,b[1] := {begins[i] + durations[i] <= begins[j]}) 35 | @constraint(model,b[2] := {begins[j] + durations[j] <= begins[i]}) 36 | @constraint(model, sum(b) >= 1) 37 | end 38 | end 39 | 40 | # From: http://www.hakank.org/julia/constraints/furniture_moving.jl 41 | function furniture_moving() 42 | 43 | model = Model(optimizer_with_attributes(CS.Optimizer, 44 | "logging"=>[], 45 | 46 | "traverse_strategy"=>:BFS, 47 | "branch_split"=>:InHalf, # <- 48 | 49 | # "lp_optimizer" => cbc_optimizer, 50 | # "lp_optimizer" => glpk_optimizer, 51 | # "lp_optimizer" => ipopt_optimizer, 52 | )) 53 | 54 | # Furniture moving problem 55 | n = 4 56 | # [piano, chair, bed, table] 57 | durations = [30,10,15,15] 58 | resources = [3,1,3,2] # people needed per task 59 | @variable(model, 0 <= start_times[1:n] <= 60, Int) 60 | @variable(model, 0 <= end_times[1:n] <= 60, Int) 61 | @variable(model, 1 <= limit <= 3, Int) 62 | @variable(model, 0 <= max_time <= 60, Int) 63 | @constraint(model, end_times .<= max_time) 64 | 65 | for i in 1:n 66 | @constraint(model,end_times[i] == start_times[i] + durations[i]) 67 | end 68 | cumulative(model, start_times, durations, resources, limit) 69 | 70 | # @objective(model,Min,max_time) 71 | 72 | optimize!(model) 73 | 74 | status = JuMP.termination_status(model) 75 | @assert status == MOI.OPTIMAL 76 | @assert JuMP.value(limit) ≈ 3 77 | end 78 | 79 | # from http://www.hakank.org/julia/constraints/organize_day.jl 80 | function organize_day(problem,all_solutions=true) 81 | # cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) 82 | # glpk_optimizer = optimizer_with_attributes(GLPK.Optimizer) 83 | # ipopt_optimizer = optimizer_with_attributes(Ipopt.Optimizer) 84 | 85 | model = Model(optimizer_with_attributes(CS.Optimizer, "all_solutions"=> all_solutions, 86 | # "all_optimal_solutions"=>all_solutions, 87 | "logging"=>[], 88 | 89 | "traverse_strategy"=>:BFS, 90 | # "traverse_strategy"=>:DFS, 91 | # "traverse_strategy"=>:DBFS, 92 | 93 | # "branch_split"=>:Smallest, 94 | # "branch_split"=>:Biggest, 95 | "branch_split"=>:InHalf, 96 | 97 | # https://wikunia.github.io/ConstraintSolver.jl/stable/options/#branch_strategy-(:Auto) 98 | "branch_strategy" => :IMPS, # default 99 | )) 100 | tasks = problem[:tasks] 101 | durations = problem[:durations] 102 | precedences = problem[:precedences] 103 | start_time = problem[:start_time] 104 | end_time = problem[:end_time] 105 | 106 | 107 | n = length(tasks) 108 | @variable(model, start_time <= begins[1:n] <= end_time, Int) 109 | @variable(model, start_time <= ends[1:n] <= end_time, Int) 110 | 111 | for i in 1:n 112 | @constraint(model,ends[i] == begins[i] + durations[i]) 113 | end 114 | 115 | no_overlap(model,begins,durations) 116 | 117 | # precedences 118 | for (a,b) in eachrow(precedences) 119 | @constraint(model, ends[a] <= begins[b]) 120 | end 121 | 122 | @constraint(model,begins[1] >= 11) 123 | 124 | # Solve the problem 125 | optimize!(model) 126 | 127 | status = JuMP.termination_status(model) 128 | # println("status:$status") 129 | @assert status == MOI.OPTIMAL 130 | if all_solutions 131 | @assert MOI.get(model, MOI.ResultCount()) == 5 132 | end 133 | end 134 | --------------------------------------------------------------------------------