├── data ├── graph_coloring │ ├── p3 │ ├── p4 │ ├── paw │ ├── square │ ├── triangle │ ├── 5_cycle │ ├── petersen │ ├── gc_15_30_0 │ ├── gc_15_30_1 │ ├── gc_15_30_2 │ ├── gc_15_30_3 │ ├── gc_15_30_4 │ ├── gc_15_30_5 │ ├── gc_15_30_6 │ ├── gc_15_30_7 │ ├── gc_15_30_8 │ ├── gc_15_30_9 │ └── solution_numbers ├── knapsack │ ├── ks_4_0 │ ├── ks_19_0 │ ├── ks_30_0 │ ├── ks_50_1 │ ├── ks_45_0 │ ├── ks_40_0 │ ├── ks_50_0 │ ├── ks_60_0 │ ├── ks_100_2 │ ├── ks_100_1 │ ├── ks_82_0 │ ├── ks_100_0 │ ├── ks_106_0 │ ├── ks_200_1 │ └── ks_200_0 ├── tsp │ ├── tsp_4.txt │ ├── tsp_7.txt │ ├── xkcd_np_tables.txt │ ├── numbers │ ├── tsp_17.txt │ ├── tsp_15.txt │ └── tsp_26.txt ├── sudoku │ ├── links │ ├── hard │ └── hardest ├── qap │ ├── numbers │ ├── qap4.txt │ ├── qap5.txt │ ├── qap6.txt │ ├── qap12a_opt288.txt │ ├── qap16_opt34.txt │ ├── qap14_opt507.txt │ └── qap12.txt └── queens │ └── numbers ├── config └── config.exs ├── papers └── gac-alldifferent.pdf ├── lib ├── application.ex ├── solver │ ├── search │ │ ├── strategy │ │ │ ├── value │ │ │ │ ├── indomain_max.ex │ │ │ │ ├── indomain_min.ex │ │ │ │ ├── indomain_random.ex │ │ │ │ ├── indomain_split.ex │ │ │ │ ├── value_selector.ex │ │ │ │ └── partition.ex │ │ │ └── variable │ │ │ │ └── local │ │ │ │ ├── input_order.ex │ │ │ │ ├── first_fail.ex │ │ │ │ ├── most_constrained.ex │ │ │ │ ├── max_regret.ex │ │ │ │ ├── dom_deg.ex │ │ │ │ └── most_completed.ex │ │ └── search.ex │ ├── constraints │ │ ├── all_different │ │ │ ├── all_different_default.ex │ │ │ ├── all_different_bc.ex │ │ │ ├── all_different_dc.ex │ │ │ ├── all_different_fwc.ex │ │ │ ├── all_different_dc_fast.ex │ │ │ ├── all_different_combined.ex │ │ │ └── all_different_binary.ex │ │ ├── circuit.ex │ │ ├── equal.ex │ │ ├── count.ex │ │ ├── not_equal.ex │ │ ├── modulo.ex │ │ ├── less.ex │ │ ├── abs.ex │ │ ├── or.ex │ │ ├── maximum.ex │ │ ├── minimum.ex │ │ ├── less_or_equal.ex │ │ ├── propagators │ │ │ ├── less.ex │ │ │ ├── less_or_equal.ex │ │ │ ├── equal.ex │ │ │ ├── not_equal.ex │ │ │ └── or.ex │ │ ├── element.ex │ │ ├── inverse.ex │ │ ├── element_var.ex │ │ ├── element2d.ex │ │ ├── sum.ex │ │ └── reified.ex │ ├── objective │ │ ├── objective_propagator.ex │ │ └── objective.ex │ ├── domain │ │ └── packed_min_max.ex │ ├── variables │ │ ├── bool_variable.ex │ │ ├── int_variable.ex │ │ ├── api_interface.ex │ │ └── variable.ex │ ├── core │ │ ├── distributed.ex │ │ ├── constraint.ex │ │ └── propagator │ │ │ └── propagator_variable.ex │ ├── solution │ │ └── solution.ex │ ├── common │ │ └── common.ex │ └── model │ │ └── model.ex ├── utils │ ├── tuple_array.ex │ ├── mutable_array.ex │ └── utils.ex └── examples │ ├── send_more_money.ex │ ├── utils.ex │ ├── graph_coloring.ex │ ├── xkcd_np.ex │ └── euler43.ex ├── .formatter.exs ├── .gitignore ├── scripts ├── 2_vars_1_propagator.exs ├── constraint.exs ├── or_performance.exs ├── 2_2_2_script.exs ├── bitvector_debug.exs ├── concurrent_removal_domain.exs ├── debug_circuit.exs ├── space_debugging.ex ├── gc_script.exs ├── debug_reified.exs ├── gc_debug_script.exs ├── sum_debug.exs └── space_propagation_debugging.exs ├── test ├── examples │ ├── zebra_test.exs │ ├── stable_marriage_test.exs │ ├── reindeers_test.exs │ ├── send_more_money_test.exs │ ├── knapsack_test.exs │ ├── quadratic_assignment_test.exs │ ├── tsp_test.exs │ ├── sudoku_test.exs │ ├── queens_test.exs │ └── graph_coloring_test.exs ├── constraints │ ├── circuit_test.exs │ ├── inverse_test.exs │ ├── less_or_equal_test.exs │ ├── count_test.exs │ ├── sum_test.exs │ ├── all_different │ │ ├── all_different_binary_test.exs │ │ ├── all_different_fwc_test.exs │ │ ├── all_different_dc_test.exs │ │ └── utils_test.exs │ ├── equal_test.exs │ ├── maximum_test.exs │ ├── modulo_test.exs │ ├── abs_test.exs │ ├── arithmetics_test.exs │ ├── or_test.exs │ └── minimum_test.exs ├── solver │ ├── shared_test.exs │ └── distributed_test.exs ├── model │ └── model_test.exs ├── propagators │ ├── all_different │ │ ├── all_different_bc_test.exs │ │ ├── all_different_dc_test.exs │ │ └── all_different_fwc_test.exs │ ├── abs_test.exs │ ├── maximum_test.exs │ ├── sum_test.exs │ ├── circuit_test.exs │ ├── not_equal_test.exs │ ├── less_or_equal_test.exs │ ├── modulo_test.exs │ └── element │ │ └── element2d_test.exs ├── test_helper.exs ├── space │ └── space_propagation_test.exs └── utils │ └── mutable_array_test.exs ├── TODO.md ├── LICENSE ├── src └── bit_vector.erl └── mix.exs /data/graph_coloring/p3: -------------------------------------------------------------------------------- 1 | 3 2 2 2 | 0 1 3 | 1 2 -------------------------------------------------------------------------------- /data/graph_coloring/p4: -------------------------------------------------------------------------------- 1 | 4 3 2 2 | 0 1 3 | 1 2 4 | 2 3 -------------------------------------------------------------------------------- /data/graph_coloring/paw: -------------------------------------------------------------------------------- 1 | 4 4 3 2 | 0 1 3 | 0 2 4 | 0 3 5 | 1 2 -------------------------------------------------------------------------------- /data/graph_coloring/square: -------------------------------------------------------------------------------- 1 | 4 4 2 2 | 0 1 3 | 0 3 4 | 1 2 5 | 2 3 -------------------------------------------------------------------------------- /data/graph_coloring/triangle: -------------------------------------------------------------------------------- 1 | 3 3 3 2 | 0 1 3 | 0 2 4 | 1 2 5 | -------------------------------------------------------------------------------- /data/knapsack/ks_4_0: -------------------------------------------------------------------------------- 1 | 4 11 2 | 8 4 3 | 10 5 4 | 15 8 5 | 4 3 6 | -------------------------------------------------------------------------------- /data/graph_coloring/5_cycle: -------------------------------------------------------------------------------- 1 | 5 5 3 2 | 0 1 3 | 0 4 4 | 1 2 5 | 2 3 6 | 3 4 -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :warning 4 | -------------------------------------------------------------------------------- /data/tsp/tsp_4.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 0 20 42 35 3 | 20 0 30 34 4 | 42 30 0 12 5 | 35 34 12 0 6 | 7 | -------------------------------------------------------------------------------- /papers/gac-alldifferent.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokner/fixpoint/HEAD/papers/gac-alldifferent.pdf -------------------------------------------------------------------------------- /lib/application.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Application do 2 | def start(:normal, []) do 3 | {:ok, self()} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /data/sudoku/links: -------------------------------------------------------------------------------- 1 | http://www.hakank.org/minizinc/sudoku_alldifferent.mzn 2 | http://www.stolaf.edu/people/hansonr/sudoku/12rules.htm -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,scripts,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /data/qap/numbers: -------------------------------------------------------------------------------- 1 | qap12 - 4776 (opt) 2 | qap25 - 3700 3 | qap12a_opt288 - 293 4 | qap14_opt507 - 515 5 | qap16_opt34 - 34 (optimality not proven) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | *.beam 9 | /config/*.secret.exs 10 | .elixir_ls/ 11 | -------------------------------------------------------------------------------- /data/qap/qap4.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 3 | 0 22 53 53 4 | 22 0 40 62 5 | 53 40 0 55 6 | 53 62 55 0 7 | 8 | 0 3 0 2 9 | 3 0 0 1 10 | 0 0 0 4 11 | 2 1 4 0 12 | -------------------------------------------------------------------------------- /data/graph_coloring/petersen: -------------------------------------------------------------------------------- 1 | 10 15 3 2 | 0 1 3 | 1 2 4 | 2 3 5 | 3 4 6 | 4 0 7 | 0 5 8 | 1 6 9 | 2 7 10 | 3 8 11 | 4 9 12 | 5 7 13 | 5 8 14 | 6 8 15 | 6 9 16 | 7 9 -------------------------------------------------------------------------------- /data/tsp/tsp_7.txt: -------------------------------------------------------------------------------- 1 | 7 2 | 0 10 17 34 18 29 16 3 | 10 0 1 8 4 27 18 4 | 17 1 0 5 5 20 25 5 | 34 8 5 0 4 13 26 6 | 18 4 5 4 0 5 10 7 | 29 27 20 13 5 0 15 8 | 16 18 25 26 10 15 0 9 | -------------------------------------------------------------------------------- /data/qap/qap5.txt: -------------------------------------------------------------------------------- 1 | 5 2 | 0 50 50 94 50 3 | 50 0 22 50 36 4 | 50 22 0 44 14 5 | 94 50 44 0 50 6 | 50 36 14 50 0 7 | 8 | 0 0 2 0 3 9 | 0 0 0 3 0 10 | 2 0 0 0 0 11 | 0 3 0 0 1 12 | 3 0 0 1 0 -------------------------------------------------------------------------------- /data/tsp/xkcd_np_tables.txt: -------------------------------------------------------------------------------- 1 | 7 2 | 0 10 17 34 18 29 16 3 | 10 0 1 8 4 27 18 4 | 17 1 0 5 5 20 25 5 | 34 8 5 0 4 13 26 6 | 18 4 5 4 0 5 10 7 | 29 27 20 13 5 0 15 8 | 16 18 25 26 10 15 0 9 | -------------------------------------------------------------------------------- /data/tsp/numbers: -------------------------------------------------------------------------------- 1 | tsp_15.txt - 291 (opt) 2 | tsp_17.txt - 2085 (opt) 3 | tsp_26.txt - 941 4 | tsp_61.txt 357 (LNS, minicp), 336 (fixpoint) 5 | tsp_81.txt 401 (LNS, minicp), 412 (fixpoint) 6 | tsp_101.txt 482 (LNS, minicp), 501 (fixpoint) 7 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/value/indomain_max.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.ValueSelector.Max do 2 | use CPSolver.Search.ValueSelector 3 | 4 | @impl true 5 | def select_value(variable) do 6 | Interface.max(variable) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/value/indomain_min.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.ValueSelector.Min do 2 | use CPSolver.Search.ValueSelector 3 | 4 | @impl true 5 | def select_value(variable) do 6 | Interface.min(variable) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /data/qap/qap6.txt: -------------------------------------------------------------------------------- 1 | 6 2 | 3 | 0 40 64 36 22 60 4 | 40 0 41 22 36 72 5 | 64 41 0 28 44 53 6 | 36 22 28 0 20 50 7 | 22 36 44 20 0 41 8 | 60 72 53 50 41 0 9 | 10 | 0 1 1 2 0 0 11 | 1 0 0 0 0 2 12 | 1 0 0 0 0 1 13 | 2 0 0 0 3 0 14 | 0 0 0 3 0 0 15 | 0 2 1 0 0 0 -------------------------------------------------------------------------------- /lib/solver/constraints/all_different/all_different_default.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.AllDifferent do 2 | use CPSolver.Constraint 3 | 4 | alias CPSolver.Constraint.AllDifferent.DC, as: DefaultAllDifferent 5 | 6 | @impl true 7 | defdelegate propagators(x), to: DefaultAllDifferent 8 | end 9 | -------------------------------------------------------------------------------- /lib/solver/constraints/all_different/all_different_bc.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.AllDifferent.BC do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.AllDifferent.BC, as: Propagator 4 | 5 | @impl true 6 | def propagators(x) do 7 | [Propagator.new(x)] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/solver/constraints/all_different/all_different_dc.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.AllDifferent.DC do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.AllDifferent.DC, as: Propagator 4 | 5 | @impl true 6 | def propagators(x) do 7 | [Propagator.new(x)] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/solver/constraints/all_different/all_different_fwc.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.AllDifferent.FWC do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.AllDifferent.FWC, as: FWCPropagator 4 | 5 | @impl true 6 | def propagators(x) do 7 | [FWCPropagator.new(x)] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /data/knapsack/ks_19_0: -------------------------------------------------------------------------------- 1 | 19 31181 2 | 1945 4990 3 | 321 1142 4 | 2945 7390 5 | 4136 10372 6 | 1107 3114 7 | 1022 2744 8 | 1101 3102 9 | 2890 7280 10 | 962 2624 11 | 1060 3020 12 | 805 2310 13 | 689 2078 14 | 1513 3926 15 | 3878 9656 16 | 13504 32708 17 | 1865 4830 18 | 667 2034 19 | 1833 4766 20 | 16553 40006 21 | -------------------------------------------------------------------------------- /scripts/2_vars_1_propagator.exs: -------------------------------------------------------------------------------- 1 | alias CPSolver.IntVariable 2 | alias CPSolver.Constraint.Less 3 | 4 | x = IntVariable.new([0, 1, 2]) 5 | y = IntVariable.new([0, 1, 2]) 6 | 7 | model = %{ 8 | variables: [x, y], 9 | constraints: [Less.new(x, y)] 10 | } 11 | 12 | {:ok, solver} = CPSolver.solve(model) 13 | -------------------------------------------------------------------------------- /lib/solver/constraints/all_different/all_different_dc_fast.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.AllDifferent.DC.Fast do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.AllDifferent.DC.Fast, as: Propagator 4 | 5 | @impl true 6 | def propagators(x) do 7 | [Propagator.new(x)] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/value/indomain_random.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.ValueSelector.Random do 2 | use CPSolver.Search.ValueSelector 3 | import CPSolver.Utils 4 | 5 | @impl true 6 | def select_value(variable) do 7 | variable 8 | |> domain_values() 9 | |> Enum.random() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_0: -------------------------------------------------------------------------------- 1 | 15 30 4 2 | 12 5 3 | 6 2 4 | 6 10 5 | 10 8 6 | 6 12 7 | 11 1 8 | 3 7 9 | 11 13 10 | 11 2 11 | 6 7 12 | 3 10 13 | 6 14 14 | 11 12 15 | 7 8 16 | 8 13 17 | 13 3 18 | 9 0 19 | 5 11 20 | 11 10 21 | 9 14 22 | 5 14 23 | 14 3 24 | 2 1 25 | 3 11 26 | 10 13 27 | 1 7 28 | 9 7 29 | 4 5 30 | 0 3 31 | 0 8 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_1: -------------------------------------------------------------------------------- 1 | 15 30 3 2 | 10 12 3 | 8 13 4 | 5 4 5 | 8 1 6 | 5 7 7 | 6 10 8 | 0 6 9 | 11 14 10 | 2 13 11 | 13 7 12 | 9 8 13 | 5 14 14 | 3 5 15 | 1 3 16 | 0 10 17 | 1 12 18 | 7 3 19 | 5 2 20 | 5 8 21 | 12 3 22 | 13 12 23 | 6 14 24 | 11 2 25 | 12 14 26 | 3 11 27 | 13 10 28 | 11 5 29 | 9 6 30 | 8 0 31 | 1 6 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_2: -------------------------------------------------------------------------------- 1 | 15 30 4 2 | 9 7 3 | 8 4 4 | 10 1 5 | 2 6 6 | 10 13 7 | 7 4 8 | 6 7 9 | 9 14 10 | 14 2 11 | 1 8 12 | 9 13 13 | 12 1 14 | 11 4 15 | 11 8 16 | 5 4 17 | 10 0 18 | 14 12 19 | 10 9 20 | 6 14 21 | 13 11 22 | 0 1 23 | 6 4 24 | 0 7 25 | 4 1 26 | 9 8 27 | 7 2 28 | 12 13 29 | 9 5 30 | 10 12 31 | 12 6 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_3: -------------------------------------------------------------------------------- 1 | 15 30 3 2 | 14 3 3 | 6 5 4 | 7 10 5 | 13 9 6 | 9 8 7 | 8 5 8 | 4 1 9 | 12 11 10 | 5 4 11 | 11 3 12 | 5 1 13 | 6 14 14 | 8 11 15 | 3 0 16 | 13 0 17 | 2 8 18 | 11 7 19 | 14 12 20 | 7 9 21 | 1 14 22 | 13 1 23 | 8 1 24 | 11 2 25 | 11 4 26 | 9 10 27 | 13 10 28 | 6 2 29 | 3 2 30 | 13 3 31 | 7 3 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_4: -------------------------------------------------------------------------------- 1 | 15 30 4 2 | 10 8 3 | 7 8 4 | 7 9 5 | 10 1 6 | 12 6 7 | 12 3 8 | 0 5 9 | 13 11 10 | 1 9 11 | 14 13 12 | 4 9 13 | 5 12 14 | 13 6 15 | 3 13 16 | 10 0 17 | 14 3 18 | 3 1 19 | 4 10 20 | 5 4 21 | 4 7 22 | 10 7 23 | 6 0 24 | 7 14 25 | 4 14 26 | 5 2 27 | 12 0 28 | 1 12 29 | 7 5 30 | 9 0 31 | 9 12 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_5: -------------------------------------------------------------------------------- 1 | 15 30 4 2 | 4 0 3 | 0 12 4 | 13 11 5 | 5 1 6 | 4 7 7 | 8 12 8 | 2 3 9 | 5 7 10 | 2 6 11 | 8 14 12 | 6 5 13 | 7 13 14 | 2 14 15 | 0 7 16 | 5 4 17 | 14 10 18 | 7 1 19 | 0 10 20 | 11 0 21 | 11 14 22 | 13 8 23 | 4 1 24 | 9 11 25 | 12 10 26 | 7 11 27 | 9 7 28 | 5 0 29 | 6 9 30 | 13 14 31 | 8 9 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_6: -------------------------------------------------------------------------------- 1 | 15 30 3 2 | 8 4 3 | 6 9 4 | 2 6 5 | 9 1 6 | 5 12 7 | 11 10 8 | 1 13 9 | 0 8 10 | 13 8 11 | 12 14 12 | 12 4 13 | 12 10 14 | 10 1 15 | 4 2 16 | 3 14 17 | 0 7 18 | 7 1 19 | 13 11 20 | 6 10 21 | 10 3 22 | 13 9 23 | 4 9 24 | 1 5 25 | 1 3 26 | 1 0 27 | 3 2 28 | 0 3 29 | 11 1 30 | 10 9 31 | 4 3 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_7: -------------------------------------------------------------------------------- 1 | 15 30 4 2 | 11 10 3 | 4 2 4 | 3 11 5 | 10 5 6 | 13 0 7 | 1 13 8 | 1 11 9 | 3 5 10 | 3 13 11 | 0 10 12 | 8 4 13 | 14 3 14 | 5 14 15 | 6 12 16 | 8 10 17 | 0 7 18 | 10 4 19 | 9 5 20 | 9 1 21 | 3 4 22 | 4 14 23 | 9 13 24 | 4 7 25 | 3 7 26 | 0 4 27 | 7 1 28 | 3 1 29 | 6 13 30 | 12 13 31 | 6 5 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_8: -------------------------------------------------------------------------------- 1 | 15 30 4 2 | 1 6 3 | 8 10 4 | 6 9 5 | 9 0 6 | 4 10 7 | 0 6 8 | 12 2 9 | 12 8 10 | 10 9 11 | 1 12 12 | 4 5 13 | 11 0 14 | 4 1 15 | 6 7 16 | 13 12 17 | 4 14 18 | 11 1 19 | 7 14 20 | 13 1 21 | 5 11 22 | 0 8 23 | 7 11 24 | 6 10 25 | 8 5 26 | 3 8 27 | 6 2 28 | 5 9 29 | 10 0 30 | 12 0 31 | 7 10 32 | 33 | -------------------------------------------------------------------------------- /data/graph_coloring/gc_15_30_9: -------------------------------------------------------------------------------- 1 | 15 30 4 2 | 14 11 3 | 2 1 4 | 0 7 5 | 7 12 6 | 0 9 7 | 7 14 8 | 6 3 9 | 6 14 10 | 9 13 11 | 12 6 12 | 6 13 13 | 6 0 14 | 5 14 15 | 6 10 16 | 9 1 17 | 3 0 18 | 13 11 19 | 9 5 20 | 8 10 21 | 0 14 22 | 13 5 23 | 1 3 24 | 8 4 25 | 12 13 26 | 11 10 27 | 3 14 28 | 3 13 29 | 10 5 30 | 3 4 31 | 9 2 32 | 33 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/variable/local/input_order.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.VariableSelector.InputOrder do 2 | use CPSolver.Search.VariableSelector 3 | 4 | @impl true 5 | def select(variables, _data, _opts) do 6 | Enum.sort_by(variables, fn %{index: idx} -> idx end) 7 | |> List.first() 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/solver/constraints/circuit.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Circuit do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.Circuit, as: CircuitPropagator 4 | 5 | @impl true 6 | def propagators(x) do 7 | [ 8 | CPSolver.Propagator.AllDifferent.DC.Fast.new(x), 9 | CircuitPropagator.new(x)] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/solver/constraints/equal.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Equal do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.Equal, as: EqualPropagator 4 | 5 | def new(x, y, offset \\ 0) 6 | 7 | def new(x, y, offset) do 8 | new([x, y, offset]) 9 | end 10 | 11 | @impl true 12 | def propagators(args) do 13 | [EqualPropagator.new(args)] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/examples/zebra_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.Zebra do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Examples.Zebra 5 | 6 | test "proper solution" do 7 | {:ok, result} = CPSolver.solve(Zebra.model()) 8 | ## Check against known solution 9 | assert %{zebra_owner: :japanese, water_drinker: :norwegian} = Zebra.puzzle_solution(result) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /scripts/constraint.exs: -------------------------------------------------------------------------------- 1 | alias CPSolver.Constraint 2 | alias CPSolver.Constraint.Less 3 | alias CPSolver.IntVariable, as: Variable 4 | alias CPSolver.ConstraintStore 5 | 6 | x = Variable.new(1..2, name: "x") 7 | y = Variable.new(1..2, name: "y") 8 | 9 | {:ok, [x_var, y_var] = bound_vars, store} = ConstraintStore.create_store([x, y]) 10 | 11 | Constraint.post(Less.new(x_var, y_var)) 12 | -------------------------------------------------------------------------------- /lib/solver/constraints/count.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Count do 2 | @moduledoc """ 3 | Constraints `c` to be the number of occurencies of `y` in `array`. 4 | 5 | """ 6 | alias CPSolver.Constraint.Factory 7 | 8 | def new(array, y, c) do 9 | new([array, y, c]) 10 | end 11 | 12 | def new([array, y, c] = _args) do 13 | Factory.count(array, y, c) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/solver/constraints/not_equal.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.NotEqual do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.NotEqual, as: NotEqualPropagator 4 | 5 | def new(x, y, offset \\ 0) 6 | 7 | def new(x, y, offset) do 8 | new([x, y, offset]) 9 | end 10 | 11 | @impl true 12 | def propagators(args) do 13 | [NotEqualPropagator.new(args)] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /data/graph_coloring/solution_numbers: -------------------------------------------------------------------------------- 1 | MINI-CP results: 2 | 3 | Instance #solutions time 4 | gc_15_30_0 133200 546 5 | gc_15_30_1 36 12 6 | gc_15_30_2 85632 244 7 | gc_15_30_3 12 10 8 | gc_15_30_4 89856 274 9 | gc_15_30_5 34848 156 10 | gc_15_30_6 12 8 11 | gc_15_30_7 72000 198 12 | gc_15_30_8 97776 216 13 | gc_15_30_9 94752 228 14 | -------------------------------------------------------------------------------- /lib/solver/constraints/all_different/all_different_combined.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.AllDifferent.Combined do 2 | use CPSolver.Constraint 3 | 4 | # alias CPSolver.Constraint.AllDifferent.FWC, as: DefaultAllDifferent 5 | 6 | @impl true 7 | def propagators(x) do 8 | [ 9 | CPSolver.Propagator.AllDifferent.DC.new(x), 10 | CPSolver.Propagator.AllDifferent.FWC.new(x) 11 | ] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /data/queens/numbers: -------------------------------------------------------------------------------- 1 | n Number of different solutions 2 | 1 1 3 | 2 0 4 | 3 0 5 | 4 2 6 | 5 10 7 | 6 4 8 | 7 40 9 | 8 92 10 | 9 352 11 | 10 724 12 | 11 2680 13 | 12 14200 14 | 13 73712 15 | 14 365596 16 | 15 2279184 17 | 16 14772512 18 | 17 95815104 19 | 18 666090624 20 | 19 4968057848 21 | 20 39029188884 22 | 21 314666222712 23 | 22 2691008701644 24 | 23 24233937684440 25 | 24 227514171973736 26 | 25 2207893435808352 27 | 26 22317699616364044 -------------------------------------------------------------------------------- /lib/solver/search/strategy/variable/local/first_fail.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.VariableSelector.FirstFail do 2 | use CPSolver.Search.VariableSelector 3 | alias CPSolver.Variable.Interface 4 | alias CPSolver.Utils 5 | 6 | def select(variables, _data \\ %{}, _opts) do 7 | get_minimals(variables) 8 | end 9 | 10 | def get_minimals(variables) do 11 | Utils.minimals(variables, &Interface.size/1) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/solver/constraints/all_different/all_different_binary.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.AllDifferent.Binary do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.NotEqual 4 | 5 | @impl true 6 | def propagators(variables) do 7 | for i <- 0..(length(variables) - 2) do 8 | for j <- (i + 1)..(length(variables) - 1) do 9 | NotEqual.new(Enum.at(variables, i), Enum.at(variables, j)) 10 | end 11 | end 12 | |> List.flatten() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/solver/constraints/modulo.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Modulo do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.Modulo, as: ModuloPropagator 4 | alias CPSolver.IntVariable 5 | 6 | def new(m, x, y) do 7 | new([m, x, y]) 8 | end 9 | 10 | @impl true 11 | def arguments(args) do 12 | Enum.map(args, fn arg -> IntVariable.to_variable(arg) end) 13 | end 14 | 15 | @impl true 16 | def propagators(args) do 17 | [ModuloPropagator.new(args)] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/solver/objective/objective_propagator.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Objective.Propagator do 2 | alias CPSolver.Objective 3 | use CPSolver.Propagator 4 | 5 | def new(variable, bound_handle) do 6 | new([variable, bound_handle]) 7 | end 8 | 9 | @impl true 10 | def variables([x | _rest]) do 11 | [set_propagate_on(x, :max_change)] 12 | end 13 | 14 | @impl true 15 | def filter([x, bound_handle | _], _state, _changes) do 16 | removeAbove(x, Objective.get_bound(bound_handle)) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /data/knapsack/ks_30_0: -------------------------------------------------------------------------------- 1 | 30 100000 2 | 90000 90001 3 | 89750 89751 4 | 10001 10002 5 | 89500 89501 6 | 10252 10254 7 | 89250 89251 8 | 10503 10506 9 | 89000 89001 10 | 10754 10758 11 | 88750 88751 12 | 11005 11010 13 | 88500 88501 14 | 11256 11262 15 | 88250 88251 16 | 11507 11514 17 | 88000 88001 18 | 11758 11766 19 | 87750 87751 20 | 12009 12018 21 | 87500 87501 22 | 12260 12270 23 | 87250 87251 24 | 12511 12522 25 | 87000 87001 26 | 12762 12774 27 | 86750 86751 28 | 13013 13026 29 | 86500 86501 30 | 13264 13278 31 | 86250 86251 32 | -------------------------------------------------------------------------------- /test/examples/stable_marriage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.StableMarriage do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Examples.StableMarriage 5 | 6 | test "Instance: Van Hentenryck (OPL)" do 7 | {:ok, result} = CPSolver.solve(StableMarriage.model(:van_hentenryck)) 8 | assert result.statistics.solution_count == 3 9 | 10 | assert Enum.all?( 11 | result.solutions, 12 | fn solution -> StableMarriage.check_solution(solution, :van_hentenryck) end 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /data/sudoku/hard: -------------------------------------------------------------------------------- 1 | +-------+-------+-------+ 2 | | 4 . . | . 3 9 | . 2 . | 3 | | . 5 6 | . . . | . . . | 4 | | . . . | . . . | 6 . 4 | 5 | +-------+-------+-------+ 6 | | . . . | . . . | 9 . . | 7 | | 5 . . | 1 . . | 2 . . | 8 | | . 9 . | . 2 7 | . 3 . | 9 | +-------+-------+-------+ 10 | | . 3 7 | . . . | . . . | 11 | | . . . | . . . | 8 . 6 | 12 | | 9 . 8 | . 1 . | . . . | 13 | +-------+-------+-------+ 14 | 15 | This instance has a single solution. 16 | 17 | ..6....9....5.17..2..9..3...7..3..5..2..9..6..4..8..2...1..3..4..52.7....3....8.. -------------------------------------------------------------------------------- /scripts/or_performance.exs: -------------------------------------------------------------------------------- 1 | defmodule OR do 2 | alias CPSolver.BooleanVariable 3 | alias CPSolver.Model 4 | alias CPSolver.Constraint.Or 5 | 6 | def test(n) do 7 | bool_vars = Enum.map(1..n, fn i -> BooleanVariable.new(name: "b#{i}") end) 8 | or_constraint = Or.new(bool_vars) 9 | 10 | model = Model.new(bool_vars, [or_constraint]) 11 | 12 | CPSolver.solve(model, stop_on: {:max_solutions, 1}, search: {:first_fail, :indomain_max}, space_threads: 1, solution_handler: fn sol -> IO.inspect("Done") end) 13 | #IO.inspect(result) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/solver/constraints/less.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Less do 2 | use CPSolver.Constraint 3 | alias CPSolver.Constraint.LessOrEqual, as: LessOrEqual 4 | 5 | def new(x, y, offset \\ 0) do 6 | LessOrEqual.new(le_args([x, y, offset])) 7 | end 8 | 9 | @impl true 10 | def propagators(args) do 11 | LessOrEqual.propagators(le_args(args)) 12 | end 13 | 14 | @impl true 15 | def arguments(args) do 16 | LessOrEqual.arguments(args) 17 | end 18 | 19 | defp le_args([x, y | offset]) do 20 | [x, y, (List.first(offset) || 0) - 1] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/solver/domain/packed_min_max.ex: -------------------------------------------------------------------------------- 1 | defmodule PackedMinMax do 2 | import Bitwise 3 | 4 | def get_min(packed, size \\ 32) do 5 | packed &&& all_bits_mask(size) 6 | end 7 | 8 | def get_max(packed, size \\ 32) do 9 | packed >>> size 10 | end 11 | 12 | def set_min(packed, min_value, size \\ 32) do 13 | get_max(packed, size) <<< size ||| min_value 14 | end 15 | 16 | def set_max(packed, max_value, size \\ 32) do 17 | max_value <<< size ||| get_min(packed, size) 18 | end 19 | 20 | defp all_bits_mask(size) do 21 | (1 <<< size) - 1 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/solver/constraints/abs.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Absolute do 2 | @moduledoc """ 3 | Absolute value constraint. 4 | Costraints y to be |x| 5 | """ 6 | use CPSolver.Constraint 7 | alias CPSolver.Propagator.Absolute, as: AbsolutePropagator 8 | alias CPSolver.IntVariable 9 | 10 | def new(x, y) do 11 | new([x, y]) 12 | end 13 | 14 | @impl true 15 | def propagators(args) do 16 | [AbsolutePropagator.new(args)] 17 | end 18 | 19 | @impl true 20 | def arguments([x, y]) do 21 | [IntVariable.to_variable(x), IntVariable.to_variable(y)] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/solver/constraints/or.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Or do 2 | @moduledoc """ 3 | ElementVar constrains list of variables `array`, variables `x` and `y` such that: 4 | array[x] = y 5 | 6 | array is a list of variables 7 | """ 8 | use CPSolver.Constraint 9 | alias CPSolver.Propagator.Or, as: OrPropagator 10 | alias CPSolver.IntVariable, as: Variable 11 | 12 | @impl true 13 | def propagators(args) do 14 | [OrPropagator.new(args)] 15 | end 16 | 17 | @impl true 18 | def arguments(array) when is_list(array) do 19 | Enum.map(array, &Variable.to_variable/1) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/solver/variables/bool_variable.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.BooleanVariable do 2 | alias CPSolver.IntVariable 3 | alias CPSolver.Variable.Interface 4 | 5 | def new(opts \\ []) do 6 | IntVariable.new(0..1, opts) 7 | end 8 | 9 | def set_false(var) do 10 | Interface.fix(var, 0) 11 | end 12 | 13 | def set_true(var) do 14 | Interface.fix(var, 1) 15 | end 16 | 17 | def true?(var) do 18 | fixed?(var, 1) 19 | end 20 | 21 | def false?(var) do 22 | fixed?(var, 0) 23 | end 24 | 25 | def fixed?(var, val) do 26 | Interface.fixed?(var) && Interface.min(var) == val 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/examples/reindeers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.Reindeers do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Examples.Reindeers 5 | 6 | test "order" do 7 | order = [Dancer, Donder, Comet, Vixen, Blitzen, Dasher, Rudolph, Cupid, Prancer] 8 | 9 | {:ok, result} = CPSolver.solve(Reindeers.model()) 10 | 11 | positions = hd(result.solutions) 12 | 13 | solution_order = 14 | Enum.zip(result.variables, positions) 15 | |> Enum.sort_by(fn {_name, pos} -> pos end) 16 | |> Enum.map(fn {name, _pos} -> name end) 17 | 18 | assert order == solution_order 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /scripts/2_2_2_script.exs: -------------------------------------------------------------------------------- 1 | space_opts = [keep_alive: true] 2 | 3 | alias CPSolver.Store.Registry, as: Store 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Space, as: Space 6 | alias CPSolver.Propagator.NotEqual 7 | alias CPSolver.Solution 8 | x_values = 1..2 9 | y_values = 1..2 10 | z_values = 1..2 11 | values = [x_values, y_values, z_values] 12 | [x, y, z] = variables = Enum.map(values, fn d -> Variable.new(d) end) 13 | propagators = [{NotEqual, [x, y]}, {NotEqual, [y, z]}] 14 | 15 | {:ok, space} = Space.create(variables, propagators, space_opts) 16 | %{space: space, propagators: propagators, variables: variables, domains: values} 17 | -------------------------------------------------------------------------------- /data/knapsack/ks_50_1: -------------------------------------------------------------------------------- 1 | 50 5000 2 | 995 945 3 | 259 242 4 | 258 244 5 | 279 281 6 | 576 582 7 | 126 119 8 | 280 303 9 | 859 913 10 | 270 279 11 | 389 408 12 | 927 925 13 | 281 305 14 | 624 662 15 | 961 938 16 | 757 718 17 | 231 250 18 | 838 767 19 | 154 158 20 | 649 595 21 | 277 268 22 | 180 167 23 | 895 957 24 | 23 22 25 | 930 948 26 | 93 102 27 | 61 62 28 | 626 604 29 | 342 349 30 | 262 279 31 | 215 221 32 | 183 203 33 | 958 889 34 | 205 213 35 | 859 835 36 | 171 166 37 | 566 575 38 | 779 758 39 | 704 706 40 | 196 182 41 | 26 28 42 | 726 729 43 | 621 671 44 | 800 864 45 | 580 579 46 | 535 553 47 | 647 632 48 | 168 163 49 | 90 95 50 | 679 745 51 | 440 438 52 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/variable/local/most_constrained.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.VariableSelector.MostConstrained do 2 | use CPSolver.Search.VariableSelector 3 | alias CPSolver.Variable.Interface 4 | alias CPSolver.Utils 5 | alias CPSolver.Propagator.ConstraintGraph 6 | 7 | @impl true 8 | def select(variables, space_data, _opts) do 9 | get_maximals(variables, space_data) 10 | end 11 | 12 | defp get_maximals(variables, space_data) do 13 | max_by_fun = fn var -> 14 | ConstraintGraph.variable_degree(space_data[:constraint_graph], Interface.id(var)) 15 | end 16 | 17 | Utils.maximals(variables, max_by_fun) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/solver/core/distributed.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Distributed do 2 | def call(node, solver, mod, function, args) do 3 | :erpc.call(node, mod, function, [solver | args]) 4 | end 5 | 6 | def worker_nodes(node_list \\ [Node.self() | Node.list()]) do 7 | node_list 8 | end 9 | 10 | def choose_worker_node(nodes \\ worker_nodes()) 11 | 12 | def choose_worker_node(true) do 13 | worker_nodes() 14 | |> choose_worker_node() 15 | end 16 | 17 | def choose_worker_node(distributed?) when not distributed? do 18 | Node.self() 19 | end 20 | 21 | def choose_worker_node(node_list) when is_list(node_list) do 22 | Enum.random(node_list) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /data/knapsack/ks_45_0: -------------------------------------------------------------------------------- 1 | 45 58181 2 | 1945 4990 3 | 321 1142 4 | 2945 7390 5 | 4136 10372 6 | 1107 3114 7 | 1022 2744 8 | 1101 3102 9 | 2890 7280 10 | 47019 112738 11 | 1530 3960 12 | 3432 8564 13 | 2165 5630 14 | 1703 4506 15 | 1106 3112 16 | 370 1240 17 | 657 2014 18 | 962 2624 19 | 1060 3020 20 | 805 2310 21 | 689 2078 22 | 1513 3926 23 | 3878 9656 24 | 13504 32708 25 | 1865 4830 26 | 667 2034 27 | 1833 4766 28 | 16553 40006 29 | 1261 3422 30 | 2593 6686 31 | 1170 3240 32 | 794 2288 33 | 671 2042 34 | 7421 18142 35 | 6009 14718 36 | 1767 4634 37 | 2622 6744 38 | 831 2362 39 | 701 2102 40 | 5222 12944 41 | 3086 7872 42 | 900 2500 43 | 3121 7942 44 | 1029 2958 45 | 52555 126010 46 | 389 1278 47 | -------------------------------------------------------------------------------- /test/examples/send_more_money_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.SendMoreMoney do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Examples.SendMoreMoney 5 | 6 | test "order" do 7 | letters = [S, E, N, D, M, O, R, Y] 8 | expected_solution = [9, 5, 6, 7, 1, 0, 8, 2] 9 | 10 | {:ok, result} = CPSolver.solve(SendMoreMoney.model()) 11 | 12 | ## Single solution 13 | assert length(result.solutions) == 1 14 | ## As expected 15 | assert Enum.zip(letters, expected_solution) == 16 | Enum.zip(result.variables, hd(result.solutions)) 17 | 18 | ## Solution checker 19 | assert SendMoreMoney.check_solution(hd(result.solutions)) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /data/knapsack/ks_40_0: -------------------------------------------------------------------------------- 1 | 40 100000 2 | 90001 90000 3 | 89751 89750 4 | 10002 10001 5 | 89501 89500 6 | 10254 10252 7 | 89251 89250 8 | 10506 10503 9 | 89001 89000 10 | 10758 10754 11 | 88751 88750 12 | 11010 11005 13 | 88501 88500 14 | 11262 11256 15 | 88251 88250 16 | 11514 11507 17 | 88001 88000 18 | 11766 11758 19 | 87751 87750 20 | 12018 12009 21 | 87501 87500 22 | 12270 12260 23 | 87251 87250 24 | 12522 12511 25 | 87001 87000 26 | 12774 12762 27 | 86751 86750 28 | 13026 13013 29 | 86501 86500 30 | 13278 13264 31 | 86251 86250 32 | 13530 13515 33 | 86001 86000 34 | 13782 13766 35 | 85751 85750 36 | 14034 14017 37 | 85501 85500 38 | 14286 14268 39 | 85251 85250 40 | 14538 14519 41 | 86131 86130 42 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/value/indomain_split.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.ValueSelector.Split do 2 | use CPSolver.Search.ValueSelector 3 | 4 | @moduledoc """ 5 | Bisect the domain. 6 | """ 7 | import CPSolver.Utils 8 | 9 | @impl true 10 | def select_value(variable) do 11 | variable 12 | |> domain_values() 13 | |> then(fn values -> 14 | Enum.at(values, div(MapSet.size(values) - 1, 2)) 15 | end) 16 | end 17 | 18 | @impl true 19 | def partition(value) do 20 | [ 21 | fn domain -> 22 | Domain.removeAbove(domain, value) end, 23 | fn domain -> 24 | Domain.removeBelow(domain, value + 1) end, 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/constraints/circuit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Circuit do 2 | use ExUnit.Case, async: false 3 | 4 | describe "Circuit" do 5 | alias CPSolver.Constraint.Circuit 6 | alias CPSolver.IntVariable 7 | alias CPSolver.Constraint 8 | alias CPSolver.Model 9 | 10 | test "produces all possible circuits" do 11 | n = 5 12 | domain = 0..(n - 1) 13 | variables = Enum.map(1..n, fn _ -> IntVariable.new(domain) end) 14 | 15 | model = Model.new(variables, [Constraint.new(Circuit, variables)]) 16 | 17 | {:ok, solver} = CPSolver.solve(model) 18 | assert length(solver.solutions) == Math.factorial(n - 1) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/solver/variables/int_variable.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.IntVariable do 2 | use CPSolver.Variable 3 | 4 | alias CPSolver.Variable 5 | 6 | defdelegate domain(var), to: Variable 7 | defdelegate size(var), to: Variable 8 | defdelegate min(var), to: Variable 9 | defdelegate max(var), to: Variable 10 | defdelegate fixed?(var), to: Variable 11 | defdelegate contains?(var, val), to: Variable 12 | defdelegate remove(var, val), to: Variable 13 | defdelegate removeAbove(var, val), to: Variable 14 | defdelegate removeBelow(var, val), to: Variable 15 | defdelegate fix(var, val), to: Variable 16 | 17 | def to_variable(arg) do 18 | (is_integer(arg) && new(arg)) || arg 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /scripts/bitvector_debug.exs: -------------------------------------------------------------------------------- 1 | defmodule DebugBitVector do 2 | alias CPSolver.BitVectorDomain, as: Domain 3 | 4 | def build_domain(data) do 5 | ref = :atomics.new(length(data.raw.content), [{:signed, false}]) 6 | 7 | Enum.each(Enum.with_index(data.raw.content, 1), fn {val, idx} -> 8 | :atomics.put(ref, idx, val) 9 | end) 10 | 11 | bit_vector = {:bit_vector, data.raw.offset, ref} 12 | domain = {bit_vector, data.raw.offset} 13 | end 14 | end 15 | 16 | data = %{ 17 | max: 76, 18 | min: 14, 19 | raw: %{offset: -11, content: [4_503_599_644_147_720, 2, 279_172_874_243]}, 20 | size: 4, 21 | remove: 76, 22 | values: [76, 63, 35, 14], 23 | failed?: false, 24 | fixed?: false 25 | } 26 | -------------------------------------------------------------------------------- /scripts/concurrent_removal_domain.exs: -------------------------------------------------------------------------------- 1 | defmodule ConcurrentRemoval do 2 | alias CPSolver.BitVectorDomain, as: Domain 3 | 4 | def run() do 5 | n_values = 2 6 | values = 1..n_values 7 | domain = Domain.new(values) 8 | 9 | Task.async_stream( 10 | 1..2, 11 | fn _thread_id -> 12 | try do 13 | ## Keep one random value, remove the rest 14 | Enum.each(Enum.take_random(values, n_values - 1), fn val -> 15 | Domain.remove(domain, val) 16 | end) 17 | catch 18 | _ -> 19 | :failed 20 | end 21 | end, 22 | max_concurrency: 2 23 | ) 24 | |> Enum.to_list() 25 | 26 | !Domain.failed?(domain) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/variable/local/max_regret.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.VariableSelector.MaxRegret do 2 | use CPSolver.Search.VariableSelector 3 | alias CPSolver.Utils 4 | 5 | ## Choose the variable(s) with largest difference 6 | ## between the two smallest values in its domain. 7 | @impl true 8 | def select(variables, space_data, _opts) do 9 | largest_difference(variables, space_data) 10 | end 11 | 12 | defp largest_difference(variables, _space_data) do 13 | Utils.maximals(variables, &difference/1) 14 | end 15 | 16 | defp difference(variable) do 17 | values = Utils.domain_values(variable) 18 | [smallest, second_smallest] = Enum.take(values, 2) 19 | second_smallest - smallest 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /scripts/debug_circuit.exs: -------------------------------------------------------------------------------- 1 | defmodule DebugCircuit do 2 | alias CPSolver.ConstraintStore 3 | alias CPSolver.IntVariable, as: Variable 4 | alias CPSolver.Variable.Interface 5 | alias CPSolver.DefaultDomain, as: Domain 6 | alias CPSolver.Propagator 7 | alias CPSolver.Propagator.Circuit 8 | 9 | def run(domains) do 10 | end 11 | 12 | def filter(domains) do 13 | domains 14 | |> make_propagator() 15 | |> Propagator.filter() 16 | end 17 | 18 | def make_propagator(domains) do 19 | variables = 20 | Enum.map(Enum.with_index(domains), fn {d, idx} -> Variable.new(d, name: "x#{idx}") end) 21 | 22 | {:ok, x_vars, _store} = ConstraintStore.create_store(variables) 23 | 24 | Circuit.new(x_vars) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/solver/constraints/maximum.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Maximum do 2 | @moduledoc """ 3 | 'Maximum' constraint: 4 | y = max[x_array] 5 | 6 | x_array is a list of variables 7 | """ 8 | 9 | use CPSolver.Constraint 10 | alias CPSolver.Propagator.Maximum, as: MaximumPropagator 11 | alias CPSolver.IntVariable, as: Variable 12 | 13 | @spec new(Variable.variable_or_view(), [Variable.variable_or_view()]) :: Constraint.t() 14 | 15 | def new(c, x_array) when is_integer(c) do 16 | new(Variable.new(c), x_array) 17 | end 18 | 19 | def new(y, x_array) do 20 | new([y | x_array]) 21 | end 22 | 23 | @impl true 24 | def propagators([_max_var | _var_list] = vars) do 25 | [MaximumPropagator.new(vars)] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/solver/constraints/minimum.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Minimum do 2 | @moduledoc """ 3 | 'Minimum' constraint: 4 | y = min[x_array] 5 | 6 | x_array is a list of variables 7 | """ 8 | 9 | use CPSolver.Constraint 10 | alias CPSolver.Propagator.Minimum, as: MinimumPropagator 11 | alias CPSolver.IntVariable, as: Variable 12 | 13 | @spec new(Variable.variable_or_view(), [Variable.variable_or_view()]) :: Constraint.t() 14 | 15 | def new(c, x_array) when is_integer(c) do 16 | new(Variable.new(c), x_array) 17 | end 18 | 19 | def new(y, x_array) do 20 | new([y | x_array]) 21 | end 22 | 23 | @impl true 24 | def propagators([_max_var | _var_list] = vars) do 25 | [MinimumPropagator.new(vars)] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /data/knapsack/ks_50_0: -------------------------------------------------------------------------------- 1 | 50 341045 2 | 1906 4912 3 | 41516 99732 4 | 23527 56554 5 | 559 1818 6 | 45136 108372 7 | 2625 6750 8 | 492 1484 9 | 1086 3072 10 | 5516 13532 11 | 4875 12050 12 | 7570 18440 13 | 4436 10972 14 | 620 1940 15 | 50897 122094 16 | 2129 5558 17 | 4265 10630 18 | 706 2112 19 | 2721 6942 20 | 16494 39888 21 | 29688 71276 22 | 3383 8466 23 | 2181 5662 24 | 96601 231302 25 | 1795 4690 26 | 7512 18324 27 | 1242 3384 28 | 2889 7278 29 | 2133 5566 30 | 103 706 31 | 4446 10992 32 | 11326 27552 33 | 3024 7548 34 | 217 934 35 | 13269 32038 36 | 281 1062 37 | 77174 184848 38 | 952 2604 39 | 15572 37644 40 | 566 1832 41 | 4103 10306 42 | 313 1126 43 | 14393 34886 44 | 1313 3526 45 | 348 1196 46 | 419 1338 47 | 246 992 48 | 445 1390 49 | 23552 56804 50 | 23552 56804 51 | 67 634 52 | -------------------------------------------------------------------------------- /lib/solver/constraints/less_or_equal.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.LessOrEqual do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.LessOrEqual, as: LessOrEqualPropagator 4 | alias CPSolver.IntVariable, as: Variable 5 | 6 | def new(x, y, offset \\ 0) 7 | 8 | def new(x, y, offset) when is_integer(y) do 9 | new(x, Variable.new(y), offset) 10 | end 11 | 12 | def new(x, y, offset) do 13 | new([x, y, offset]) 14 | end 15 | 16 | @impl true 17 | def propagators(args) do 18 | [LessOrEqualPropagator.new(args)] 19 | end 20 | 21 | @impl true 22 | def arguments([x, y]) do 23 | arguments([x, y, 0]) 24 | end 25 | 26 | def arguments([x, y, offset]) do 27 | [Variable.to_variable(x), Variable.to_variable(y), offset] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/solver/constraints/propagators/less.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Propagator.Less do 2 | use CPSolver.Propagator 3 | 4 | alias CPSolver.Propagator.LessOrEqual, as: LessOrEqual 5 | 6 | # def new(x, y, offset \\ 0) do 7 | # LessOrEqual.new(x, y, offset - 1) 8 | # end 9 | 10 | defdelegate variables(args), to: LessOrEqual 11 | 12 | def filter(args, state, changes) do 13 | LessOrEqual.filter(le_args(args), state, changes) 14 | end 15 | 16 | def entailed?(args, state) do 17 | LessOrEqual.entailed?(le_args(args), state) 18 | end 19 | 20 | def failed?(args, state) do 21 | LessOrEqual.failed?(le_args(args), state) 22 | end 23 | 24 | defp le_args([x, y]) do 25 | le_args([x, y, 0]) 26 | end 27 | 28 | defp le_args([x, y, offset]) do 29 | [x, y, offset - 1] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/variable/local/dom_deg.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.VariableSelector.DomDeg do 2 | use CPSolver.Search.VariableSelector 3 | alias CPSolver.Variable.Interface 4 | alias CPSolver.Propagator.ConstraintGraph 5 | alias CPSolver.Utils 6 | 7 | @impl true 8 | def select(variables, space_data, _opts) do 9 | smallest_ratio(variables, space_data) 10 | end 11 | 12 | ## Smallest ratio of dom(x)/deg(x) 13 | defp smallest_ratio(variables, space_data) do 14 | graph = space_data[:constraint_graph] 15 | 16 | min_by_fun = fn var -> 17 | case ConstraintGraph.variable_degree(graph, Interface.id(var)) do 18 | deg when deg > 0 -> Interface.size(var) / deg 19 | _ -> nil 20 | end 21 | end 22 | 23 | Utils.minimals(variables, min_by_fun) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/value/value_selector.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.ValueSelector do 2 | @callback select_value(Variable.t()) :: integer() 3 | @callback partition(integer()) :: [function()] 4 | @callback initialize(map()) :: :ok 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | alias CPSolver.Search.ValueSelector 9 | alias CPSolver.Variable.Interface 10 | alias CPSolver.DefaultDomain, as: Domain 11 | 12 | @behaviour ValueSelector 13 | def initialize(data) do 14 | :ok 15 | end 16 | 17 | def partition(value) do 18 | [ 19 | fn domain -> Domain.fix(domain, value) end, 20 | fn domain -> Domain.remove(domain, value) end, 21 | ] 22 | end 23 | 24 | defoverridable initialize: 1, partition: 1 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /data/qap/qap12a_opt288.txt: -------------------------------------------------------------------------------- 1 | 12 2 | 3 | 0 5 2 4 1 0 0 6 2 1 1 1 4 | 5 0 3 0 2 2 2 0 4 5 0 0 5 | 2 3 0 0 0 0 0 5 5 2 2 2 6 | 4 0 0 0 5 2 2 10 0 0 5 5 7 | 1 2 0 5 0 10 0 0 0 5 1 1 8 | 0 2 0 2 10 0 5 1 1 5 4 0 9 | 0 2 0 2 0 5 0 10 5 2 3 3 10 | 6 0 5 10 0 1 10 0 0 0 5 0 11 | 2 4 5 0 0 1 5 0 0 0 10 10 12 | 1 5 2 0 5 5 2 0 0 0 5 0 13 | 1 0 2 5 1 4 3 5 10 5 0 2 14 | 1 0 2 5 1 0 3 0 10 0 2 0 15 | 16 | 0 1 2 3 1 2 3 4 2 3 4 5 17 | 1 0 1 2 2 1 2 3 3 2 3 4 18 | 2 1 0 1 3 2 1 2 4 3 2 3 19 | 3 2 1 0 4 3 2 1 5 4 3 2 20 | 1 2 3 4 0 1 2 3 1 2 3 4 21 | 2 1 2 3 1 0 1 2 2 1 2 3 22 | 3 2 1 2 2 1 0 1 3 2 1 2 23 | 4 3 2 1 3 2 1 0 4 3 2 1 24 | 2 3 4 5 1 2 3 4 0 1 2 3 25 | 3 2 3 4 2 1 2 3 1 0 1 2 26 | 4 3 2 3 3 2 1 2 2 1 0 1 27 | 5 4 3 2 4 3 2 1 3 2 1 0 -------------------------------------------------------------------------------- /scripts/space_debugging.ex: -------------------------------------------------------------------------------- 1 | alias CPSolver.IntVariable, as: Variable 2 | alias CPSolver.Space, as: Space 3 | alias CPSolver.Propagator.NotEqual 4 | alias CPSolver.Solution 5 | alias CPSolver.Shared 6 | 7 | x_values = 1..2 8 | y_values = 1..2 9 | z_values = 1..2 10 | values = [x_values, y_values, z_values] 11 | [x, y, z] = variables = Enum.map(values, fn d -> Variable.new(d) end) 12 | propagators = [NotEqual.new(x, y), NotEqual.new(y, z)] 13 | 14 | space_opts = [ 15 | store: CPSolver.ConstraintStore.default_store(), 16 | solution_handler: Solution.default_handler(), 17 | search: CPSolver.Search.Strategy.default_strategy() 18 | ] 19 | 20 | {:ok, space} = 21 | Space.create( 22 | variables, 23 | propagators, 24 | space_opts 25 | |> Keyword.put(:solver_data, Shared.init_shared_data(self())) 26 | |> Keyword.put(:keep_alive, true) 27 | ) 28 | -------------------------------------------------------------------------------- /scripts/gc_script.exs: -------------------------------------------------------------------------------- 1 | example = "triangle_uni" 2 | 3 | defmodule GraphScript do 4 | def solve_graph(example, solver_opts \\ []) do 5 | instance = "data/graph_coloring/#{example}" 6 | {:ok, solver} = CPSolver.Examples.GraphColoring.solve_async(instance, solver_opts) 7 | end 8 | 9 | def propagating_spaces(solver) do 10 | solver_state = :sys.get_state(solver) 11 | 12 | solver_state.active_nodes 13 | |> Enum.filter(fn space -> 14 | Process.alive?(space) && 15 | ( 16 | {state, data} = :sys.get_state(space) 17 | state == :propagating 18 | ) 19 | end) 20 | end 21 | 22 | def threads(spaces) do 23 | Enum.map(spaces, fn pid -> 24 | {_, data} = :sys.get_state(pid) 25 | data.propagator_threads 26 | end) 27 | end 28 | end 29 | 30 | GraphScript.solve_graph(example, solver_opts) 31 | -------------------------------------------------------------------------------- /test/examples/knapsack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.Knapsack do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Examples.Knapsack 5 | 6 | test "small knapsack" do 7 | values = [8, 10, 15, 4] 8 | weights = [4, 5, 8, 3] 9 | capacity = 11 10 | {:ok, results} = CPSolver.solve(Knapsack.model(values, weights, capacity)) 11 | objective_value = results.objective 12 | 13 | assert Enum.all?(results.solutions, fn solution -> 14 | Knapsack.check_solution(solution, objective_value, values, weights, capacity) 15 | end) 16 | 17 | optimal_solution = List.last(results.solutions) 18 | 19 | objective_variable_index = 20 | Enum.find_index(results.variables, fn name -> name == "total_value" end) 21 | 22 | assert objective_value == Enum.at(optimal_solution, objective_variable_index) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/solver/constraints/element.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Element do 2 | @moduledoc """ 3 | Element constrains variables y and z such that: 4 | array[x] = y 5 | 6 | array is 1d list of integer constants 7 | """ 8 | use CPSolver.Constraint 9 | alias CPSolver.Constraint.Element2D, as: Element2D 10 | alias CPSolver.IntVariable 11 | 12 | @spec new( 13 | [integer()], 14 | Variable.variable_or_view() | integer(), 15 | Variable.variable_or_view() | integer() 16 | ) :: Constraint.t() 17 | def new(array, x, y) do 18 | Element2D.new([[array], IntVariable.new(0), x, y]) 19 | end 20 | 21 | @impl true 22 | def propagators(args) do 23 | Element2D.propagators(args) 24 | end 25 | 26 | @impl true 27 | def arguments([array, x, y]) when is_list(array) do 28 | [array, IntVariable.to_variable(x), IntVariable.to_variable(y)] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/solver/constraints/inverse.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Inverse do 2 | @moduledoc """ 3 | Constraints two arrays of variables `f` and `inv_f` 4 | to represent an inverse function. 5 | That is, for all i = 1..n, where n is a size of x: 6 | inv_f[f[i]] == i 7 | and: 8 | f[inv_f[i]] == i 9 | 10 | MiniZinc definition (fzn_inverse.mzn): 11 | 12 | forall(i in index_set(f)) ( 13 | f[i] in index_set(invf) /\ 14 | (invf[f[i]] == i) 15 | ) /\ 16 | forall(j in index_set(invf)) ( 17 | invf[j] in index_set(f) /\ 18 | (f[invf[j]] == j) 19 | ); 20 | 21 | Note: the current implementation assumes the index set for both `f` and `inv_f` is always 0-based 22 | """ 23 | alias CPSolver.Constraint.Factory 24 | 25 | def new(f, inv_f) do 26 | new([f, inv_f]) 27 | end 28 | 29 | def new([f, inv_f]) do 30 | Factory.inverse(f, inv_f) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 1. ~~Stateful propagators (needed for 'sum')~~ done 2 | 2. ~~Handle propagators that do not have a chance to be fired (like 'less_or_equal' in case bounds of vars do not intersect)~~ done 3 | 3. ~~Level of parallelism for space processes as a solver option~~ done 4 | 4. ~~Support solver final status (:satisfied, :all_solutions, :optimal, :unsatisfiable)~~ done, final and runtime statuses supported 5 | 5. ~~More search strategies~~ done 6 | 6. ~~Distributed solving~~ done 7 | 7. ~~Document Solver API~~ done 8 | 8. Add stats for propagators 9 | 9. (Maybe) add timestamps and/or elapsed time for solutions 10 | 10. (Maybe) add objective values to solutions or give an API to derive them from solution + shared objective 11 | 11. 'equal' 12 | 12. ~~AllDifferent with forward checking~~ 13 | 13. AllDifferent with graphs 14 | 14. ~~'element'~~, ~~'circuit'~~ 15 | 15. Reification 16 | 16. AFC (accumulated failure count) strategy 17 | -------------------------------------------------------------------------------- /data/sudoku/hardest: -------------------------------------------------------------------------------- 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.... -------------------------------------------------------------------------------- /lib/solver/constraints/element_var.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.ElementVar do 2 | @moduledoc """ 3 | ElementVar constrains list of variables `array`, variables `x` and `y` such that: 4 | array[x] = y 5 | 6 | array is a list of variables 7 | """ 8 | use CPSolver.Constraint 9 | alias CPSolver.Propagator.ElementVar, as: ElementVarPropagator 10 | alias CPSolver.IntVariable, as: Variable 11 | 12 | @spec new( 13 | [Variable.variable_or_view()], 14 | Variable.variable_or_view(), 15 | Variable.variable_or_view() 16 | ) :: Constraint.t() 17 | def new(array, x, y) do 18 | new([array, x, y]) 19 | end 20 | 21 | @impl true 22 | def propagators(args) do 23 | [ElementVarPropagator.new(args)] 24 | end 25 | 26 | @impl true 27 | def arguments([array, x, y]) when is_list(array) do 28 | [Enum.map(array, &Variable.to_variable/1), Variable.to_variable(x), Variable.to_variable(y)] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /scripts/debug_reified.exs: -------------------------------------------------------------------------------- 1 | defmodule DebugReified do 2 | alias CPSolver.Constraint.{Reified, HalfReified, InverseHalfReified} 3 | alias CPSolver.Constraint.{Equal, NotEqual, LessOrEqual} 4 | alias CPSolver.IntVariable 5 | alias CPSolver.BooleanVariable 6 | alias CPSolver.Variable.Interface 7 | alias CPSolver.Model 8 | 9 | def run(d_x, d_y, d_b \\ [0, 1], constraint_mod \\ LessOrEqual, mode \\ :full) do 10 | x = IntVariable.new(d_x, name: "x") 11 | y = IntVariable.new(d_y, name: "y") 12 | b = IntVariable.new(d_b, name: "b") 13 | # BooleanVariable.new(name: "b") 14 | 15 | le_constraint = constraint_mod.new(x, y) 16 | model = Model.new([x, y, b], [impl(mode).new(le_constraint, b)]) 17 | 18 | {:ok, _res} = CPSolver.solve(model, space_threads: 1) 19 | end 20 | 21 | defp impl(:full) do 22 | Reified 23 | end 24 | 25 | defp impl(:half) do 26 | HalfReified 27 | end 28 | 29 | defp impl(:inverse_half) do 30 | InverseHalfReified 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /data/knapsack/ks_60_0: -------------------------------------------------------------------------------- 1 | 60 100000 2 | 90000 90001 3 | 89750 89751 4 | 10001 10002 5 | 89500 89501 6 | 10252 10254 7 | 89250 89251 8 | 10503 10506 9 | 89000 89001 10 | 10754 10758 11 | 88750 88751 12 | 11005 11010 13 | 88500 88501 14 | 11256 11262 15 | 88250 88251 16 | 11507 11514 17 | 88000 88001 18 | 11758 11766 19 | 87750 87751 20 | 12009 12018 21 | 87500 87501 22 | 12260 12270 23 | 87250 87251 24 | 12511 12522 25 | 87000 87001 26 | 12762 12774 27 | 86750 86751 28 | 13013 13026 29 | 86500 86501 30 | 13264 13278 31 | 86250 86251 32 | 13515 13530 33 | 86000 86001 34 | 13766 13782 35 | 85750 85751 36 | 14017 14034 37 | 85500 85501 38 | 14268 14286 39 | 85250 85251 40 | 14519 14538 41 | 85000 85001 42 | 14770 14790 43 | 84750 84751 44 | 15021 15042 45 | 84500 84501 46 | 15272 15294 47 | 84250 84251 48 | 15523 15546 49 | 84000 84001 50 | 15774 15798 51 | 83750 83751 52 | 16025 16050 53 | 83500 83501 54 | 16276 16302 55 | 83250 83251 56 | 16527 16554 57 | 83000 83001 58 | 16778 16806 59 | 82750 82751 60 | 17029 17058 61 | 82500 82501 62 | -------------------------------------------------------------------------------- /test/examples/quadratic_assignment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.QAP do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Examples.QAP 5 | 6 | test "small instance (n = 4)" do 7 | distances = [ 8 | [0, 22, 53, 53], 9 | [22, 0, 40, 62], 10 | [53, 40, 0, 55], 11 | [53, 62, 55, 0] 12 | ] 13 | 14 | weights = [ 15 | [0, 3, 0, 2], 16 | [3, 0, 0, 1], 17 | [0, 0, 0, 4], 18 | [2, 1, 4, 0] 19 | ] 20 | 21 | qap_model = QAP.model(distances, weights) 22 | {:ok, results} = CPSolver.solve(qap_model) 23 | 24 | assert Enum.all?(results.solutions, fn solution -> 25 | QAP.check_solution(solution, distances, weights) 26 | end) 27 | 28 | optimal_solution = List.last(results.solutions) 29 | 30 | objective_variable_index = 31 | Enum.find_index(results.variables, fn name -> name == qap_model.extra.total_cost_var_id end) 32 | 33 | assert results.objective == Enum.at(optimal_solution, objective_variable_index) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/solver/shared_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Shared do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Shared 5 | 6 | test "space thread checkins/checkouts" do 7 | max_threads = 3 8 | shared = Shared.init_shared_data(space_threads: max_threads) 9 | ## No threads were checked out 10 | refute Shared.checkin_space_thread(shared) 11 | assert Enum.all?(1..max_threads, fn _ -> Shared.checkout_space_thread(shared) end) 12 | ## No more threads available 13 | refute Shared.checkout_space_thread(shared) 14 | ## Put them all back 15 | assert Enum.all?(1..max_threads, fn _ -> Shared.checkin_space_thread(shared) end) 16 | ## No more space for checkins 17 | refute Shared.checkin_space_thread(shared) 18 | end 19 | 20 | test "manage auxillaries" do 21 | shared = Shared.init_shared_data(space_threads: 4) 22 | refute Shared.get_auxillary(shared, :hello) 23 | assert Shared.put_auxillary(shared, :hello, "hello") 24 | assert "hello" == Shared.get_auxillary(shared, :hello) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/solver/constraints/element2d.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Element2D do 2 | @moduledoc """ 3 | Element2d constrains variables z, x and y such that: 4 | array2d[x][y] = z 5 | 6 | array2d is a regular (all rows are of the same length) list of lists of integers. 7 | """ 8 | use CPSolver.Constraint 9 | alias CPSolver.Propagator.Element2D, as: Element2DPropagator 10 | alias CPSolver.IntVariable, as: Variable 11 | 12 | @spec new( 13 | [[integer()]], 14 | Variable.variable_or_view(), 15 | Variable.variable_or_view(), 16 | Variable.variable_or_view() 17 | ) :: Constraint.t() 18 | def new(arr2d, x, y, z) do 19 | new([arr2d, x, y, z]) 20 | end 21 | 22 | @impl true 23 | def propagators(args) do 24 | [Element2DPropagator.new(args)] 25 | end 26 | 27 | @impl true 28 | def arguments([array2d, x, y, z]) when is_list(array2d) and is_list(hd(array2d)) do 29 | [array2d, Variable.to_variable(x), Variable.to_variable(y), Variable.to_variable(z)] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/solver/constraints/sum.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Sum do 2 | use CPSolver.Constraint 3 | alias CPSolver.Propagator.Sum, as: SumPropagator 4 | alias CPSolver.Variable.View.Factory 5 | alias CPSolver.IntVariable, as: Variable 6 | 7 | @spec new(Variable.variable_or_view(), [Variable.variable_or_view()]) :: Constraint.t() 8 | 9 | def new(c, x) when is_integer(c) do 10 | new(Variable.new(c), x) 11 | end 12 | 13 | def new(y, x) do 14 | new([y | x]) 15 | end 16 | 17 | @impl true 18 | def propagators([y | x]) do 19 | # Separate constants and variables 20 | {constant, vars} = 21 | List.foldr(x, {0, []}, fn arg, {constant_acc, vars_acc} -> 22 | (is_integer(arg) && {constant_acc + arg, vars_acc}) || 23 | {constant_acc, [arg | vars_acc]} 24 | end) 25 | 26 | ## Adjust sum (variable or constant) 27 | y_arg = 28 | (is_integer(y) && Variable.new(y - constant)) || 29 | Factory.inc(y, -constant) 30 | 31 | [SumPropagator.new(y_arg, vars)] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Boris Okner 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 | -------------------------------------------------------------------------------- /data/tsp/tsp_17.txt: -------------------------------------------------------------------------------- 1 | 17 2 | 0 633 257 91 412 150 80 134 259 505 353 324 70 211 268 246 121 3 | 633 0 390 661 227 488 572 530 555 289 282 638 567 466 420 745 518 4 | 257 390 0 228 169 112 196 154 372 262 110 437 191 74 53 472 142 5 | 91 661 228 0 383 120 77 105 175 476 324 240 27 182 239 237 84 6 | 412 227 169 383 0 267 351 309 338 196 61 421 346 243 199 528 297 7 | 150 488 112 120 267 0 63 34 264 360 208 329 83 105 123 364 35 8 | 80 572 196 77 351 63 0 29 232 444 292 297 47 150 207 332 29 9 | 134 530 154 105 309 34 29 0 249 402 250 314 68 108 165 349 36 10 | 259 555 372 175 338 264 232 249 0 495 352 95 189 326 383 202 236 11 | 505 289 262 476 196 360 444 402 495 0 154 578 439 336 240 685 390 12 | 353 282 110 324 61 208 292 250 352 154 0 435 287 184 140 542 238 13 | 324 638 437 240 421 329 297 314 95 578 435 0 254 391 448 157 301 14 | 70 567 191 27 346 83 47 68 189 439 287 254 0 145 202 289 55 15 | 211 466 74 182 243 105 150 108 326 336 184 391 145 0 57 426 96 16 | 268 420 53 239 199 123 207 165 383 240 140 448 202 57 0 483 153 17 | 246 745 472 237 528 364 332 349 202 685 542 157 289 426 483 0 336 18 | 121 518 142 84 297 35 29 36 236 390 238 301 55 96 153 336 0 -------------------------------------------------------------------------------- /data/qap/qap16_opt34.txt: -------------------------------------------------------------------------------- 1 | 16 2 | 3 | 0 2 1 1 2 1 1 2 2 3 0 0 0 0 0 0 4 | 2 0 1 1 1 0 0 1 1 2 0 0 0 0 0 0 5 | 1 1 0 1 0 0 0 1 1 1 0 0 0 0 0 0 6 | 1 1 1 0 0 0 1 1 1 1 0 0 0 0 0 0 7 | 2 1 0 0 0 1 1 1 1 2 0 0 0 0 0 0 8 | 1 0 0 0 1 0 1 1 1 1 0 0 0 0 0 0 9 | 1 0 0 1 1 1 0 1 1 1 0 0 0 0 0 0 10 | 2 1 1 1 1 1 1 0 2 2 0 0 0 0 0 0 11 | 2 1 1 1 1 1 1 2 0 2 0 0 0 0 0 0 12 | 3 2 1 1 2 1 1 2 2 0 0 0 0 0 0 0 13 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 14 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 15 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 17 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 19 | 0 0 0 1 0 1 1 2 0 1 1 2 1 2 2 3 20 | 0 0 1 0 1 0 2 1 1 0 2 1 2 1 3 2 21 | 0 1 0 0 1 2 0 1 1 2 0 1 2 3 1 2 22 | 1 0 0 0 2 1 1 0 2 1 1 0 3 2 2 1 23 | 0 1 1 2 0 0 0 1 1 2 2 3 0 1 1 2 24 | 1 0 2 1 0 0 1 0 2 1 3 2 1 0 2 1 25 | 1 2 0 1 0 1 0 0 2 3 1 2 1 2 0 1 26 | 2 1 1 0 1 0 0 0 3 2 2 1 2 1 1 0 27 | 0 1 1 2 1 2 2 3 0 0 0 1 0 1 1 2 28 | 1 0 2 1 2 1 3 2 0 0 1 0 1 0 2 1 29 | 1 2 0 1 2 3 1 2 0 1 0 0 1 2 0 1 30 | 2 1 1 0 3 2 2 1 1 0 0 0 2 1 1 0 31 | 1 2 2 3 0 1 1 2 0 1 1 2 0 0 0 1 32 | 2 1 3 2 1 0 2 1 1 0 2 1 0 0 1 0 33 | 2 3 1 2 1 2 0 1 1 2 0 1 0 1 0 0 34 | 3 2 2 1 2 1 1 0 2 1 1 0 1 0 0 0 -------------------------------------------------------------------------------- /test/solver/distributed_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Distributed do 2 | use ExUnit.Case 3 | alias CPSolver.Examples.Sudoku 4 | 5 | @moduletag :slow 6 | 7 | setup do 8 | # {:ok, spawned} = ExUnited.spawn([:test_worker1, :test_worker2, :test_worker3]) 9 | :ok = LocalCluster.start() 10 | 11 | _spawned = LocalCluster.start_nodes(:spawn, 2) 12 | 13 | on_exit(fn -> 14 | LocalCluster.stop() 15 | end) 16 | 17 | :ok 18 | end 19 | 20 | test "distributed solving uses cluster nodes assigned to it" do 21 | # Run the solver with the model that takes noticeable time to complete. 22 | difficult_sudoku = Sudoku.puzzles().s9x9_clue17_hard 23 | {:ok, solver} = CPSolver.solve_async(Sudoku.model(difficult_sudoku), distributed: Node.list()) 24 | # Wait for a bit so all nodes get involved into solving. 25 | Process.sleep(1000) 26 | # Collect active spaces and group them by the nodes. 27 | spaces = CPSolver.Shared.active_nodes(solver) 28 | spaces_by_node = Enum.group_by(spaces, fn s -> node(s) end) 29 | ## All nodes participated in the solving 30 | assert Map.keys(spaces_by_node) |> Enum.sort() == Node.list() |> Enum.sort() 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /scripts/gc_debug_script.exs: -------------------------------------------------------------------------------- 1 | require Logger 2 | Logger.configure(level: :error) 3 | 4 | instance = "p3" 5 | expected_sols = 2 6 | number_of_runs = 50 7 | trace_pattern = ["CPSolver.Space"] 8 | solver_timeout = 1000 9 | 10 | defmodule DebugGC do 11 | def debug(instance, expected_sols, number_of_runs, trace_pattern, solver_timeout \\ 500) do 12 | # trace_pattern = ["CPSolver.Space.create/_", "CPSolver.distribute/_", "CPsolver.Space.shutdown/_"] 13 | result = 14 | Enum.reduce_while(1..number_of_runs, 0, fn i, succ -> 15 | # Replbug.start(trace_pattern, time: :timer.seconds(10), msgs: 100_000, max_queue: 10000, silent: true) 16 | Process.sleep(100) 17 | {:ok, solver} = CPSolver.Examples.GraphColoring.solve_async("data/graph_coloring/#{instance}") 18 | Process.sleep(solver_timeout) 19 | traces = Replbug.stop() 20 | Process.sleep(100) 21 | 22 | (CPSolver.statistics(solver).solution_count == expected_sols && 23 | {:cont, succ + 1}) || {:halt, %{solver: solver, traces: traces, successes: succ}} 24 | end) 25 | end 26 | end 27 | 28 | if result.successes == number_of_runs do 29 | Logger.info("All good") 30 | # else 31 | # space_pids = Map.keresult.traces 32 | end 33 | -------------------------------------------------------------------------------- /lib/utils/tuple_array.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Utils.TupleArray do 2 | def new(values) when is_list(values) do 3 | Enum.reduce(values, {}, fn 4 | val, acc when is_list(val) -> 5 | Tuple.insert_at(acc, tuple_size(acc), new(val)) 6 | 7 | val, acc -> 8 | Tuple.insert_at(acc, tuple_size(acc), val) 9 | end) 10 | end 11 | 12 | def at(tuple_array, idx) when is_integer(idx) do 13 | (idx >= 0 && tuple_size(tuple_array) - 1 >= idx && 14 | elem(tuple_array, idx)) || 15 | nil 16 | end 17 | 18 | def at(tuple_array, []) do 19 | tuple_array 20 | end 21 | 22 | def at(tuple_array, [idx | rest]) do 23 | at(tuple_array, idx) 24 | |> then(fn sub_arr -> (sub_arr && at(sub_arr, rest)) || nil end) 25 | end 26 | 27 | def map(tuple_array, mapper) when is_function(mapper) do 28 | Enum.reduce(0..(tuple_size(tuple_array) - 1), {}, fn idx, acc -> 29 | Tuple.insert_at(acc, tuple_size(acc), mapper.(elem(tuple_array, idx))) 30 | end) 31 | end 32 | 33 | def reduce(tuple_array, initial_value, reducer) when is_function(reducer) do 34 | Enum.reduce(0..(tuple_size(tuple_array) - 1), initial_value, fn idx, acc -> 35 | reducer.(elem(tuple_array, idx), acc) 36 | end) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/constraints/inverse_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Inverse do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Model 6 | alias CPSolver.Constraint.Factory, as: ConstraintFactory 7 | 8 | describe "Inverse constraint" do 9 | test "`inverse` functionality" do 10 | indexed_domains = List.duplicate(0..5, 6) |> Enum.with_index() 11 | x_vars = Enum.map(indexed_domains, fn {d, idx} -> Variable.new(d, name: "x#{idx}") end) 12 | y_vars = Enum.map(indexed_domains, fn {d, idx} -> Variable.new(d, name: "y#{idx}") end) 13 | 14 | model = Model.new(x_vars ++ y_vars, ConstraintFactory.inverse(x_vars, y_vars)) 15 | 16 | {:ok, result} = CPSolver.solve(model) 17 | 18 | assert result.statistics.solution_count == 720 19 | assert_inverse(result.solutions, length(x_vars)) 20 | end 21 | 22 | defp assert_inverse(solutions, array_len) do 23 | assert Enum.all?(solutions, fn solution -> 24 | x_y = Enum.take(solution, array_len * 2) 25 | {x, y} = Enum.split(x_y, array_len) 26 | Enum.all?(0..(array_len - 1), fn idx -> Enum.at(x, Enum.at(y, idx)) == idx end) 27 | end) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/solver/constraints/propagators/less_or_equal.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Propagator.LessOrEqual do 2 | use CPSolver.Propagator 3 | 4 | def new(x, y, offset \\ 0) do 5 | new([x, y, offset]) 6 | end 7 | 8 | @impl true 9 | def variables([x, y | _]) do 10 | [set_propagate_on(x, :min_change), set_propagate_on(y, :max_change)] 11 | end 12 | 13 | def filter([x, y], state, changes) do 14 | filter([x, y, 0], state, changes) 15 | end 16 | 17 | @impl true 18 | def filter([x, y, offset], state, _changes) do 19 | filter_impl(x, y, offset, state || %{active?: true}) 20 | end 21 | 22 | @impl true 23 | def failed?([x, y, offset], _state) do 24 | min(x) > plus(max(y), offset) 25 | end 26 | 27 | @impl true 28 | def entailed?([x, y, offset], _state) do 29 | entailed?(x, y, offset) 30 | end 31 | 32 | defp entailed?(x, y, offset) do 33 | ## x <= y holds on the condition below 34 | max(x) <= plus(min(y), offset) 35 | end 36 | 37 | defp filter_impl(_x, _y, _offset, %{active?: false} = _state) do 38 | :passive 39 | end 40 | 41 | defp filter_impl(x, y, offset, %{active?: true} = _state) do 42 | removeAbove(x, plus(max(y), offset)) 43 | removeBelow(y, plus(min(x), -offset)) 44 | {:state, %{active?: !entailed?(x, y, offset)}} 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/model/model_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CpSolverTest.Model do 2 | use ExUnit.Case 3 | alias CPSolver.IntVariable, as: Variable 4 | alias CPSolver.Model 5 | alias CPSolver.Objective 6 | alias CPSolver.Variable.Interface 7 | 8 | describe "Model" do 9 | alias CPSolver.Constraint.{LessOrEqual, Sum} 10 | 11 | test "create" do 12 | sum_bound = 1000 13 | x_bound = 100 14 | y_bound = 200 15 | x = Variable.new(1..x_bound, name: "x") 16 | y = Variable.new(1..y_bound, name: "y") 17 | z = Variable.new(1..sum_bound) 18 | 19 | variables = [x, y] 20 | constraints = [LessOrEqual.new(x, y), Sum.new(z, [x, y])] 21 | 22 | model = 23 | Model.new( 24 | variables, 25 | constraints, 26 | objective: Objective.minimize(z) 27 | ) 28 | 29 | ## Additional variable z is pulled from the Sum constraints 30 | assert length(model.variables) == 3 31 | ## All variables are indexed starting from 1 32 | assert Enum.all?(Enum.with_index(model.variables, 1), fn {var, idx} -> var.index == idx end) 33 | ## The variable in the objective has the same index as the variable in the all_vars list 34 | assert model.objective.variable |> Interface.variable() |> Map.get(:index) == 3 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /data/qap/qap14_opt507.txt: -------------------------------------------------------------------------------- 1 | 14 2 | 3 | 0 1 2 3 4 1 2 3 4 5 2 3 4 5 4 | 1 0 1 2 3 2 1 2 3 4 3 2 3 4 5 | 2 1 0 1 2 3 2 1 2 3 4 3 2 3 6 | 3 2 1 0 1 4 3 2 1 2 5 4 3 2 7 | 4 3 2 1 0 5 4 3 2 1 6 5 4 3 8 | 1 2 3 4 5 0 1 2 3 4 1 2 3 4 9 | 2 1 2 3 4 1 0 1 2 3 2 1 2 3 10 | 3 2 1 2 3 2 1 0 1 2 3 2 1 2 11 | 4 3 2 1 2 3 2 1 0 1 4 3 2 1 12 | 5 4 3 2 1 4 3 2 1 0 5 4 3 2 13 | 2 3 4 5 6 1 2 3 4 5 0 1 2 3 14 | 3 2 3 4 5 2 1 2 3 4 1 0 1 2 15 | 4 3 2 3 4 3 2 1 2 3 2 1 0 1 16 | 5 4 3 2 3 4 3 2 1 2 3 2 1 0 17 | 18 | 0 10 0 5 1 0 1 2 2 2 2 0 4 0 19 | 10 0 1 3 2 2 2 3 2 0 2 0 10 5 20 | 0 1 0 10 2 0 2 5 4 5 2 2 5 5 21 | 5 3 10 0 1 1 5 0 0 2 1 0 2 5 22 | 1 2 2 1 0 3 5 5 5 1 0 3 0 5 23 | 0 2 0 1 3 0 2 2 1 5 0 0 2 5 24 | 1 2 2 5 5 2 0 6 0 1 5 5 5 1 25 | 2 3 5 0 5 2 6 0 5 2 10 0 5 0 26 | 2 2 4 0 5 1 0 5 0 0 10 5 10 0 27 | 2 0 5 2 1 5 1 2 0 0 0 4 0 0 28 | 2 2 2 1 0 0 5 10 10 0 0 5 0 5 29 | 0 0 2 0 3 0 5 0 5 4 5 0 3 3 30 | 4 10 5 2 0 2 5 5 10 0 0 3 0 10 31 | 0 5 5 5 5 5 1 0 0 0 5 3 10 0 32 | -------------------------------------------------------------------------------- /lib/solver/solution/solution.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Solution do 2 | alias CPSolver.Variable.Interface 3 | 4 | @callback handle(solution :: %{reference() => number() | boolean()}) :: any() 5 | def default_handler() do 6 | CPSolver.Solution.DefaultHandler 7 | end 8 | 9 | def run_handler(solution, handler) when is_atom(handler) do 10 | handler.handle(solution) 11 | end 12 | 13 | def run_handler(solution, handler) when is_function(handler) do 14 | handler.(solution) 15 | end 16 | 17 | def solution_handler(handler, variables) do 18 | fn solution -> 19 | solution 20 | |> reconcile(variables) 21 | |> run_handler(handler) 22 | end 23 | end 24 | 25 | defp reconcile(solution, variables) do 26 | ## We want to present a solution (which is var_name => value map) in order of initial variable names. 27 | solution 28 | |> Enum.sort_by(fn {var_name, _val} -> 29 | Enum.find_index( 30 | variables, 31 | fn var -> 32 | Interface.variable(var).name == var_name 33 | end 34 | ) 35 | end) 36 | end 37 | end 38 | 39 | defmodule CPSolver.Solution.DefaultHandler do 40 | @behaviour CPSolver.Solution 41 | 42 | require Logger 43 | @impl true 44 | def handle(_solution) do 45 | Logger.debug("Solution found") 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/propagators/all_different/all_different_bc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.AllDifferent.BC do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Variable.Interface 6 | alias CPSolver.Propagator 7 | alias CPSolver.Propagator.AllDifferent.BC 8 | 9 | describe "Initial filtering" do 10 | test "cascading filtering" do 11 | ## all variables become fixed, and this will take a single filtering call. 12 | ## 13 | x_vars = 14 | Enum.map([{"x2", 1..2}, {"x1", 1}, {"x3", 1..3}, {"x4", 1..4}, {"x5", 1..5}], fn {name, d} -> 15 | Variable.new(d, name: name) 16 | end) 17 | 18 | bc_propagator = BC.new(x_vars) 19 | %{changes: changes} = Propagator.filter(bc_propagator) 20 | assert map_size(changes) == length(x_vars) - 1 21 | assert Enum.all?(Map.values(changes), fn change -> change == :fixed end) 22 | ## All variables are now fixed 23 | assert Enum.all?(x_vars, &Interface.fixed?/1) 24 | end 25 | 26 | test "inconsistency (pigeonhole)" do 27 | domains = List.duplicate(1..3, 4) 28 | 29 | vars = 30 | Enum.map(domains, fn d -> Variable.new(d) end) 31 | 32 | bc_propagator = BC.new(vars) 33 | assert Propagator.filter(bc_propagator) == :fail 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/examples/tsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.TSP do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.Examples.TSP 5 | 6 | test "wikipedia, 4 cities (https://en.wikipedia.org/wiki/File:Weighted_K4.svg)" do 7 | distances = [ 8 | [0, 20, 42, 35], 9 | [20, 0, 30, 34], 10 | [42, 30, 0, 12], 11 | [35, 34, 12, 0] 12 | ] 13 | 14 | model = TSP.model(distances) 15 | {:ok, result} = CPSolver.solve(model) 16 | 17 | optimal_solution = List.last(result.solutions) 18 | assert TSP.check_solution(optimal_solution, model) 19 | 20 | assert result.status == {:optimal, [objective: 97]} 21 | end 22 | 23 | test "7 cities, optimality" do 24 | tsp_7_instance = "data/tsp/tsp_7.txt" 25 | model = TSP.model(tsp_7_instance) 26 | {:ok, result} = TSP.run(tsp_7_instance) 27 | 28 | assert Enum.all?(result.solutions, fn sol -> TSP.check_solution(sol, model) end) 29 | 30 | assert result.status == {:optimal, [objective: 56]} 31 | end 32 | 33 | test "15 cities, optimality" do 34 | tsp_instance = "data/tsp/tsp_15.txt" 35 | model = TSP.model(tsp_instance) 36 | {:ok, result} = TSP.run(tsp_instance) 37 | 38 | assert Enum.all?(result.solutions, fn sol -> TSP.check_solution(sol, model) end) 39 | assert result.status == {:optimal, [objective: 291]} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/propagators/abs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.Absolute do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Variable.Interface 7 | alias CPSolver.Propagator 8 | alias CPSolver.Propagator.Absolute 9 | 10 | test "filtering, initial call" do 11 | x = -1..10 12 | y = -5..5 13 | vars = [x_var, y_var] = Enum.map([x, y], fn d -> Variable.new(d) end) 14 | 15 | p = Absolute.new(vars) 16 | _res = Propagator.filter(p) 17 | ## y has negative values removed 18 | assert Interface.min(y_var) >= 0 19 | ## min(y) = min(|x|) 20 | assert Interface.min(y_var) == 0 21 | assert Interface.max(y_var) == 5 22 | ## max(y) is now min(max(|x|), max(y) (i.e. din't change in this case) 23 | assert Interface.max(y_var) == Enum.max(y) 24 | ## domain of x is adjusted to domain of y 25 | assert Interface.min(x_var) == -1 26 | assert Interface.max(x_var) == 5 27 | end 28 | 29 | test "inconsistency, if domains y and |x| do not intersect" do 30 | x = 1..10 31 | y = 11..20 32 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 33 | 34 | p = Absolute.new(variables) 35 | 36 | assert :fail = Propagator.filter(p) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/solver/constraints/propagators/equal.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Propagator.Equal do 2 | use CPSolver.Propagator 3 | 4 | def new(x, y, offset \\ 0) do 5 | new([x, y, offset]) 6 | end 7 | 8 | @impl true 9 | def variables(args) do 10 | args 11 | |> Propagator.default_variables_impl() 12 | |> Enum.map(fn var -> set_propagate_on(var, :fixed) end) 13 | end 14 | 15 | @impl true 16 | def filter([x, y], state, changes) do 17 | filter([x, y, 0], state, changes) 18 | end 19 | 20 | def filter([x, y, offset], _state, _changes) do 21 | filter_impl(x, y, offset) 22 | end 23 | 24 | def filter_impl(x, y, offset \\ 0) 25 | 26 | def filter_impl(x, c, offset) when is_integer(c) do 27 | fix(x, c + offset) 28 | :passive 29 | end 30 | 31 | def filter_impl(x, y, offset) do 32 | cond do 33 | fixed?(x) -> 34 | fix(y, plus(min(x), -offset)) 35 | 36 | fixed?(y) -> 37 | fix(x, plus(min(y), offset)) 38 | 39 | true -> 40 | :stable 41 | end 42 | end 43 | 44 | @impl true 45 | def failed?([x, y, offset], _state) do 46 | fixed?(x) && fixed?(y) && min(x) != plus(min(y), offset) 47 | end 48 | 49 | @impl true 50 | def entailed?([x, y, offset], _state) do 51 | ## x != y holds on the condition below 52 | fixed?(x) && fixed?(y) && min(x) == plus(min(y), offset) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/utils/mutable_array.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Utils.MutableArray do 2 | def swap(array, index1, index2) do 3 | val1 = array_get(array, index1) 4 | val2 = array_get(array, index2) 5 | array_update(array, index1, val2) 6 | array_update(array, index2, val1) 7 | end 8 | 9 | def new(arity) when is_integer(arity) do 10 | :atomics.new(arity, signed: true) 11 | end 12 | 13 | def new(list) when is_list(list) do 14 | ref = new(length(list)) 15 | 16 | Enum.reduce(list, 1, fn el, idx -> 17 | :atomics.put(ref, idx, el) 18 | idx + 1 19 | end) 20 | 21 | ref 22 | end 23 | 24 | def array_size(ref) do 25 | :atomics.info(ref).size 26 | end 27 | 28 | def array_update(ref, zb_index, value) 29 | when is_reference(ref) and zb_index >= 0 and is_integer(value) do 30 | :atomics.put(ref, zb_index + 1, value) 31 | end 32 | 33 | def array_add(ref, zb_index, value) 34 | when is_reference(ref) and zb_index >= 0 and is_integer(value) do 35 | :atomics.add(ref, zb_index + 1, value) 36 | end 37 | 38 | def array_get(ref, zb_index) when is_reference(ref) and zb_index >= 0 do 39 | :atomics.get(ref, zb_index + 1) 40 | end 41 | 42 | def to_array(ref, fun \\ fn _i, val -> val end) do 43 | for i <- 1..:atomics.info(ref).size do 44 | fun.(i, :atomics.get(ref, i)) 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /test/constraints/less_or_equal_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.LessOrEqual do 2 | use ExUnit.Case, async: false 3 | 4 | describe "LessOrEqual" do 5 | alias CPSolver.Constraint.{Less, LessOrEqual} 6 | alias CPSolver.IntVariable, as: Variable 7 | alias CPSolver.Constraint 8 | alias CPSolver.Model 9 | 10 | test "less_or_equal, 2 variables" do 11 | x = Variable.new(0..1, name: "x") 12 | y = Variable.new(0..1, name: "y") 13 | model = Model.new([x, y], [Constraint.new(LessOrEqual, [x, y])]) 14 | {:ok, res} = CPSolver.solve(model) 15 | 16 | assert length(res.solutions) == 3 17 | assert Enum.all?(res.solutions, fn [x_val, y_val] -> x_val <= y_val end) 18 | end 19 | 20 | test "less_or_equal, variable and constant" do 21 | x = Variable.new(0..4) 22 | upper_bound = 2 23 | le_constraint = LessOrEqual.new(x, upper_bound) 24 | model = Model.new([x], [le_constraint]) 25 | {:ok, res} = CPSolver.solve(model) 26 | assert length(res.solutions) == 3 27 | assert Enum.all?(res.solutions, fn [x_val, _] -> x_val <= upper_bound end) 28 | end 29 | 30 | test "Less (inconsistent)" do 31 | x = Variable.new(1) 32 | y = Variable.new(1) 33 | less_constraint = Less.new(x, y) 34 | assert catch_throw({:fail, _} = Model.new([x, y], [less_constraint])) 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :error) 2 | ExUnit.start(capture_log: true, exclude: [:superslow]) 3 | 4 | alias CPSolver.Propagator 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Propagator.NotEqual 7 | alias CPSolver.Propagator.ConstraintGraph 8 | alias Iter.Iterable 9 | 10 | defmodule CPSolver.Test.Helpers do 11 | def number_of_occurences(string, pattern) do 12 | string |> String.split(pattern) |> length() |> Kernel.-(1) 13 | end 14 | 15 | def space_setup(x, y, z) do 16 | variables = 17 | Enum.map([{x, "x"}, {y, "y"}, {z, "z"}], fn {d, name} -> Variable.new(d, name: name) end) 18 | 19 | [x_var, y_var, z_var] = variables 20 | 21 | propagators = 22 | Enum.map( 23 | [{x_var, y_var, "x != y"}, {y_var, z_var, "y != z"}, {x_var, z_var, "x != z"}], 24 | fn {v1, v2, name} -> Propagator.new(NotEqual, [v1, v2], name: name) end 25 | ) 26 | 27 | graph = ConstraintGraph.create(propagators) 28 | 29 | updated_graph = ConstraintGraph.update(graph, variables) 30 | 31 | %{ 32 | propagators: propagators, 33 | variables: variables, 34 | constraint_graph: updated_graph 35 | } 36 | end 37 | 38 | ## Compare two iterables 39 | def iterables_equal?(iterable1, iterable2) do 40 | Iterable.to_list(iterable1) |> Enum.sort() 41 | == Iterable.to_list(iterable2) |> Enum.sort() 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/solver/constraints/propagators/not_equal.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Propagator.NotEqual do 2 | use CPSolver.Propagator 3 | 4 | def new(x, y, offset \\ 0) do 5 | new([x, y, offset]) 6 | end 7 | 8 | @impl true 9 | def variables(args) do 10 | args 11 | |> Propagator.default_variables_impl() 12 | |> Enum.map(fn var -> set_propagate_on(var, :fixed) end) 13 | end 14 | 15 | @impl true 16 | def filter([x, y], state, changes) do 17 | filter([x, y, 0], state, changes) 18 | end 19 | 20 | def filter([x, y, offset], _state, _changes) do 21 | filter_impl(x, y, offset) 22 | end 23 | 24 | def filter_impl(x, y, offset \\ 0) 25 | 26 | def filter_impl(x, c, offset) when is_integer(c) do 27 | remove(x, c + offset) 28 | :passive 29 | end 30 | 31 | def filter_impl(x, y, offset) do 32 | cond do 33 | fixed?(x) -> 34 | remove(y, plus(min(x), -offset)) 35 | :passive 36 | 37 | fixed?(y) -> 38 | remove(x, plus(min(y), offset)) 39 | :passive 40 | 41 | true -> 42 | :stable 43 | end 44 | end 45 | 46 | @impl true 47 | def failed?([x, y, offset], _state) do 48 | fixed?(x) && fixed?(y) && min(x) == plus(min(y), offset) 49 | end 50 | 51 | @impl true 52 | def entailed?([x, y, offset], _state) do 53 | ## x != y holds on the condition below 54 | fixed?(x) && fixed?(y) && min(x) != plus(min(y), offset) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /scripts/sum_debug.exs: -------------------------------------------------------------------------------- 1 | defmodule SumDebug do 2 | alias CPSolver.ConstraintStore 3 | alias CPSolver.IntVariable, as: Variable 4 | alias CPSolver.Propagator 5 | alias CPSolver.Propagator.Sum 6 | alias CPSolver.Constraint.Sum, as: SumConstraint 7 | alias CPSolver.Model 8 | 9 | def debug(y_domain, x_domains) do 10 | y = Variable.new(y_domain, name: "Y") 11 | 12 | xs = 13 | Enum.map(x_domains |> Enum.with_index(1), fn {d, idx} -> 14 | Variable.new(d, name: "x.#{idx}") 15 | end) 16 | 17 | {:ok, [y_var | x_vars] = bound_vars, store} = ConstraintStore.create_store([y | xs]) 18 | 19 | # Enum.each(bound_vars, fn v -> IO.inspect("#{v.name} => #{inspect v.id}, #{min(v)}..#{max(v)}") end) 20 | # assert :stable == 21 | sum_propagator = Sum.new(y_var, x_vars) 22 | IO.inspect(sum_propagator.args) 23 | 24 | Propagator.filter(sum_propagator, store: store) 25 | end 26 | 27 | def debug_constraint(y_domain, x_domains) do 28 | y = Variable.new(y_domain, name: "Y") 29 | 30 | xs = 31 | Enum.map(x_domains |> Enum.with_index(1), fn {d, idx} -> 32 | Variable.new(d, name: "x.#{idx}") 33 | end) 34 | 35 | sum_constraint = SumConstraint.new(y, xs) 36 | 37 | model = 38 | Model.new( 39 | [y | xs], 40 | [sum_constraint] 41 | ) 42 | 43 | {:ok, _res} = CPSolver.solve(model) 44 | end 45 | end 46 | 47 | SumDebug.debug_constraint(1..55, [1..10, 1..50]) 48 | -------------------------------------------------------------------------------- /test/constraints/count_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Count do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Model 6 | alias CPSolver.Constraint.Factory, as: ConstraintFactory 7 | 8 | describe "Count constraint" do 9 | test "`count` functionality" do 10 | ~S""" 11 | MiniZinc: 12 | 13 | var -5..5: c; 14 | var 0..10: y; 15 | array[1..5] of var 1..3: arr; 16 | 17 | constraint count_eq(arr, y, c); 18 | """ 19 | 20 | c = Variable.new(-5..5, name: "count") 21 | y = Variable.new(0..10, name: "value") 22 | array = Enum.map(1..5, fn i -> Variable.new(1..3, name: "arr#{i}") end) 23 | 24 | model = 25 | Model.new( 26 | [array, y, c] |> List.flatten(), 27 | ConstraintFactory.count(array, y, c) |> List.flatten() 28 | ) 29 | 30 | {:ok, result} = CPSolver.solve(model) 31 | 32 | assert result.statistics.solution_count == 2673 33 | assert_count(result.solutions, length(array)) 34 | end 35 | 36 | defp assert_count(solutions, array_len) do 37 | assert Enum.all?(solutions, fn solution -> 38 | arr = Enum.take(solution, array_len) 39 | value = Enum.at(solution, array_len) 40 | c = Enum.at(solution, array_len + 1) 41 | Enum.count(arr, fn el -> el == value end) == c 42 | end) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/constraints/sum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Sum do 2 | use ExUnit.Case, async: false 3 | 4 | describe "Sum" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Constraint.Factory 7 | alias CPSolver.Model 8 | 9 | test "sum (3 variables)" do 10 | x = Variable.new(0..1, name: "x") 11 | y = Variable.new(0..1, name: "y") 12 | z = Variable.new(0..1, name: "z") 13 | 14 | {_sum_var, sum_constraint} = Factory.sum([x, y, z]) 15 | 16 | model = Model.new([x, y, z], [sum_constraint]) 17 | {:ok, res} = CPSolver.solve(model) 18 | 19 | assert 8 == length(res.solutions) 20 | 21 | assert Enum.all?(res.solutions, fn s -> 22 | Enum.sum(Enum.take(s, length(s) - 1)) == List.last(s) 23 | end) 24 | end 25 | 26 | test "sum (mixed arguments)" do 27 | c1 = 3 28 | c2 = -4 29 | c3 = 5 30 | 31 | x = Variable.new(0..1, name: "x") 32 | y = Variable.new(0..1, name: "y") 33 | z = Variable.new(0..1, name: "z") 34 | {_sum_var, sum_constraint} = Factory.sum([x, c1, y, c2, c3, z]) 35 | 36 | model = Model.new([x, y, z], [sum_constraint]) 37 | {:ok, res} = CPSolver.solve(model) 38 | 39 | assert 8 == length(res.solutions) 40 | 41 | assert Enum.all?(res.solutions, fn s -> 42 | Enum.sum(Enum.take(s, length(s) - 1)) + c1 + c2 + c3 == List.last(s) 43 | end) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/constraints/all_different/all_different_binary_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.AllDifferent do 2 | use ExUnit.Case, async: false 3 | 4 | describe "AllDifferent" do 5 | alias CPSolver.Propagator.NotEqual, as: PropagatorNotEqual 6 | alias CPSolver.Constraint.AllDifferent.Binary, as: AllDifferent 7 | alias CPSolver.IntVariable 8 | alias CPSolver.Constraint 9 | alias CPSolver.Model 10 | 11 | test "propagators" do 12 | domain = 1..3 13 | variables = Enum.map(1..3, fn i -> IntVariable.new(domain, name: "x#{i}") end) 14 | 15 | assert variables 16 | |> AllDifferent.propagators() 17 | |> Enum.map(fn %{mod: PropagatorNotEqual, args: [x, y, _]} -> 18 | "#{x.name} != #{y.name}" 19 | end) 20 | |> Enum.sort() == 21 | ["x1 != x2", "x1 != x3", "x2 != x3"] 22 | end 23 | 24 | test "produces all possible permutations" do 25 | var_nums = 4 26 | domain = 1..var_nums 27 | variables = Enum.map(domain, fn _ -> IntVariable.new(domain) end) 28 | 29 | permutations = Permutation.permute!(Enum.to_list(domain)) 30 | 31 | model = Model.new(variables, [Constraint.new(AllDifferent, variables)]) 32 | 33 | {:ok, result} = CPSolver.solve(model) 34 | 35 | assert result.statistics.solution_count == MapSet.size(permutations) 36 | 37 | assert result.solutions |> MapSet.new() == permutations 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /data/knapsack/ks_100_2: -------------------------------------------------------------------------------- 1 | 100 10000 2 | 339 342 3 | 1629 1514 4 | 697 696 5 | 1299 1433 6 | 1613 1762 7 | 36 40 8 | 1737 1635 9 | 473 442 10 | 1859 1899 11 | 2055 1960 12 | 362 378 13 | 1104 1177 14 | 1880 1970 15 | 1349 1434 16 | 1545 1691 17 | 132 139 18 | 341 371 19 | 1430 1350 20 | 1878 1775 21 | 1870 1980 22 | 1536 1651 23 | 818 814 24 | 289 282 25 | 1690 1573 26 | 1437 1587 27 | 310 302 28 | 53 56 29 | 720 726 30 | 1707 1820 31 | 258 269 32 | 1842 1680 33 | 757 842 34 | 1642 1730 35 | 1149 1243 36 | 1970 1794 37 | 749 775 38 | 1904 1810 39 | 2 3 40 | 967 970 41 | 1310 1261 42 | 1004 997 43 | 1295 1192 44 | 1056 1036 45 | 51 52 46 | 1320 1453 47 | 1580 1673 48 | 480 440 49 | 604 624 50 | 1766 1813 51 | 1198 1326 52 | 1762 1637 53 | 2046 1902 54 | 315 323 55 | 714 746 56 | 434 471 57 | 1461 1366 58 | 1652 1511 59 | 1876 1785 60 | 906 1002 61 | 1483 1560 62 | 1355 1403 63 | 510 513 64 | 2114 1958 65 | 1479 1505 66 | 1618 1538 67 | 1472 1378 68 | 310 315 69 | 1478 1493 70 | 970 1066 71 | 43 40 72 | 1231 1172 73 | 1792 1972 74 | 870 956 75 | 1484 1541 76 | 1049 1014 77 | 56 55 78 | 814 793 79 | 978 985 80 | 1215 1311 81 | 720 737 82 | 210 204 83 | 460 492 84 | 1798 1961 85 | 1944 1952 86 | 208 204 87 | 1836 1872 88 | 882 806 89 | 239 234 90 | 141 136 91 | 49 49 92 | 1352 1363 93 | 915 883 94 | 1318 1259 95 | 72 70 96 | 937 886 97 | 1783 1843 98 | 1253 1319 99 | 1268 1375 100 | 1144 1234 101 | 878 818 102 | -------------------------------------------------------------------------------- /test/constraints/equal_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Equal do 2 | use ExUnit.Case, async: false 3 | 4 | describe "Equal" do 5 | alias CPSolver.Constraint.Equal 6 | alias CPSolver.IntVariable, as: Variable 7 | alias CPSolver.Model 8 | 9 | test "equal, 2 variables" do 10 | x = Variable.new(0..1, name: "x") 11 | y = Variable.new(0..1, name: "y") 12 | z = Variable.new(2..4, name: "z") 13 | satisfiable_model = Model.new([x, y], [Equal.new([x, y])]) 14 | {:ok, res} = CPSolver.solve(satisfiable_model) 15 | 16 | assert length(res.solutions) == 2 17 | assert Enum.all?(res.solutions, fn [x_val, y_val] -> x_val == y_val end) 18 | 19 | unsatisfiable_model = Model.new([x, y, z], [Equal.new([x, y]), Equal.new([y, z])]) 20 | {:ok, res} = CPSolver.solve(unsatisfiable_model) 21 | assert res.status == :unsatisfiable 22 | end 23 | 24 | test "variable and constant" do 25 | x = Variable.new(0..4) 26 | satisfiable_value = 3 27 | equal_constraint = Equal.new(x, satisfiable_value) 28 | satisfiable_model = Model.new([x], [equal_constraint]) 29 | {:ok, res} = CPSolver.solve(satisfiable_model) 30 | assert length(res.solutions) == 1 31 | assert hd(hd(res.solutions)) == satisfiable_value 32 | 33 | unsatisfiable_value = -1 34 | equal_constraint = Equal.new(x, unsatisfiable_value) 35 | assert catch_throw({:fail, _} = Model.new([x], [equal_constraint])) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/constraints/maximum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Maximum do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Model 6 | alias CPSolver.Constraint.Maximum 7 | alias CPSolver.Constraint.Factory, as: ConstraintFactory 8 | 9 | describe "Maximum constraint" do 10 | test "`maximum` functionality" do 11 | x_arr = Enum.map(1..4, fn idx -> Variable.new(0..4, name: "x#{idx}") end) 12 | y = Variable.new(-5..20, name: "y") 13 | 14 | model = Model.new([y | x_arr], [Maximum.new(y, x_arr)]) 15 | 16 | {:ok, result} = CPSolver.solve(model) 17 | 18 | assert_maximum(result.solutions) 19 | assert result.statistics.solution_count == 625 20 | end 21 | 22 | test "Factory" do 23 | x_arr = Enum.map(1..4, fn idx -> Variable.new(0..4, name: "x#{idx}") end) 24 | {max_var, maximum_constraint} = ConstraintFactory.maximum(x_arr) 25 | 26 | assert Variable.min(max_var) == 0 27 | assert Variable.max(max_var) == 4 28 | 29 | model = Model.new([], [maximum_constraint]) 30 | 31 | {:ok, result} = CPSolver.solve(model) 32 | 33 | assert_maximum(result.solutions) 34 | assert result.statistics.solution_count == 625 35 | end 36 | 37 | ## Constraint check: y = max(x_array) 38 | ## 39 | defp assert_maximum(solutions) do 40 | assert Enum.all?(solutions, fn [y | xs] -> 41 | y == Enum.max(xs) 42 | end) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/examples/send_more_money.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Examples.SendMoreMoney do 2 | @moduledoc """ 3 | The classic "cryptarithmetic" (https://en.wikipedia.org/wiki/Verbal_arithmetic) problem. 4 | Solve the following (each letter is a separate digit): 5 | 6 | SEND + MORE = MONEY 7 | 8 | """ 9 | 10 | alias CPSolver.IntVariable, as: Variable 11 | alias CPSolver.Model 12 | alias CPSolver.Constraint.Sum 13 | alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent 14 | import CPSolver.Variable.View.Factory 15 | 16 | def model() do 17 | letters = [S, E, N, D, M, O, R, Y] 18 | 19 | variables = 20 | [s, e, n, d, m, o, r, y] = 21 | Enum.map(letters, fn letter -> 22 | d = (letter in [S, M] && 1..9) || 0..9 23 | Variable.new(d, name: letter) 24 | end) 25 | 26 | sum_constraint = 27 | Sum.new(y, [ 28 | d, 29 | mul(n, -90), 30 | mul(e, 91), 31 | mul(s, 1000), 32 | mul(r, 10), 33 | mul(o, -900), 34 | mul(m, -9_000) 35 | ]) 36 | 37 | Model.new(variables, [sum_constraint, AllDifferent.new(variables)]) 38 | end 39 | 40 | def solve() do 41 | {:ok, res} = CPSolver.solve(model(), stop_on: {:max_solutions, 1}) 42 | Enum.zip(res.variables, hd(res.solutions)) 43 | end 44 | 45 | def check_solution([s, e, n, d, m, o, r, y] = _solution) do 46 | 1000 * s + 100 * e + 10 * n + d + 47 | 1000 * m + 100 * o + 10 * r + e == 48 | 10_000 * m + 1000 * o + 100 * n + 10 * e + y 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/examples/sudoku_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.Sudoku do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.Examples.Sudoku 5 | 6 | alias CPSolver.Search.VariableSelector, as: Strategy 7 | 8 | test "4x4" do 9 | test_sudoku(Sudoku.puzzles().s4x4, 2, trials: 10, timeout: 100) 10 | end 11 | 12 | test "9x9 single solution 1" do 13 | test_sudoku(Sudoku.puzzles().s9x9_1, 1, timeout: 2000) 14 | end 15 | 16 | test "9x9 single solution 2" do 17 | test_sudoku(Sudoku.puzzles().hard9x9, 1) 18 | end 19 | 20 | test "9x9 multiple solutions" do 21 | test_sudoku(Sudoku.puzzles().s9x9_5, 5, timeout: 2000) 22 | end 23 | 24 | defp test_sudoku(puzzle_instance, expected_solutions, opts \\ []) do 25 | opts = 26 | Keyword.merge([timeout: 500, trials: 1], opts) 27 | 28 | Enum.each(1..opts[:trials], fn _i -> 29 | {:ok, result} = 30 | CPSolver.solve(Sudoku.model(puzzle_instance), 31 | search: { 32 | # Strategy.first_fail(&Enum.random/1), 33 | Strategy.afc({:afc_size_min, 0.75}, &List.first/1), 34 | :indomain_random 35 | }, 36 | timeout: opts[:timeout] 37 | ) 38 | 39 | Enum.each(result.solutions, &assert_solution/1) 40 | solution_count = result.statistics.solution_count 41 | 42 | assert solution_count == expected_solutions 43 | end) 44 | end 45 | 46 | defp assert_solution(solution) do 47 | assert Sudoku.check_solution(solution), "Wrong solution!" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/examples/queens_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.Queens do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.Examples.Queens 5 | 6 | require Logger 7 | 8 | ## No solutions 9 | test "3 Queens" do 10 | test_queens(3, 0) 11 | end 12 | 13 | test "4 Queens" do 14 | test_queens(4, 2) 15 | end 16 | 17 | test "5 Queens" do 18 | test_queens(5, 10) 19 | end 20 | 21 | test "6 Queens" do 22 | test_queens(6, 4) 23 | end 24 | 25 | test "7 Queens" do 26 | test_queens(7, 40) 27 | end 28 | 29 | test "8 Queens" do 30 | test_queens(8, 92, timeout: 500) 31 | end 32 | 33 | test "50 Queens" do 34 | test_queens(33, 1, timeout: 500, trials: 2, space_threads: 4, stop_on: {:max_solutions, 1}) 35 | end 36 | 37 | defp test_queens(n, expected_solutions, opts \\ []) do 38 | opts = 39 | Keyword.merge([timeout: 100, trials: 10, symmetry: :half_symmetry], opts) 40 | 41 | Enum.each(1..opts[:trials], fn i -> 42 | {:ok, result} = CPSolver.solve(Queens.model(n), opts) 43 | Enum.each(result.solutions, &assert_solution/1) 44 | solution_count = result.statistics.solution_count 45 | 46 | if opts[:stop_on] do 47 | assert solution_count >= expected_solutions 48 | else 49 | assert solution_count == expected_solutions, 50 | "Failed on trial #{i} with #{inspect(solution_count)} out of #{expected_solutions} solution(s)" 51 | end 52 | end) 53 | end 54 | 55 | defp assert_solution(solution) do 56 | assert Queens.check_solution(solution) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /data/knapsack/ks_100_1: -------------------------------------------------------------------------------- 1 | 100 3190802 2 | 1491 3882 3 | 399 1298 4 | 77 654 5 | 969 2638 6 | 8485 20670 7 | 55 610 8 | 1904 4908 9 | 703 2106 10 | 657 2014 11 | 932 2564 12 | 1201 3302 13 | 1697 4494 14 | 462 1424 15 | 1201 3302 16 | 111632 267364 17 | 9044 21988 18 | 147380 352660 19 | 31852 76604 20 | 9044 21988 21 | 9300 22700 22 | 8660 21020 23 | 174684 418068 24 | 19844 47788 25 | 9044 21988 26 | 1635 4370 27 | 62788 150476 28 | 6932 16964 29 | 6308 15516 30 | 50 600 31 | 4600 11300 32 | 565204 1351508 33 | 7463 18226 34 | 2988 7476 35 | 9044 21988 36 | 9044 21988 37 | 4040 9980 38 | 137732 329764 39 | 7150 17400 40 | 9300 22700 41 | 177 854 42 | 372 1244 43 | 499 1498 44 | 15108 36516 45 | 11108 26916 46 | 2468 6236 47 | 1133 3166 48 | 1490 3880 49 | 865 2430 50 | 2468 6236 51 | 2468 6236 52 | 5974 14648 53 | 5972 14644 54 | 9532 23164 55 | 1872 4844 56 | 3964 9828 57 | 2799 7098 58 | 527708 1261916 59 | 7212 17724 60 | 3002 7504 61 | 21004 50708 62 | 47728 114556 63 | 565204 1351508 64 | 100600 240900 65 | 118920 284740 66 | 2822 7144 67 | 612 1924 68 | 6324 15548 69 | 9508 23116 70 | 9268 22636 71 | 11636 28172 72 | 210708 504116 73 | 2176944 5204588 74 | 930 2560 75 | 4481 11062 76 | 50 600 77 | 112 724 78 | 14434 34968 79 | 0 500 80 | 248 996 81 | 48 596 82 | 820 2340 83 | 278 1056 84 | 643 1986 85 | 1413 3726 86 | 1408 3716 87 | 0 500 88 | 2581 6662 89 | 287 1074 90 | 2040 5180 91 | 289 1078 92 | 1380 3660 93 | 372 1244 94 | 0 500 95 | 472 1444 96 | 360 1220 97 | 0 500 98 | 622 1944 99 | 3504 8708 100 | 5924 14548 101 | 2784 7068 102 | -------------------------------------------------------------------------------- /test/constraints/modulo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Modulo do 2 | use ExUnit.Case, async: false 3 | 4 | describe "Modulo" do 5 | alias CPSolver.Constraint.Modulo 6 | alias CPSolver.IntVariable, as: Variable 7 | alias CPSolver.Variable.Interface 8 | alias CPSolver.Model 9 | alias CPSolver.Constraint.Factory, as: ConstraintFactory 10 | 11 | ~c""" 12 | MiniZinc model (for verification): 13 | 14 | var -2..2: x; 15 | var -2..2: y; 16 | var -100..100: m; 17 | constraint m = x mod y; 18 | 19 | """ 20 | 21 | test "`modulo` functionality" do 22 | x = Variable.new(-2..2, name: "x") 23 | y = Variable.new(-2..2, name: "y") 24 | m = Variable.new(-100..100, name: "m") 25 | 26 | model = Model.new([x, y, m], [Modulo.new(m, x, y)]) 27 | 28 | {:ok, res} = CPSolver.solve(model) 29 | assert res.statistics.solution_count == 20 30 | assert check_solutions(res) 31 | end 32 | 33 | test "Factory.mod/2,3" do 34 | x = Variable.new(-100..100, name: "x") 35 | y = Variable.new(-7..7, name: "y") 36 | {mod_var, mod_constraint} = ConstraintFactory.mod(x, y) 37 | assert Interface.min(mod_var) == -6 38 | assert Interface.max(mod_var) == 6 39 | 40 | model = Model.new([x, y], [mod_constraint]) 41 | {:ok, res} = CPSolver.solve(model) 42 | ## Verification against MiniZinc count 43 | assert res.statistics.solution_count == 2814 44 | assert check_solutions(res) 45 | end 46 | 47 | defp check_solutions(result) do 48 | Enum.all?(result.solutions, fn [x_val, y_val, m_val] -> rem(x_val, y_val) == m_val end) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/bit_vector.erl: -------------------------------------------------------------------------------- 1 | -module(bit_vector). 2 | 3 | -export([new/1, get/2, set/2, clear/2, flip/2, print/1]). 4 | 5 | 6 | % Allocate atomics to contain the data + 2 bytes for min and max 7 | new(Size) -> 8 | Words = (Size + 63) div 64, 9 | Atomics = atomics:new(Words + 1, [{signed, false}]), 10 | atomics:put(Atomics, Words + 1, 0), %% Set 'min_max' to lowest possible 11 | 12 | 13 | {?MODULE, Atomics}. 14 | 15 | 16 | 17 | get({?MODULE, Aref}, Bix) -> 18 | Wix = (Bix div 64) + 1, 19 | Mask = (1 bsl (Bix rem 64)), 20 | case atomics:get(Aref, Wix) band Mask of 21 | 0 -> 0; 22 | Mask -> 1 23 | end. 24 | 25 | set({?MODULE, Aref}, Bix) -> 26 | Mask = (1 bsl (Bix rem 64)), 27 | update(Aref, Bix, fun(Word) -> Word bor Mask end). 28 | 29 | clear({?MODULE, Aref}, Bix) -> 30 | Mask = bnot (1 bsl (Bix rem 64)), 31 | update(Aref, Bix, fun(Word) -> Word band Mask end). 32 | 33 | flip({?MODULE, Aref}, Bix) -> 34 | Mask = (1 bsl (Bix rem 64)), 35 | update(Aref, Bix, fun(Word) -> Word bxor Mask end). 36 | 37 | print({?MODULE, Aref} = BV) -> 38 | #{size := Size} = atomics:info(Aref), 39 | print(BV, Size). 40 | print(BV, 0) -> 41 | io:format("~B~n",[get(BV, 0)]); 42 | print(BV, Slot) -> 43 | io:format("~B",[get(BV, Slot)]), 44 | print(BV, Slot-1). 45 | 46 | update(Aref, Bix, Fun) -> 47 | Wix = (Bix div 64) + 1, 48 | update_loop(Aref, Wix, Fun, atomics:get(Aref, Wix)). 49 | 50 | update_loop(Aref, Wix, Fun, Current) -> 51 | case atomics:compare_exchange(Aref, Wix, Current, Fun(Current)) of 52 | ok -> 53 | ok; 54 | Was -> 55 | update_loop(Aref, Wix, Fun, Was) 56 | end. -------------------------------------------------------------------------------- /lib/examples/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Examples.Utils do 2 | require Logger 3 | 4 | def notify_client_handler() do 5 | client = self() 6 | fn solution -> send(client, {:solution, solution}) end 7 | end 8 | 9 | def wait_for_solution(timeout, solution_checker_fun) do 10 | receive do 11 | {:solution, solution} -> 12 | solution 13 | |> Enum.map(fn {_ref, s} -> s end) 14 | |> then(fn sol -> solution_checker_fun.(sol) |> tap(fn res -> handle_checked(res) end) end) 15 | after 16 | timeout -> 17 | handle_timeout() 18 | end 19 | |> tap(fn _ -> flush_solutions() end) 20 | end 21 | 22 | def wait_for_solutions(expected, timeout, solution_checker_fun) do 23 | wait_for_solutions(expected, timeout, solution_checker_fun, 0) 24 | end 25 | 26 | def wait_for_solutions(0, _timeout, _solution_checker_fun, successes) do 27 | successes 28 | end 29 | 30 | def wait_for_solutions(num, timeout, solution_checker_fun, successes) do 31 | case wait_for_solution(timeout, solution_checker_fun) do 32 | {:error, :timeout} -> 33 | {:error, :timeout} 34 | 35 | true -> 36 | wait_for_solutions(num - 1, timeout, solution_checker_fun, successes + 1) 37 | end 38 | end 39 | 40 | defp handle_timeout() do 41 | Logger.error("Timed out :-(") 42 | {:error, :timeout} 43 | end 44 | 45 | defp handle_checked(solution_checked?) do 46 | (solution_checked? && Logger.notice("Solution checked!")) || Logger.error("Wrong solution!") 47 | end 48 | 49 | def flush_solutions() do 50 | receive do 51 | {:solution, _} -> flush_solutions() 52 | after 53 | 0 -> :ok 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/examples/graph_coloring.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Examples.GraphColoring do 2 | alias CPSolver.Constraint.NotEqual 3 | alias CPSolver.IntVariable 4 | alias CPSolver.Model 5 | 6 | def solve(instance, solver_opts \\ []) 7 | 8 | def solve(instance, solver_opts) when is_binary(instance) do 9 | instance 10 | |> model() 11 | |> solve(solver_opts) 12 | end 13 | 14 | def solve(data, solver_opts) do 15 | {:ok, _solver} = 16 | CPSolver.solve_async(model(data), solver_opts) 17 | |> tap(fn _ -> Process.sleep(100) end) 18 | end 19 | 20 | def model(data) when is_binary(data) do 21 | parse_instance(data) 22 | |> model() 23 | end 24 | 25 | def model(data) when is_map(data) do 26 | color_vars = Enum.map(1..data.vertices, fn _idx -> IntVariable.new(1..data.max_color) end) 27 | 28 | edge_color_constraints = 29 | Enum.map(data.edges, fn [v1, v2] -> 30 | NotEqual.new(Enum.at(color_vars, v1), Enum.at(color_vars, v2)) 31 | end) 32 | 33 | Model.new(color_vars, edge_color_constraints) 34 | end 35 | 36 | defp parse_instance(instance) do 37 | [header | edge_lines] = File.read!(instance) |> String.split("\n", trim: true) 38 | [v, _e, max_color] = header |> String.split(" ", trim: true) |> Enum.map(&String.to_integer/1) 39 | 40 | edges = 41 | Enum.map(edge_lines, fn e -> String.split(e, " ") |> Enum.map(&String.to_integer/1) end) 42 | 43 | %{vertices: v, edges: edges, max_color: max_color} 44 | end 45 | 46 | def check_solution(colors, instance) when is_list(colors) do 47 | graph = parse_instance(instance) 48 | Enum.all?(graph.edges, fn [v1, v2] -> Enum.at(colors, v1) != Enum.at(colors, v2) end) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/constraints/abs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Absolute do 2 | use ExUnit.Case, async: false 3 | 4 | describe "Absolute" do 5 | alias CPSolver.Constraint.Absolute 6 | alias CPSolver.IntVariable, as: Variable 7 | alias CPSolver.Variable.Interface 8 | alias CPSolver.Model 9 | alias CPSolver.Constraint.Factory, as: ConstraintFactory 10 | 11 | ~c""" 12 | MiniZinc model (for verification): 13 | 14 | var -2..2: x; 15 | var -2..2: y; 16 | constraint y = abs(x); 17 | 18 | """ 19 | 20 | test "`Absolute` functionality" do 21 | x = Variable.new(-2..2, name: "x") 22 | y = Variable.new(-2..2, name: "y") 23 | 24 | model = Model.new([x, y], [Absolute.new(x, y)]) 25 | 26 | {:ok, res} = CPSolver.solve(model) 27 | assert res.statistics.solution_count == 5 28 | assert check_solutions(res) 29 | end 30 | 31 | test "inconsistency" do 32 | x = Variable.new(0, name: "x") 33 | y = Variable.new(1, name: "y") 34 | 35 | assert catch_throw({:fail, _} = Model.new([x, y], [Absolute.new(x, y)])) 36 | 37 | end 38 | 39 | test "factory" do 40 | x = Variable.new(-2..2) 41 | 42 | {abs_var, abs_constraint} = ConstraintFactory.absolute(x) 43 | 44 | assert Interface.min(abs_var) == 0 45 | assert Interface.max(abs_var) == 2 46 | 47 | model = Model.new([x], [abs_constraint]) 48 | 49 | {:ok, res} = CPSolver.solve(model) 50 | assert res.statistics.solution_count == 5 51 | assert check_solutions(res) 52 | end 53 | 54 | defp check_solutions(result) do 55 | Enum.all?(result.solutions, fn [x_val, y_val] -> y_val == abs(x_val) end) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/examples/xkcd_np.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Examples.XKCD.NP do 2 | @doc """ 3 | xkcd-np. 4 | """ 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Model 7 | 8 | import CPSolver.Variable.View.Factory 9 | import CPSolver.Constraint.Factory 10 | 11 | def model() do 12 | appetizers = [ 13 | {:mixed_fruit, 215}, 14 | {:french_fries, 275}, 15 | {:side_salad, 335}, 16 | {:hot_wings, 355}, 17 | {:mozarella_sticks, 420}, 18 | {:sampler_plate, 580} 19 | ] 20 | 21 | total = 1505 22 | 23 | quantities = 24 | Enum.map(appetizers, fn {name, price} -> 25 | mul(Variable.new(0..div(total, price), name: name), price) 26 | end) 27 | 28 | sum_constraint = sum(quantities, total) 29 | 30 | Model.new(quantities, [sum_constraint], extra: %{appetizers: appetizers, total: total}) 31 | end 32 | 33 | def check_solution(solution, %{extra: %{appetizers: appetizers, total: total}} = _model) do 34 | appetizers 35 | |> Enum.zip(solution |> Enum.take(length(appetizers))) 36 | |> Enum.reduce(0, fn {{_name, price}, quantity}, acc -> acc + price * quantity end) 37 | |> then(fn sum -> sum == total end) 38 | end 39 | 40 | def solve() do 41 | model = model() 42 | num_appetizers = length(model.extra.appetizers) 43 | {:ok, res} = CPSolver.solve(model) 44 | 45 | Enum.map_join(res.solutions, "\n OR \n", fn sol -> 46 | sol 47 | |> Enum.zip(res.variables) 48 | |> Enum.take(num_appetizers) 49 | |> Enum.reject(fn {q, _name} -> q == 0 end) 50 | |> Enum.map_join(", ", fn {q, name} -> "#{name} : #{q}" end) 51 | end) 52 | |> IO.puts() 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /data/knapsack/ks_82_0: -------------------------------------------------------------------------------- 1 | 82 104723596 2 | 13211 13211 3 | 26422 26422 4 | 52844 52844 5 | 105688 105688 6 | 211376 211376 7 | 422752 422752 8 | 845504 845504 9 | 1691008 1691008 10 | 3382016 3382016 11 | 6764032 6764032 12 | 13528064 13528064 13 | 27056128 27056128 14 | 54112256 54112256 15 | 13212 13212 16 | 26424 26424 17 | 52848 52848 18 | 105696 105696 19 | 211392 211392 20 | 422784 422784 21 | 845568 845568 22 | 1691136 1691136 23 | 3382272 3382272 24 | 6764544 6764544 25 | 13529088 13529088 26 | 27058176 27058176 27 | 54116352 54116352 28 | 39638 39638 29 | 79276 79276 30 | 158552 158552 31 | 317104 317104 32 | 634208 634208 33 | 1268416 1268416 34 | 2536832 2536832 35 | 5073664 5073664 36 | 10147328 10147328 37 | 20294656 20294656 38 | 40589312 40589312 39 | 81178624 81178624 40 | 52844 52844 41 | 105688 105688 42 | 211376 211376 43 | 422752 422752 44 | 845504 845504 45 | 1691008 1691008 46 | 3382016 3382016 47 | 6764032 6764032 48 | 13528064 13528064 49 | 27056128 27056128 50 | 54112256 54112256 51 | 66060 66060 52 | 132120 132120 53 | 264240 264240 54 | 528480 528480 55 | 1056960 1056960 56 | 2113920 2113920 57 | 4227840 4227840 58 | 8455680 8455680 59 | 16911360 16911360 60 | 33822720 33822720 61 | 67645440 67645440 62 | 79268 79268 63 | 158536 158536 64 | 317072 317072 65 | 634144 634144 66 | 1268288 1268288 67 | 2536576 2536576 68 | 5073152 5073152 69 | 10146304 10146304 70 | 20292608 20292608 71 | 40585216 40585216 72 | 81170432 81170432 73 | 92482 92482 74 | 184964 184964 75 | 369928 369928 76 | 739856 739856 77 | 1479712 1479712 78 | 2959424 2959424 79 | 5918848 5918848 80 | 11837696 11837696 81 | 23675392 23675392 82 | 47350784 47350784 83 | 94701568 94701568 84 | -------------------------------------------------------------------------------- /data/knapsack/ks_100_0: -------------------------------------------------------------------------------- 1 | 100 100000 2 | 90000 90001 3 | 89750 89751 4 | 10001 10002 5 | 89500 89501 6 | 10252 10254 7 | 89250 89251 8 | 10503 10506 9 | 89000 89001 10 | 10754 10758 11 | 88750 88751 12 | 11005 11010 13 | 88500 88501 14 | 11256 11262 15 | 88250 88251 16 | 11507 11514 17 | 88000 88001 18 | 11758 11766 19 | 87750 87751 20 | 12009 12018 21 | 87500 87501 22 | 12260 12270 23 | 87250 87251 24 | 12511 12522 25 | 87000 87001 26 | 12762 12774 27 | 86750 86751 28 | 13013 13026 29 | 86500 86501 30 | 13264 13278 31 | 86250 86251 32 | 13515 13530 33 | 86000 86001 34 | 13766 13782 35 | 85750 85751 36 | 14017 14034 37 | 85500 85501 38 | 14268 14286 39 | 85250 85251 40 | 14519 14538 41 | 85000 85001 42 | 14770 14790 43 | 84750 84751 44 | 15021 15042 45 | 84500 84501 46 | 15272 15294 47 | 84250 84251 48 | 15523 15546 49 | 84000 84001 50 | 15774 15798 51 | 83750 83751 52 | 16025 16050 53 | 83500 83501 54 | 16276 16302 55 | 83250 83251 56 | 16527 16554 57 | 83000 83001 58 | 16778 16806 59 | 82750 82751 60 | 17029 17058 61 | 82500 82501 62 | 17280 17310 63 | 82250 82251 64 | 17531 17562 65 | 82000 82001 66 | 17782 17814 67 | 81750 81751 68 | 18033 18066 69 | 81500 81501 70 | 18284 18318 71 | 81250 81251 72 | 18535 18570 73 | 81000 81001 74 | 18786 18822 75 | 80750 80751 76 | 19037 19074 77 | 80500 80501 78 | 19288 19326 79 | 80250 80251 80 | 19539 19578 81 | 80000 80001 82 | 19790 19830 83 | 79750 79751 84 | 20041 20082 85 | 79500 79501 86 | 20292 20334 87 | 79250 79251 88 | 20543 20586 89 | 79000 79001 90 | 20794 20838 91 | 78750 78751 92 | 21045 21090 93 | 78500 78501 94 | 21296 21342 95 | 78250 78251 96 | 21547 21594 97 | 78000 78001 98 | 21798 21846 99 | 77750 77751 100 | 22049 22098 101 | 77500 77501 102 | -------------------------------------------------------------------------------- /data/qap/qap12.txt: -------------------------------------------------------------------------------- 1 | 12 2 | 3 | 0 90 10 23 43 0 0 0 0 0 0 0 4 | 90 0 0 0 0 88 0 0 0 0 0 0 5 | 10 0 0 0 0 0 26 16 0 0 0 0 6 | 23 0 0 0 0 0 0 0 0 0 0 0 7 | 43 0 0 0 0 0 0 0 0 0 0 0 8 | 0 88 0 0 0 0 0 0 1 0 0 0 9 | 0 0 26 0 0 0 0 0 0 0 0 0 10 | 0 0 16 0 0 0 0 0 0 96 0 0 11 | 0 0 0 0 0 1 0 0 0 0 29 0 12 | 0 0 0 0 0 0 0 96 0 0 0 37 13 | 0 0 0 0 0 0 0 0 29 0 0 0 14 | 0 0 0 0 0 0 0 0 0 37 0 0 15 | 16 | 0 36 54 26 59 72 9 34 79 17 46 95 17 | 36 0 73 35 90 58 30 78 35 44 79 36 18 | 54 73 0 21 10 97 58 66 69 61 54 63 19 | 26 35 21 0 93 12 46 40 37 48 68 85 20 | 59 90 10 93 0 64 5 29 76 16 5 76 21 | 72 58 97 12 64 0 96 55 38 54 0 34 22 | 9 30 58 46 5 96 0 83 35 11 56 37 23 | 34 78 66 40 29 55 83 0 44 12 15 80 24 | 79 35 69 37 76 38 35 44 0 64 39 33 25 | 17 44 61 48 16 54 11 12 64 0 70 86 26 | 46 79 54 68 5 0 56 15 39 70 0 18 27 | 95 36 63 85 76 34 37 80 33 86 18 0 -------------------------------------------------------------------------------- /lib/solver/variables/api_interface.ex: -------------------------------------------------------------------------------- 1 | defprotocol CPSolver.Variable.Interface do 2 | alias CPSolver.Variable 3 | alias CPSolver.Variable.View 4 | alias CPSolver.Common 5 | 6 | @spec id(Variable.t() | View.t()) :: reference() 7 | def id(variable) 8 | 9 | @spec variable(Variable.t() | View.t() | any()) :: Variable.t() | nil 10 | @fallback_to_any true 11 | def variable(arg) 12 | 13 | @spec map(Variable.t() | View.t(), integer()) :: integer() 14 | def map(variable, value) 15 | 16 | @spec iterator(Variable.t() | View.t(), Keyword.t()) :: any() 17 | def iterator(variable, opts \\ []) 18 | 19 | @spec domain(Variable.t() | View.t()) :: any() 20 | def domain(variable) 21 | 22 | @spec size(Variable.t() | View.t()) :: non_neg_integer() 23 | def size(variable) 24 | 25 | @spec min(Variable.t() | View.t()) :: integer() 26 | def min(variable) 27 | 28 | @spec max(Variable.t() | View.t()) :: integer() 29 | def max(variable) 30 | 31 | @spec contains?(Variable.t() | View.t(), integer()) :: boolean() 32 | def contains?(variable, value) 33 | 34 | @spec fixed?(Variable.t() | View.t()) :: boolean() 35 | def fixed?(variable) 36 | 37 | @spec remove(Variable.t() | View.t(), integer()) :: Common.domain_change() | :no_change 38 | def remove(variable, value) 39 | 40 | @spec removeAbove(Variable.t() | View.t(), integer()) :: Common.domain_change() | :no_change 41 | def removeAbove(variable, value) 42 | 43 | @spec removeBelow(Variable.t() | View.t(), integer()) :: Common.domain_change() | :no_change 44 | def removeBelow(variable, value) 45 | 46 | @spec fix(Variable.t() | View.t(), integer()) :: :fixed | :fail 47 | def fix(variable, value) 48 | 49 | @spec update(Variable.t() | View.t(), atom(), any()) :: Variable.t() | View.t() 50 | def update(variable, field, value) 51 | end 52 | -------------------------------------------------------------------------------- /test/examples/graph_coloring_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Examples.GraphColoring do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.Examples.GraphColoring 5 | 6 | test "P3" do 7 | test_graph("p3", 2, trials: 10) 8 | end 9 | 10 | test "P4" do 11 | test_graph("p4", 2, trials: 10) 12 | end 13 | 14 | test "Triangle" do 15 | test_graph("triangle", 6, trials: 10) 16 | end 17 | 18 | test "Square" do 19 | test_graph("square", 2, trials: 10) 20 | end 21 | 22 | test "Paw" do 23 | test_graph("paw", 12, timeout: 100, trials: 10) 24 | end 25 | 26 | test "Petersen" do 27 | test_graph("petersen", 120, timeout: 500, trials: 5) 28 | end 29 | 30 | test "gc_15_30_1" do 31 | test_graph("gc_15_30_1", 36) 32 | end 33 | 34 | @tag :slow 35 | test "gc_15_30_5" do 36 | test_graph("gc_15_30_5", 34848, timeout: 2000, trials: 1) 37 | end 38 | 39 | test "gc_15_30_3" do 40 | test_graph("gc_15_30_3", 12) 41 | end 42 | 43 | test "Multiple P4 runs" do 44 | test_graph("p4", 2, trials: 20) 45 | end 46 | 47 | test "Multiple P3 runs" do 48 | test_graph("p3", 2, trials: 20) 49 | end 50 | 51 | defp test_graph(graph_name, expected_solutions, opts \\ []) do 52 | opts = 53 | Keyword.merge([timeout: 100, trials: 5], opts) 54 | 55 | instance = "data/graph_coloring/#{graph_name}" 56 | 57 | Enum.each(1..opts[:trials], fn _ -> 58 | {:ok, result} = CPSolver.solve(GraphColoring.model(instance), timeout: opts[:timeout]) 59 | Enum.each(result.solutions, fn sol -> assert_solution(sol, instance) end) 60 | solution_count = result.statistics.solution_count 61 | 62 | assert solution_count == expected_solutions 63 | end) 64 | end 65 | 66 | defp assert_solution(solution, instance) do 67 | assert GraphColoring.check_solution(solution, instance) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/solver/constraints/reified.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint.Reified do 2 | @moduledoc """ 3 | Reified (equivalence) constraint. 4 | Extends constraint C to constraint R(C, b), 5 | where 'b' is a boolean variable, and 6 | C holds iff b is fixed to true 7 | """ 8 | use CPSolver.Constraint 9 | alias CPSolver.Propagator.Reified, as: ReifPropagator 10 | 11 | def new(constraint, b) do 12 | new([constraint, b]) 13 | end 14 | 15 | @impl true 16 | def propagators([constraint, b]) do 17 | [reified_propagator(constraint, b, :full)] 18 | end 19 | 20 | def reified_propagator(constraint, b, mode) when mode in [:full, :half, :inverse_half] do 21 | ReifPropagator.new(Constraint.constraint_to_propagators(constraint), b, mode) 22 | end 23 | end 24 | 25 | defmodule CPSolver.Constraint.HalfReified do 26 | @moduledoc """ 27 | Half-reified (implication) constraint. 28 | Extends constraint C to constraint R(C, b), 29 | where 'b' is a boolean variable, and 30 | b is fixed to true if C holds 31 | """ 32 | use CPSolver.Constraint 33 | alias CPSolver.Constraint.Reified 34 | 35 | def new(constraint, b) do 36 | new([constraint, b]) 37 | end 38 | 39 | @impl true 40 | def propagators([constraint, b]) do 41 | [Reified.reified_propagator(constraint, b, :half)] 42 | end 43 | end 44 | 45 | defmodule CPSolver.Constraint.InverseHalfReified do 46 | @moduledoc """ 47 | Inverse half-reified (inverse implication) constraint. 48 | Extends constraint C to constraint R(C, b), 49 | where 'b' is a boolean variable, and 50 | C holds if b is fixed to true. 51 | """ 52 | use CPSolver.Constraint 53 | alias CPSolver.Constraint.Reified 54 | 55 | def new(constraint, b) do 56 | new([constraint, b]) 57 | end 58 | 59 | @impl true 60 | def propagators([constraint, b]) do 61 | [Reified.reified_propagator(constraint, b, :inverse_half)] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/constraints/arithmetics_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Arithmetics do 2 | use ExUnit.Case, async: false 3 | 4 | describe "Arithmetics" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Constraint.Factory, as: ConstraintFactory 7 | alias CPSolver.Variable.View.Factory, as: ViewFactory 8 | alias CPSolver.Model 9 | alias CPSolver.Constraint.Equal 10 | 11 | test "add 2 variables" do 12 | {:ok, res} = setup_and_solve(:add_variable) 13 | 14 | assert Enum.all?(res.solutions, fn [x_val, y_val, sum_val] -> 15 | x_val + y_val == sum_val 16 | end) 17 | end 18 | 19 | test "subtract 2 variables" do 20 | {:ok, res} = setup_and_solve(:subtract_variable) 21 | 22 | assert Enum.all?(res.solutions, fn [x_val, y_val, sum_val] -> 23 | x_val - y_val == sum_val 24 | end) 25 | end 26 | 27 | test "add variable and constant" do 28 | x_domain = 1..10 29 | c = 3 30 | x = Variable.new(x_domain, name: "x") 31 | model = Model.new([x], [Equal.new(ViewFactory.inc(x, c), 10)]) 32 | {:ok, res} = CPSolver.solve(model) 33 | 34 | assert length(res.solutions) == 1 35 | assert hd(hd(res.solutions)) == 10 - c 36 | end 37 | 38 | defp setup_and_solve(constraint_kind) 39 | when constraint_kind in [:add_variable, :subtract_variable] do 40 | x_domain = 0..2 41 | y_domain = 0..2 42 | x = Variable.new(x_domain, name: "x") 43 | y = Variable.new(y_domain, name: "y") 44 | 45 | {_sum_var, constraint} = 46 | case constraint_kind do 47 | :add_variable -> ConstraintFactory.add(x, y) 48 | :subtract_variable -> ConstraintFactory.subtract(x, y) 49 | end 50 | 51 | model = Model.new([x, y], [constraint]) 52 | {:ok, _res} = CPSolver.solve(model) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/space/space_propagation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.SpacePropagation do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Space.Propagation 6 | alias CPSolver.Test.Helpers 7 | import CPSolver.Utils 8 | 9 | test "Propagation on stable space" do 10 | %{ 11 | propagators: _propagators, 12 | variables: [x, y, z] = _variables, 13 | constraint_graph: graph 14 | } = stable_setup() 15 | 16 | :solved = Propagation.run(graph, %{}) 17 | 18 | assert Variable.fixed?(x) && Variable.fixed?(z) 19 | ## Check not_equal(x, z) 20 | assert Variable.min(x) != Variable.min(z) 21 | refute Variable.fixed?(y) 22 | 23 | ## All values of reduced domain of 'y' participate in proper solutions. 24 | assert Enum.all?(domain_values(y), fn y_value -> 25 | y_value != Variable.min(x) && y_value != Variable.min(z) 26 | end) 27 | end 28 | 29 | test "Propagation on solvable space" do 30 | %{variables: variables, constraint_graph: graph} = 31 | solved_setup() 32 | 33 | refute Enum.all?(variables, fn var -> Variable.fixed?(var) end) 34 | assert :solved == Propagation.run(graph, %{}) 35 | assert Enum.all?(variables, fn var -> Variable.fixed?(var) end) 36 | end 37 | 38 | test "Propagation on failed space" do 39 | %{constraint_graph: graph} = fail_setup() 40 | assert {:fail, _propagator_id} = Propagation.run(graph, %{}) 41 | end 42 | 43 | defp stable_setup() do 44 | x = 1..1 45 | y = -5..5 46 | z = 0..1 47 | 48 | space_setup(x, y, z) 49 | end 50 | 51 | defp solved_setup() do 52 | x = 1..1 53 | y = 0..2 54 | z = 0..1 55 | 56 | space_setup(x, y, z) 57 | end 58 | 59 | defp fail_setup() do 60 | x = 1..1 61 | y = 0..1 62 | z = 0..1 63 | 64 | space_setup(x, y, z) 65 | end 66 | 67 | defp space_setup(x, y, z) do 68 | Helpers.space_setup(x, y, z) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/solver/search/search.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search do 2 | alias CPSolver.DefaultDomain, as: Domain 3 | 4 | alias CPSolver.Search.VariableSelector 5 | alias CPSolver.Search.Partition 6 | 7 | require Logger 8 | 9 | def default_strategy() do 10 | { 11 | :first_fail, 12 | :indomain_min 13 | } 14 | end 15 | 16 | def initialize({variable_choice, value_choice} = _search, space_data) do 17 | { 18 | VariableSelector.initialize(variable_choice, space_data), 19 | Partition.initialize(value_choice, space_data) 20 | } 21 | end 22 | 23 | ### Helpers 24 | 25 | def branch(variables, {variable_choice, partition_strategy}, data \\ %{}) do 26 | branch(variables, variable_choice, partition_strategy, data) 27 | end 28 | 29 | def branch(variables, variable_choice, partition_strategy, data) do 30 | case VariableSelector.select_variable(variables, data, variable_choice) do 31 | nil -> 32 | [] 33 | 34 | selected_variable -> 35 | {:ok, domain_partitions} = 36 | Partition.partition(selected_variable, partition_strategy) 37 | 38 | partitions(selected_variable, domain_partitions, variables, data) 39 | end 40 | end 41 | 42 | defp set_domain(variable, domain) do 43 | Map.put(variable, :domain, domain) 44 | end 45 | 46 | defp partitions(selected_variable, domain_partitions, variables, data) when is_list(variables) do 47 | partitions(selected_variable, domain_partitions, Arrays.new(variables, implementation: Aja.Vector), data) 48 | end 49 | 50 | defp partitions(selected_variable, domain_partitions, variables, _data) do 51 | Enum.map(domain_partitions, fn {domain, constraint} -> 52 | {Arrays.map(variables, fn var -> 53 | domain_copy = 54 | ((var.id == selected_variable.id && domain) || var.domain) 55 | |> Domain.copy() 56 | 57 | set_domain(var, domain_copy) 58 | end), 59 | constraint} 60 | end) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :fixpoint, 7 | version: "0.16.1", 8 | elixir: "~> 1.18", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: description(), 12 | docs: docs(), 13 | package: package(), 14 | name: "Fixpoint" 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {CPSolver.Application, []} 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:libgraph, "~> 0.16.0"}, 30 | {:bitgraph, "~> 0.3.4"}, 31 | {:iterex, "~> 0.1"}, 32 | {:arrays, "~> 2.1"}, 33 | {:arrays_aja, "~> 0.2.0"}, 34 | {:math, "~> 0.7.0", only: :test}, 35 | {:permutation, "~> 0.1.0", only: [:dev, :test]}, 36 | {:ex_doc, ">= 0.0.0", only: [:dev, :test], runtime: false}, 37 | {:dialyxir, "~> 1.2", only: [:dev], runtime: false}, 38 | {:ex_united, "~> 0.1.5", only: :test}, 39 | {:local_cluster, "~> 1.2", only: :test}, 40 | {:replbug, "~> 1.0.2", only: :dev} 41 | ] 42 | end 43 | 44 | defp description() do 45 | "Constraint Programming Solver" 46 | end 47 | 48 | defp docs do 49 | [ 50 | main: "readme", 51 | formatter_opts: [gfm: true], 52 | extras: [ 53 | "README.md" 54 | ] 55 | ] 56 | end 57 | 58 | defp package() do 59 | [ 60 | # This option is only needed when you don't want to use the OTP application name 61 | name: "fixpoint", 62 | # These are the default files included in the package 63 | files: ~w(lib src test data .formatter.exs mix.exs README* LICENSE* 64 | ), 65 | exclude_patterns: ["misc/**", "scripts/**", "**/*._exs", "**/*._ex"], 66 | licenses: ["MIT"], 67 | links: %{"GitHub" => "https://github.com/bokner/fixpoint"} 68 | ] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /scripts/space_propagation_debugging.exs: -------------------------------------------------------------------------------- 1 | defmodule PropagationDebug do 2 | alias CPSolver.IntVariable, as: Variable 3 | alias CPSolver.Propagator.NotEqual 4 | alias CPSolver.ConstraintStore 5 | alias CPSolver.Propagator 6 | alias CPSolver.Space.Propagation 7 | alias CPSolver.Propagator.ConstraintGraph 8 | 9 | def setup() do 10 | Replbug.start( 11 | ["CPSolver.Propagator.filter/_", "CPSolver.Space.Propagation.reschedule/_"], 12 | time: :timer.minutes(10), 13 | msgs: 100 14 | ) 15 | end 16 | 17 | def run() do 18 | x = 1..1 19 | y = 1..2 20 | z = 1..3 21 | %{propagators: propagators, constraint_graph: graph, store: store} = space_setup(x, y, z) 22 | {_scheduled_propagators, _reduced_graph} = Propagation.propagate(propagators, graph, store) 23 | end 24 | 25 | def analyze() do 26 | traces = Replbug.stop() 27 | 28 | calls = 29 | Replbug.calls(traces) 30 | |> Map.values() 31 | |> List.flatten() 32 | |> Enum.sort_by(fn c -> c.call_timestamp end, Time) 33 | 34 | Enum.map(calls, fn c -> 35 | {c.call_timestamp, c.function, 36 | (c.function == :reschedule && 37 | {Enum.at(c.args, 1), c.return |> Enum.map(fn {_p_id, p} -> p.name end)}) || 38 | {hd(c.args).name, c.return}} 39 | end) 40 | end 41 | 42 | defp space_setup(x, y, z) do 43 | variables = 44 | Enum.map([{x, "x"}, {y, "y"}, {z, "z"}], fn {d, name} -> Variable.new(d, name: name) end) 45 | 46 | {:ok, [x_var, y_var, z_var] = bound_vars, store} = 47 | ConstraintStore.create_store(variables) 48 | 49 | propagators = 50 | Enum.map( 51 | [{x_var, z_var, "x != z"}, {x_var, y_var, "x != y"}, {y_var, z_var, "y != z"}], 52 | fn {v1, v2, name} -> Propagator.new(NotEqual, [v1, v2], name: name) end 53 | ) 54 | |> Enum.reverse() 55 | 56 | graph = ConstraintGraph.create(propagators) 57 | 58 | %{ 59 | propagators: propagators, 60 | variables: bound_vars, 61 | constraint_graph: ConstraintGraph.remove_fixed(graph, bound_vars), 62 | store: store 63 | } 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/variable/local/most_completed.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.VariableSelector.MostCompleted do 2 | use CPSolver.Search.VariableSelector 3 | alias CPSolver.Propagator.ConstraintGraph 4 | 5 | @impl true 6 | def select(_variables, space_data, _opts) do 7 | most_completed_propagators_selection(space_data[:constraint_graph]) 8 | end 9 | 10 | ## Choose variables connected to "most completed" propagators. 11 | ## 1) Choose propagators with smallest number of still unfixed variables 12 | ## (this corresponds to propagators with smallest degree in constraint graph). 13 | ## 2) Choose variables most constrained by the propagators above. 14 | def most_completed_propagators_selection(constraint_graph) do 15 | ## Make p => (unfixed variables) map 16 | constraint_graph 17 | |> ConstraintGraph.edges() 18 | |> Enum.group_by( 19 | fn edge -> edge.v2 end, 20 | fn edge -> edge.v1 end 21 | ) 22 | ## Pick out the propagators with minimal number of unfixed variables. 23 | ## Build the list of variable ids constrained by those propagators. 24 | |> Enum.reduce( 25 | {[], nil}, 26 | fn {_propagator_id, var_ids}, {var_ids_acc, current_min} = acc -> 27 | var_count = length(var_ids) 28 | 29 | cond do 30 | is_nil(current_min) || var_count < current_min -> {var_ids, var_count} 31 | var_count > current_min -> acc 32 | var_count == current_min -> {var_ids ++ var_ids_acc, var_count} 33 | end 34 | end 35 | ) 36 | |> elem(0) 37 | ## Choose variables with the largest counts of constraints (i.e., attached propagators) 38 | |> Enum.frequencies() 39 | |> Enum.reduce({[], nil}, fn {var_id, var_count}, {vars_acc, current_max} = acc -> 40 | graph_var = ConstraintGraph.get_variable(constraint_graph, var_id) 41 | 42 | cond do 43 | is_nil(current_max) || var_count > current_max -> {[graph_var], var_count} 44 | var_count < current_max -> acc 45 | var_count == current_max -> {[graph_var | vars_acc], var_count} 46 | end 47 | end) 48 | |> elem(0) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/propagators/all_different/all_different_dc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.AllDifferent.DC do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Variable.Interface 6 | alias CPSolver.Propagator 7 | alias CPSolver.Propagator.AllDifferent.DC, as: DC 8 | 9 | describe "Initial filtering" do 10 | test "reduction" do 11 | domains = [1..2, 1, 2..6, 2..6] 12 | 13 | vars = [x0, _x1, x2, x3] = 14 | Enum.map(domains, fn d -> Variable.new(d) end) 15 | 16 | dc_propagator = DC.new(vars) 17 | %{changes: changes, active?: active?} = Propagator.filter(dc_propagator) 18 | 19 | ## Value 1 removed from x0 20 | assert Interface.min(x0) == 2 21 | ## Value 2 is removed from variables x2 and x3 22 | assert Interface.min(x2) == 3 && Interface.min(x3) == 3 23 | ## Changes: x0 is fixed, x2 and x3 chnge their minimum 24 | assert changes[x0.id] == :fixed 25 | assert changes[x2.id] == :min_change 26 | assert changes[x3.id] == :min_change 27 | ## The propagator is active 28 | assert active? 29 | end 30 | 31 | test "cascading filtering" do 32 | ## all variables become fixed, and this will take a single filtering call. 33 | ## 34 | x_vars = 35 | Enum.map([{"x2", 1..2}, {"x1", 1}, {"x3", 1..3}, {"x4", 1..4}, {"x5", 1..5}], fn {name, d} -> 36 | Variable.new(d, name: name) 37 | end) 38 | 39 | dc_propagator = DC.new(x_vars) 40 | %{changes: changes, active?: active?} = Propagator.filter(dc_propagator) 41 | ## The propagator is passive 42 | refute active? 43 | assert map_size(changes) == length(x_vars) - 1 44 | assert Enum.all?(Map.values(changes), fn change -> change == :fixed end) 45 | ## All variables are now fixed 46 | assert Enum.all?(x_vars, &Interface.fixed?/1) 47 | end 48 | 49 | test "inconsistency (pigeonhole)" do 50 | domains = List.duplicate(1..3, 4) 51 | 52 | vars = 53 | Enum.map(domains, fn d -> Variable.new(d) end) 54 | 55 | dc_propagator = DC.new(vars) 56 | assert Propagator.filter(dc_propagator) == :fail 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/constraints/or_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Or do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.BooleanVariable 5 | alias CPSolver.Model 6 | alias CPSolver.Constraint.Or 7 | import CPSolver.Variable.View.Factory 8 | 9 | describe "Or constraint" do 10 | test "`or` functionality" do 11 | bool_vars = Enum.map(1..4, fn i -> BooleanVariable.new(name: "b#{i}") end) 12 | or_constraint = Or.new(bool_vars) 13 | 14 | model = Model.new(bool_vars, [or_constraint]) 15 | 16 | {:ok, result} = CPSolver.solve(model) 17 | 18 | assert result.statistics.solution_count == 15 19 | assert_or(result.solutions, length(bool_vars)) 20 | end 21 | 22 | test "inconsistency (all-false)" do 23 | bool_vars = List.duplicate(0, 4) 24 | 25 | or_constraint = Or.new(bool_vars) 26 | 27 | assert catch_throw({:fail, _} = Model.new(bool_vars, [or_constraint])) 28 | end 29 | 30 | test "with negation vars" do 31 | x = BooleanVariable.new(name: "x") 32 | not_y = negation(BooleanVariable.new(name: "y")) 33 | vars = [x, not_y] 34 | or_constraint = Or.new(vars) 35 | model = Model.new(vars, [or_constraint]) 36 | 37 | {:ok, result} = CPSolver.solve(model) 38 | assert [0, 0] in result.solutions 39 | end 40 | 41 | test "peformance" do 42 | n = 1000 43 | bool_vars = Enum.map(1..n, fn i -> BooleanVariable.new(name: "b#{i}") end) 44 | or_constraint = Or.new(bool_vars) 45 | 46 | model = Model.new(bool_vars, [or_constraint]) 47 | 48 | {:ok, res} = 49 | CPSolver.solve(model, 50 | stop_on: {:max_solutions, 1}, 51 | search: {:first_fail, :indomain_max}, 52 | space_threads: 1 53 | ) 54 | 55 | assert res.statistics.solution_count >= 1 56 | ## Arbitrary elapsed time, the main point it shouldn't be too big 57 | assert res.statistics.elapsed_time < 250_000 58 | end 59 | 60 | defp assert_or(solutions, array_len) do 61 | assert Enum.all?(solutions, fn solution -> 62 | arr = Enum.take(solution, array_len) 63 | Enum.sum(arr) > 0 64 | end) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/solver/common/common.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Common do 2 | alias CPSolver.Variable 3 | alias CPSolver.Variable.View 4 | 5 | @type domain_change :: :fixed | :domain_change | :min_change | :max_change 6 | @type domain_get_operation :: :size | :fixed? | :min | :max | :contains? 7 | @type domain_update_operation :: :remove | :removeAbove | :removeBelow | :fix 8 | 9 | @type variable_or_view :: Variable.t() | View.t() 10 | 11 | def domain_events() do 12 | [:fixed, :domain_change, :min_change, :max_change] 13 | end 14 | 15 | ## Value for unfixed variables. 16 | ## 17 | def unfixed() do 18 | :atomics.new(1, signed: true) 19 | |> :atomics.info() 20 | |> Map.get(:max) 21 | end 22 | 23 | ## Choose a "stronger" domain change 24 | ## from two. 25 | ## "Stronger" domain change implies the weaker one. 26 | ## For instance, 27 | ## - :bound_change implies :domain_change; 28 | ## - :fixed implies all domain changes. 29 | ## - :domain_change implies no other domain changes. 30 | 31 | def stronger_domain_change(nil, new_change) do 32 | new_change 33 | end 34 | 35 | def stronger_domain_change(new_change, nil) do 36 | new_change 37 | end 38 | 39 | def stronger_domain_change(:fixed, _new_change) do 40 | :fixed 41 | end 42 | 43 | def stronger_domain_change(_current_change, :fixed) do 44 | :fixed 45 | end 46 | 47 | def stronger_domain_change(:domain_change, new_change) do 48 | new_change 49 | end 50 | 51 | def stronger_domain_change(current_change, :domain_change) do 52 | current_change 53 | end 54 | 55 | def stronger_domain_change(:bound_change, bound_change) 56 | when bound_change in [:min_change, :max_change] do 57 | bound_change 58 | end 59 | 60 | def stronger_domain_change(bound_change, :bound_change) 61 | when bound_change in [:min_change, :max_change] do 62 | bound_change 63 | end 64 | 65 | def stronger_domain_change(:min_change, :max_change) do 66 | :bound_change 67 | end 68 | 69 | def stronger_domain_change(:max_change, :min_change) do 70 | :bound_change 71 | end 72 | 73 | def stronger_domain_change(current_change, new_change) when current_change == new_change do 74 | current_change 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/solver/core/constraint.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Constraint do 2 | alias CPSolver.Variable.Interface 3 | alias CPSolver.Propagator 4 | 5 | @callback new(args :: list()) :: Constraint.t() 6 | @callback propagators(args :: list()) :: [atom()] 7 | @callback arguments(args :: list()) :: list() 8 | 9 | defmacro __using__(_) do 10 | quote do 11 | @behaviour CPSolver.Constraint 12 | alias CPSolver.Constraint 13 | alias CPSolver.Common 14 | 15 | def new(args) do 16 | Constraint.new(__MODULE__, arguments(args)) 17 | end 18 | 19 | def arguments(args) do 20 | args 21 | end 22 | 23 | defoverridable new: 1, arguments: 1 24 | end 25 | end 26 | 27 | def new(constraint_impl, args) do 28 | (Enum.empty?(args) && throw({constraint_impl, :no_args})) || 29 | {constraint_impl, args} 30 | end 31 | 32 | def constraint_to_propagators(constraint, reducer_fun \\ &Function.identity/1) 33 | 34 | def constraint_to_propagators({constraint_mod, args}, reducer_fun) when is_list(args) do 35 | List.foldr(constraint_mod.propagators(args), [], fn p, plist_acc -> 36 | case reducer_fun.(p) do 37 | nil -> plist_acc 38 | p_result -> 39 | [p_result | plist_acc] 40 | end 41 | end) 42 | end 43 | 44 | def constraint_to_propagators(constraint, reducer_fun) when is_tuple(constraint) do 45 | [constraint_mod | args] = Tuple.to_list(constraint) 46 | constraint_to_propagators({constraint_mod, args}, reducer_fun) 47 | end 48 | 49 | def post(constraint) when is_tuple(constraint) do 50 | constraint_to_propagators(constraint, 51 | fn p -> 52 | case Propagator.filter(p, reset?: true, changes: %{}) do 53 | :fail -> throw({:fail, p.id}) 54 | %{state: _state, active?: true, changes: _changes} -> p 55 | %{active?: false} -> nil 56 | end 57 | end) 58 | end 59 | 60 | def extract_variables(constraint) do 61 | constraint 62 | |> constraint_to_propagators() 63 | |> Enum.map(fn p -> 64 | p 65 | |> Propagator.variables() 66 | |> Enum.map(fn var -> Interface.variable(var) end) 67 | end) 68 | |> List.flatten() 69 | |> Enum.uniq() 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /data/tsp/tsp_15.txt: -------------------------------------------------------------------------------- 1 | 15 2 | 0 29 82 46 68 52 72 42 51 55 29 74 23 72 46 3 | 29 0 55 46 42 43 43 23 23 31 41 51 11 52 21 4 | 82 55 0 68 46 55 23 43 41 29 79 21 64 31 51 5 | 46 46 68 0 82 15 72 31 62 42 21 51 51 43 64 6 | 68 42 46 82 0 74 23 52 21 46 82 58 46 65 23 7 | 52 43 55 15 74 0 61 23 55 31 33 37 51 29 59 8 | 72 43 23 72 23 61 0 42 23 31 77 37 51 46 33 9 | 42 23 43 31 52 23 42 0 33 15 37 33 33 31 37 10 | 51 23 41 62 21 55 23 33 0 29 62 46 29 51 11 11 | 55 31 29 42 46 31 31 15 29 0 51 21 41 23 37 12 | 29 41 79 21 82 33 77 37 62 51 0 65 42 59 61 13 | 74 51 21 51 58 37 37 33 46 21 65 0 61 11 55 14 | 23 11 64 51 46 51 51 33 29 41 42 61 0 62 23 15 | 72 52 31 43 65 29 46 31 51 23 59 11 62 0 59 16 | 46 21 51 64 23 59 33 37 11 37 61 55 23 59 0 17 | -------------------------------------------------------------------------------- /test/constraints/minimum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.Minimum do 2 | use ExUnit.Case, async: false 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Model 6 | alias CPSolver.Constraint.Minimum 7 | alias CPSolver.Constraint.Factory, as: ConstraintFactory 8 | import CPSolver.Variable.View.Factory 9 | 10 | describe "Minimum constraint" do 11 | test "`minimum` functionality" do 12 | x_arr = Enum.map(1..4, fn idx -> Variable.new(0..4, name: "x#{idx}") end) 13 | y = Variable.new(-5..20, name: "y") 14 | 15 | model = Model.new([y | x_arr], [Minimum.new(y, x_arr)]) 16 | 17 | {:ok, result} = CPSolver.solve(model) 18 | 19 | assert_minimum(result.solutions) 20 | assert result.statistics.solution_count == 625 21 | end 22 | 23 | test "Factory" do 24 | x_arr = Enum.map(1..4, fn idx -> Variable.new(0..4, name: "x#{idx}") end) 25 | {max_var, maximum_constraint} = ConstraintFactory.minimum(x_arr) 26 | 27 | assert Variable.min(max_var) == 0 28 | assert Variable.max(max_var) == 4 29 | 30 | model = Model.new([], [maximum_constraint]) 31 | 32 | {:ok, result} = CPSolver.solve(model) 33 | 34 | assert_minimum(result.solutions) 35 | assert result.statistics.solution_count == 625 36 | end 37 | 38 | test "Consistent with Maximum" do 39 | x_arr = Enum.map(1..4, fn idx -> minus(Variable.new(0..4, name: "x#{idx}")) end) 40 | {negative_max_var, negative_maximum_constraint} = ConstraintFactory.minimum(x_arr) 41 | 42 | assert Variable.min(negative_max_var) == -4 43 | assert Variable.max(negative_max_var) == 0 44 | 45 | model = Model.new([], [negative_maximum_constraint]) 46 | 47 | {:ok, result} = CPSolver.solve(model) 48 | 49 | assert_maximum(result.solutions, fn y -> -y end) 50 | assert result.statistics.solution_count == 625 51 | end 52 | 53 | ## Constraint check: y = min(x_array) 54 | ## 55 | defp assert_minimum(solutions) do 56 | assert Enum.all?(solutions, fn [y | xs] -> 57 | y == Enum.min(xs) 58 | end) 59 | end 60 | 61 | defp assert_maximum(solutions, transform_fun) do 62 | assert Enum.all?(solutions, fn [y | xs] -> 63 | transform_fun.(y) == Enum.max(xs) 64 | end) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /data/tsp/tsp_26.txt: -------------------------------------------------------------------------------- 1 | 26 2 | 0 83 93 129 133 139 151 169 135 114 110 98 99 95 81 152 159 181 172 185 147 157 185 220 127 181 3 | 83 0 40 53 62 64 91 116 93 84 95 98 89 68 67 127 156 175 152 165 160 180 223 268 179 197 4 | 93 40 0 42 42 49 59 81 54 44 58 64 54 31 36 86 117 135 112 125 124 147 193 241 157 161 5 | 129 53 42 0 11 11 46 72 65 70 88 100 89 66 76 102 142 156 127 139 155 180 228 278 197 190 6 | 133 62 42 11 0 9 35 61 55 62 82 95 84 62 74 93 133 146 117 128 148 173 222 272 194 182 7 | 139 64 49 11 9 0 39 65 63 71 90 103 92 71 82 100 141 153 124 135 156 181 230 280 202 190 8 | 151 91 59 46 35 39 0 26 34 52 71 88 77 63 78 66 110 119 88 98 130 156 206 257 188 160 9 | 169 116 81 72 61 65 26 0 37 59 75 92 83 76 91 54 98 103 70 78 122 148 198 250 188 148 10 | 135 93 54 65 55 63 34 37 0 22 39 56 47 40 55 37 78 91 62 74 96 122 172 223 155 128 11 | 114 84 44 70 62 71 52 59 22 0 20 36 26 20 34 43 74 91 68 82 86 111 160 210 136 121 12 | 110 95 58 88 82 90 71 75 39 20 0 18 11 27 32 42 61 80 64 77 68 92 140 190 116 103 13 | 98 98 64 100 95 103 88 92 56 36 18 0 11 34 31 56 63 85 75 87 62 83 129 178 100 99 14 | 99 89 54 89 84 92 77 83 47 26 11 11 0 23 24 53 68 89 74 87 71 93 140 189 111 107 15 | 95 68 31 66 62 71 63 76 40 20 27 34 23 0 15 62 87 106 87 100 93 116 163 212 132 130 16 | 81 67 36 76 74 82 78 91 55 34 32 31 24 15 0 73 92 112 96 109 93 113 158 205 122 130 17 | 152 127 86 102 93 100 66 54 37 43 42 56 53 62 73 0 44 54 26 39 68 94 144 196 139 95 18 | 159 156 117 142 133 141 110 98 78 74 61 63 68 87 92 44 0 22 34 38 30 53 102 154 109 51 19 | 181 175 135 156 146 153 119 103 91 91 80 85 89 106 112 54 22 0 33 29 46 64 107 157 125 51 20 | 172 152 112 127 117 124 88 70 62 68 64 75 74 87 96 26 34 33 0 13 63 87 135 186 141 81 21 | 185 165 125 139 128 135 98 78 74 82 77 87 87 100 109 39 38 29 13 0 68 90 136 186 148 79 22 | 147 160 124 155 148 156 130 122 96 86 68 62 71 93 93 68 30 46 63 68 0 26 77 128 80 37 23 | 157 180 147 180 173 181 156 148 122 111 92 83 93 116 113 94 53 64 87 90 26 0 50 102 65 27 24 | 185 223 193 228 222 230 206 198 172 160 140 129 140 163 158 144 102 107 135 136 77 50 0 51 64 58 25 | 220 268 241 278 272 280 257 250 223 210 190 178 189 212 205 196 154 157 186 186 128 102 51 0 93 107 26 | 127 179 157 197 194 202 188 188 155 136 116 100 111 132 122 139 109 125 141 148 80 65 64 93 0 90 27 | 181 197 161 190 182 190 160 148 128 121 103 99 107 130 130 95 51 51 81 79 37 27 58 107 90 0 -------------------------------------------------------------------------------- /test/propagators/maximum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.Maximum do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Propagator 7 | alias CPSolver.Propagator.Maximum 8 | import CPSolver.Variable.Interface 9 | import CPSolver.Utils 10 | 11 | test "Test 1 (scenario: MiniCP, MaximumTest.maximumTest1)" do 12 | [x0, x1, x2] = x_vars = Enum.map(1..3, fn _ -> Variable.new(0..9) end) 13 | y_var = Variable.new(-5..20) 14 | max_propagator = Maximum.new(y_var, x_vars) 15 | 16 | step1 = Propagator.filter(max_propagator) 17 | 18 | assert max(y_var) == 9 19 | assert min(y_var) == 0 20 | 21 | removeAbove(y_var, 8) 22 | 23 | 24 | step2 = Propagator.filter(update_propagator(max_propagator, step1)) 25 | 26 | assert Enum.all?(x_vars, fn x -> max(x) == 8 end) 27 | 28 | removeBelow(y_var, 5) 29 | removeAbove(x0, 2) 30 | removeBelow(x1, 6) 31 | removeBelow(x2, 6) 32 | 33 | step3 = Propagator.filter(update_propagator(max_propagator, step2)) 34 | 35 | assert max(y_var) == 8 36 | assert min(y_var) == 6 37 | 38 | removeBelow(y_var, 7) 39 | removeAbove(x1, 6) 40 | 41 | _step4 = Propagator.filter(update_propagator(max_propagator, step3)) 42 | 43 | assert min(x2) == 7 44 | 45 | end 46 | 47 | test "when 'y' variable is fixed" do 48 | y_var = Variable.new([5], name: "y") 49 | 50 | x_vars = [x0, x1, x2] = 51 | Enum.map([1..5, 1..10, [0, 6]], fn d -> 52 | Variable.new(d) 53 | end) 54 | 55 | max_propagator = Maximum.new(y_var, x_vars) 56 | Propagator.filter(max_propagator) 57 | 58 | assert domain_values(x0) == MapSet.new(1..5) 59 | assert domain_values(x1) == MapSet.new(1..5) 60 | assert min(x2) == 0 and max(x2) == 0 61 | end 62 | 63 | test "fails on inconsistency" do 64 | y_var = Variable.new(6..10) 65 | x1_var = Variable.new(0..4) 66 | x2_var = Variable.new(0..5) 67 | 68 | assert :fail == Propagator.filter(Maximum.new(y_var, [x1_var, x2_var])) 69 | end 70 | 71 | end 72 | 73 | defp update_propagator(propagator, previous_run) do 74 | Map.put(propagator, :state, previous_run.state) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /data/knapsack/ks_106_0: -------------------------------------------------------------------------------- 1 | 106 106925262 2 | 45276 45276 3 | 90552 90552 4 | 181104 181104 5 | 362208 362208 6 | 724416 724416 7 | 1448832 1448832 8 | 2897664 2897664 9 | 5795328 5795328 10 | 11590656 11590656 11 | 23181312 23181312 12 | 46362624 46362624 13 | 92725248 92725248 14 | 70778 70778 15 | 141556 141556 16 | 283112 283112 17 | 566224 566224 18 | 1132448 1132448 19 | 2264896 2264896 20 | 4529792 4529792 21 | 9059584 9059584 22 | 18119168 18119168 23 | 36238336 36238336 24 | 72476672 72476672 25 | 86911 86911 26 | 173822 173822 27 | 347644 347644 28 | 695288 695288 29 | 1390576 1390576 30 | 2781152 2781152 31 | 5562304 5562304 32 | 11124608 11124608 33 | 22249216 22249216 34 | 44498432 44498432 35 | 88996864 88996864 36 | 92634 92634 37 | 185268 185268 38 | 370536 370536 39 | 741072 741072 40 | 1482144 1482144 41 | 2964288 2964288 42 | 5928576 5928576 43 | 11857152 11857152 44 | 23714304 23714304 45 | 47428608 47428608 46 | 94857216 94857216 47 | 97839 97839 48 | 195678 195678 49 | 391356 391356 50 | 782712 782712 51 | 1565424 1565424 52 | 3130848 3130848 53 | 6261696 6261696 54 | 12523392 12523392 55 | 25046784 25046784 56 | 50093568 50093568 57 | 100187136 100187136 58 | 125941 125941 59 | 251882 251882 60 | 503764 503764 61 | 1007528 1007528 62 | 2015056 2015056 63 | 4030112 4030112 64 | 8060224 8060224 65 | 16120448 16120448 66 | 32240896 32240896 67 | 64481792 64481792 68 | 134269 134269 69 | 268538 268538 70 | 537076 537076 71 | 1074152 1074152 72 | 2148304 2148304 73 | 4296608 4296608 74 | 8593216 8593216 75 | 17186432 17186432 76 | 34372864 34372864 77 | 68745728 68745728 78 | 141033 141033 79 | 282066 282066 80 | 564132 564132 81 | 1128264 1128264 82 | 2256528 2256528 83 | 4513056 4513056 84 | 9026112 9026112 85 | 18052224 18052224 86 | 36104448 36104448 87 | 72208896 72208896 88 | 147279 147279 89 | 294558 294558 90 | 589116 589116 91 | 1178232 1178232 92 | 2356464 2356464 93 | 4712928 4712928 94 | 9425856 9425856 95 | 18851712 18851712 96 | 37703424 37703424 97 | 75406848 75406848 98 | 153525 153525 99 | 307050 307050 100 | 614100 614100 101 | 1228200 1228200 102 | 2456400 2456400 103 | 4912800 4912800 104 | 9825600 9825600 105 | 19651200 19651200 106 | 39302400 39302400 107 | 78604800 78604800 108 | -------------------------------------------------------------------------------- /test/propagators/sum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.Sum do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Propagator 7 | alias CPSolver.Propagator.Sum 8 | import CPSolver.Variable.Interface 9 | import CPSolver.Variable.View.Factory 10 | 11 | test "The domain bounds of 'sum' variable are reduced to the sum of bounds of the summands" do 12 | y_var = Variable.new(-100..100, name: "y") 13 | 14 | x_vars = 15 | Enum.map([{"x1", 0..5}, {"x2", 1..5}, {"x3", 0..5}], fn {name, d} -> 16 | Variable.new(d, name: name) 17 | end) 18 | 19 | 20 | Propagator.filter(Sum.new(y_var, x_vars)) 21 | 22 | assert 1 == min(y_var) 23 | assert 15 == max(y_var) 24 | end 25 | 26 | test "Test 2" do 27 | y_var = Variable.new(0..100, name: "y") 28 | 29 | x_vars = 30 | Enum.map([{-5..5, "x1"}, {[1, 2], "x2"}, {[0, 1], "x3"}], fn {d, name} -> 31 | Variable.new(d, name: name) 32 | end) 33 | 34 | [x1_var, _x2_var, _x3_var] = x_vars 35 | 36 | sum_propagator = Sum.new(y_var, x_vars) 37 | 38 | Propagator.filter(sum_propagator) 39 | 40 | assert -3 == min(x1_var) 41 | assert 0 == min(y_var) 42 | assert 8 == max(y_var) 43 | end 44 | 45 | test "when 'y' variable is fixed" do 46 | y_var = Variable.new([5], name: "y") 47 | 48 | x_vars = 49 | Enum.map([{1..5, "x1"}, {[1], "x2"}, {[0, 1], "x3"}], fn {d, name} -> 50 | Variable.new(d, name: name) 51 | end) 52 | 53 | [x1_var, _x2_var, x3_var] = x_vars 54 | 55 | sum_propagator = Sum.new(y_var, x_vars) 56 | 57 | Propagator.filter(sum_propagator) 58 | 59 | assert 4 == max(x1_var) 60 | assert 3 == min(x1_var) 61 | assert 1 == max(x3_var) 62 | assert 0 == min(x3_var) 63 | end 64 | 65 | test "fails on inconsistency" do 66 | y_var = Variable.new(10, name: "y") 67 | x1_var = Variable.new(0..4, name: "x1") 68 | x2_var = Variable.new(0..5, name: "x2") 69 | 70 | assert :fail == Propagator.filter(Sum.new(y_var, [x1_var, x2_var])) 71 | end 72 | 73 | test "when summands are views" do 74 | y_var = Variable.new(50, name: "y") 75 | x1_var = Variable.new(0..2, name: "x1") 76 | x2_var = Variable.new(1..2, name: "x2") 77 | 78 | refute :fail == 79 | Propagator.filter(Sum.new(y_var, [mul(x1_var, 10), mul(x2_var, 20)])) 80 | 81 | assert 1 == Variable.min(x1_var) 82 | assert 1 == Variable.max(x1_var) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/examples/euler43.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Examples.Euler43 do 2 | @moduledoc """ 3 | https://projecteuler.net/problem=43 4 | """ 5 | 6 | @doc """ 7 | int: n = 10; 8 | array[1..n] of var 0..9: x; 9 | array[int] of int: primes = [2,3,5,7,11,13,17]; 10 | solve satisfy; 11 | constraint 12 | all_different(x) /\ 13 | forall(i in 2..8) ( 14 | (100*x[i] + 10*x[i+1] + x[i+2]) mod primes[i-1] = 0 15 | ) 16 | ; 17 | output [ show(x),"\n"]; 18 | """ 19 | 20 | alias CPSolver.IntVariable, as: Variable 21 | alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent 22 | alias CPSolver.Constraint.Modulo 23 | alias CPSolver.Model 24 | import CPSolver.Constraint.Factory 25 | import CPSolver.Variable.View.Factory 26 | 27 | require Logger 28 | 29 | @minizinc_solutions Enum.sort([ 30 | [4, 1, 6, 0, 3, 5, 7, 2, 8, 9], 31 | [1, 4, 6, 0, 3, 5, 7, 2, 8, 9], 32 | [4, 1, 0, 6, 3, 5, 7, 2, 8, 9], 33 | [1, 4, 0, 6, 3, 5, 7, 2, 8, 9], 34 | [4, 1, 3, 0, 9, 5, 2, 8, 6, 7], 35 | [1, 4, 3, 0, 9, 5, 2, 8, 6, 7] 36 | ]) 37 | 38 | def model() do 39 | primes = [2, 3, 5, 7, 11, 13, 17] 40 | domain = 0..9 41 | 42 | x = Enum.map(1..10, fn i -> Variable.new(domain, name: "x#{i}") end) 43 | 44 | all_different_constraint = AllDifferent.new(x) 45 | 46 | constraints = 47 | Enum.reduce(2..8, [all_different_constraint], fn i, constraints_acc -> 48 | x_i = Enum.at(x, i - 1) 49 | x_i_1 = Enum.at(x, i) 50 | x_i_2 = Enum.at(x, i + 1) 51 | prime = Enum.at(primes, i - 2) 52 | {sum_var, sum_constraint} = sum([mul(x_i, 100), mul(x_i_1, 10), x_i_2]) 53 | mod_constraint = Modulo.new(0, sum_var, prime) 54 | [sum_constraint, mod_constraint | constraints_acc] 55 | end) 56 | 57 | Model.new(x, constraints) 58 | end 59 | 60 | def check_solution(solution) do 61 | solution 62 | |> Enum.take(10) 63 | |> Kernel.in(@minizinc_solutions) 64 | end 65 | 66 | def run(opts \\ [search: {:input_order, :indomain_random}, space_threads: 8]) do 67 | {:ok, res} = CPSolver.solve(model(), opts) 68 | 69 | Enum.sort(Enum.map(res.solutions, fn s -> Enum.take(s, 10) end)) 70 | |> tap(fn sorted_solutions -> 71 | (sorted_solutions == @minizinc_solutions && 72 | Logger.notice("Solutions correspond to the ones given by MinZinc")) || 73 | Logger.error("Solutions do not match MiniZinc") 74 | end) 75 | |> Enum.reduce(0, fn s, sum_acc -> Integer.undigits(s) + sum_acc end) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/propagators/circuit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.Circuit do 2 | use ExUnit.Case 3 | 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Variable.Interface 6 | alias CPSolver.Propagator 7 | alias CPSolver.Propagator.Circuit 8 | import CPSolver.Utils 9 | 10 | describe "Propagator filtering" do 11 | test "fixed variables: valid circuit" do 12 | valid_circuit = [2, 3, 4, 0, 5, 1] 13 | result = filter(valid_circuit) 14 | refute result.active? 15 | end 16 | 17 | test "fails on invalid circuit" do 18 | invalid_circuits = 19 | [ 20 | [1, 2, 3, 4, 5, 2], 21 | [1, 2, 0, 4, 5, 3] 22 | ] 23 | 24 | assert Enum.all?(invalid_circuits, fn circuit -> 25 | :fail == 26 | try do 27 | filter(circuit) 28 | catch 29 | x -> x 30 | end 31 | end) 32 | end 33 | 34 | test "domains are cut accordingly on initialization" do 35 | n = 10 36 | domains = Enum.map(0..(n - 1), fn _idx -> -n..n end) 37 | propagator = make_propagator(domains) 38 | Propagator.filter(propagator) 39 | ## args of a propagator are bounded variables 40 | assert_initial_reduction(propagator) 41 | end 42 | 43 | test "filtering" do 44 | domains = [0..2, 0..2, 0..2] 45 | propagator = make_propagator(domains) 46 | [x0, x1, x2] = Arrays.to_list(propagator.args) 47 | res1 = Propagator.filter(propagator) 48 | 49 | assert_initial_reduction(propagator) 50 | 51 | ## Make x1 to be successor of x0 52 | Interface.fix(x0, 1) 53 | 54 | propagator1 = propagator |> Map.put(:state, res1.state) 55 | 56 | _res2 = Propagator.filter(propagator1, changes: %{0 => :fixed}) 57 | 58 | ## x0 is now a successor of x2 59 | assert Interface.fixed?(x2) 60 | assert Interface.min(x2) == 0 61 | ## x1 is also fixed 62 | assert Interface.fixed?(x1) 63 | end 64 | end 65 | 66 | defp filter(domains) do 67 | domains 68 | |> make_propagator() 69 | |> Propagator.filter() 70 | end 71 | 72 | defp make_propagator(domains) do 73 | variables = 74 | Enum.map(Enum.with_index(domains), fn {d, idx} -> Variable.new(d, name: "x#{idx}") end) 75 | Circuit.new(variables) 76 | end 77 | 78 | defp assert_initial_reduction(propagator) do 79 | n = Arrays.size(propagator.args) 80 | 81 | assert Enum.all?( 82 | Enum.with_index(propagator.args), 83 | fn {var, idx} -> 84 | domain = domain_values(var) 85 | MapSet.new(domain) == MapSet.new(0..(n - 1)) |> MapSet.delete(idx) 86 | end 87 | ) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/solver/model/model.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Model do 2 | alias CPSolver.Constraint 3 | alias CPSolver.Propagator 4 | alias CPSolver.IntVariable, as: Variable 5 | alias CPSolver.Variable.Interface 6 | alias CPSolver.Objective 7 | 8 | defstruct [:name, :variables, :constraints, :propagators, :objective, :extra, :id] 9 | 10 | @type t :: %__MODULE__{ 11 | id: reference(), 12 | name: term(), 13 | variables: [Variable.t()], 14 | constraints: [Constraint.t()], 15 | propagators: [Propagator.t()], 16 | objective: Objective.t(), 17 | extra: term() 18 | } 19 | 20 | def new(variables, constraints, opts \\ []) do 21 | constraints = 22 | normalize_constraints(constraints) 23 | 24 | {all_variables, objective} = init_model(variables, constraints, opts[:objective]) 25 | 26 | %__MODULE__{ 27 | variables: all_variables, 28 | constraints: constraints, 29 | propagators: Enum.flat_map(constraints, fn c -> Constraint.post(c) end), 30 | objective: objective, 31 | id: Keyword.get(opts, :id, make_ref()), 32 | name: opts[:name], 33 | extra: opts[:extra] 34 | } 35 | end 36 | 37 | defp init_model(variables, constraints, objective) do 38 | safe_variables = 39 | Enum.map(variables, fn v -> 40 | (is_integer(v) && Variable.new(v)) || v 41 | end) 42 | 43 | variable_map = 44 | Map.new(safe_variables, fn v -> 45 | {Interface.id(v), Interface.variable(v)} 46 | end) 47 | 48 | ## Additional variables may come from constraint definitions 49 | ## (example: LessOrEqual constraint, where the second argument is a constant value). 50 | ## 51 | additional_variables = 52 | constraints 53 | |> extract_variables_from_constraints() 54 | |> Enum.reject(fn c_var -> Map.has_key?(variable_map, c_var.id) end) 55 | 56 | (safe_variables ++ additional_variables) 57 | |> Enum.with_index(1) 58 | |> Enum.map_reduce(objective, fn {var, idx}, obj_acc -> 59 | { 60 | Interface.update(var, :index, idx), 61 | if obj_acc && Interface.id(var) == Interface.id(obj_acc.variable) do 62 | obj_var = Interface.update(obj_acc.variable, :index, idx) 63 | Map.put(objective, :variable, obj_var) 64 | else 65 | obj_acc 66 | end 67 | } 68 | end) 69 | end 70 | 71 | defp extract_variables_from_constraints(constraints) do 72 | constraints 73 | |> Enum.map(&Constraint.extract_variables/1) 74 | |> List.flatten() 75 | |> Enum.uniq_by(fn var -> Map.get(var, :id) end) 76 | end 77 | 78 | defp normalize_constraints(constraints) do 79 | List.flatten(constraints) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/constraints/all_different/all_different_fwc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.AllDifferent.FWC do 2 | use ExUnit.Case, async: false 3 | 4 | describe "AllDifferentFWC" do 5 | alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferentFWC 6 | alias CPSolver.IntVariable 7 | alias CPSolver.Constraint 8 | alias CPSolver.Model 9 | import CPSolver.Variable.View.Factory 10 | 11 | test "all fixed" do 12 | variables = Enum.map(1..5, fn i -> IntVariable.new(i) end) 13 | model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)]) 14 | {:ok, result} = CPSolver.solve(model) 15 | 16 | assert hd(result.solutions) == [1, 2, 3, 4, 5] 17 | assert result.statistics.solution_count == 1 18 | end 19 | 20 | test "produces all possible permutations" do 21 | var_nums = 4 22 | domain = 1..var_nums 23 | variables = Enum.map(domain, fn _ -> IntVariable.new(domain) end) 24 | 25 | permutations = Permutation.permute!(Enum.to_list(domain)) 26 | 27 | model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)]) 28 | 29 | {:ok, result} = CPSolver.solve(model) 30 | 31 | assert result.statistics.solution_count == MapSet.size(permutations) 32 | 33 | assert result.solutions |> MapSet.new() == permutations 34 | end 35 | 36 | test "unsatisfiable (duplicates)" do 37 | variables = Enum.map(1..3, fn _ -> IntVariable.new(1) end) 38 | assert catch_throw({:fail, _} = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)])) 39 | end 40 | 41 | test "unsatisfiable(pigeonhole)" do 42 | variables = Enum.map(1..4, fn _ -> IntVariable.new(1..3) end) 43 | model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)]) 44 | 45 | {:ok, result} = CPSolver.solve(model) 46 | 47 | assert result.status == :unsatisfiable 48 | end 49 | 50 | test "views in variable list" do 51 | n = 3 52 | variables = Enum.map(1..n, fn i -> IntVariable.new(1..n, name: "row#{i}") end) 53 | 54 | diagonal_down = 55 | Enum.map(Enum.with_index(variables, 1), fn {var, idx} -> linear(var, 1, -idx) end) 56 | 57 | diagonal_up = 58 | Enum.map(Enum.with_index(variables, 1), fn {var, idx} -> linear(var, 1, idx) end) 59 | 60 | model = 61 | Model.new( 62 | variables, 63 | [ 64 | Constraint.new(AllDifferentFWC, diagonal_down), 65 | Constraint.new(AllDifferentFWC, diagonal_up), 66 | Constraint.new(AllDifferentFWC, variables) 67 | ] 68 | ) 69 | 70 | {:ok, res} = CPSolver.solve(model) 71 | 72 | assert res.status == :unsatisfiable 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/solver/search/strategy/value/partition.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Search.Partition do 2 | alias CPSolver.DefaultDomain, as: Domain 3 | 4 | alias CPSolver.Variable.Interface 5 | 6 | alias CPSolver.Search.ValueSelector.{Min, Max, Random, Split} 7 | 8 | require Logger 9 | 10 | def initialize(partition, _space_data) do 11 | ## TODO: 12 | # strategy(partition).initialize(space_data) 13 | partition 14 | end 15 | 16 | def partition(variable, value_choice) do 17 | {:ok, partition_impl(variable, value_choice)} 18 | end 19 | 20 | defp partition_impl(variable, value_choice) when is_function(value_choice) do 21 | value_choice.(variable) 22 | |> partition_by_fix(variable) 23 | end 24 | 25 | defp partition_impl(variable, value_choice) when is_atom(value_choice) do 26 | domain = Interface.domain(variable) 27 | impl = strategy(value_choice) 28 | 29 | 30 | selected_value = impl.select_value(variable) 31 | 32 | impl.partition(selected_value) 33 | |> Enum.map(fn partition_fun -> 34 | d_copy = Domain.copy(domain) 35 | domain_changes = partition_fun.(d_copy) |> normalize_domain_changes() 36 | { 37 | d_copy, 38 | %{variable.id => domain_changes} 39 | } 40 | end) 41 | end 42 | 43 | defp normalize_domain_changes({changes, _domain}), do: changes 44 | defp normalize_domain_changes(changes) when is_atom(changes), do: changes 45 | 46 | defp strategy(:indomain_min) do 47 | Min 48 | end 49 | 50 | defp strategy(:indomain_max) do 51 | Max 52 | end 53 | 54 | defp strategy(:indomain_random) do 55 | Random 56 | end 57 | 58 | defp strategy(:indomain_split) do 59 | Split 60 | end 61 | 62 | defp strategy(impl) when is_atom(impl) do 63 | if Code.ensure_loaded(impl) == {:module, impl} && function_exported?(impl, :select, 2) do 64 | impl 65 | else 66 | throw({:unknown_strategy, impl}) 67 | end 68 | end 69 | 70 | ## Default partitioning 71 | defp partition_by_fix(value, variable) do 72 | domain = Interface.domain(variable) 73 | 74 | try do 75 | {remove_changes, _domain} = Domain.remove(domain, value) 76 | 77 | [ 78 | { 79 | Domain.new(value), 80 | %{variable.id => :fixed} 81 | # Equal.new(variable, value) 82 | }, 83 | { 84 | domain, 85 | %{variable.id => remove_changes} 86 | # NotEqual.new(variable, value) 87 | } 88 | ] 89 | catch 90 | :fail -> 91 | Logger.error( 92 | "Failure on partitioning with value #{inspect(value)}, domain: #{inspect(CPSolver.BitVectorDomain.raw(domain))}" 93 | ) 94 | 95 | throw(:fail) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/solver/core/propagator/propagator_variable.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Propagator.Variable do 2 | alias CPSolver.Variable 3 | alias CPSolver.Variable.View 4 | alias CPSolver.Variable.Interface.ThrowIfFails, as: Interface 5 | alias CPSolver.Propagator 6 | import CPSolver.Common 7 | 8 | @propagator_events Propagator.propagator_events() 9 | @domain_events CPSolver.Common.domain_events() 10 | 11 | @variable_op_results_key :variable_op_results 12 | 13 | defdelegate domain(var), to: Interface 14 | defdelegate size(var), to: Interface 15 | defdelegate min(var), to: Interface 16 | defdelegate max(var), to: Interface 17 | defdelegate contains?(var, val), to: Interface 18 | defdelegate id(var), to: Interface 19 | defdelegate fixed?(var), to: Interface 20 | 21 | def remove(var, val) do 22 | wrap(:remove, var, val) 23 | end 24 | 25 | def removeAbove(var, val) do 26 | wrap(:removeAbove, var, val) 27 | end 28 | 29 | def removeBelow(var, val) do 30 | wrap(:removeBelow, var, val) 31 | end 32 | 33 | def fix(var, val) do 34 | wrap(:fix, var, val) 35 | end 36 | 37 | def set_propagate_on(var, nil) do 38 | set_propagate_on(var, :fixed) 39 | end 40 | 41 | def set_propagate_on(%View{variable: variable} = view, propagator_event) do 42 | variable 43 | |> set_propagate_on(propagator_event) 44 | |> then(fn var -> Map.put(view, :variable, var) end) 45 | end 46 | 47 | def set_propagate_on(%Variable{} = var, propagator_event) 48 | when propagator_event in @propagator_events do 49 | Map.put(var, :propagate_on, Propagator.to_domain_events(propagator_event)) 50 | end 51 | 52 | defp wrap(op, var, val) do 53 | apply(Interface, op, [ 54 | var, 55 | val 56 | ]) 57 | |> tap(fn res -> save_op(var, res) end) 58 | end 59 | 60 | defp save_op(_var, :no_change) do 61 | :ok 62 | end 63 | 64 | defp save_op(var, domain_change) when domain_change in @domain_events do 65 | current_changes = ((changes = get_variable_ops()) && changes) || Map.new() 66 | 67 | {_, updated_changes} = 68 | Map.get_and_update(current_changes, Interface.id(var), fn current_var_change -> 69 | {current_var_change, stronger_domain_change(current_var_change, domain_change)} 70 | end) 71 | 72 | Process.put( 73 | @variable_op_results_key, 74 | updated_changes 75 | ) 76 | end 77 | 78 | def get_variable_ops() do 79 | Process.get(@variable_op_results_key) 80 | end 81 | 82 | def reset_variable_ops() do 83 | Process.delete(@variable_op_results_key) 84 | end 85 | 86 | def plus(:fail, offset) when is_integer(offset) do 87 | :fail 88 | end 89 | 90 | def plus(offset, :fail) when is_integer(offset) do 91 | :fail 92 | end 93 | 94 | def plus(a, b) when is_integer(a) and is_integer(b) do 95 | a + b 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/propagators/not_equal_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.NotEqual do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Propagator.Variable, as: PropagatorVariable 7 | alias CPSolver.Propagator 8 | alias CPSolver.Propagator.NotEqual 9 | 10 | test "propagation events" do 11 | x = 1..10 12 | y = -5..5 13 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 14 | assert Enum.all?(NotEqual.variables(variables), fn v -> v.propagate_on == [:fixed] end) 15 | end 16 | 17 | test "filtering, unfixed domains" do 18 | ## Both vars are unfixed 19 | x = 1..10 20 | y = -5..5 21 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 22 | 23 | assert %{changes: %{}} = reset_and_filter(variables) 24 | refute PropagatorVariable.get_variable_ops() 25 | 26 | [x_var, y_var] = variables 27 | ## Fix one of vars 28 | assert :fixed = Variable.fix(x_var, 5) 29 | assert %{active?: false} = reset_and_filter(variables) 30 | 31 | ## The filtering should have removed '5' from y_var 32 | assert Variable.max(y_var) == 4 33 | assert Variable.min(y_var) == -5 34 | 35 | ## Fix second var and filter again 36 | assert :fixed == Variable.fix(y_var, 4) 37 | assert %{active?: false} = reset_and_filter(variables) 38 | ## Make sure filtering doesn't fail on further calls 39 | refute Enum.any?( 40 | [x_var, y_var], 41 | fn var -> :fail == Variable.min(var) end 42 | ) 43 | 44 | ## Consequent filtering does not trigger domain change events 45 | assert %{active?: false} = reset_and_filter(variables) 46 | end 47 | 48 | test "inconsistency" do 49 | x = 0..0 50 | y = 0..0 51 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 52 | 53 | assert Propagator.filter(NotEqual.new(variables)) == :fail 54 | assert PropagatorVariable.get_variable_ops() == nil 55 | end 56 | 57 | test "offset" do 58 | x = 5..5 59 | y = -5..10 60 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 61 | [x_var, y_var] = variables 62 | assert Variable.contains?(y_var, 0) 63 | # (x != y + 5) 64 | offset = 5 65 | Propagator.filter(NotEqual.new([x_var, y_var, offset])) 66 | refute Variable.contains?(y_var, 0) 67 | 68 | # (x != y - 5) 69 | offset = -5 70 | assert Variable.contains?(y_var, 10) 71 | Propagator.filter(NotEqual.new([x_var, y_var, offset])) 72 | refute Variable.contains?(y_var, 10) 73 | end 74 | 75 | defp reset_and_filter(args) do 76 | PropagatorVariable.reset_variable_ops() 77 | Propagator.filter(NotEqual.new(args)) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/propagators/less_or_equal_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.LessOrEqual do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Propagator 7 | alias CPSolver.Propagator.{Less, LessOrEqual} 8 | 9 | test "filtering" do 10 | ## Both vars are unfixed 11 | x = 0..10 12 | y = -5..5 13 | vars = Enum.map([x, y], fn d -> Variable.new(d) end) 14 | 15 | [x_var, y_var] = vars 16 | 17 | %{changes: changes} = Propagator.filter(LessOrEqual.new(vars)) 18 | assert Map.get(changes, x_var.id) == :max_change 19 | assert Map.get(changes, y_var.id) == :min_change 20 | assert 0 == Variable.min(x_var) 21 | assert 5 == Variable.max(x_var) 22 | ## Both domains are cut to 0..5 23 | assert Variable.min(x_var) == Variable.min(y_var) 24 | assert Variable.max(x_var) == Variable.max(y_var) 25 | end 26 | 27 | test "inconsistency" do 28 | x = 1..10 29 | y = -10..0 30 | ## Inconsistency: no solution to x <= y 31 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 32 | ## The propagator will fail on one of the variables 33 | assert Propagator.filter(LessOrEqual.new(variables)) == :fail 34 | end 35 | 36 | test "offset" do 37 | x = 0..10 38 | y = -10..0 39 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 40 | [x_var, y_var] = variables 41 | # (x <= y + 5) 42 | offset = 5 43 | Propagator.filter(LessOrEqual.new([x_var, y_var, offset])) 44 | ## The domain of (y+5) variable is -5..5 45 | assert 0 == Variable.min(x_var) 46 | assert 5 == Variable.max(x_var) 47 | assert Variable.min(x_var) == Variable.min(y_var) + offset 48 | assert Variable.max(x_var) == Variable.max(y_var) + offset 49 | end 50 | 51 | test "Filtering reports :passive if the domains intersect in no more than one point" do 52 | x = 1..4 53 | y = 2..4 54 | 55 | variables = Enum.map([x, y], fn d -> Variable.new(d) end) 56 | 57 | [x_var, y_var] = variables 58 | 59 | refute %{active: false} == Propagator.filter(LessOrEqual.new([x_var, y_var])) 60 | 61 | ## Cut domain of x so it intersects with domain of y in exactly one point 62 | Variable.removeAbove(x_var, 2) 63 | result = Propagator.filter(LessOrEqual.new([x_var, y_var])) 64 | refute result.active? 65 | ## Cut domain of x so it does not intersect with domain of y 66 | Variable.remove(x_var, 2) 67 | assert %{active?: false} = Propagator.filter(LessOrEqual.new([x_var, y_var])) 68 | end 69 | 70 | test "Less" do 71 | x = 1 72 | y = 1 73 | 74 | [x_var, y_var] = Enum.map([x, y], fn d -> Variable.new(d) end) 75 | 76 | less_propagator = Propagator.new(Less, [x_var, y_var]) 77 | 78 | assert Propagator.filter(less_propagator) == :fail 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/utils/mutable_array_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Utils.MutableArray do 2 | use ExUnit.Case 3 | import CPSolver.Utils.MutableArray 4 | 5 | describe "Mutable order" do 6 | alias CPSolver.Utils.MutableOrder 7 | 8 | test "create" do 9 | values = [2, 8, 3, 5, 2] 10 | order_rec = MutableOrder.new(values) 11 | assert {_sort_index, [2, 2, 3, 5, 8]} = MutableOrder.to_sorted(order_rec) |> Enum.unzip() 12 | end 13 | 14 | test "update (current value decreasing)" do 15 | values = [2, 8, 3, 5, 2] 16 | order_rec = MutableOrder.new(values) 17 | 18 | ## Current value of element at position 1 (that is, 8) changes to 2 19 | change = {1, 2} 20 | MutableOrder.update(order_rec, change) 21 | ## The changed value has been updated internally 22 | assert to_array(order_rec.values) == [2, 2, 3, 5, 2] 23 | ## The order is maintained 24 | {asc_sort_index, asc_sorted_values} = MutableOrder.to_sorted(order_rec, :asc) |> Enum.unzip() 25 | {desc_sort_index, desc_sorted_values} = MutableOrder.to_sorted(order_rec, :desc) |> Enum.unzip() 26 | 27 | assert Enum.reverse(asc_sort_index) == desc_sort_index 28 | assert Enum.reverse(asc_sorted_values) == desc_sorted_values 29 | 30 | assert MutableOrder.valid?(order_rec) 31 | end 32 | 33 | test "update (current value increasing)" do 34 | values = [2, 8, 3, 5, 2] 35 | order_rec = MutableOrder.new(values) 36 | 37 | ## Current value of element at position 2 (that is, 3) changes to 9 38 | change = {2, 9} 39 | MutableOrder.update(order_rec, change) 40 | ## The changed value has been updated internally 41 | assert to_array(order_rec.values) == [2, 8, 9, 5, 2] 42 | ## The order is maintained 43 | {asc_sort_index, asc_sorted_values} = MutableOrder.to_sorted(order_rec, :asc) |> Enum.unzip() 44 | {desc_sort_index, desc_sorted_values} = MutableOrder.to_sorted(order_rec, :desc) |> Enum.unzip() 45 | 46 | assert Enum.reverse(asc_sort_index) == desc_sort_index 47 | assert Enum.reverse(asc_sorted_values) == desc_sorted_values 48 | 49 | assert MutableOrder.valid?(order_rec) 50 | end 51 | 52 | test "get" do 53 | values = [2, 8, 3, 5, 2] 54 | order_rec = MutableOrder.new(values) 55 | assert Enum.sort(values) == Enum.map(0..(length(values) - 1), fn idx -> MutableOrder.get(order_rec, idx) end) 56 | end 57 | 58 | test "valid?" do 59 | values = [4, 1, 5, 3, 9, 6, 7, 8, 2] 60 | order = MutableOrder.new(values) 61 | assert MutableOrder.valid?(order) 62 | MutableOrder.update(order, {0, 0}) 63 | assert MutableOrder.valid?(order) 64 | 65 | ## Swap values without changing the sort index 66 | range = Enum.to_list(0..length(values)-1) 67 | rnd1 = Enum.random(range) 68 | rnd2 = Enum.random(List.delete(range, rnd1)) 69 | swap(order.values, rnd1, rnd2) 70 | refute MutableOrder.valid?(order) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/propagators/all_different/all_different_fwc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.AllDifferent.FWC do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Variable.Interface 7 | alias CPSolver.Propagator 8 | alias CPSolver.Propagator.AllDifferent.FWC 9 | 10 | test "unsatisfiable" do 11 | x_vars = Enum.map([2, 1, 1, 3], fn val -> Variable.new(val) end) 12 | fwc_propagator = FWC.new(x_vars) 13 | assert :fail == Propagator.filter(fwc_propagator) 14 | end 15 | 16 | test "maintains the list of unfixed variables" do 17 | x_vars = 18 | Enum.map([{"x1", 0..5}, {"x2", 1..4}, {"x3", 0..5}, {"x4", 4}, {"x5", 5}], fn {name, d} -> 19 | Variable.new(d, name: name) 20 | end) 21 | 22 | [x1_var, x2_var, x3_var, _x4_var, _x5_var] = x_vars 23 | 24 | ## Initial state 25 | ## 26 | fwc_propagator = FWC.new(x_vars) 27 | filtering_results = Propagator.filter(fwc_propagator) 28 | 29 | ## The values of fixed variables (namely, 4 and 5) have been removed from unfixed variables 30 | assert Enum.all?([x1_var, x2_var, x3_var], fn var -> Interface.max(var) == 3 end) 31 | 32 | ## Fixing one of the variables will remove the value it's fixed to from other variables 33 | :fixed = Interface.fix(x1_var, 3) 34 | 35 | ## Emulate changes that would come from the propagation process. 36 | ## x1 is at position 0 in the propagator arguments 37 | changes = %{0 => :fixed} 38 | 39 | fwc_propagator_step2 = Map.put(fwc_propagator, :state, filtering_results.state) 40 | _filtering_results2 = Propagator.filter(fwc_propagator_step2, changes: changes) 41 | 42 | assert Interface.min(x2_var) == 1 && Interface.max(x2_var) == 2 43 | assert Interface.min(x3_var) == 0 && Interface.max(x3_var) == 2 44 | end 45 | 46 | test "cascading filtering" do 47 | ## x1 is fixed, so the filtering removes 1 from all other variables. 48 | ## This makes x2 fixed, which in turn triggers a removal of 2 from x3 to x5 etc. 49 | ## Eventually all variables become fixed, and this will take a single filtering call. 50 | ## 51 | x_vars = 52 | Enum.map([{"x2", 1..2}, {"x1", 1}, {"x3", 1..3}, {"x4", 1..4}, {"x5", 1..5}], fn {name, d} -> 53 | Variable.new(d, name: name) 54 | end) 55 | 56 | fwc_propagator = FWC.new(x_vars) 57 | %{changes: changes} = Propagator.filter(fwc_propagator) 58 | 59 | ## x1 was already fixed; the filtering fixes the rest 60 | assert map_size(changes) == length(x_vars) - 1 61 | assert Enum.all?(Map.values(changes), fn change -> change == :fixed end) 62 | assert Enum.all?(x_vars, &Interface.fixed?/1) 63 | 64 | ## Consequent filtering does not result in more changes and/or failures 65 | assert Map.get(Propagator.filter(fwc_propagator), :changes) in [nil, %{}] 66 | assert Enum.all?(x_vars, &Interface.fixed?/1) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/constraints/all_different/all_different_dc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.AllDifferent.DC do 2 | use ExUnit.Case, async: false 3 | 4 | describe "AllDifferent" do 5 | alias CPSolver.Constraint.AllDifferent.DC, as: AllDifferent 6 | alias CPSolver.IntVariable 7 | alias CPSolver.Constraint 8 | alias CPSolver.Model 9 | import CPSolver.Variable.View.Factory 10 | 11 | test "all fixed" do 12 | variables = Enum.map(1..5, fn i -> IntVariable.new(i) end) 13 | model = Model.new(variables, [Constraint.new(AllDifferent, variables)]) 14 | {:ok, result} = CPSolver.solve(model) 15 | 16 | assert hd(result.solutions) == [1, 2, 3, 4, 5] 17 | assert result.statistics.solution_count == 1 18 | end 19 | 20 | test "reduction" do 21 | minizinc_solutions = [[1, 2, 4, 5], [1, 2, 3, 4], [1, 2, 3, 5]] 22 | variables = Enum.map([1, 1..2, 1..4, [1, 2, 4, 5]], fn d -> IntVariable.new(d) end) 23 | model = Model.new(variables, [Constraint.new(AllDifferent, variables)]) 24 | {:ok, result} = CPSolver.solve(model) 25 | assert Enum.sort(result.solutions) == Enum.sort(minizinc_solutions) 26 | end 27 | 28 | test "produces all possible permutations" do 29 | var_nums = 4 30 | domain = 1..var_nums 31 | variables = Enum.map(domain, fn _ -> IntVariable.new(domain) end) 32 | 33 | permutations = Permutation.permute!(Enum.to_list(domain)) 34 | 35 | model = Model.new(variables, [Constraint.new(AllDifferent, variables)]) 36 | 37 | {:ok, result} = CPSolver.solve(model) 38 | 39 | 40 | assert result.statistics.solution_count == MapSet.size(permutations) 41 | 42 | assert result.solutions |> MapSet.new() == permutations 43 | end 44 | 45 | test "unsatisfiable (duplicates)" do 46 | variables = Enum.map(1..3, fn _ -> IntVariable.new(1) end) 47 | assert catch_throw({:fail, _} = Model.new(variables, [Constraint.new(AllDifferent, variables)])) 48 | end 49 | 50 | test "unsatisfiable(pigeonhole)" do 51 | variables = Enum.map(1..4, fn _ -> IntVariable.new(1..3) end) 52 | assert catch_throw({:fail, _} = Model.new(variables, [Constraint.new(AllDifferent, variables)])) 53 | end 54 | 55 | test "views in variable list" do 56 | n = 3 57 | variables = Enum.map(1..n, fn i -> IntVariable.new(1..n, name: "row#{i}") end) 58 | 59 | diagonal_down = 60 | Enum.map(Enum.with_index(variables, 1), fn {var, idx} -> linear(var, 1, -idx) end) 61 | 62 | diagonal_up = 63 | Enum.map(Enum.with_index(variables, 1), fn {var, idx} -> linear(var, 1, idx) end) 64 | 65 | model = 66 | Model.new( 67 | variables, 68 | [ 69 | Constraint.new(AllDifferent, diagonal_down), 70 | Constraint.new(AllDifferent, diagonal_up), 71 | Constraint.new(AllDifferent, variables) 72 | ] 73 | ) 74 | 75 | {:ok, res} = CPSolver.solve(model) 76 | 77 | assert res.status == :unsatisfiable 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/solver/constraints/propagators/or.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Propagator.Or do 2 | use CPSolver.Propagator 3 | 4 | @moduledoc """ 5 | The propagator for 'or' constraint. 6 | Takes the list of boolean variables. 7 | Constraints to have at least one variable to be resolved to true. 8 | """ 9 | 10 | @impl true 11 | def variables(args) do 12 | Enum.map(args, fn x_el -> set_propagate_on(x_el, :fixed) end) 13 | end 14 | 15 | @impl true 16 | def arguments(args) do 17 | Arrays.new(args, implementation: Aja.Vector) 18 | end 19 | 20 | @impl true 21 | def filter(all_vars, nil, changes) do 22 | case initial_reduction(all_vars) do 23 | :resolved -> 24 | :passive 25 | 26 | unfixed -> 27 | (MapSet.size(unfixed) == 0 && fail()) || 28 | filter(all_vars, %{unfixed: unfixed}, changes) 29 | end 30 | end 31 | 32 | def filter(all_vars, %{unfixed: unfixed} = _state, changes) when map_size(changes) == 0 do 33 | Enum.reduce_while(unfixed, unfixed, fn idx, unfixed_acc -> 34 | var = Arrays.get(all_vars, idx) 35 | 36 | if fixed?(var) do 37 | if min(var) == 1 do 38 | {:halt, :resolved} 39 | else 40 | {:cont, MapSet.delete(unfixed_acc, idx)} 41 | end 42 | else 43 | {:cont, unfixed_acc} 44 | end 45 | end) 46 | |> finalize(all_vars) 47 | end 48 | 49 | def filter(all_vars, %{unfixed: unfixed} = _state, changes) do 50 | Enum.reduce_while(changes, unfixed, fn {var_index, :fixed}, unfixed_acc -> 51 | if MapSet.member?(unfixed_acc, var_index) do 52 | if min(Arrays.get(all_vars, var_index)) == 1 do 53 | {:halt, :resolved} 54 | else 55 | {:cont, MapSet.delete(unfixed_acc, var_index)} 56 | end 57 | else 58 | {:cont, unfixed_acc} 59 | end 60 | end) 61 | |> finalize(all_vars) 62 | end 63 | 64 | defp initial_reduction(all_vars) do 65 | Enum.reduce_while(0..(Arrays.size(all_vars) - 1), MapSet.new(), fn var_idx, candidates_acc -> 66 | var = Arrays.get(all_vars, var_idx) 67 | 68 | if fixed?(var) do 69 | case min(var) do 70 | 0 -> {:cont, candidates_acc} 71 | 1 -> {:halt, :resolved} 72 | _other_value -> throw(:not_boolean) 73 | end 74 | else 75 | {:cont, MapSet.put(candidates_acc, var_idx)} 76 | end 77 | end) 78 | end 79 | 80 | defp finalize(filtering_result, all_vars) do 81 | case filtering_result do 82 | :resolved -> 83 | :passive 84 | 85 | unfixed -> 86 | case MapSet.size(unfixed) do 87 | 0 -> 88 | fail() 89 | 90 | 1 -> 91 | last_var_idx = MapSet.to_list(unfixed) |> List.first() 92 | fix(Arrays.get(all_vars, last_var_idx), 1) 93 | :passive 94 | 95 | _more -> 96 | {:state, %{unfixed: unfixed}} 97 | end 98 | end 99 | end 100 | 101 | defp fail() do 102 | throw(:fail) 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/solver/objective/objective.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Objective do 2 | alias CPSolver.Objective.Propagator, as: ObjectivePropagator 3 | alias CPSolver.Variable.Interface 4 | import CPSolver.Variable.View.Factory 5 | import CPSolver.Utils 6 | 7 | @spec minimize(Variable.t() | View.t()) :: %{ 8 | :propagator => Propagator.t(), 9 | :variable => Variable.t() | View.t(), 10 | :bound_handle => reference() 11 | } 12 | 13 | def minimize(variable) do 14 | bound_handle = init_bound_handle() 15 | propagator = ObjectivePropagator.new(variable, bound_handle) 16 | 17 | %{ 18 | propagator: propagator, 19 | variable: variable, 20 | bound_handle: bound_handle, 21 | target: :minimize 22 | } 23 | end 24 | 25 | def maximize(variable) do 26 | minimize(minus(variable)) 27 | |> Map.put(:target, :maximize) 28 | end 29 | 30 | def get_bound(%{bound_handle: handle}) do 31 | get_bound(handle) 32 | end 33 | 34 | def get_bound(handle) when is_reference(handle) do 35 | (on_primary_node?(handle) && get_bound_impl(handle)) || remote_call(handle, :get_bound_impl) 36 | end 37 | 38 | def get_bound_impl(handle) when is_reference(handle) do 39 | :atomics.get(handle, 1) 40 | end 41 | 42 | def update_bound(handle, value) when is_reference(handle) do 43 | (on_primary_node?(handle) && update_bound_impl(handle, value)) || 44 | remote_call(handle, :update_bound_impl, [value]) 45 | end 46 | 47 | def update_bound_impl(bound_handle, value) do 48 | case :atomics.get(bound_handle, 1) do 49 | prev_value when prev_value > value -> 50 | case :atomics.compare_exchange(bound_handle, 1, prev_value, value) do 51 | :ok -> 52 | value 53 | 54 | _changed_in_between -> 55 | update_bound_impl(bound_handle, value) 56 | end 57 | 58 | prev_value -> 59 | # previous value lesser than the proposed one - keep 60 | prev_value 61 | end 62 | end 63 | 64 | def tighten(%{variable: variable, bound_handle: handle} = _objective) do 65 | tighten(variable, handle) 66 | end 67 | 68 | def tighten(variable, bound_handle) do 69 | update_bound(bound_handle, Interface.max(variable) - 1) 70 | end 71 | 72 | def init_bound_handle() do 73 | ref = :atomics.new(1, signed: true) 74 | reset_bound(ref) 75 | ref 76 | end 77 | 78 | def reset_bound(%{bound_handle: ref} = _objective) do 79 | reset_bound(ref) 80 | end 81 | 82 | def reset_bound(handle) when is_reference(handle) do 83 | (on_primary_node?(handle) && reset_bound_impl(handle)) || 84 | remote_call(handle, :reset_bound_impl) 85 | end 86 | 87 | def reset_bound_impl(ref) when is_reference(ref) do 88 | :atomics.put(ref, 1, :atomics.info(ref).max) 89 | end 90 | 91 | def get_objective_value(%{target: target, bound_handle: handle} = _objective) do 92 | (get_bound(handle) + 1) * ((target == :minimize && 1) || -1) 93 | end 94 | 95 | defp remote_call(ref, fun_name, args \\ []) when is_reference(ref) and is_atom(fun_name) do 96 | :erpc.call(node(ref), __MODULE__, fun_name, [ref | args]) 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/constraints/all_different/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Constraint.AllDifferent.Utils do 2 | use ExUnit.Case, async: false 3 | alias CPSolver.Propagator.AllDifferent.Utils, as: AllDiffUtils 4 | 5 | describe "Forward checking" do 6 | alias CPSolver.IntVariable, as: Variable 7 | test "cascading" do 8 | domains = [1, 1..2, 1..3, 1..4, 1..5] 9 | vars = Enum.map(Enum.shuffle(domains), fn d -> Variable.new(d) end) 10 | {unfixed_indices, fixed_values} = AllDiffUtils.forward_checking(vars) 11 | 12 | assert MapSet.size(fixed_values) == length(vars) 13 | ## Everything is fixed 14 | assert Enum.empty?(unfixed_indices) 15 | assert Enum.all?(vars, fn var -> Variable.fixed?(var) end) 16 | ## AllDifferent check 17 | assert MapSet.new(vars, fn var -> Variable.min(var) end) |> MapSet.size() == length(vars) 18 | end 19 | 20 | test "pigeonhole" do 21 | domains = [1..2, 1..2, 1..2] 22 | vars = Enum.map(Enum.shuffle(domains), fn d -> Variable.new(d) end) 23 | {unfixed_indices, fixed_values} = AllDiffUtils.forward_checking(vars) 24 | ## FWC does not reduce if no fixed variables 25 | assert MapSet.size(unfixed_indices) == length(vars) 26 | assert Enum.empty?(fixed_values) 27 | ## Trigger reduction by fixing one of the variables 28 | Variable.fix(Enum.random(vars), Enum.random(1..2)) 29 | assert catch_throw(:fail = AllDiffUtils.forward_checking(vars)) 30 | end 31 | 32 | test "reuse" do 33 | # An example from Zhang 34 | domains = [1, 1..2, 1..4, [1, 2, 4, 5]] 35 | vars = Enum.map(domains, fn d -> Variable.new(d) end) 36 | {unfixed_indices, fixed_values} = AllDiffUtils.forward_checking(vars) 37 | ## First 2 variables fixed with values 1 and 2 38 | assert unfixed_indices == MapSet.new([2, 3]) 39 | assert fixed_values == MapSet.new([1, 2]) 40 | ## Run FWC again with the data from previous run - no effect 41 | assert {unfixed_indices, fixed_values} == AllDiffUtils.forward_checking(vars, unfixed_indices, fixed_values) 42 | ## Fix and run FWC again with previous results 43 | ## 44 | ## domain(var3) = [3,4]; domain(var4) = [4,5] 45 | ## Fix shared value (4) for any of the unfixed variables 46 | Variable.fix(Enum.at(vars, Enum.random([2,3])), 4) 47 | {unfixed_indices2, _fixed_values2} = AllDiffUtils.forward_checking(vars, unfixed_indices, fixed_values) 48 | ## Everything is fixed 49 | assert Enum.empty?(unfixed_indices2) 50 | assert Enum.all?(vars, fn var -> Variable.fixed?(var) end) 51 | end 52 | end 53 | 54 | describe "Component locator" do 55 | test "build and locate" do 56 | vertices = MapSet.new(1..10) 57 | components = Enum.map([[1, 3, 5, 7, 9], [2, 4, 6, 8]], fn c -> MapSet.new(c) end) 58 | component_locator = AllDiffUtils.build_component_locator(vertices, components) 59 | assert AllDiffUtils.get_component(component_locator, 1) == hd(components) 60 | assert AllDiffUtils.get_component(component_locator, 4) == List.last(components) 61 | refute AllDiffUtils.get_component(component_locator, 10) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/propagators/modulo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.Modulo do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.DefaultDomain, as: Domain 7 | alias CPSolver.Variable.Interface 8 | alias CPSolver.Propagator 9 | alias CPSolver.Propagator.Modulo 10 | 11 | test "filtering, initial call" do 12 | ## Both vars are unfixed 13 | x = 1..10 14 | y = -5..5 15 | m = -10..10 16 | [m_var, _x_var, y_var] = variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) 17 | 18 | # before filtering 19 | assert Interface.contains?(y_var, 0) 20 | assert Interface.min(m_var) == -10 21 | 22 | p = Modulo.new(variables) 23 | _res = Propagator.filter(p) 24 | ## y has 0 removed 25 | refute Interface.contains?(y_var, 0) 26 | 27 | ## Nothing is fixed 28 | refute Enum.any?(variables, fn var -> Interface.fixed?(var) end) 29 | end 30 | 31 | test "filtering, dividend and divisor fixed" do 32 | x = -7 33 | y = 3 34 | m = -10..10 35 | [m_var, _x_var, _y_var] = variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) 36 | 37 | p = Modulo.new(variables) 38 | res = Propagator.filter(p) 39 | 40 | assert res.changes == %{m_var.id => :fixed} 41 | 42 | ## Modulo is fixed to x % y 43 | ## Dividend and modulo have the same sign 44 | assert Interface.fixed?(m_var) && Interface.min(m_var) == rem(x, y) 45 | end 46 | 47 | test "filtering, modulo and dividend fixed" do 48 | x = -10 49 | y = -100..100 50 | m = -2 51 | [_m_var, _x_var, y_var] = variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) 52 | 53 | p = Modulo.new(variables) 54 | res = Propagator.filter(p) 55 | refute res == :fail 56 | ## All values in domain of y satisfy x % y = m 57 | assert Enum.all?(Domain.to_list(y_var.domain), fn y_val -> 58 | rem(x, y_val) == m 59 | end) 60 | end 61 | 62 | test "filtering, modulo and divider fixed" do 63 | x = -100..100 64 | y = -10 65 | m = -2 66 | [_m_var, x_var, _y_var] = variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) 67 | 68 | p = Modulo.new(variables) 69 | res = Propagator.filter(p) 70 | refute res == :fail 71 | ## All values in domain of x satisfy x % y = m 72 | assert Enum.all?(Domain.to_list(x_var.domain), fn x_val -> rem(x_val, y) == m end) 73 | end 74 | 75 | test "inconsistency, if modulo and dividend are fixed to values of different sign" do 76 | x = 10 77 | y = -100..100 78 | m = -2 79 | variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) 80 | 81 | p = Modulo.new(variables) 82 | 83 | assert :fail = Propagator.filter(p) 84 | end 85 | 86 | test "inconsistency, if every modulo value has a different sign with every divident value" do 87 | m = 1..10 88 | y = -100..100 89 | x = -10..-1 90 | variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) 91 | 92 | p = Modulo.new(variables) 93 | 94 | assert :fail = Propagator.filter(p) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/solver/variables/variable.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Variable do 2 | defstruct [:id, :index, :name, :domain, :initial_size, :store, :propagate_on] 3 | 4 | @type t :: %__MODULE__{ 5 | id: reference(), 6 | index: integer(), 7 | name: term(), 8 | domain: Domain.t(), 9 | initial_size: integer(), 10 | propagate_on: Propagator.propagator_event() 11 | } 12 | 13 | alias CPSolver.Variable 14 | alias CPSolver.Variable.View 15 | alias CPSolver.DefaultDomain, as: Domain 16 | 17 | require Logger 18 | 19 | @callback new(values :: Enum.t(), opts :: Keyword.t()) :: Variable.t() 20 | 21 | defmacro __using__(_) do 22 | quote do 23 | @behaviour CPSolver.Variable 24 | 25 | def new(values, opts \\ []) do 26 | id = make_ref() 27 | 28 | domain = Domain.new(values) 29 | 30 | %Variable{ 31 | id: id, 32 | name: Keyword.get(opts, :name, id), 33 | domain: domain, 34 | initial_size: Domain.size(domain) 35 | } 36 | end 37 | 38 | def copy(variable) do 39 | Map.put(variable, :id, make_ref()) 40 | end 41 | 42 | defp default_opts() do 43 | [domain_impl: CPSolver.DefaultDomain] 44 | end 45 | 46 | defoverridable new: 2 47 | end 48 | end 49 | 50 | def domain(variable) do 51 | apply_op(:domain, variable) 52 | end 53 | 54 | def iterator(%{domain: domain} = _variable, _opts \\ []) do 55 | Domain.iterator(domain) 56 | end 57 | 58 | def size(variable) do 59 | apply_op(:size, variable) 60 | end 61 | 62 | def fixed?(variable) do 63 | apply_op(:fixed?, variable) 64 | end 65 | 66 | def min(variable) do 67 | apply_op(:min, variable) 68 | end 69 | 70 | def max(variable) do 71 | apply_op(:max, variable) 72 | end 73 | 74 | def contains?(variable, value) do 75 | apply_op(:contains?, variable, value) 76 | end 77 | 78 | def remove(variable, value) do 79 | apply_op(:remove, variable, value) 80 | end 81 | 82 | def removeAbove(variable, value) do 83 | apply_op(:removeAbove, variable, value) 84 | end 85 | 86 | def removeBelow(variable, value) do 87 | apply_op(:removeBelow, variable, value) 88 | end 89 | 90 | def fix(variable, value) do 91 | apply_op(:fix, variable, value) 92 | end 93 | 94 | defp apply_op(op, %{domain: domain} = _variable, value) 95 | when op in [:remove, :removeAbove, :removeBelow, :fix] do 96 | apply(Domain, op, [domain, value]) |> normalize_update_result() 97 | end 98 | 99 | defp apply_op(:contains?, %{domain: domain} = _variable, value) do 100 | Domain.contains?(domain, value) 101 | end 102 | 103 | defp apply_op(op, %View{variable: variable}) do 104 | apply_op(op, variable) 105 | end 106 | 107 | defp apply_op(op, %{domain: domain} = _variable) 108 | when op in [:size, :fixed?, :min, :max] do 109 | apply(Domain, op, [domain]) 110 | end 111 | 112 | defp apply_op(:domain, %{domain: domain}) when not is_nil(domain) do 113 | domain 114 | end 115 | 116 | defp normalize_update_result({change, _}), do: change 117 | 118 | defp normalize_update_result(change), do: change 119 | end 120 | -------------------------------------------------------------------------------- /data/knapsack/ks_200_1: -------------------------------------------------------------------------------- 1 | 200 2640230 2 | 31860 76620 3 | 11884 28868 4 | 10492 25484 5 | 901 2502 6 | 43580 104660 7 | 9004 21908 8 | 6700 16500 9 | 29940 71980 10 | 7484 18268 11 | 5932 14564 12 | 7900 19300 13 | 6564 16028 14 | 6596 16092 15 | 8172 19844 16 | 5324 13148 17 | 8436 20572 18 | 7332 17964 19 | 6972 17044 20 | 7668 18636 21 | 6524 15948 22 | 6244 15388 23 | 635 1970 24 | 5396 13292 25 | 13596 32892 26 | 51188 122676 27 | 13684 33068 28 | 8596 20892 29 | 156840 375380 30 | 7900 19300 31 | 6460 15820 32 | 14132 34164 33 | 4980 12260 34 | 5216 12932 35 | 6276 15452 36 | 701 2102 37 | 3084 7868 38 | 6924 16948 39 | 5500 13500 40 | 3148 7996 41 | 47844 114788 42 | 226844 542788 43 | 25748 61996 44 | 7012 17124 45 | 3440 8580 46 | 15580 37660 47 | 314 1128 48 | 2852 7204 49 | 15500 37500 50 | 9348 22796 51 | 17768 42836 52 | 16396 39692 53 | 16540 39980 54 | 395124 944948 55 | 10196 24692 56 | 6652 16204 57 | 4848 11996 58 | 74372 178244 59 | 4556 11212 60 | 4900 12100 61 | 3508 8716 62 | 3820 9540 63 | 5460 13420 64 | 16564 40028 65 | 3896 9692 66 | 3832 9564 67 | 9012 21924 68 | 4428 10956 69 | 57796 138492 70 | 12052 29204 71 | 7052 17204 72 | 85864 205628 73 | 5068 12436 74 | 10484 25468 75 | 4516 11132 76 | 3620 9140 77 | 18052 43604 78 | 21 542 79 | 15804 38108 80 | 19020 45940 81 | 170844 408788 82 | 3732 9364 83 | 2920 7340 84 | 4120 10340 85 | 6828 16756 86 | 26252 63204 87 | 11676 28252 88 | 19916 47932 89 | 65488 156876 90 | 7172 17644 91 | 3772 9444 92 | 132868 318036 93 | 8332 20364 94 | 5308 13116 95 | 3780 9460 96 | 5208 12916 97 | 56788 136076 98 | 7172 17644 99 | 7868 19236 100 | 31412 75524 101 | 9252 22604 102 | 12276 29652 103 | 3712 9324 104 | 4516 11132 105 | 105876 253452 106 | 20084 48468 107 | 11492 27884 108 | 49092 117684 109 | 83452 199804 110 | 71372 171044 111 | 66572 159644 112 | 25268 60836 113 | 64292 154084 114 | 21228 51156 115 | 16812 40524 116 | 19260 46420 117 | 7740 18980 118 | 5632 13964 119 | 3256 8212 120 | 15580 37660 121 | 4824 11948 122 | 59700 143100 123 | 14500 35100 124 | 7208 17716 125 | 6028 14756 126 | 75716 181332 127 | 22364 53828 128 | 7636 18572 129 | 6444 15788 130 | 5192 12884 131 | 7388 18076 132 | 33156 79612 133 | 3032 7564 134 | 6628 16156 135 | 7036 17172 136 | 3200 8100 137 | 7300 17900 138 | 4452 11004 139 | 26364 63428 140 | 14036 33972 141 | 16932 40964 142 | 5788 14276 143 | 70476 168852 144 | 4552 11204 145 | 33980 81660 146 | 19300 46500 147 | 39628 95156 148 | 4484 11068 149 | 55044 131988 150 | 574 1848 151 | 29644 71188 152 | 9460 23020 153 | 106284 254468 154 | 304 1108 155 | 3580 8860 156 | 6308 15516 157 | 10492 25484 158 | 12820 31140 159 | 14436 34972 160 | 5044 12388 161 | 1155 3210 162 | 12468 30236 163 | 4380 10860 164 | 9876 24052 165 | 8752 21404 166 | 8676 21052 167 | 42848 102796 168 | 22844 54988 169 | 6244 15388 170 | 314 1128 171 | 314 1128 172 | 314 1128 173 | 314 1128 174 | 314 1128 175 | 314 1128 176 | 387480 926660 177 | 314 1128 178 | 314 1128 179 | 314 1128 180 | 314 1128 181 | 314 1128 182 | 15996 38692 183 | 8372 20444 184 | 65488 156876 185 | 304 1108 186 | 4756 11812 187 | 5012 12324 188 | 304 1108 189 | 314 1128 190 | 314 1128 191 | 314 1128 192 | 314 1128 193 | 314 1128 194 | 314 1128 195 | 314 1128 196 | 304 1108 197 | 1208 3316 198 | 47728 114556 199 | 314 1128 200 | 314 1128 201 | 314 1128 202 | -------------------------------------------------------------------------------- /lib/utils/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule CPSolver.Utils do 2 | alias CPSolver.Variable.Interface 3 | alias CPSolver.IntVariable, as: Variable 4 | alias CPSolver.DefaultDomain, as: Domain 5 | alias Iter.Iterable 6 | 7 | def on_primary_node?(arg) when is_reference(arg) or is_pid(arg) or is_port(arg) do 8 | Node.self() == node(arg) 9 | end 10 | 11 | def array2d_min_max(arr) do 12 | n_rows = length(arr) 13 | n_cols = length(hd(arr)) 14 | first = Enum.at(arr, 0) |> Enum.at(0) 15 | 16 | for i <- 0..(n_rows - 1), j <- 0..(n_cols - 1), reduce: {first, first} do 17 | {acc_min, acc_max} -> 18 | val = Enum.at(arr, i) |> Enum.at(j) 19 | 20 | cond do 21 | val < acc_min -> {val, acc_max} 22 | val > acc_max -> {acc_min, val} 23 | true -> {acc_min, acc_max} 24 | end 25 | end 26 | end 27 | 28 | ## Cartesian product of list of lists 29 | def cartesian([h]) do 30 | for i <- h do 31 | [i] 32 | end 33 | end 34 | 35 | def cartesian([h | t] = _values, handler \\ nil) do 36 | for i <- h, j <- cartesian(t) do 37 | [i | j] 38 | |> tap(fn res -> handler && handler.(res) end) 39 | end 40 | end 41 | 42 | def lazy_cartesian(lists, callback \\ &Function.identity/1) do 43 | lazy_cartesian(lists, callback, []) 44 | end 45 | 46 | def lazy_cartesian([head], callback, acc) do 47 | Enum.each(head, fn v -> callback.([v | acc]) end) 48 | end 49 | 50 | def lazy_cartesian([head | rest] = _lists, callback, acc) do 51 | Enum.each(head, fn i -> 52 | lazy_cartesian(rest, callback, [i | acc]) 53 | end) 54 | end 55 | 56 | def domain_values(variable_or_view, access \\ :interface) do 57 | cond do 58 | access == :interface -> Interface.domain(variable_or_view) 59 | access == :variable -> Variable.domain(variable_or_view) 60 | true -> throw{:error, :unknown_access_type} 61 | end 62 | |> Domain.to_list() 63 | end 64 | 65 | ## Pick all minimal elements according to given minimizing function 66 | def minimals(enumerable, min_by_fun) do 67 | List.foldr(enumerable, {[], nil}, fn el, {minimals_acc, current_min} = acc -> 68 | val = min_by_fun.(el) 69 | 70 | cond do 71 | is_nil(current_min) || val < current_min -> {[el], val} 72 | is_nil(val) || val > current_min -> acc 73 | val == current_min -> {[el | minimals_acc], current_min} 74 | end 75 | end) 76 | |> elem(0) 77 | end 78 | 79 | ## Pick all maximal elements according to given maximizing function 80 | def maximals(enumerable, max_by_fun) do 81 | List.foldr(enumerable, {[], -1}, fn el, {maximals_acc, current_max} = acc -> 82 | val = max_by_fun.(el) 83 | 84 | cond do 85 | is_nil(val) || val < current_max -> acc 86 | val > current_max -> {[el], val} 87 | val == current_max -> {[el | maximals_acc], val} 88 | end 89 | end) 90 | |> elem(0) 91 | end 92 | 93 | def iterate(iterator, acc, fun) do 94 | case Iterable.next(iterator) do 95 | :done -> acc 96 | {:ok, neighbor, rest} -> 97 | case fun.(neighbor, acc) do 98 | {:halt, acc_new} -> 99 | acc_new 100 | {:cont, acc_new} -> 101 | iterate(rest, acc_new, fun) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /test/propagators/element/element2d_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CPSolverTest.Propagator.Element2D do 2 | use ExUnit.Case 3 | 4 | describe "Propagator filtering" do 5 | alias CPSolver.IntVariable, as: Variable 6 | alias CPSolver.Variable.Interface 7 | alias CPSolver.Propagator 8 | alias CPSolver.Propagator.Element2D 9 | 10 | test "filtering" do 11 | x = -2..40 12 | y = -3..10 13 | z = 2..40 14 | 15 | t = [ 16 | [9, 8, 7, 5, 6], 17 | [9, 1, 5, 2, 8], 18 | [8, 3, 1, 4, 9], 19 | [9, 1, 2, 8, 6] 20 | ] 21 | 22 | variables = Enum.map([x, y, z], fn d -> Variable.new(d) end) 23 | 24 | [x_var, y_var, z_var] = variables 25 | 26 | propagator = Element2D.new(t, x_var, y_var, z_var) 27 | 28 | %{state: state1} = Propagator.filter(propagator) 29 | 30 | assert Interface.min(x_var) == 0 31 | assert Interface.max(x_var) == 3 32 | assert Interface.min(y_var) == 0 33 | assert Interface.max(y_var) == 4 34 | assert Interface.min(z_var) == 2 35 | assert Interface.max(z_var) == 9 36 | 37 | Interface.removeAbove(z_var, 7) 38 | 39 | %{state: state2} = Propagator.filter(Map.put(propagator, :state, state1)) 40 | 41 | assert Interface.min(y_var) == 1 42 | 43 | Interface.remove(x_var, 0) 44 | %{state: state3} = Propagator.filter(Map.put(propagator, :state, state2)) 45 | 46 | assert 6 == Interface.max(z_var) 47 | assert 3 == Interface.max(x_var) 48 | 49 | Interface.remove(y_var, 4) 50 | 51 | %{state: state4} = Propagator.filter(Map.put(propagator, :state, state3)) 52 | 53 | assert 5 == Interface.max(z_var) 54 | assert 2 == Interface.min(z_var) 55 | 56 | %{state: state5} = Propagator.filter(Map.put(propagator, :state, state4)) 57 | 58 | Interface.remove(y_var, 2) 59 | %{state: _state6} = Propagator.filter(Map.put(propagator, :state, state5)) 60 | 61 | assert 4 == Interface.max(z_var) 62 | assert 2 == Interface.min(z_var) 63 | end 64 | 65 | test "inconsistency" do 66 | x = 1..2 67 | y = -10..1 68 | z = -2..6 69 | 70 | t = [ 71 | [3, 5], 72 | [7, 8] 73 | ] 74 | 75 | variables = Enum.map([x, y, z], fn d -> Variable.new(d) end) 76 | 77 | [x_var, y_var, z_var] = variables 78 | ## The propagator will fail. 79 | ## D(x) = 1..2 implies filtering to {1} (because T is 2x2, and it's a 0-based index) 80 | ## This leaves only the second row for the values of z, which is inconsistent with D(z). 81 | propagator = Element2D.new(t, x_var, y_var, z_var) 82 | assert :fail == Propagator.filter(propagator) 83 | end 84 | 85 | test "2 of 3 fixed" do 86 | x = 3..3 87 | y = 3..3 88 | z = 2..40 89 | 90 | t = [ 91 | [9, 8, 7, 5, 6], 92 | [9, 1, 5, 2, 8], 93 | [8, 3, 1, 4, 9], 94 | [9, 1, 2, 8, 6] 95 | ] 96 | 97 | variables = Enum.map([x, y, z], fn d -> Variable.new(d) end) 98 | 99 | [x_var, y_var, z_var] = variables 100 | 101 | propagator = Element2D.new(t, x_var, y_var, z_var) 102 | 103 | %{state: nil, active?: false} = Propagator.filter(propagator) 104 | 105 | assert Interface.min(z_var) == 106 | Enum.at(t, Interface.min(x_var)) |> Enum.at(Interface.min(y_var)) 107 | 108 | assert Interface.fixed?(z_var) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /data/knapsack/ks_200_0: -------------------------------------------------------------------------------- 1 | 200 100000 2 | 90001 90000 3 | 89751 89750 4 | 10002 10001 5 | 89501 89500 6 | 10254 10252 7 | 89251 89250 8 | 10506 10503 9 | 89001 89000 10 | 10758 10754 11 | 88751 88750 12 | 11010 11005 13 | 88501 88500 14 | 11262 11256 15 | 88251 88250 16 | 11514 11507 17 | 88001 88000 18 | 11766 11758 19 | 87751 87750 20 | 12018 12009 21 | 87501 87500 22 | 12270 12260 23 | 87251 87250 24 | 12522 12511 25 | 87001 87000 26 | 12774 12762 27 | 86751 86750 28 | 13026 13013 29 | 86501 86500 30 | 13278 13264 31 | 86251 86250 32 | 13530 13515 33 | 86001 86000 34 | 13782 13766 35 | 85751 85750 36 | 14034 14017 37 | 85501 85500 38 | 14286 14268 39 | 85251 85250 40 | 14538 14519 41 | 85001 85000 42 | 14790 14770 43 | 84751 84750 44 | 15042 15021 45 | 84501 84500 46 | 15294 15272 47 | 84251 84250 48 | 15546 15523 49 | 84001 84000 50 | 15798 15774 51 | 83751 83750 52 | 16050 16025 53 | 83501 83500 54 | 16302 16276 55 | 83251 83250 56 | 16554 16527 57 | 83001 83000 58 | 16806 16778 59 | 82751 82750 60 | 17058 17029 61 | 82501 82500 62 | 17310 17280 63 | 82251 82250 64 | 17562 17531 65 | 82001 82000 66 | 17814 17782 67 | 81751 81750 68 | 18066 18033 69 | 81501 81500 70 | 18318 18284 71 | 81251 81250 72 | 18570 18535 73 | 81001 81000 74 | 18822 18786 75 | 80751 80750 76 | 19074 19037 77 | 80501 80500 78 | 19326 19288 79 | 80251 80250 80 | 19578 19539 81 | 80001 80000 82 | 19830 19790 83 | 79751 79750 84 | 20082 20041 85 | 79501 79500 86 | 20334 20292 87 | 79251 79250 88 | 20586 20543 89 | 79001 79000 90 | 20838 20794 91 | 78751 78750 92 | 21090 21045 93 | 78501 78500 94 | 21342 21296 95 | 78251 78250 96 | 21594 21547 97 | 78001 78000 98 | 21846 21798 99 | 77751 77750 100 | 22098 22049 101 | 77501 77500 102 | 22350 22300 103 | 77251 77250 104 | 22602 22551 105 | 77001 77000 106 | 22854 22802 107 | 76751 76750 108 | 23106 23053 109 | 76501 76500 110 | 23358 23304 111 | 76251 76250 112 | 23610 23555 113 | 76001 76000 114 | 23862 23806 115 | 75751 75750 116 | 24114 24057 117 | 75501 75500 118 | 24366 24308 119 | 75251 75250 120 | 24618 24559 121 | 75001 75000 122 | 24870 24810 123 | 74751 74750 124 | 25122 25061 125 | 74501 74500 126 | 25374 25312 127 | 74251 74250 128 | 25626 25563 129 | 74001 74000 130 | 25878 25814 131 | 73751 73750 132 | 26130 26065 133 | 73501 73500 134 | 26382 26316 135 | 73251 73250 136 | 26634 26567 137 | 73001 73000 138 | 26886 26818 139 | 72751 72750 140 | 27138 27069 141 | 72501 72500 142 | 27390 27320 143 | 72251 72250 144 | 27642 27571 145 | 72001 72000 146 | 27894 27822 147 | 71751 71750 148 | 28146 28073 149 | 71501 71500 150 | 28398 28324 151 | 71251 71250 152 | 28650 28575 153 | 71001 71000 154 | 28902 28826 155 | 70751 70750 156 | 29154 29077 157 | 70501 70500 158 | 29406 29328 159 | 70251 70250 160 | 29658 29579 161 | 70001 70000 162 | 29910 29830 163 | 69751 69750 164 | 30162 30081 165 | 69501 69500 166 | 30414 30332 167 | 69251 69250 168 | 30666 30583 169 | 69001 69000 170 | 30918 30834 171 | 68751 68750 172 | 31170 31085 173 | 68501 68500 174 | 31422 31336 175 | 68251 68250 176 | 31674 31587 177 | 68001 68000 178 | 31926 31838 179 | 67751 67750 180 | 32178 32089 181 | 67501 67500 182 | 32430 32340 183 | 67251 67250 184 | 32682 32591 185 | 67001 67000 186 | 32934 32842 187 | 66751 66750 188 | 33186 33093 189 | 66501 66500 190 | 33438 33344 191 | 66251 66250 192 | 33690 33595 193 | 66001 66000 194 | 33942 33846 195 | 65751 65750 196 | 34194 34097 197 | 65501 65500 198 | 34446 34348 199 | 65251 65250 200 | 34698 34599 201 | 68451 68450 202 | --------------------------------------------------------------------------------