diff --git a/README.md b/README.md index 5a102c95..52d09c92 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,17 @@ Proof of concept. Not suitable for use in production. Significant API changes an ## Implemented constraints -- `not_equal` -- `less_or_equal` +- `equal`, `not_equal`, `less_or_equal` +- `absolute` - `all_different` -- `sum` +- `sum`, `modulo` - `element`, `element2d` - `circuit` +- `OR` ## Features -- views (linear combinations of variables in constraints) +- views (linear combinations of variables in constraints) +- partial support for reified constraints - solving constraint satisfaction (CSP) and constrained optimization (COP) problems - parallel search - pluggable search strategies @@ -36,7 +38,7 @@ The package can be installed by adding `fixpoint` to your list of dependencies i ```elixir def deps do [ - {:fixpoint, "~> 0.8.4"} + {:fixpoint, "~> 0.8.28"} ] end ``` @@ -194,7 +196,7 @@ Available options: [Search strategy](#search). -- max_space_threads: integer() +- space_threads: integer() Defines the number of processes for parallel search. Defaults to 8. @@ -333,7 +335,15 @@ Constraint Optimization Problem - assign facilities to locations so the cost of https://en.wikipedia.org/wiki/Travelling_salesman_problem +#### [SAT Solver](lib/examples/sat_solver.ex) + +#### [Stable Marriage problem](lib/examples/stable_marriage) + +https://en.wikipedia.org/wiki/Stable_marriage_problem + #### [`xkcd` comic](livebooks/xkcd_np.livemd) Two combinatorial problems from https://xkcd.com/287/ +#### [Fixpoint models created by HÃ¥kan Kjellerstrand](http://hakank.org/elixir/) + diff --git a/data/qap/numbers b/data/qap/numbers new file mode 100644 index 00000000..ddc7e314 --- /dev/null +++ b/data/qap/numbers @@ -0,0 +1,5 @@ +qap12 - 4776 (opt) +qap25 - 4752 +qap12a_opt288 - 293 +qap14_opt507 - 515 +qap16_opt34 - 34 diff --git a/data/sat/uf100-01.cnf b/data/sat/uf100-01.cnf new file mode 100644 index 00000000..57060e34 --- /dev/null +++ b/data/sat/uf100-01.cnf @@ -0,0 +1,441 @@ +c This Formular is generated by mcnf +c +c horn? no +c forced? no +c mixed sat? no +c clause length = 3 +c +p cnf 100 430 + 26 -99 7 0 +-90 84 -94 0 +74 -99 -7 0 +31 5 61 0 +29 16 -10 0 +9 -41 -98 0 +30 46 -43 0 +-78 -5 67 0 +85 -82 22 0 +-69 26 -57 0 +-77 -40 -12 0 +-34 85 13 0 +43 -86 -49 0 +26 -54 -82 0 +88 67 4 0 +16 -21 75 0 +95 29 14 0 +-51 -47 -8 0 +-99 80 31 0 +-58 -14 97 0 +-97 75 -62 0 +100 97 -86 0 +36 -54 -35 0 +-20 52 -70 0 +-80 -89 -10 0 +-17 47 42 0 +35 17 40 0 +9 66 97 0 +33 -28 1 0 +99 58 -21 0 +76 51 18 0 +62 -63 -51 0 +44 91 -47 0 +-89 -67 -82 0 +22 -67 -18 0 +14 -51 1 0 +65 29 -92 0 +97 73 -65 0 +-97 -1 64 0 +-39 -12 -100 0 +7 -67 -24 0 +37 -62 20 0 +-2 73 22 0 +64 -13 34 0 +-61 -30 -34 0 +-33 -92 -54 0 +-48 -36 -13 0 +17 66 -62 0 +71 96 -89 0 +-27 84 -9 0 +48 75 -86 0 +-52 79 -21 0 +53 -63 -50 0 +-90 -70 78 0 +-58 -99 84 0 +60 6 1 0 +29 59 -11 0 +-34 -75 19 0 +65 -78 -88 0 +43 -13 60 0 +-53 -38 25 0 +-86 -12 90 0 +-53 -94 -88 0 +-90 -42 54 0 +-38 -26 22 0 +16 -5 39 0 +-71 43 -33 0 +-74 -92 -87 0 +-36 53 -4 0 +98 -68 -13 0 +-20 81 69 0 +79 -91 69 0 +-3 -25 -99 0 +-52 21 15 0 +-43 7 -12 0 +-20 45 -57 0 +-15 -81 68 0 +-91 -94 43 0 +-52 -34 -48 0 +10 -31 -62 0 +-99 -6 32 0 +-33 -9 14 0 +-18 -83 -98 0 +88 -23 13 0 +-91 99 32 0 +-40 -19 23 0 +4 7 25 0 +21 -25 -98 0 +97 73 -92 0 +10 98 -79 0 +-59 -89 -18 0 +96 -63 100 0 +36 100 30 0 +-3 14 -87 0 +-76 -33 91 0 +-53 33 -30 0 +-63 8 51 0 +-68 -64 29 0 +-12 -1 -60 0 +-92 70 -16 0 +-23 77 22 0 +-40 -10 -42 0 +19 39 33 0 +-94 48 31 0 +35 37 95 0 +-40 -42 95 0 +94 33 51 0 +4 -42 -61 0 +-67 83 -44 0 +22 70 25 0 +35 59 -85 0 +70 13 11 0 +42 -51 -76 0 +99 -54 -48 0 +-31 -72 -52 0 +-99 -23 30 0 +-61 21 62 0 +-5 -82 59 0 +-46 83 99 0 +74 55 21 0 +97 -8 -32 0 +76 51 3 0 +45 28 25 0 +49 53 91 0 +97 19 -70 0 +-27 -41 -86 0 +40 -43 27 0 +86 24 -77 0 +18 75 -37 0 +-24 18 100 0 +-71 62 -85 0 +-33 60 -94 0 +-9 -87 97 0 +69 15 98 0 +-86 47 -38 0 +30 -10 -34 0 +-65 -99 13 0 +32 -66 -1 0 +51 19 13 0 +67 -10 83 0 +77 24 -31 0 +-64 -2 -95 0 +-90 92 -17 0 +-88 70 86 0 +36 -84 19 0 +-39 -65 -37 0 +-65 39 76 0 +10 -3 27 0 +-94 -81 15 0 +89 16 7 0 +71 69 56 0 +-4 -82 6 0 +-73 -98 68 0 +-93 -72 -29 0 +-61 -68 -77 0 +87 71 -49 0 +-72 -14 -73 0 +-19 49 -66 0 +100 41 -28 0 +-89 74 47 0 +-46 -27 17 0 +85 41 58 0 +37 65 92 0 +-23 -52 -75 0 +40 -38 100 0 +50 -5 -63 0 +-29 15 30 0 +12 -70 11 0 +50 -15 74 0 +-79 6 -76 0 +-25 60 59 0 +76 32 -48 0 +-52 30 51 0 +-84 82 58 0 +-95 -86 29 0 +-24 -89 98 0 +-15 -51 33 0 +-44 -7 79 0 +-36 52 -93 0 +17 -3 85 0 +-83 -96 36 0 +81 -93 62 0 +63 -98 -24 0 +52 27 -44 0 +44 -37 54 0 +76 63 -21 0 +-22 23 91 0 +100 99 43 0 +-16 92 -61 0 +-10 79 -18 0 +88 -81 61 0 +-6 -14 -26 0 +-41 96 -60 0 +-17 -36 90 0 +-58 -5 4 0 +73 -56 -91 0 +-36 -70 -95 0 +-22 -6 87 0 +-80 -6 42 0 +95 41 99 0 +88 63 -98 0 +56 -95 -75 0 +38 -66 -53 0 +-98 61 1 0 +-61 -84 -78 0 +-67 88 -48 0 +44 -63 69 0 +84 16 94 0 +-95 -31 -10 0 +33 58 37 0 +95 83 23 0 +-15 92 -94 0 +11 94 88 0 +-53 25 52 0 +-15 -53 7 0 +87 -56 -6 0 +40 -13 -45 0 +-5 6 -48 0 +-13 64 2 0 +13 -40 74 0 +42 -77 -91 0 +-1 -26 -45 0 +-63 2 46 0 +67 -75 24 0 +-44 40 -18 0 +-5 -7 44 0 +-53 -19 32 0 +95 -40 29 0 +-62 -61 15 0 +30 -36 -54 0 +100 -3 -90 0 +75 7 -1 0 +-33 98 18 0 +1 -100 -73 0 +-30 -14 -100 0 +-67 60 -100 0 +40 -77 -61 0 +19 -16 -61 0 +49 -77 -98 0 +-86 22 -95 0 +-84 72 15 0 +-37 68 -94 0 +60 -26 -91 0 +19 11 44 0 +-98 18 -57 0 +-47 -86 -33 0 +-78 5 29 0 +47 -61 63 0 +4 100 -35 0 +82 14 -83 0 +100 52 71 0 +94 -91 44 0 +59 8 -6 0 +-45 -48 87 0 +46 76 15 0 +14 -73 85 0 +-39 76 68 0 +20 -44 19 0 +-86 35 -92 0 +10 4 9 0 +52 -31 -2 0 +60 -15 -62 0 +-40 96 42 0 +-64 -21 56 0 +96 95 -59 0 +86 42 -89 0 +82 33 36 0 +49 4 76 0 +71 61 -68 0 +76 -42 -53 0 +22 -61 7 0 +84 -64 -57 0 +54 20 69 0 +34 98 69 0 +60 100 56 0 +-77 -55 -91 0 +-8 -47 36 0 +-30 21 -28 0 +-6 -83 -52 0 +88 -44 75 0 +-74 33 62 0 +-37 -23 72 0 +79 16 59 0 +61 -20 -91 0 +-87 81 -18 0 +68 -97 5 0 +54 -96 43 0 +64 -47 -46 0 +30 13 -78 0 +95 -11 -52 0 +-85 -38 71 0 +-10 21 -27 0 +83 10 -41 0 +-87 -62 7 0 +-35 -76 41 0 +61 -72 49 0 +-63 70 -37 0 +84 5 93 0 +-18 -85 -24 0 +18 42 95 0 +-64 -100 -55 0 +58 -30 -7 0 +-30 -56 81 0 +-72 -5 -90 0 +50 4 97 0 +50 -71 -24 0 +-74 -24 -12 0 +69 -99 19 0 +55 24 -10 0 +-99 68 100 0 +20 -61 77 0 +26 39 66 0 +1 88 -35 0 +24 92 10 0 +29 34 96 0 +34 -48 68 0 +53 -17 -71 0 +-54 -55 77 0 +-74 98 5 0 +11 -59 73 0 +-14 19 49 0 +-14 -81 57 0 +74 52 66 0 +-63 -13 -37 0 +6 25 -78 0 +26 -30 -28 0 +20 40 23 0 +77 90 99 0 +-90 42 75 0 +62 -43 -66 0 +19 -80 -6 0 +13 6 4 0 +-68 86 -31 0 +-4 -1 2 0 +-19 81 -84 0 +77 16 66 0 +-34 6 -33 0 +36 1 -5 0 +-6 -13 66 0 +-2 89 85 0 +45 23 35 0 +-40 77 -41 0 +46 -81 -56 0 +-14 -89 81 0 +-84 -73 -38 0 +87 2 6 0 +-56 51 -27 0 +53 68 52 0 +-76 32 -23 0 +6 -94 66 0 +28 -48 24 0 +83 24 86 0 +80 -73 -75 0 +-66 56 24 0 +82 -18 -28 0 +-69 89 36 0 +27 5 12 0 +-94 -20 -25 0 +-56 9 -79 0 +85 -71 -6 0 +-34 -94 -67 0 +-60 -24 5 0 +-67 -90 18 0 +-83 45 -12 0 +21 96 -25 0 +-1 75 -77 0 +-60 -90 -98 0 +88 31 58 0 +8 -4 92 0 +-75 85 100 0 +-82 -11 15 0 +33 37 -35 0 +-98 -88 28 0 +-3 1 54 0 +-98 -94 3 0 +-37 75 24 0 +96 -65 39 0 +-28 92 27 0 +-5 50 20 0 +-76 41 33 0 +91 74 100 0 +-17 -21 -98 0 +93 79 84 0 +-39 -38 80 0 +98 71 -63 0 +-56 -39 -73 0 +-87 70 -17 0 +-76 17 -46 0 +-90 76 17 0 +-92 -31 82 0 +54 -16 69 0 +22 58 92 0 +-7 -17 -33 0 +31 65 -44 0 +59 -25 -8 0 +71 -32 -83 0 +-62 79 -11 0 +69 -24 68 0 +70 -6 7 0 +-91 -11 64 0 +66 -21 74 0 +2 -22 8 0 +64 44 48 0 +-73 15 -28 0 +-19 20 91 0 +-25 84 62 0 +28 35 43 0 +60 10 61 0 +-31 -13 21 0 +39 -10 -68 0 +69 16 -31 0 +-41 -35 95 0 +36 91 -73 0 +75 42 99 0 +-71 43 15 0 +-68 -50 -70 0 +50 -4 -5 0 +56 -15 77 0 +6 -33 -48 0 +-51 36 -23 0 +66 3 -7 0 +5 -51 -74 0 +-96 -50 68 0 +78 -83 59 0 +5 37 -59 0 +-84 69 -96 0 +59 8 47 0 +-85 -13 -91 0 +78 -63 51 0 +60 92 -73 0 +% +0 + diff --git a/data/sat/uf50-01.cnf b/data/sat/uf50-01.cnf new file mode 100644 index 00000000..8b693a0d --- /dev/null +++ b/data/sat/uf50-01.cnf @@ -0,0 +1,229 @@ +c This Formular is generated by mcnf +c +c horn? no +c forced? no +c mixed sat? no +c clause length = 3 +c +p cnf 50 218 + -3 36 7 0 +-3 -42 -48 0 +-49 -47 -41 0 +8 -40 17 0 +-21 -31 -39 0 +36 -22 49 0 +27 38 14 0 +15 -18 6 0 +6 7 -43 0 +34 -7 23 0 +2 14 -13 0 +2 47 -42 0 +-33 -35 3 0 +44 40 49 0 +50 36 31 0 +-36 -3 -37 0 +26 -29 43 0 +15 29 -45 0 +24 -11 18 0 +-47 -26 6 0 +-50 -33 -10 0 +32 6 16 0 +-34 37 41 0 +7 -28 -17 0 +-44 46 19 0 +7 22 -48 0 +3 39 34 0 +31 46 -43 0 +-27 32 23 0 +37 -50 -18 0 +20 5 11 0 +-45 -24 6 0 +-34 -23 -14 0 +-22 21 20 0 +-17 50 24 0 +-25 -24 -27 0 +3 35 21 0 +-26 47 -36 0 +-28 -45 49 0 +-21 -6 12 0 +-17 -15 -39 0 +41 2 -14 0 +25 36 -23 0 +-39 -3 -40 0 +50 20 35 0 +27 31 -39 0 +45 -15 -40 0 +34 50 35 0 +-1 -48 12 0 +18 -35 -30 0 +27 -24 -25 0 +-4 -33 -12 0 +-43 -24 -37 0 +-37 31 -44 0 +-9 -38 14 0 +33 -16 34 0 +4 -35 -5 0 +-3 -21 -19 0 +-35 -36 -29 0 +7 -43 36 0 +30 14 41 0 +-35 -24 -7 0 +35 -42 6 0 +-1 -15 39 0 +27 49 -16 0 +-37 49 -10 0 +50 -46 -3 0 +-41 20 34 0 +-1 23 28 0 +-12 -30 -20 0 +-24 29 -37 0 +12 5 -44 0 +-6 -2 48 0 +-2 -49 -43 0 +1 -50 24 0 +-7 -50 -44 0 +-41 43 4 0 +13 15 -11 0 +-3 -11 23 0 +33 48 41 0 +9 23 -49 0 +-43 47 1 0 +-40 16 -29 0 +30 19 3 0 +19 -34 48 0 +-16 -44 14 0 +38 -45 -12 0 +-4 -14 -31 0 +-48 35 -1 0 +45 -13 19 0 +9 42 -7 0 +-1 -15 8 0 +-13 -44 -14 0 +-43 -37 -31 0 +-27 -29 47 0 +7 4 17 0 +7 10 35 0 +-25 20 17 0 +35 -5 -42 0 +-50 24 -5 0 +-21 -26 2 0 +-8 45 -21 0 +-16 33 49 0 +-38 6 16 0 +5 21 37 0 +8 38 31 0 +-21 33 14 0 +20 40 -5 0 +-29 -9 31 0 +-7 42 -22 0 +-48 8 26 0 +48 -38 33 0 +-34 49 46 0 +-14 -46 25 0 +-46 4 18 0 +36 -12 -31 0 +12 -18 14 0 +-7 46 -16 0 +9 -8 7 0 +49 -42 -22 0 +22 -15 38 0 +34 -41 47 0 +22 -26 32 0 +-25 -45 -21 0 +-26 32 -11 0 +15 26 -25 0 +-1 46 25 0 +-14 -31 30 0 +-9 -22 12 0 +-18 26 -35 0 +-16 -32 -21 0 +31 -49 -21 0 +11 9 41 0 +-13 -30 19 0 +-10 4 6 0 +-4 3 -22 0 +-25 -50 -18 0 +-40 4 9 0 +37 20 46 0 +-27 22 -29 0 +34 14 3 0 +3 -31 20 0 +-50 2 -26 0 +17 -29 38 0 +-49 12 -41 0 +15 -35 -43 0 +-22 -23 -49 0 +-9 33 48 0 +26 29 35 0 +27 -50 37 0 +-7 46 -43 0 +-46 -37 -8 0 +-40 36 -24 0 +-44 46 15 0 +-3 36 -16 0 +-48 9 43 0 +-25 -4 44 0 +-22 37 -7 0 +-31 -17 -22 0 +-11 -48 17 0 +23 34 -28 0 +23 -48 -39 0 +-37 -1 -23 0 +-19 27 14 0 +-22 33 -6 0 +-6 -32 -26 0 +18 -20 -46 0 +43 22 27 0 +-13 34 49 0 +-35 -46 3 0 +32 39 -43 0 +6 -39 -9 0 +27 39 -16 0 +25 -17 -15 0 +-43 27 34 0 +-6 49 5 0 +-38 11 14 0 +40 -38 47 0 +37 -14 17 0 +39 29 36 0 +-39 -28 1 0 +-18 14 -16 0 +-40 50 15 0 +37 -42 18 0 +-13 31 33 0 +2 -42 33 0 +8 -3 -22 0 +1 23 -31 0 +-20 -45 26 0 +42 11 49 0 +29 11 -43 0 +-20 -21 30 0 +23 45 -35 0 +38 -30 -14 0 +-9 48 -29 0 +11 -18 -23 0 +-41 -1 -29 0 +5 41 26 0 +44 -30 -7 0 +38 -6 -41 0 +46 48 -15 0 +-18 -10 -47 0 +38 46 -32 0 +-32 46 12 0 +31 40 14 0 +-18 2 49 0 +28 -38 27 0 +-16 -21 14 0 +-29 15 12 0 +49 34 5 0 +14 22 -12 0 +30 33 20 0 +-24 22 25 0 +4 -48 -23 0 +-30 -36 9 0 +44 12 -35 0 +38 3 -21 0 +-11 33 49 0 +% +0 + diff --git a/data/sat/uuf100-01.cnf b/data/sat/uuf100-01.cnf new file mode 100644 index 00000000..abcd6cda --- /dev/null +++ b/data/sat/uuf100-01.cnf @@ -0,0 +1,441 @@ +c This Formular is generated by mcnf +c +c horn? no +c forced? no +c mixed sat? no +c clause length = 3 +c +p cnf 100 430 + -46 90 77 0 +-100 -61 -2 0 +5 -95 79 0 +-34 -56 -23 0 +80 66 70 0 +-82 54 -34 0 +-77 28 33 0 +36 -17 -7 0 +-40 -90 -25 0 +55 -59 -46 0 +68 56 8 0 +74 -91 -62 0 +18 5 91 0 +94 58 37 0 +-98 74 -43 0 +66 4 67 0 +-9 -100 -46 0 +-82 64 -89 0 +-57 -48 -100 0 +51 -37 40 0 +-14 89 -40 0 +-20 -15 77 0 +30 -74 14 0 +-100 30 71 0 +97 57 -70 0 +73 -18 43 0 +-89 -18 -65 0 +15 13 30 0 +52 31 -22 0 +-26 -93 -25 0 +35 14 -22 0 +35 -29 100 0 +-24 21 -42 0 +85 -53 -15 0 +40 -81 -93 0 +-37 92 -93 0 +18 31 -98 0 +-32 66 9 0 +-66 4 -83 0 +49 -57 52 0 +13 -95 21 0 +-68 95 -55 0 +-54 44 -62 0 +-93 -82 85 0 +-46 -10 58 0 +27 -64 -12 0 +71 37 -85 0 +-21 56 -40 0 +63 -10 -37 0 +48 30 -8 0 +23 -55 34 0 +-72 28 7 0 +-17 84 -42 0 +61 -11 34 0 +-55 -26 -38 0 +83 7 19 0 +39 -80 -38 0 +-49 -92 -50 0 +88 -50 -62 0 +-51 -12 83 0 +85 -54 42 0 +-14 -9 29 0 +-17 -3 -96 0 +-18 81 -36 0 +37 53 -3 0 +-54 26 -28 0 +-44 -43 -6 0 +-76 -11 -63 0 +-57 -95 38 0 +-84 11 91 0 +-61 -83 -58 0 +-86 -20 29 0 +51 68 33 0 +71 -96 -4 0 +47 -71 89 0 +70 18 -100 0 +13 -92 -96 0 +-64 33 -55 0 +5 18 -89 0 +-87 72 47 0 +-93 78 22 0 +-68 -17 19 0 +51 22 -34 0 +98 22 -87 0 +-18 95 9 0 +-85 -82 -34 0 +44 -2 -81 0 +-58 -81 -16 0 +60 20 94 0 +8 -67 11 0 +-28 43 -95 0 +24 -63 82 0 +86 -27 22 0 +-98 -49 -38 0 +8 2 54 0 +33 45 -55 0 +-8 30 36 0 +31 20 -52 0 +19 -68 -7 0 +18 -64 -48 0 +-69 26 -14 0 +-4 -62 23 0 +-23 16 -58 0 +-44 55 51 0 +81 69 91 0 +-85 75 -64 0 +-85 -68 -73 0 +-1 80 57 0 +-18 70 -77 0 +12 15 -51 0 +53 -16 -74 0 +67 -81 48 0 +-39 58 91 0 +28 50 76 0 +86 30 -24 0 +36 -84 -70 0 +90 67 46 0 +49 45 -62 0 +46 -68 -44 0 +-31 -58 90 0 +-9 30 5 0 +-43 -28 -72 0 +-92 -14 -22 0 +-47 99 -29 0 +51 57 -40 0 +-13 79 -67 0 +33 46 42 0 +-12 -25 40 0 +12 -76 29 0 +-51 46 85 0 +52 -63 14 0 +89 85 -77 0 +-49 -17 -12 0 +90 -93 -64 0 +47 44 -5 0 +-66 86 -17 0 +-10 92 -53 0 +-73 -70 -74 0 +73 -48 47 0 +-96 13 83 0 +-54 -9 2 0 +-39 -97 96 0 +48 55 87 0 +76 12 -93 0 +61 44 71 0 +-60 -97 23 0 +-65 -6 -60 0 +-44 -42 63 0 +-40 92 -99 0 +-23 84 28 0 +-25 -24 -64 0 +50 -96 -79 0 +86 36 -8 0 +-55 41 80 0 +10 -25 2 0 +79 -15 52 0 +-16 -49 37 0 +-60 -44 -65 0 +55 -46 6 0 +-53 8 33 0 +83 -25 37 0 +76 -83 84 0 +41 38 -85 0 +-71 -59 20 0 +85 46 -26 0 +-39 84 -96 0 +-11 93 46 0 +36 -46 -60 0 +95 -22 23 0 +-33 -83 -60 0 +30 39 -44 0 +87 25 -53 0 +-54 -3 -14 0 +-67 -75 99 0 +-13 -56 73 0 +100 94 77 0 +11 32 89 0 +97 -3 49 0 +-87 42 77 0 +-98 35 -5 0 +-4 6 -53 0 +16 -41 92 0 +-51 -80 44 0 +-1 -76 -60 0 +27 58 17 0 +90 32 -28 0 +-3 27 -98 0 +-35 -83 60 0 +-96 -83 -25 0 +-37 86 -41 0 +76 7 100 0 +-60 52 -38 0 +-37 -81 51 0 +-46 54 -15 0 +-54 -46 -61 0 +-63 -75 83 0 +74 -37 83 0 +79 30 -8 0 +-62 72 20 0 +51 -95 -75 0 +-88 52 49 0 +-2 -88 -62 0 +12 77 85 0 +86 68 83 0 +35 95 19 0 +84 -94 -59 0 +57 66 -60 0 +-73 -61 34 0 +68 -66 -8 0 +-74 5 -47 0 +-99 98 47 0 +-30 86 76 0 +-47 77 -86 0 +21 -1 32 0 +79 25 8 0 +77 -14 19 0 +34 -100 33 0 +35 13 -20 0 +-36 -47 18 0 +-42 21 27 0 +-43 51 66 0 +99 21 -45 0 +-42 -44 92 0 +-48 -95 -84 0 +85 -99 1 0 +59 -8 88 0 +-36 -72 29 0 +-60 -22 82 0 +-90 -31 -36 0 +-53 -80 -93 0 +-91 -49 64 0 +-87 -9 -16 0 +49 -10 -35 0 +-90 24 12 0 +-15 -99 -98 0 +59 38 -18 0 +23 19 -64 0 +92 -55 -61 0 +3 45 -77 0 +20 -21 -95 0 +-71 -47 22 0 +44 7 -50 0 +58 -37 -11 0 +72 -66 10 0 +44 99 37 0 +21 -86 -72 0 +73 17 61 0 +-78 -81 42 0 +54 70 -64 0 +-61 -48 17 0 +-19 48 -13 0 +-33 -39 29 0 +33 43 -76 0 +76 11 -49 0 +19 -88 45 0 +-76 -5 -55 0 +-57 -98 -15 0 +19 -86 -94 0 +82 -70 22 0 +-39 -85 -63 0 +50 75 25 0 +-96 -37 -15 0 +-99 -75 -10 0 +-67 -63 38 0 +-7 -22 -18 0 +-39 -45 62 0 +-64 -79 37 0 +-69 -58 -100 0 +-46 17 43 0 +17 -11 46 0 +-22 -19 -86 0 +84 45 48 0 +46 -91 64 0 +-25 -15 30 0 +92 14 45 0 +-79 -69 43 0 +-34 -35 -69 0 +87 1 -82 0 +-64 70 -65 0 +-27 -29 75 0 +95 45 90 0 +62 43 76 0 +21 -11 -31 0 +53 24 -46 0 +-78 43 -12 0 +-74 9 22 0 +-5 7 -73 0 +12 -57 -7 0 +-19 12 90 0 +-47 -6 -50 0 +-35 -32 72 0 +-87 -100 -24 0 +-56 92 -16 0 +29 53 87 0 +-25 64 -20 0 +37 57 9 0 +25 -100 -4 0 +7 -16 77 0 +-98 7 -64 0 +98 50 -44 0 +95 -60 23 0 +7 92 -35 0 +-95 -47 -9 0 +82 -67 93 0 +-25 -49 -66 0 +-34 -60 49 0 +91 -70 8 0 +20 75 -94 0 +15 -27 -30 0 +-24 -95 -29 0 +78 -98 53 0 +-43 -44 -86 0 +-98 89 -40 0 +-55 31 -12 0 +-64 60 77 0 +86 -27 -37 0 +55 96 34 0 +5 44 92 0 +30 -90 -39 0 +-23 38 -36 0 +-40 -16 -12 0 +-67 8 61 0 +-4 -52 -51 0 +4 -26 76 0 +-18 -89 87 0 +-39 27 -32 0 +-15 -69 94 0 +83 -72 -93 0 +-19 70 -14 0 +45 -30 -69 0 +-48 -22 -83 0 +-66 -87 55 0 +-100 -45 -96 0 +-81 -97 32 0 +75 -36 -73 0 +-98 -14 78 0 +66 59 -36 0 +44 17 43 0 +1 -89 16 0 +75 -58 42 0 +-4 -98 22 0 +-31 -44 -73 0 +64 7 6 0 +26 -45 -3 0 +-48 -52 44 0 +98 -3 40 0 +35 -15 61 0 +97 44 -30 0 +16 75 -19 0 +4 -63 46 0 +91 -51 48 0 +48 5 46 0 +-31 7 70 0 +-35 24 93 0 +-48 -46 71 0 +-39 84 74 0 +-18 -61 -48 0 +-89 -99 -66 0 +-76 93 98 0 +76 -94 -86 0 +69 26 -62 0 +87 -30 35 0 +-93 81 -100 0 +33 -14 -51 0 +-70 58 -89 0 +-33 72 2 0 +69 1 43 0 +6 -28 -16 0 +64 18 68 0 +-10 -18 7 0 +-75 -5 -82 0 +89 -68 97 0 +-66 -77 59 0 +35 -45 63 0 +-51 -68 -28 0 +67 40 4 0 +45 5 71 0 +72 -1 -19 0 +76 16 45 0 +-49 69 70 0 +-75 93 -76 0 +-82 86 96 0 +95 98 32 0 +-13 5 29 0 +60 -91 14 0 +65 42 -33 0 +32 -2 -59 0 +-57 -48 82 0 +1 73 -15 0 +-76 89 48 0 +-29 83 -55 0 +-35 -54 76 0 +77 37 -59 0 +-26 10 83 0 +30 28 87 0 +48 28 56 0 +43 -3 6 0 +-12 20 11 0 +38 69 -57 0 +61 -54 -23 0 +48 20 96 0 +33 71 48 0 +-72 -17 -49 0 +-30 24 -62 0 +82 -4 39 0 +-73 99 -30 0 +-12 -33 29 0 +90 42 50 0 +-31 53 60 0 +78 20 -7 0 +-26 -97 -56 0 +14 76 -89 0 +76 -24 100 0 +61 -93 75 0 +24 -62 -33 0 +88 -3 52 0 +-100 72 91 0 +47 89 49 0 +69 -75 -2 0 +-57 18 47 0 +-45 -35 -26 0 +11 -78 -55 0 +-26 -13 -57 0 +-84 -28 45 0 +71 28 57 0 +-41 -60 39 0 +50 -67 -94 0 +73 -94 72 0 +99 51 58 0 +81 7 -57 0 +% +0 + diff --git a/data/sat/uuf50-01.cnf b/data/sat/uuf50-01.cnf new file mode 100644 index 00000000..801bcf92 --- /dev/null +++ b/data/sat/uuf50-01.cnf @@ -0,0 +1,229 @@ +c This Formular is generated by mcnf +c +c horn? no +c forced? no +c mixed sat? no +c clause length = 3 +c +p cnf 50 218 + 18 -8 29 0 +-16 3 18 0 +-36 -11 -30 0 +-50 20 32 0 +-6 9 35 0 +42 -38 29 0 +43 -15 10 0 +-48 -47 1 0 +-45 -16 33 0 +38 42 22 0 +-49 41 -34 0 +12 17 35 0 +22 -49 7 0 +-10 -11 -39 0 +-28 -36 -37 0 +-13 -46 -41 0 +21 -4 9 0 +12 48 10 0 +24 23 15 0 +-8 -41 -43 0 +-44 -2 -35 0 +-27 18 31 0 +47 35 6 0 +-11 -27 41 0 +-33 -47 -45 0 +-16 36 -37 0 +27 -46 2 0 +15 -28 10 0 +-38 46 -39 0 +-33 -4 24 0 +-12 -45 50 0 +-32 -21 -15 0 +8 42 24 0 +30 -49 4 0 +45 -9 28 0 +-33 -47 -1 0 +1 27 -16 0 +-11 -17 -35 0 +-42 -15 45 0 +-19 -27 30 0 +3 28 12 0 +48 -11 -33 0 +-6 37 -9 0 +-37 13 -7 0 +-2 26 16 0 +46 -24 -38 0 +-13 -24 -8 0 +-36 -42 -21 0 +-37 -19 3 0 +-31 -50 35 0 +-7 -26 29 0 +-42 -45 29 0 +33 25 -6 0 +-45 -5 7 0 +-7 28 -6 0 +-48 31 -11 0 +32 16 -37 0 +-24 48 1 0 +18 -46 23 0 +-30 -50 48 0 +-21 39 -2 0 +24 47 42 0 +-36 30 4 0 +-5 28 -1 0 +-47 32 -42 0 +16 37 -22 0 +-43 42 -34 0 +-40 39 -20 0 +-49 29 6 0 +-41 -3 39 0 +-16 -12 43 0 +24 22 3 0 +47 -45 43 0 +45 -37 46 0 +-9 26 5 0 +-3 23 -13 0 +5 -34 13 0 +12 39 13 0 +22 50 37 0 +19 9 46 0 +-24 8 -27 0 +-28 7 21 0 +8 -25 50 0 +20 50 4 0 +27 36 13 0 +26 31 -25 0 +39 -44 -32 0 +-20 41 -10 0 +49 -28 35 0 +1 44 34 0 +39 35 -11 0 +-50 -42 -7 0 +-24 7 47 0 +-13 5 -48 0 +-9 -20 -23 0 +2 17 -19 0 +11 23 21 0 +-45 30 15 0 +11 26 -24 0 +38 33 -13 0 +44 -27 -7 0 +41 49 2 0 +-18 12 -37 0 +-2 12 -26 0 +-19 7 32 0 +-22 11 33 0 +8 12 -20 0 +16 40 -48 0 +-2 -24 -11 0 +26 -17 37 0 +-14 -19 46 0 +5 47 36 0 +-29 -9 19 0 +32 4 28 0 +-34 20 -46 0 +-4 -36 -13 0 +-15 -37 45 0 +-21 29 23 0 +-6 -40 7 0 +-42 31 -29 0 +-36 24 31 0 +-45 -37 -1 0 +3 -6 -29 0 +-28 -50 27 0 +44 26 5 0 +-17 -48 49 0 +12 -40 -7 0 +-12 31 -48 0 +27 32 -42 0 +-27 -10 1 0 +6 -49 10 0 +-24 8 43 0 +23 31 1 0 +11 -47 38 0 +-28 26 -13 0 +-40 12 -42 0 +-3 39 46 0 +17 41 46 0 +23 21 13 0 +-14 -1 -38 0 +20 18 6 0 +-50 20 -9 0 +10 -32 -18 0 +-21 49 -34 0 +44 23 -35 0 +40 -19 34 0 +-1 6 -12 0 +6 -2 -7 0 +32 -20 34 0 +-12 43 -29 0 +24 2 -49 0 +10 -4 40 0 +11 5 12 0 +-3 47 -31 0 +43 -23 21 0 +-41 -36 -50 0 +-8 -42 -24 0 +39 45 7 0 +7 37 -45 0 +41 40 8 0 +-50 -10 -8 0 +-5 -39 -14 0 +-22 -24 -43 0 +-36 40 35 0 +17 49 41 0 +-32 7 24 0 +-30 -8 -9 0 +-41 -13 -10 0 +31 26 -33 0 +17 -22 -39 0 +-21 28 3 0 +-14 46 23 0 +29 16 19 0 +42 -32 -44 0 +-24 10 23 0 +-1 -32 -21 0 +-8 -44 -39 0 +39 11 9 0 +19 14 -46 0 +46 44 -42 0 +37 23 -29 0 +32 25 20 0 +14 -43 -12 0 +-36 -18 46 0 +14 -26 -10 0 +-2 -30 5 0 +6 -18 46 0 +-26 2 -44 0 +20 -8 -11 0 +-31 3 16 0 +-22 -9 39 0 +-49 44 -42 0 +-45 -44 31 0 +-31 50 -11 0 +-32 -46 2 0 +-6 -7 17 0 +19 -32 48 0 +39 20 -10 0 +-22 -37 38 0 +-31 9 -48 0 +40 12 7 0 +-24 -4 9 0 +-22 49 33 0 +-12 43 10 0 +25 -30 -10 0 +46 47 31 0 +13 27 -7 0 +-45 32 -35 0 +-50 34 9 0 +2 34 30 0 +3 16 2 0 +-18 45 -12 0 +33 37 10 0 +43 7 -18 0 +-22 44 -19 0 +-31 -27 -42 0 +-3 -40 8 0 +-23 -31 38 0 +% +0 + diff --git a/data/tsp/numbers b/data/tsp/numbers index cb0ed160..0243cf49 100644 --- a/data/tsp/numbers +++ b/data/tsp/numbers @@ -1,6 +1,6 @@ tsp_15.txt - 291 (opt) -tsp_17.txt - ? -tsp_26.txt - ? -tsp_61.txt 357 (LNS) -tsp_81.txt 401 (LNS) -tsp_101.txt 482 (LNS) \ No newline at end of file +tsp_17.txt - 2085 (opt) +tsp_26.txt - 941 +tsp_61.txt 357 (LNS, minicp), 348 (fixpoint) +tsp_81.txt 401 (LNS, minicp), 412 (fixpoint) +tsp_101.txt 482 (LNS, minicp), 501 (fixpoint) diff --git a/lib/examples/euler43.ex b/lib/examples/euler43.ex new file mode 100644 index 00000000..b19c0512 --- /dev/null +++ b/lib/examples/euler43.ex @@ -0,0 +1,77 @@ +defmodule CPSolver.Examples.Euler43 do + @moduledoc """ + https://projecteuler.net/problem=43 + """ + + @doc """ + int: n = 10; + array[1..n] of var 0..9: x; + array[int] of int: primes = [2,3,5,7,11,13,17]; + solve satisfy; + constraint + all_different(x) /\ + forall(i in 2..8) ( + (100*x[i] + 10*x[i+1] + x[i+2]) mod primes[i-1] = 0 + ) + ; + output [ show(x),"\n"]; + """ + + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent + alias CPSolver.Constraint.Modulo + alias CPSolver.Model + import CPSolver.Constraint.Factory + import CPSolver.Variable.View.Factory + + require Logger + + @minizinc_solutions Enum.sort([ + [4, 1, 6, 0, 3, 5, 7, 2, 8, 9], + [1, 4, 6, 0, 3, 5, 7, 2, 8, 9], + [4, 1, 0, 6, 3, 5, 7, 2, 8, 9], + [1, 4, 0, 6, 3, 5, 7, 2, 8, 9], + [4, 1, 3, 0, 9, 5, 2, 8, 6, 7], + [1, 4, 3, 0, 9, 5, 2, 8, 6, 7] + ]) + + def model() do + primes = [2, 3, 5, 7, 11, 13, 17] + domain = 0..9 + + x = Enum.map(1..10, fn i -> Variable.new(domain, name: "x#{i}") end) + + all_different_constraint = AllDifferent.new(x) + + constraints = + Enum.reduce(2..8, [all_different_constraint], fn i, constraints_acc -> + x_i = Enum.at(x, i - 1) + x_i_1 = Enum.at(x, i) + x_i_2 = Enum.at(x, i + 1) + prime = Enum.at(primes, i - 2) + {sum_var, sum_constraint} = sum([mul(x_i, 100), mul(x_i_1, 10), x_i_2]) + mod_constraint = Modulo.new(0, sum_var, prime) + [sum_constraint, mod_constraint | constraints_acc] + end) + + Model.new(x, constraints) + end + + def check_solution(solution) do + solution + |> Enum.take(10) + |> Kernel.in(@minizinc_solutions) + end + + def run(opts \\ [search: {:input_order, :indomain_random}, space_threads: 8]) do + {:ok, res} = CPSolver.solve_sync(model(), opts) + + Enum.sort(Enum.map(res.solutions, fn s -> Enum.take(s, 10) end)) + |> tap(fn sorted_solutions -> + (sorted_solutions == @minizinc_solutions && + Logger.notice("Solutions correspond to the ones given by MinZinc")) || + Logger.error("Solutions do not match MiniZinc") + end) + |> Enum.reduce(0, fn s, sum_acc -> Integer.undigits(s) + sum_acc end) + end +end diff --git a/lib/examples/quadratic_assignment.ex b/lib/examples/quadratic_assignment.ex index a1ce28dc..a73e7445 100644 --- a/lib/examples/quadratic_assignment.ex +++ b/lib/examples/quadratic_assignment.ex @@ -19,6 +19,7 @@ defmodule CPSolver.Examples.QAP do """ alias CPSolver.IntVariable, as: Variable + alias CPSolver.Variable.Interface alias CPSolver.Model alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent alias CPSolver.Objective @@ -37,7 +38,7 @@ defmodule CPSolver.Examples.QAP do [ search: search(model), solution_handler: solution_handler(model), - max_search_threads: 12, + space_threads: 8, timeout: :timer.minutes(5) ], opts @@ -83,7 +84,7 @@ defmodule CPSolver.Examples.QAP do end end - {total_cost, sum_constraint} = sum(weighted_distances, name: "total_cost") + {total_cost, sum_constraint} = sum(weighted_distances) Model.new( assignments, @@ -92,7 +93,7 @@ defmodule CPSolver.Examples.QAP do sum_constraint ] ++ element2d_constraints, objective: Objective.minimize(total_cost), - extra: %{n: n, distances: distances, weights: weights} + extra: %{n: n, distances: distances, weights: weights, total_cost_var_id: Interface.id(total_cost)} ) end @@ -113,8 +114,8 @@ defmodule CPSolver.Examples.QAP do fn solution -> solution |> Enum.at(model.extra.n) - |> tap(fn total_cost_tuple -> - ans_str = inspect(total_cost_tuple) + |> tap(fn {_ref, total} -> + ans_str = inspect({"total", total}) (check_solution( Enum.map(solution, fn {_, val} -> val end), diff --git a/lib/examples/queens.ex b/lib/examples/queens.ex index e30585d4..f6dc8a8b 100644 --- a/lib/examples/queens.ex +++ b/lib/examples/queens.ex @@ -49,8 +49,10 @@ defmodule CPSolver.Examples.Queens do {:ok, result} = CPSolver.solve_sync(model(nqueens, :half_symmetry), + search: {:input_order, :indomain_random}, stop_on: {:max_solutions, 1}, - timeout: timeout + timeout: timeout, + space_threads: 4 ) case result.solutions do @@ -58,7 +60,7 @@ defmodule CPSolver.Examples.Queens do "No solutions found within #{timeout} milliseconds" [s | _rest] -> - print_board(s) + print_board(inside_out_to_normal(s)) |> tap(fn _ -> check_solution(s) && Logger.notice("Solution checked!") end) end diff --git a/lib/examples/sat_solver.ex b/lib/examples/sat_solver.ex new file mode 100644 index 00000000..1671568f --- /dev/null +++ b/lib/examples/sat_solver.ex @@ -0,0 +1,159 @@ +defmodule CPSolver.Examples.SatSolver do + alias CPSolver.Constraint.Or + alias CPSolver.Model + alias CPSolver.BooleanVariable + alias CPSolver.Variable.Interface + import CPSolver.Variable.View.Factory + + require Logger + + @moduledoc """ + This module solves SAT problems represented in CNF form. + It's a list of lists of integers, where a positive integer `i` represents a boolean variable mapped to `i`, + and a negative integer `j` represents negation of a boolean variable mapped to `j`. + Examples of CNF representation: + + ```elixir + # x1 AND (NOT x1) + [[1], [-1]] + + # x1 AND (x1 OR x2 OR x3) + [[1], [1, 2, 3]] + + # x1 AND x2 AND x3 + [[1], [2], [3]] + ``` + """ + def solve(clauses, opts \\ []) do + Keyword.get(opts, :print) && Logger.configure(level: :notice) + model = model(clauses) + + default_opts = + [ + search: {:first_fail, :indomain_max}, + stop_on: {:max_solutions, 1} + ] + {:ok, res} = + CPSolver.solve_sync(model, + Keyword.merge(default_opts, opts) + ) + + cond do + res.status == :unsatisfiable -> :unsatisfiable + Enum.empty?(res.solutions) -> :unknown + true -> + List.first(res.solutions) |> sort_by_variables(res.variables) + end + |> tap(fn _ -> Logger.notice(inspect(res, pretty: true)) end) + + + end + + def model(dimacs_instance) when is_atom(dimacs_instance) do + dimacs_instance + |> clauses() + |> model() + end + + def model(clauses) when is_list(clauses) do + {vars, constraints} = + Enum.reduce(clauses, {Map.new(), []}, fn clause, {vars_acc, constraints_acc} -> + {clause_vars, new_vars_acc} = build_clause(clause, vars_acc) + {new_vars_acc, [Or.new(clause_vars) | constraints_acc]} + end) + + final_vars = + Enum.flat_map(vars, fn {literal_id, var} -> (literal_id > 0 && [var]) || [] end) + |> Enum.sort_by(fn var -> Interface.variable(var).name end) + + Model.new(final_vars, constraints) + end + + ## We assume it's a 3-SAT instance + def clauses(dimacs_instance) do + dimacs_instances() + |> Map.get(dimacs_instance) + |> File.read!() + |> String.split("\n") + |> Enum.flat_map( + fn line -> + case String.split(line, " ", trim: true) do + [x1, x2, x3, _0] = _clause when x1 not in ["p", "c"] -> + [Enum.map([x1, x2, x3], fn x -> String.to_integer(x) end)] + _other -> + [] + end + end) + end + + def check_solution(solution, dimacs_instance) when is_atom(dimacs_instance) do + dimacs_instance + |> clauses() + |> then(fn clauses -> check_solution(solution, clauses) end) + end + + def check_solution(solution, clauses) when is_list(clauses) do + ## Transform the solution into the form compatible with clause representation. + cnf_solution = to_cnf(solution) + + Enum.all?(clauses, fn clause -> + Enum.any?(clause, fn literal -> literal in cnf_solution end) + end) + end + + def to_cnf(solution) do + Enum.reduce(Enum.with_index(solution, 1), MapSet.new(), + fn {bool, idx}, acc -> + set_val = (bool == 0 && -idx || idx) + MapSet.put(acc, set_val) + end) + end + + defp build_clause(clause, literal_map) do + Enum.reduce(clause, {[], literal_map}, fn literal, {clause_acc, literal_map_acc} -> + ## Get or create literal variable + ## Has literal variable been already registered? + {literal_var, updated_literal_map} = + case Map.get(literal_map_acc, literal) do + nil -> + ## New literal variable, create it and update the literal map + new_literal_variable = create_variable(literal, Map.get(literal_map_acc, -literal)) + {new_literal_variable, Map.put(literal_map_acc, literal, new_literal_variable)} + + existing_literal_variable -> + {existing_literal_variable, literal_map_acc} + end + + {[literal_var | clause_acc], updated_literal_map} + end) + end + + ## Creates a literal variable + defp create_variable(var_id, nil) when is_integer(var_id) do + var = BooleanVariable.new(name: "#{abs(var_id)}") + (var_id > 0 && var) || negation(var) + end + + defp create_variable(var_id, negation) when var_id > 0 do + Interface.variable(negation) + end + + defp create_variable(var_id, positive_literal_variable) when var_id < 0 do + negation(positive_literal_variable) + end + + defp sort_by_variables(solution, variables) do + Enum.zip(solution, variables) + |> Enum.sort_by(fn {_val, var_name} -> String.to_integer(var_name) end) + |> Enum.map(fn {val, _var_name} -> val end) + end + + def dimacs_instances() do + %{ + sat50_218: "data/sat/uf50-01.cnf", + unsat50_218: "data/sat/uuf50-01.cnf", + sat100_403: "data/sat/uf100-01.cnf", + unsat100_403: "data/sat/uuf100-01.cnf" + } + end +end diff --git a/lib/examples/stable_marriage.ex b/lib/examples/stable_marriage.ex new file mode 100644 index 00000000..928a5891 --- /dev/null +++ b/lib/examples/stable_marriage.ex @@ -0,0 +1,189 @@ +defmodule CPSolver.Examples.StableMarriage do + @doc """ + Stable marriage problem. + https://en.wikipedia.org/wiki/Stable_marriage_problem. + + """ + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Model + alias CPSolver.Constraint.{ElementVar, Less} + alias CPSolver.Constraint.Factory, as: ConstraintFactory + alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent + + def instances() do + %{ + van_hentenryck: %{ + rankWomen: [ + [1, 2, 4, 3, 5], + [3, 5, 1, 2, 4], + [5, 4, 2, 1, 3], + [1, 3, 5, 4, 2], + [4, 2, 3, 5, 1] + ], + rankMen: [ + [5, 1, 2, 4, 3], + [4, 1, 3, 2, 5], + [5, 3, 2, 4, 1], + [1, 5, 4, 3, 2], + [4, 3, 2, 1, 5] + ] + }, + # http://mathworld.wolfram.com/StableMarriageProblem.html + mathworld: %{ + rankWomen: [ + [3, 1, 5, 2, 8, 7, 6, 9, 4], + [9, 4, 8, 1, 7, 6, 3, 2, 5], + [3, 1, 8, 9, 5, 4, 2, 6, 7], + [8, 7, 5, 3, 2, 6, 4, 9, 1], + [6, 9, 2, 5, 1, 4, 7, 3, 8], + [2, 4, 5, 1, 6, 8, 3, 9, 7], + [9, 3, 8, 2, 7, 5, 4, 6, 1], + [6, 3, 2, 1, 8, 4, 5, 9, 7], + [8, 2, 6, 4, 9, 1, 3, 7, 5] + ], + rankMen: [ + [7, 3, 8, 9, 6, 4, 2, 1, 5], + [5, 4, 8, 3, 1, 2, 6, 7, 9], + [4, 8, 3, 9, 7, 5, 6, 1, 2], + [9, 7, 4, 2, 5, 8, 3, 1, 6], + [2, 6, 4, 9, 8, 7, 5, 1, 3], + [2, 7, 8, 6, 5, 3, 4, 1, 9], + [1, 6, 2, 3, 8, 5, 4, 9, 7], + [5, 6, 9, 1, 2, 8, 4, 3, 7], + [6, 1, 4, 7, 5, 8, 3, 9, 2] + ] + }, + problem3: %{ + rankWomen: [ + [1, 2, 3, 4], + [4, 3, 2, 1], + [1, 2, 3, 4], + [3, 4, 1, 2] + ], + rankMen: [ + [1, 2, 3, 4], + [2, 1, 3, 4], + [1, 4, 3, 2], + [4, 3, 1, 2] + ] + }, + problem4: %{ + rankWomen: + [[1, 5, 4, 6, 2, 3], [4, 1, 5, 2, 6, 3], [6, 4, 2, 1, 5, 3], [1, 5, 2, 4, 3, 6], + [4, 2, 1, 5, 6, 3], [2, 6, 3, 5, 1, 4]], + rankMen: + [[1, 4, 2, 5, 6, 3], [3, 4, 6, 1, 5, 2], [1, 6, 4, 2, 3, 5], [6, 5, 3, 4, 2, 1], + [3, 1, 2, 4, 5, 6], [2, 3, 1, 6, 5, 4]] + } + } + end + + def solve(instance, opts \\ []) do + {:ok, res} = CPSolver.solve_sync(model(instance), opts) + + res.solutions + |> Enum.each(fn solution -> + Enum.zip(res.variables, solution) + |> print(instance_dimension(instances()|> + Map.get(instance))) + end) + + + {:ok, res} + end + + def model(instance) do + data = Map.get(instances(), instance) + dim = instance_dimension(data) + range = 0..dim-1 + wife = Enum.map(range, fn i -> Variable.new(range, name: "wife#{i+1}") end) + husband = Enum.map(range, fn i -> Variable.new(range, name: "husband#{i+1}") end) + ## Bijection (1-to-1) husband <-> wife + bijections = for h <- range do + #husband[wife[m]] = m + ElementVar.new(husband, Enum.at(wife, h), h) + end ++ + for w <- range do + #[wife[husband[m]] = m + ElementVar.new(wife, Enum.at(husband, w), w) + end + + pref_constraints = for w <- range, h <- range, reduce: [] do + constraints_acc -> + rankMen_h = Map.get(data, :rankMen) |> Enum.at(h) + rankMen_h_w = rankMen_h |> Enum.at(w) + {rankMen_h_w_var, elementRankMen} = ConstraintFactory.element(rankMen_h, Enum.at(wife, h)) + + rankWomen_w = Map.get(data, :rankWomen) |> Enum.at(w) + rankWomen_w_h = rankWomen_w |> Enum.at(h) + {rankWomen_w_h_var, elementRankWomen} = ConstraintFactory.element(rankWomen_w, Enum.at(husband,w)) + + impl_submodel = ConstraintFactory.impl( + Less.new([rankMen_h_w, rankMen_h_w_var]), + Less.new([rankWomen_w_h_var, rankWomen_w_h])) + + constraints_acc ++ + impl_submodel.constraints ++ + [elementRankMen, elementRankWomen] + + end + |> List.flatten + + Model.new(wife ++ husband, + bijections ++ pref_constraints ++ [AllDifferent.new(husband), AllDifferent.new(wife)] ) + + end + + defp instance_dimension(data) do + Map.get(data, :rankWomen) |> length + end + + def print(solution, n) do + IO.puts("\n") + solution + |> Enum.take(n) + |> Enum.with_index(0) + |> Enum.each(fn {{_wife_name, h}, w} -> IO.puts("\u2640:#{w+1} #{IO.ANSI.red}\u26ad#{IO.ANSI.reset} #{h+1}:\u2642") end) + end + + @doc """ + Pseudocode for checking stability + (https://stackoverflow.com/questions/58439880/algorithm-to-verify-stable-matching) + + for w in women: + for m in [men w would prefer over current_partner(w)]: + if m prefers w to current_partner(m) return false + + return true + """ + def check_solution(solution, instance) do + data = instances()[instance] + women_prefs = Map.get(data, :rankWomen) + men_prefs = Map.get(data, :rankMen) + n = instance_dimension(data) + {women_assignments, men_assignments} = Enum.take(solution, 2*n) |> Enum.split(n) + men_lookup = Enum.with_index(men_assignments, 0) |> Map.new(fn {partner, idx} -> {idx, partner} end) + + women_assignments + |> Enum.with_index(0) + |> Enum.take(1) + |> Enum.all?(fn {current_partner, w} -> + w_prefs = Enum.at(women_prefs, w) |> Enum.map(fn p -> p - 1 end) + current_partner_rank = Enum.find_index(w_prefs, fn p -> p == current_partner end) + ## Walk over candidates with higher ranks. + ## If any of candidates prefers w over his current partner, stability doesn't hold + Enum.take(w_prefs, current_partner_rank) + |> Enum.all?(fn candidate -> + candidate_prefs = Enum.at(men_prefs, candidate) |> Enum.map(fn p -> p - 1 end) + candidate_current_partner = Map.get(men_lookup, candidate) + candidate_current_partner_rank = Enum.find_index(candidate_prefs, fn p -> p == candidate_current_partner end) + candidate_w_rank = Enum.find_index(candidate_prefs, fn p -> p == w end) + ## Candidate prefers current partner to w + candidate_w_rank < candidate_current_partner_rank + end) + + + end) + + end +end diff --git a/lib/examples/sudoku.ex b/lib/examples/sudoku.ex index 36f0d167..34b449a0 100644 --- a/lib/examples/sudoku.ex +++ b/lib/examples/sudoku.ex @@ -50,7 +50,9 @@ defmodule CPSolver.Examples.Sudoku do "52...6.........7.13...........4..8..6......5...........418.........3..2...87.....", s9x9_clue17_hard: "......8.16..2........7.5......6...2..1....3...8.......2......7..4..8....5...3....", - s9x9_clue17_rosetta_difficult: + s9x9_rosetta_simple: + "394..267....3..4..5..69..2..45...9..6.......7..7...58..1..67..8..9..8....264..735", + s9x9_rosetta_difficult: "..............3.85..1.2.......5.7.....4...1...9.......5......73..2.1........4...9" } |> Map.new(fn {name, puzzle} -> {name, normalize(puzzle)} end) diff --git a/lib/examples/tsp.ex b/lib/examples/tsp.ex index 2d4ceb09..1c8f7daa 100644 --- a/lib/examples/tsp.ex +++ b/lib/examples/tsp.ex @@ -18,9 +18,13 @@ defmodule CPSolver.Examples.TSP do alias CPSolver.DefaultDomain, as: Domain alias CPSolver.Search.VariableSelector.FirstFail import CPSolver.Constraint.Factory + alias CPSolver.Utils.TupleArray require Logger + @checkmark_symbol "\u2713" + @failure_symbol "\u1D350" + def run(instance, opts \\ []) do model = model(instance) @@ -29,7 +33,7 @@ defmodule CPSolver.Examples.TSP do [ search: search(model), solution_handler: solution_handler(model), - max_search_threads: 12, + space_threads: 8, timeout: :timer.minutes(5) ], opts @@ -63,7 +67,7 @@ defmodule CPSolver.Examples.TSP do end) |> Enum.unzip() - {total_distance, sum_constraint} = sum(dist_succ, name: "total_distance") + {total_distance, sum_constraint} = sum(dist_succ) Model.new( successors, @@ -94,12 +98,14 @@ defmodule CPSolver.Examples.TSP do end def search(%{extra: %{distances: distances, n: n}} = _model) do + tuple_matrix = TupleArray.new(distances) + choose_value_fun = fn %{index: idx} = var -> domain = Interface.domain(var) d_values = Domain.to_list(domain) (idx in 1..n && - Enum.min_by(d_values, fn dom_idx -> Enum.at(distances, idx - 1) |> Enum.at(dom_idx) end)) || + Enum.min_by(d_values, fn dom_idx -> TupleArray.at(tuple_matrix, [idx - 1, dom_idx]) end)) || Enum.random(d_values) end @@ -107,7 +113,7 @@ defmodule CPSolver.Examples.TSP do {circuit_vars, rest_vars} = Enum.split_with(variables, fn v -> v.index <= n end) (circuit_vars == [] && FirstFail.select_variable(rest_vars)) || - difference_between_closest_distances(circuit_vars, distances) + difference_between_closest_distances(circuit_vars, tuple_matrix) end {choose_variable_fun, choose_value_fun} @@ -118,14 +124,15 @@ defmodule CPSolver.Examples.TSP do fn solution -> solution |> Enum.at(model.extra.n) - |> tap(fn total_cost_tuple -> - ans_str = inspect(total_cost_tuple) + |> tap(fn {_ref, total} -> + ans_str = inspect({"total", total}) (check_solution( Enum.map(solution, fn {_, val} -> val end), model ) && - Logger.warning(ans_str)) || Logger.error(ans_str <> ": wrong -((") + Logger.warning("#{@checkmark_symbol} #{ans_str}")) || + Logger.error("#{@failure_symbol} #{ans_str}" <> ": wrong -((") end) end end @@ -136,18 +143,16 @@ defmodule CPSolver.Examples.TSP do Enum.max_by(circuit_vars, fn %{index: idx} = var -> dom = Interface.domain(var) |> Domain.to_list() - (length(dom) < 2 && 0) || + (MapSet.size(dom) < 2 && 0) || dom |> Enum.map(fn value -> - Enum.at(distances, idx - 1) |> Enum.at(value) + TupleArray.at(distances, [idx - 1, value]) end) |> Enum.sort(:desc) |> then(fn dists -> abs(Enum.at(dists, 1) - hd(dists)) end) end) end - # end - ## solution -> sequence of visits def to_route(solution, %{extra: %{n: n}} = _model) do circuit = Enum.take(solution, n) diff --git a/lib/examples/xkcd_np.ex b/lib/examples/xkcd_np.ex index f88a0966..752c8e43 100644 --- a/lib/examples/xkcd_np.ex +++ b/lib/examples/xkcd_np.ex @@ -25,7 +25,7 @@ defmodule CPSolver.Examples.XKCD.NP do mul(Variable.new(0..div(total, price), name: name), price) end) - {_total_price_var, sum_constraint} = sum(quantities, domain: total, name: :total) + sum_constraint = sum(quantities, total) Model.new(quantities, [sum_constraint], extra: %{appetizers: appetizers, total: total}) end diff --git a/lib/solver/common/common.ex b/lib/solver/common/common.ex index cd875597..1917e63b 100644 --- a/lib/solver/common/common.ex +++ b/lib/solver/common/common.ex @@ -20,4 +20,58 @@ defmodule CPSolver.Common do |> :atomics.info() |> Map.get(:max) end + + ## Choose a "stronger" domain change + ## from two. + ## "Stronger" domain change implies the weaker one. + ## For instance, + ## - :bound_change implies :domain_change; + ## - :fixed implies all domain changes. + ## - :domain_change implies no other domain changes. + + def stronger_domain_change(nil, new_change) do + new_change + end + + def stronger_domain_change(new_change, nil) do + new_change + end + + def stronger_domain_change(:fixed, _new_change) do + :fixed + end + + def stronger_domain_change(_current_change, :fixed) do + :fixed + end + + def stronger_domain_change(:domain_change, new_change) do + new_change + end + + def stronger_domain_change(current_change, :domain_change) do + current_change + end + + def stronger_domain_change(:bound_change, bound_change) + when bound_change in [:min_change, :max_change] do + bound_change + end + + def stronger_domain_change(bound_change, :bound_change) + when bound_change in [:min_change, :max_change] do + bound_change + end + + def stronger_domain_change(:min_change, :max_change) do + :bound_change + end + + def stronger_domain_change(:max_change, :min_change) do + :bound_change + end + + def stronger_domain_change(current_change, new_change) when current_change == new_change do + current_change + end end diff --git a/lib/solver/constraints/abs.ex b/lib/solver/constraints/abs.ex new file mode 100644 index 00000000..e790a070 --- /dev/null +++ b/lib/solver/constraints/abs.ex @@ -0,0 +1,23 @@ +defmodule CPSolver.Constraint.Absolute do + @moduledoc """ + Absolute value constraint. + Costraints y to be |x| + """ + use CPSolver.Constraint + alias CPSolver.Propagator.Absolute, as: AbsolutePropagator + alias CPSolver.IntVariable + + def new(x, y) do + new([x, y]) + end + + @impl true + def propagators(args) do + [AbsolutePropagator.new(args)] + end + + @impl true + def arguments([x, y]) do + [IntVariable.to_variable(x), IntVariable.to_variable(y)] + end +end diff --git a/lib/solver/constraints/all_different.ex b/lib/solver/constraints/all_different_binary.ex similarity index 85% rename from lib/solver/constraints/all_different.ex rename to lib/solver/constraints/all_different_binary.ex index 2cbc761e..53a7b072 100644 --- a/lib/solver/constraints/all_different.ex +++ b/lib/solver/constraints/all_different_binary.ex @@ -1,4 +1,4 @@ -defmodule CPSolver.Constraint.AllDifferent do +defmodule CPSolver.Constraint.AllDifferent.Binary do use CPSolver.Constraint alias CPSolver.Propagator.NotEqual diff --git a/lib/solver/constraints/all_different_default.ex b/lib/solver/constraints/all_different_default.ex new file mode 100644 index 00000000..0f7b8e69 --- /dev/null +++ b/lib/solver/constraints/all_different_default.ex @@ -0,0 +1,8 @@ +defmodule CPSolver.Constraint.AllDifferent do + use CPSolver.Constraint + + alias CPSolver.Constraint.AllDifferent.FWC, as: DefaultAllDifferent + + @impl true + defdelegate propagators(x), to: DefaultAllDifferent +end diff --git a/lib/solver/constraints/circuit.ex b/lib/solver/constraints/circuit.ex index 273b1fdc..358d515a 100644 --- a/lib/solver/constraints/circuit.ex +++ b/lib/solver/constraints/circuit.ex @@ -1,10 +1,9 @@ defmodule CPSolver.Constraint.Circuit do use CPSolver.Constraint alias CPSolver.Propagator.Circuit, as: CircuitPropagator - alias CPSolver.Propagator.AllDifferent.FWC, as: AllDifferent - + @impl true def propagators(x) do - [CircuitPropagator.new(x), AllDifferent.new(x)] + [CircuitPropagator.new(x)] end end diff --git a/lib/solver/constraints/constraint_factory.ex b/lib/solver/constraints/constraint_factory.ex new file mode 100644 index 00000000..40719bb2 --- /dev/null +++ b/lib/solver/constraints/constraint_factory.ex @@ -0,0 +1,167 @@ +defmodule CPSolver.Constraint.Factory do + alias CPSolver.Constraint.{ + Sum, + ElementVar, + Element2D, + Modulo, + Absolute, + LessOrEqual, + Equal, + Reified + } + + alias CPSolver.Propagator.Modulo, as: ModuloPropagator + alias CPSolver.IntVariable, as: Variable + alias CPSolver.BooleanVariable + alias CPSolver.Variable.Interface + alias CPSolver.DefaultDomain, as: Domain + import CPSolver.Variable.View.Factory + + def element(array, x, y) do + ElementVar.new(array, x, y) + end + + def element(array, x) do + y_domain = + Enum.reduce(array, MapSet.new(), fn el, acc -> + Interface.domain(el) |> Domain.to_list() |> MapSet.union(acc) + end) + |> MapSet.to_list() + + y = Variable.new(y_domain) + result(y, element(array, x, y)) + end + + def element2d(array2d, x, y) do + domain = array2d |> List.flatten() + z = Variable.new(domain) + result(z, element2d(array2d, x, y, z)) + end + + def element2d(array2d, x, y, z) do + Element2D.new([array2d, x, y, z]) + end + + def element2d_var(array2d, x, y, z) do + num_rows = length(array2d) + num_cols = length(hd(array2d)) + Interface.removeBelow(x, 0) + Interface.removeAbove(x, num_rows - 1) + Interface.removeBelow(y, 0) + Interface.removeAbove(y, num_cols - 1) + + {flat_idx_var, sum_constraint} = add(mul(x, num_cols), y) + Interface.removeBelow(flat_idx_var, 0) + Interface.removeAbove(flat_idx_var, num_rows * num_cols - 1) + element_constraint = element(List.flatten(array2d), flat_idx_var, z) + [sum_constraint, element_constraint] + end + + def element2d_var(array2d, x, y) do + domain = + Enum.reduce(array2d |> List.flatten(), MapSet.new(), fn el, acc -> + Interface.domain(el) + |> Domain.to_list() + |> MapSet.union(acc) + end) + |> MapSet.to_list() + + z = Variable.new(domain) + result(z, element2d_var(array2d, x, y, z)) + end + + def sum(vars, sum_var) do + Sum.new(sum_var, vars) + end + + def sum(vars) do + {domain_min, domain_max} = + Enum.reduce(vars, {0, 0}, fn var, {min_acc, max_acc} -> + domain = Interface.domain(var) |> Domain.to_list() + {min_acc + Enum.min(domain), max_acc + Enum.max(domain)} + end) + + domain = domain_min..domain_max + + sum_var = Variable.new(domain) + result(sum_var, Sum.new(sum_var, vars)) + end + + def count(array, y, c) do + {b_vars, reif_propagators} = + for a <- array, reduce: {[], []} do + {vars_acc, propagators_acc} -> + b = BooleanVariable.new() + equal_p = Reified.new([Equal.new(a, y), b]) + {[b | vars_acc], [equal_p | propagators_acc]} + end + + Interface.removeBelow(c, 0) + Interface.removeAbove(c, length(array)) + [Sum.new(c, b_vars) | reif_propagators] + end + + def add(var1, var2) do + sum([var1, var2]) + end + + def subtract(var1, var2) do + add(var1, linear(var2, -1, 0)) + end + + def mod(x, y) do + {lb, ub} = ModuloPropagator.mod_bounds(x, y) + domain = + lb..ub + + mod_var = Variable.new(domain) + result(mod_var, Modulo.new(mod_var, x, y)) + end + + def mod(mod_var, x, y) do + Modulo.new(mod_var, x, y) + end + + def absolute(x) do + abs_min = abs(Interface.min(x)) + abs_max = abs(Interface.max(x)) + domain = 0..max(abs_min, abs_max) + + abs_var = Variable.new(domain) + result(abs_var, Absolute.new(x, abs_var)) + end + + def absolute(x, abs_var) do + Absolute.new(x, abs_var) + end + + defp compose(constraint1, constraint2, relation) do + b1 = BooleanVariable.new() + b2 = BooleanVariable.new() + reif_c1 = Reified.new([constraint1, b1]) + reif_c2 = Reified.new([constraint2, b2]) + %{constraints: [reif_c1, reif_c2, relation.new([b1, b2])], derived_variables: [b1, b2]} + end + + ## Implication, equivalence, inverse implication. + ## These function produce the list of constraints: + ## - 2 reified constraints for constraint1 and constraint2 + ## - relational constraint (LessOrEqual for implications, Equal for equivalence) + ## over control variables induced by reified constraints. + ## + def impl(constraint1, constraint2) do + compose(constraint1, constraint2, LessOrEqual) + end + + def equiv(constraint1, constraint2) do + compose(constraint1, constraint2, Equal) + end + + def inverse_impl(constraint1, constraint2) do + impl(constraint2, constraint1) + end + + defp result(derived_variable, constraint) do + {derived_variable, constraint} + end +end diff --git a/lib/solver/constraints/count.ex b/lib/solver/constraints/count.ex new file mode 100644 index 00000000..e1145cbb --- /dev/null +++ b/lib/solver/constraints/count.ex @@ -0,0 +1,15 @@ +defmodule CPSolver.Constraint.Count do + @moduledoc """ + Constraints `c` to be the number of occurencies of `y` in `array`. + + """ + alias CPSolver.Constraint.Factory + + def new(array, y, c) do + new([array, y, c]) + end + + def new([array, y, c] = _args) do + Factory.count(array, y, c) + end +end diff --git a/lib/solver/constraints/element.ex b/lib/solver/constraints/element.ex index 3e4d3c3e..88b18863 100644 --- a/lib/solver/constraints/element.ex +++ b/lib/solver/constraints/element.ex @@ -1,7 +1,9 @@ defmodule CPSolver.Constraint.Element do @moduledoc """ Element constrains variables y and z such that: - array[y] = z + array[x] = y + + array is 1d list of integer constants """ use CPSolver.Constraint alias CPSolver.Constraint.Element2D, as: Element2D @@ -9,15 +11,20 @@ defmodule CPSolver.Constraint.Element do @spec new( [integer()], - Variable.variable_or_view(), - Variable.variable_or_view() + Variable.variable_or_view() | integer(), + Variable.variable_or_view() | integer() ) :: Constraint.t() - def new(array, y, z) do - Element2D.new([[array], IntVariable.new(0), y, z]) + def new(array, x, y) do + Element2D.new([[array], IntVariable.new(0), x, y]) end @impl true def propagators(args) do Element2D.propagators(args) end + + @impl true + def arguments([array, x, y]) when is_list(array) do + [array, IntVariable.to_variable(x), IntVariable.to_variable(y)] + end end diff --git a/lib/solver/constraints/element2d.ex b/lib/solver/constraints/element2d.ex index 1b733488..8e5fc964 100644 --- a/lib/solver/constraints/element2d.ex +++ b/lib/solver/constraints/element2d.ex @@ -2,9 +2,12 @@ defmodule CPSolver.Constraint.Element2D do @moduledoc """ Element2d constrains variables z, x and y such that: array2d[x][y] = z + + array2d is a regular (all rows are of the same length) list of lists of integers. """ use CPSolver.Constraint alias CPSolver.Propagator.Element2D, as: Element2DPropagator + alias CPSolver.IntVariable, as: Variable @spec new( [[integer()]], @@ -20,4 +23,9 @@ defmodule CPSolver.Constraint.Element2D do def propagators(args) do [Element2DPropagator.new(args)] end + + @impl true + def arguments([array2d, x, y, z]) when is_list(array2d) and is_list(hd(array2d)) do + [array2d, Variable.to_variable(x), Variable.to_variable(y), Variable.to_variable(z)] + end end diff --git a/lib/solver/constraints/element_var.ex b/lib/solver/constraints/element_var.ex new file mode 100644 index 00000000..14c71db3 --- /dev/null +++ b/lib/solver/constraints/element_var.ex @@ -0,0 +1,30 @@ +defmodule CPSolver.Constraint.ElementVar do + @moduledoc """ + ElementVar constrains list of variables `array`, variables `x` and `y` such that: + array[x] = y + + array is a list of variables + """ + use CPSolver.Constraint + alias CPSolver.Propagator.ElementVar, as: ElementVarPropagator + alias CPSolver.IntVariable, as: Variable + + @spec new( + [Variable.variable_or_view()], + Variable.variable_or_view(), + Variable.variable_or_view() + ) :: Constraint.t() + def new(array, x, y) do + new([array, x, y]) + end + + @impl true + def propagators(args) do + [ElementVarPropagator.new(args)] + end + + @impl true + def arguments([array, x, y]) when is_list(array) do + [Enum.map(array, &Variable.to_variable/1), Variable.to_variable(x), Variable.to_variable(y)] + end +end diff --git a/lib/solver/constraints/equal.ex b/lib/solver/constraints/equal.ex new file mode 100644 index 00000000..1664dcc1 --- /dev/null +++ b/lib/solver/constraints/equal.ex @@ -0,0 +1,15 @@ +defmodule CPSolver.Constraint.Equal do + use CPSolver.Constraint + alias CPSolver.Propagator.Equal, as: EqualPropagator + + def new(x, y, offset \\ 0) + + def new(x, y, offset) do + new([x, y, offset]) + end + + @impl true + def propagators(args) do + [EqualPropagator.new(args)] + end +end diff --git a/lib/solver/constraints/less.ex b/lib/solver/constraints/less.ex index 0533beb4..ea225dab 100644 --- a/lib/solver/constraints/less.ex +++ b/lib/solver/constraints/less.ex @@ -3,11 +3,20 @@ defmodule CPSolver.Constraint.Less do alias CPSolver.Constraint.LessOrEqual, as: LessOrEqual def new(x, y, offset \\ 0) do - LessOrEqual.new(x, y, offset - 1) + LessOrEqual.new(le_args([x, y, offset])) end @impl true def propagators(args) do - LessOrEqual.new(args) + LessOrEqual.propagators(le_args(args)) + end + + @impl true + def arguments(args) do + LessOrEqual.arguments(args) + end + + defp le_args([x, y | offset]) do + [x, y, (List.first(offset) || 0) - 1] end end diff --git a/lib/solver/constraints/less_or_equal.ex b/lib/solver/constraints/less_or_equal.ex index 54aa31b4..0fa8058e 100644 --- a/lib/solver/constraints/less_or_equal.ex +++ b/lib/solver/constraints/less_or_equal.ex @@ -17,4 +17,13 @@ defmodule CPSolver.Constraint.LessOrEqual do def propagators(args) do [LessOrEqualPropagator.new(args)] end + + @impl true + def arguments([x, y]) do + arguments([x, y, 0]) + end + + def arguments([x, y, offset]) do + [Variable.to_variable(x), Variable.to_variable(y), offset] + end end diff --git a/lib/solver/constraints/modulo.ex b/lib/solver/constraints/modulo.ex new file mode 100644 index 00000000..36fa0f06 --- /dev/null +++ b/lib/solver/constraints/modulo.ex @@ -0,0 +1,19 @@ +defmodule CPSolver.Constraint.Modulo do + use CPSolver.Constraint + alias CPSolver.Propagator.Modulo, as: ModuloPropagator + alias CPSolver.IntVariable + + def new(m, x, y) do + new([m, x, y]) + end + + @impl true + def arguments(args) do + Enum.map(args, fn arg -> IntVariable.to_variable(arg) end) + end + + @impl true + def propagators(args) do + [ModuloPropagator.new(args)] + end +end diff --git a/lib/solver/constraints/not_equal.ex b/lib/solver/constraints/not_equal.ex index 4167877d..bfc06a78 100644 --- a/lib/solver/constraints/not_equal.ex +++ b/lib/solver/constraints/not_equal.ex @@ -2,7 +2,9 @@ defmodule CPSolver.Constraint.NotEqual do use CPSolver.Constraint alias CPSolver.Propagator.NotEqual, as: NotEqualPropagator - def new(x, y, offset \\ 0) do + def new(x, y, offset \\ 0) + + def new(x, y, offset) do new([x, y, offset]) end diff --git a/lib/solver/constraints/or.ex b/lib/solver/constraints/or.ex new file mode 100644 index 00000000..c91eb845 --- /dev/null +++ b/lib/solver/constraints/or.ex @@ -0,0 +1,26 @@ +defmodule CPSolver.Constraint.Or do + @moduledoc """ + ElementVar constrains list of variables `array`, variables `x` and `y` such that: + array[x] = y + + array is a list of variables + """ + use CPSolver.Constraint + alias CPSolver.Propagator.Or, as: OrPropagator + alias CPSolver.IntVariable, as: Variable + + @spec new( + [Variable.variable_or_view()] + ) :: Constraint.t() + + + @impl true + def propagators(args) do + [OrPropagator.new(args)] + end + + @impl true + def arguments(array) when is_list(array) do + Enum.map(array, &Variable.to_variable/1) + end +end diff --git a/lib/solver/constraints/propagators/abs.ex b/lib/solver/constraints/propagators/abs.ex new file mode 100644 index 00000000..818eaeb5 --- /dev/null +++ b/lib/solver/constraints/propagators/abs.ex @@ -0,0 +1,159 @@ +defmodule CPSolver.Propagator.Absolute do + use CPSolver.Propagator + + alias CPSolver.DefaultDomain, as: Domain + + def new(x, y) do + new([x, y]) + end + + @impl true + def variables(args) do + args + |> Propagator.default_variables_impl() + |> Enum.map(fn var -> set_propagate_on(var, :bound_change) end) + end + + + @impl true + def filter([x, y] = args, state, changes) do + ((state && map_size(changes) > 0) || initial_reduction(x, y)) && filter_impl(x, y, changes) + + cond do + failed?(args) -> + throw(:fail) + + entailed?(args) -> + :passive + + true -> + {:state, %{active: true}} + end + end + + @impl true + def failed?([x, y], _state \\ nil) do + max_y = max(y) + + max(y) < 0 || + ( + {abs_min_x, abs_max_x} = + Enum.min_max_by(domain(x) |> Domain.to_list(), fn val -> abs(val) end) + |> then(fn {min_val, max_val} -> {abs(min_val), abs(max_val)} end) + + min_y = max(0, min(y)) + + abs_min_x > max_y || abs_max_x < min_y + ) + end + + @impl true + def entailed?([x, y], _state \\ nil) do + ## x and y have to be fixed... + ## y = |x| + fixed?(x) && fixed?(y) && abs(min(x)) == min(y) + end + + def filter_impl(x, y, changes) do + ## x and y have 0 and 1 indices in the list of args + x_idx = 0 + y_idx = 1 + + Enum.each( + changes, + fn + {idx, _change} when idx == x_idx -> + abs_min_x = abs(min(x)) + abs_max_x = abs(max(x)) + abs_x_lb = min(abs_min_x, abs_max_x) + abs_x_ub = max(abs_min_x, abs_max_x) + + removeBelow(y, min(min(y), abs_x_lb)) + removeAbove(y, max(max(y), abs_x_ub)) + + {idx, _change} when idx == y_idx -> + y_min = min(y) + y_max = max(y) + + cond do + min(x) >= 0 -> + removeBelow(x, y_min) + removeAbove(x, y_max) + + max(x) <= 0 -> + :ok + removeBelow(x, -y_max) + removeAbove(x, -y_min) + + true -> + removeAbove(x, y_max) + removeBelow(x, -y_max) + end + {_idx, _change} -> :ignore + end + ) + + fixed?(x) && fix(y, abs(min(x))) + fixed?(y) && fix_abs(x, min(y)) + end + + defp initial_reduction(x, y) do + ## y is non-negative + removeBelow(y, 0) + filter_impl(x, y, %{0 => :domain_change, 1 => :domain_change}) + end + + defp fix_abs(x, value) do + Enum.each(domain(x) |> Domain.to_list(), fn val -> abs(val) != value && remove(x, val) end) + end +end + +defmodule CPSolver.Propagator.AbsoluteNotEqual do + use CPSolver.Propagator + alias CPSolver.Propagator.Absolute + + def new(x, y) do + new([x, y]) + end + + @impl true + defdelegate variables(args), to: Absolute + + @impl true + def filter([x, y], _state, _changes) do + filter_impl(x, y) + end + + def filter_impl(x, c) when is_integer(c) do + remove(x, c) + remove(x, -c) + :passive + end + + def filter_impl(x, y) do + cond do + fixed?(x) -> + remove(y, abs(min(x))) + :passive + + fixed?(y) -> + y_val = min(y) + remove(x, y_val) + remove(x, -y_val) + :passive + + true -> + {:state, %{active: true}} + end + end + + @impl true + def failed?(args, state) do + Absolute.entailed?(args, state) + end + + @impl true + def entailed?(args, state) do + Absolute.failed?(args, state) + end +end diff --git a/lib/solver/constraints/propagators/all_different_fwc.ex b/lib/solver/constraints/propagators/all_different_fwc.ex index b85ddd14..92c03845 100644 --- a/lib/solver/constraints/propagators/all_different_fwc.ex +++ b/lib/solver/constraints/propagators/all_different_fwc.ex @@ -5,8 +5,9 @@ defmodule CPSolver.Propagator.AllDifferent.FWC do The forward-checking propagator for AllDifferent constraint. """ - defp initial_state(args) do - %{unfixed_vars: Enum.to_list(0..(length(args) - 1))} + @impl true + def arguments(args) do + Arrays.new(args, implementation: Aja.Vector) end @impl true @@ -15,59 +16,89 @@ defmodule CPSolver.Propagator.AllDifferent.FWC do end @impl true - def filter(args) do - filter(args, initial_state(args)) - end + def filter(all_vars, state, changes) do + new_fixed = Map.keys(changes) |> MapSet.new() - def filter(args, nil) do - filter(args, initial_state(args)) + {unresolved, fixed} = + (state && + {state[:unresolved] |> MapSet.difference(new_fixed), fixed_values(all_vars, new_fixed)}) || + initial_split(all_vars) + + case fwc(all_vars, unresolved, fixed) do + false -> :passive + unfixed_updated_set -> {:state, %{unresolved: unfixed_updated_set}} + end end - @impl true - def filter(all_vars, %{unfixed_vars: unfixed_vars} = _state) do - updated_unfixed_vars = filter_impl(all_vars, unfixed_vars) - {:state, %{unfixed_vars: updated_unfixed_vars}} + defp fixed_values(vars, fixed) do + Enum.reduce(fixed, MapSet.new(), fn idx, values_acc -> + val = Propagator.arg_at(vars, idx) |> min() + (val in values_acc && fail()) || MapSet.put(values_acc, val) + end) end - defp filter_impl(all_vars, unfixed_vars) do - fwc(all_vars, unfixed_vars, MapSet.new(), [], false) + defp initial_split(vars) do + Enum.reduce(0..(Arrays.size(vars) - 1), {MapSet.new(), MapSet.new()}, fn idx, + {unfixed_acc, + fixed_vals_acc} -> + var = Propagator.arg_at(vars, idx) + + if fixed?(var) do + val = min(var) + (val in fixed_vals_acc && fail()) || {unfixed_acc, MapSet.put(fixed_vals_acc, val)} + else + {MapSet.put(unfixed_acc, idx), fixed_vals_acc} + end + end) end - ## The list of unfixed variables exhausted, and there were no fixed values. - ## We stop here - defp fwc(_all_vars, [], _fixed_values, unfixed_ids, false) do - unfixed_ids + defp fwc(vars, unfixed_set, fixed_values) do + {updated_unfixed, _fixed_vals} = remove_values(vars, unfixed_set, fixed_values) + MapSet.size(updated_unfixed) > 1 && updated_unfixed end - ## The list of unfixed variables exhausted, and some new fixed values showed up. - ## We go through unfixed ids we have collected during previous stage again + ## unfixed_set - set of indices for yet unfixed variables + ## fixed_values - the set of fixed values we will use to reduce unfixed set. + defp remove_values(vars, unfixed_set, fixed_values) do + for idx <- unfixed_set, reduce: {MapSet.new(), fixed_values} do + {still_unfixed_acc, fixed_vals_acc} -> + var = Propagator.arg_at(vars, idx) - defp fwc(all_vars, [], fixed_values, ids_to_revisit, true) do - fwc(all_vars, ids_to_revisit, fixed_values, [], false) - end + case remove_all(var, fixed_vals_acc) do + false -> + ## Variable is still unfixed, keep it + {MapSet.put(still_unfixed_acc, idx), fixed_vals_acc} + + new_fixed_value -> + fixed_vals_acc = MapSet.put(fixed_vals_acc, new_fixed_value) - ## There is still some (previously) unfixed values to check - defp fwc(all_vars, [idx | rest], fixed_values, ids_to_revisit, changed?) do - var = Enum.at(all_vars, idx) - remove_all(var, fixed_values) - - if fixed?(var) do - ## Variable is fixed or was fixed as a result of removing all fixed values - fwc(all_vars, rest, MapSet.put(fixed_values, min(var)), ids_to_revisit, true) - else - ## Still not fixed, put it to 'revisit' list - fwc(all_vars, rest, fixed_values, [idx | ids_to_revisit], changed?) + {unfixed_here, fixed_here} = + remove_values(vars, still_unfixed_acc, MapSet.new([new_fixed_value])) + + {unfixed_here, MapSet.union(fixed_here, fixed_vals_acc)} + end end end - ## Remove values from the domain of variable - defp remove_all(variable, values) do - Enum.map( - values, - fn value -> - remove(variable, value) + defp remove_all(var, values) do + Enum.reduce_while(values, false, fn val, acc -> + if remove(var, val) == :fixed do + {:halt, :fixed} + else + {:cont, acc} end - ) - |> Enum.any?(fn res -> res == :fixed end) + end) + |> case do + false -> + fixed?(var) && min(var) + + :fixed -> + min(var) + end + |> then(fn new_min -> new_min && ((new_min in values && fail()) || new_min) end) + end + + defp fail() do + throw(:fail) end end diff --git a/lib/solver/constraints/propagators/circuit.ex b/lib/solver/constraints/propagators/circuit.ex index cdcff422..66bca48d 100644 --- a/lib/solver/constraints/propagators/circuit.ex +++ b/lib/solver/constraints/propagators/circuit.ex @@ -11,16 +11,16 @@ defmodule CPSolver.Propagator.Circuit do end @impl true - def filter(args) do - filter(args, initial_state(args)) + def arguments(args) do + Arrays.new(args, implementation: Aja.Vector) end @impl true - def filter(args, nil) do - filter(args) + def filter(all_vars, nil, changes) do + filter(all_vars, initial_state(all_vars), changes) end - def filter(all_vars, state) do + def filter(all_vars, state, _changes) do case update_domain_graph(all_vars, state) do :complete -> :passive @@ -31,13 +31,13 @@ defmodule CPSolver.Propagator.Circuit do end defp initial_state(args) do - l = length(args) + l = Arrays.size(args) {circuit, unfixed_vertices, domain_graph} = args |> Enum.with_index() |> Enum.reduce( - {List.duplicate(nil, l), [], Graph.new()}, + {Arrays.new(List.duplicate(nil, l), implementation: Aja.Vector), [], Graph.new()}, fn {var, idx}, {circuit_acc, unfixed_acc, graph_acc} -> initial_reduction(var, idx, l) fixed? = fixed?(var) @@ -57,7 +57,7 @@ defmodule CPSolver.Propagator.Circuit do %{ domain_graph: domain_graph, - circuit: Enum.reverse(circuit), + circuit: Enum.reverse(circuit) |> Arrays.new(implementation: Aja.Vector), unfixed_vertices: unfixed_vertices } end @@ -92,14 +92,14 @@ defmodule CPSolver.Propagator.Circuit do end defp reduce_graph(vars, graph, circuit, [vertex | rest], remaining_unfixed) do - var = Enum.at(vars, vertex) + var = Arrays.get(vars, vertex) if fixed?(var) do succ = min(var) {updated_graph, in_neighbours} = fix_vertex(graph, vertex, succ) ## As the successor is assigned to vertex, no other neighbours of successor can have it in their domains - Enum.each(in_neighbours, fn in_n_vertex -> remove(Enum.at(vars, in_n_vertex), succ) end) + Enum.each(in_neighbours, fn in_n_vertex -> remove(Arrays.get(vars, in_n_vertex), succ) end) reduce_graph( vars, @@ -153,7 +153,7 @@ defmodule CPSolver.Propagator.Circuit do end def check_circuit(partial_circuit, start_at) do - check_circuit(partial_circuit, start_at, Enum.at(partial_circuit, start_at), 1) + check_circuit(partial_circuit, start_at, Arrays.get(partial_circuit, start_at), 1) end defp check_circuit(_partial_circuit, _started_at, nil, _step) do @@ -162,11 +162,16 @@ defmodule CPSolver.Propagator.Circuit do defp check_circuit(partial_circuit, started_at, currently_at, step) when started_at == currently_at do - step == length(partial_circuit) + step == Arrays.size(partial_circuit) end defp check_circuit(partial_circuit, started_at, currently_at, step) do - check_circuit(partial_circuit, started_at, Enum.at(partial_circuit, currently_at), step + 1) + check_circuit( + partial_circuit, + started_at, + Arrays.get(partial_circuit, currently_at), + step + 1 + ) end defp check_graph(%Graph{} = graph, _fixed_vertices) do @@ -174,7 +179,7 @@ defmodule CPSolver.Propagator.Circuit do end defp update_circuit(circuit, idx, value) do - List.replace_at(circuit, idx, value) + Arrays.replace(circuit, idx, value) |> tap(fn partial_circuit -> check_circuit(partial_circuit, idx) || fail() end) end diff --git a/lib/solver/constraints/propagators/element2d.ex b/lib/solver/constraints/propagators/element2d.ex index 2c5755ae..df78b586 100644 --- a/lib/solver/constraints/propagators/element2d.ex +++ b/lib/solver/constraints/propagators/element2d.ex @@ -5,48 +5,49 @@ defmodule CPSolver.Propagator.Element2D do @moduledoc """ The propagator for Element2D constraint. + array2d[row_index][col_index] = value """ - def new(array2d, x, y, z) do - new([array2d, x, y, z]) + def new(array2d, row_index, col_index, value) do + new([array2d, row_index, col_index, value]) end @impl true - def variables([_array2d, x, y, z]) do + def variables([_array2d, row_index, col_index, value]) do [ - set_propagate_on(x, :domain_change), - set_propagate_on(y, :domain_change), - set_propagate_on(z, :domain_change) + set_propagate_on(row_index, :domain_change), + set_propagate_on(col_index, :domain_change), + set_propagate_on(value, :domain_change) ] end - defp initial_state([[], _x, _y, _z]) do + defp initial_state([[], _row_index, _col_index, _value]) do throw(:fail) end - defp initial_state([array2d, x, y, z]) do + defp initial_state([array2d, row_index, col_index, value]) do num_rows = length(array2d) num_cols = length(hd(array2d)) - initial_reduction(array2d, x, y, z, num_rows, num_cols) - build_state(array2d, x, y, z, num_rows, num_cols) + initial_reduction(array2d, row_index, col_index, value, num_rows, num_cols) + build_state(array2d, row_index, col_index, value, num_rows, num_cols) end - def build_state(array2d, x, y, z, num_rows, num_cols) do + def build_state(array2d, row_index, col_index, value, num_rows, num_cols) do ## Build a state graph. - ## Three sets of vertices: ([{:z, value}], [{:x, value}], [{:y, value}]) - ## with edges from {:z, z_value} to {:x, x_value}, + ## Three sets of vertices: ([{:value, value}], [{:row_index, value}], [{:col_index, value}]) + ## with edges from {:value, z_value} to {:row_index, x_value}, ## where z_value is present in x_value row of array2d. - ## Likewise, with edges from {:z, z_value} to {:y, y_value}, + ## Likewise, with edges from {:value, z_value} to {:col_index, y_value}, ## where z_value is present in y_value column of array2d. for i <- 0..(num_rows - 1), j <- 0..(num_cols - 1), reduce: Graph.new() do acc -> - if contains?(x, i) && contains?(y, j) do + if contains?(row_index, i) && contains?(col_index, j) do table_value = Enum.at(array2d, i) |> Enum.at(j) - if contains?(z, table_value) do + if contains?(value, table_value) do acc - |> Graph.add_edge({:z, table_value}, {:x, i}, label: {:y, j}) - |> Graph.add_edge({:z, table_value}, {:y, j}, label: {:x, i}) + |> Graph.add_edge({:value, table_value}, {:row_index, i}, label: {:col_index, j}) + |> Graph.add_edge({:value, table_value}, {:col_index, j}, label: {:row_index, i}) else acc end @@ -56,37 +57,37 @@ defmodule CPSolver.Propagator.Element2D do end end - defp initial_reduction(array2d, x, y, z, num_rows, num_cols) do + defp initial_reduction(array2d, row_index, col_index, value, num_rows, num_cols) do # x and y are indices in array2d, # so we trim D(x) and D(y) accordingly. - removeBelow(x, 0) - removeAbove(x, num_rows - 1) - removeBelow(y, 0) - removeAbove(y, num_cols - 1) - ## D(z) is bounded by min and max of the array2d + removeBelow(row_index, 0) + removeAbove(row_index, num_rows - 1) + removeBelow(col_index, 0) + removeAbove(col_index, num_cols - 1) + ## D(value) is bounded by min and max of the array2d {arr_min, arr_max} = array2d_min_max(array2d) - removeAbove(z, arr_max) - removeBelow(z, arr_min) + removeAbove(value, arr_max) + removeBelow(value, arr_min) end - defp maybe_reduce_domains(x, y, z, %Graph{} = graph) do - (maybe_fix(x, y, z, graph) && :passive) || + defp maybe_reduce_domains(row_index, col_index, value, %Graph{} = graph) do + (maybe_fix(row_index, col_index, value, graph) && :passive) || ( {updated_graph, changed?} = Enum.reduce(Graph.vertices(graph), {graph, false}, fn - {:z, _} = v, acc -> - maybe_remove_vertex(v, z, acc) + {:value, _} = v, acc -> + maybe_remove_vertex(v, value, acc) - {:x, _} = v, acc -> - maybe_remove_vertex(v, x, acc) + {:row_index, _} = v, acc -> + maybe_remove_vertex(v, row_index, acc) - {:y, _} = v, acc -> - maybe_remove_vertex(v, y, acc) + {:col_index, _} = v, acc -> + maybe_remove_vertex(v, col_index, acc) end) ## Repeat if any reductions were made if changed? do - maybe_reduce_domains(x, y, z, updated_graph) + maybe_reduce_domains(row_index, col_index, value, updated_graph) else updated_graph end @@ -112,11 +113,12 @@ defmodule CPSolver.Propagator.Element2D do end end - defp remove_vertex(graph, {:z, _value} = vertex) do + defp remove_vertex(graph, {:value, _value} = vertex) do Graph.delete_vertex(graph, vertex) end - defp remove_vertex(graph, {signature, _value} = vertex) when signature in [:x, :y] do + defp remove_vertex(graph, {signature, _value} = vertex) + when signature in [:row_index, :col_index] do graph |> Graph.delete_vertex(vertex) ## We delete all edges related to this vertex (that is, labelled {:signature, value}) @@ -134,17 +136,11 @@ defmodule CPSolver.Propagator.Element2D do end @impl true - def filter(args) do - filter(args, initial_state(args)) + def filter(args, state, changes) do + filter_impl(args, state && state || initial_state(args), changes) end - def filter(args, nil) do - filter(args, initial_state(args)) - end - - @impl true - - def filter(args, state) do + def filter_impl(args, state, _changes) do case filter_impl(args, state) do :passive -> :passive @@ -158,20 +154,20 @@ defmodule CPSolver.Propagator.Element2D do end end - defp filter_impl([_array2d, x, y, z], state) do - maybe_reduce_domains(x, y, z, state) + defp filter_impl([_array2d, row_index, col_index, value], state) do + maybe_reduce_domains(row_index, col_index, value, state) end - defp maybe_fix(x, y, z, graph) do + defp maybe_fix(row_index, col_index, value, graph) do ## If any 2 are fixed, fix the 3rd case Graph.vertices(graph) do [_vertex1, _vertex2, _vertex3] = triple -> Enum.each( triple, fn - {:x, x_value} -> fix(x, x_value) - {:y, y_value} -> fix(y, y_value) - {:z, z_value} -> fix(z, z_value) + {:row_index, x_value} -> fix(row_index, x_value) + {:col_index, y_value} -> fix(col_index, y_value) + {:value, z_value} -> fix(value, z_value) end ) diff --git a/lib/solver/constraints/propagators/element_var.ex b/lib/solver/constraints/propagators/element_var.ex new file mode 100644 index 00000000..7a595544 --- /dev/null +++ b/lib/solver/constraints/propagators/element_var.ex @@ -0,0 +1,109 @@ +defmodule CPSolver.Propagator.ElementVar do + use CPSolver.Propagator + + @moduledoc """ + The propagator for Element constraint. + array[index] = value, + where array is an array of variables. + """ + def new(var_array, var_index, var_value) do + new([var_array, var_index, var_value]) + end + + @impl true + def arguments([var_array, var_index, var_value]) do + [Arrays.new(var_array, implementation: Aja.Vector), var_index, var_value] + end + + + @impl true + def bind(%{args: [var_array, var_index, var_value] = _args} = propagator, source, var_field) do + bound_args = + [ + Arrays.map(var_array, fn var -> Propagator.bind_to_variable(var, source, var_field) end), + Propagator.bind_to_variable(var_index, source, var_field), + Propagator.bind_to_variable(var_value, source, var_field) + ] + + Map.put(propagator, :args, bound_args) + end + + @impl true + def variables([var_array, var_index, var_value]) do + Enum.map(var_array, fn var -> + set_propagate_on(var, :fixed) + end) ++ + [ + set_propagate_on(var_index, :domain_change), + set_propagate_on(var_value, :domain_change) + ] + end + + defp initial_reduction([], _var_index, _var_value, _state, _changes) do + throw(:fail) + end + + defp initial_reduction(var_array, var_index, var_value, state, changes) do + # var_index is an index in array2d, + # so we trim D(var_index) to the size of array (0-based). + removeBelow(var_index, 0) + removeAbove(var_index, Arrays.size(var_array) - 1) + reduction(var_array, var_index, var_value, state, changes) + end + + @impl true + def filter([var_array, var_index, var_value] = _args, state, changes) do + new_state = state || %{var_index_position: Arrays.size(var_array)} + res = state && filter_impl(var_array, var_index, var_value, new_state, changes) + || initial_reduction(var_array, var_index, var_value, new_state, changes) + + res == :passive && :passive || {:state, new_state} + end + + + + + defp filter_impl(var_array, var_index, var_value, %{var_index_position: idx_position} = state, changes) do + ## Run reduction when either of index or value variables are fixed + map_size(changes) > 0 && (Map.has_key?(changes, idx_position) || Map.has_key?(changes, idx_position + 1)) + && reduction(var_array, var_index, var_value, state, changes) + end + + defp reduction(var_array, var_index, var_value, _state, _changes) do + index_domain = domain(var_index) |> Domain.to_list() + + # Step 1 + ## For all variables in var_array, if no values in D(var_value) + ## present in their domains, then the corresponding index has to be removed. + value_domain = domain(var_value) |> Domain.to_list() + + + total_value_intersection = + Enum.reduce(index_domain, MapSet.new(), fn idx, intersection_acc -> + case Arrays.get(var_array, idx) do + nil -> + IO.inspect("Unexpected: no element at #{idx}") + throw(:unexpected_no_element) + + elem_var -> + elem_var_domain = domain(elem_var) |> Domain.to_list() + intersection = MapSet.intersection(value_domain, elem_var_domain) + + (MapSet.size(intersection) == 0 && remove(var_index, idx) && intersection_acc) || + MapSet.union(intersection, intersection_acc) + end + end) + + ## Step 2 + ## `total_value_intersection` has domain values from D(var_value) + ## such that each of them is present in at least one domain of variables + ## of `var_array` + ## Hence, we can remove values that are not in `total_value_intersection` from + ## D(var_value) + + Enum.each(value_domain, fn val -> + !MapSet.member?(total_value_intersection, val) && remove(var_value, val) + end) +end + +end diff --git a/lib/solver/constraints/propagators/equal.ex b/lib/solver/constraints/propagators/equal.ex new file mode 100644 index 00000000..6c552c5d --- /dev/null +++ b/lib/solver/constraints/propagators/equal.ex @@ -0,0 +1,54 @@ +defmodule CPSolver.Propagator.Equal do + use CPSolver.Propagator + + def new(x, y, offset \\ 0) do + new([x, y, offset]) + end + + @impl true + def variables(args) do + args + |> Propagator.default_variables_impl() + |> Enum.map(fn var -> set_propagate_on(var, :fixed) end) + end + + @impl true + def filter([x, y], state, changes) do + filter([x, y, 0], state, changes) + end + + def filter([x, y, offset], _state, _changes) do + filter_impl(x, y, offset) + end + + def filter_impl(x, y, offset \\ 0) + + def filter_impl(x, c, offset) when is_integer(c) do + fix(x, c + offset) + :passive + end + + def filter_impl(x, y, offset) do + cond do + fixed?(x) -> + fix(y, plus(min(x), -offset)) + + fixed?(y) -> + fix(x, plus(min(y), offset)) + + true -> + :stable + end + end + + @impl true + def failed?([x, y, offset], _state) do + fixed?(x) && fixed?(y) && min(x) != plus(min(y), offset) + end + + @impl true + def entailed?([x, y, offset], _state) do + ## x != y holds on the condition below + fixed?(x) && fixed?(y) && min(x) == plus(min(y), offset) + end +end diff --git a/lib/solver/constraints/propagators/less.ex b/lib/solver/constraints/propagators/less.ex new file mode 100644 index 00000000..77565e8f --- /dev/null +++ b/lib/solver/constraints/propagators/less.ex @@ -0,0 +1,29 @@ +defmodule CPSolver.Propagator.Less do + alias CPSolver.Propagator.LessOrEqual, as: LessOrEqual + + # def new(x, y, offset \\ 0) do + # LessOrEqual.new(x, y, offset - 1) + # end + + defdelegate variables(args), to: LessOrEqual + + def filter(args, state, changes) do + LessOrEqual.filter(le_args(args), state, changes) + end + + def entailed?(args, state) do + LessOrEqual.entailed?(le_args(args), state) + end + + def failed?(args, state) do + LessOrEqual.failed?(le_args(args), state) + end + + defp le_args([x, y]) do + le_args([x, y, 0]) + end + + defp le_args([x, y, offset]) do + [x, y, offset - 1] + end +end diff --git a/lib/solver/constraints/propagators/less_or_equal.ex b/lib/solver/constraints/propagators/less_or_equal.ex index 090c997e..41aeea02 100644 --- a/lib/solver/constraints/propagators/less_or_equal.ex +++ b/lib/solver/constraints/propagators/less_or_equal.ex @@ -10,28 +10,37 @@ defmodule CPSolver.Propagator.LessOrEqual do [set_propagate_on(x, :min_change), set_propagate_on(y, :max_change)] end + def filter([x, y], state, changes) do + filter([x, y, 0], state, changes) + end + @impl true - def filter(args, state \\ %{active?: true}) + def filter([x, y, offset], state, _changes) do + filter_impl(x, y, offset, state || %{active?: true}) + end - def filter([x, y], state) do - filter([x, y, 0], state) + @impl true + def failed?([x, y, offset], _state) do + min(x) > plus(max(y), offset) end @impl true - def filter([x, y, offset], state) do - filter_impl(x, y, offset, state || %{active?: true}) + def entailed?([x, y, offset], _state) do + entailed?(x, y, offset) + end + + defp entailed?(x, y, offset) do + ## x <= y holds on the condition below + max(x) <= plus(min(y), offset) end - def filter_impl(_x, _y, _offset, %{active?: false} = _state) do + defp filter_impl(_x, _y, _offset, %{active?: false} = _state) do :passive end - def filter_impl(x, y, offset, %{active?: true} = _state) do + defp filter_impl(x, y, offset, %{active?: true} = _state) do removeAbove(x, plus(max(y), offset)) removeBelow(y, plus(min(x), -offset)) - - ## It doesn't make sense to filter at all after this happens. - ## as it will be stable forever. - {:state, %{active?: max(x) > plus(min(y), offset)}} + {:state, %{active?: !entailed?(x, y, offset)}} end end diff --git a/lib/solver/constraints/propagators/modulo.ex b/lib/solver/constraints/propagators/modulo.ex new file mode 100644 index 00000000..3e62b23f --- /dev/null +++ b/lib/solver/constraints/propagators/modulo.ex @@ -0,0 +1,137 @@ +defmodule CPSolver.Propagator.Modulo do + use CPSolver.Propagator + + @x_y_fixed [false, true, true] + @m_x_fixed [true, true, false] + @m_y_fixed [true, false, true] + @all_fixed [true, true, true] + + def new(m, x, y) do + new([m, x, y]) + end + + @impl true + def variables(args) do + args + |> Propagator.default_variables_impl() + |> Enum.map(fn var -> set_propagate_on(var, :bound_change) end) + end + + + @impl true + def filter(args, nil, changes) do + filter(args, initial_state(args), changes) + end + + def filter(args, %{fixed_flags: fixed_flags}, changes) do + updated_fixed = update_fixed(args, fixed_flags, changes) + + if filter_impl(args, updated_fixed) do + :passive + else + {:state, %{fixed_flags: updated_fixed}} + end + end + + ## This (no changes) will happen when the propagator doesn't receive changes + ## (either because it was first to run or there were no changes) + defp update_fixed(args, fixed_flags, _changes) #when map_size(changes) == 0 + do + for idx <- 0..2 do + Enum.at(fixed_flags, idx) || fixed?(Enum.at(args, idx)) + end + end + + # defp update_fixed(_args, fixed_flags, changes) do + # Enum.reduce(changes, fixed_flags, fn + # {idx, :fixed}, flags_acc -> + # List.replace_at(flags_acc, idx, true) + + # {_idx, _bound_change}, flags_acc -> + # flags_acc + # end) + # end + + def filter_impl([m, x, y] = _args, @x_y_fixed) do + fix(m, rem(min(x), min(y))) + end + + def filter_impl([m, x, y], @m_x_fixed) do + m_value = min(m) + x_value = min(x) + + domain(y) + |> Domain.to_list() + |> Enum.each(fn y_value -> + rem(x_value, y_value) != m_value && + remove(y, y_value) + end) + + fixed?(y) + end + + def filter_impl([m, x, y], @m_y_fixed) do + m_value = min(m) + y_value = min(y) + + domain(x) + |> Domain.to_list() + |> Enum.each(fn x_value -> + rem(x_value, y_value) != m_value && + remove(x, x_value) + end) + + fixed?(x) + end + + def filter_impl(_args, @all_fixed) do + true + end + + def filter_impl(_args, [true | _x_y_flags]) do + false + end + + def filter_impl([m, _x, _y] = args, [false | x_y_flags]) do + update_bounds(args) + fixed?(m) && filter_impl(args, [true | x_y_flags]) + end + + defp update_bounds([m, x, y] = _args) do + max_x = max(x) + min_x = min(x) + max_y = max(y) + min_y = min(y) + + {m_lower_bound, m_upper_bound} = + mod_bounds(min_x, max_x, min_y, max_y) + + removeAbove(m, m_upper_bound) + removeBelow(m, m_lower_bound) + end + + defp initial_state([_m, _x, y] = args) do + remove(y, 0) + %{fixed_flags: Enum.map(args, fn arg -> fixed?(arg) end)} + end + + defp mod_bounds(min_x, max_x, min_y, max_y) do + cond do + min_x >= 0 -> + {0, (max(abs(min_y), abs(max_y)) - 1) |> min(max_x)} + + max_x < 0 -> + {(-max(abs(min_y), abs(max_y)) + 1) |> max(min_x), 0} + + true -> + { + (min(min(min_y, -min_y), min(max_y, -max_y)) + 1) |> max(min_x), + (max(max(min_y, -min_y), max(max_y, -max_y)) - 1) |> min(max_x) + } + end + end + + def mod_bounds(x, y) do + mod_bounds(min(x), max(x), min(y), max(y)) + end +end diff --git a/lib/solver/constraints/propagators/not_equal.ex b/lib/solver/constraints/propagators/not_equal.ex index aa7c0e45..ef0dc94b 100644 --- a/lib/solver/constraints/propagators/not_equal.ex +++ b/lib/solver/constraints/propagators/not_equal.ex @@ -13,24 +13,44 @@ defmodule CPSolver.Propagator.NotEqual do end @impl true - def filter([x, y]) do - filter([x, y, 0]) + def filter([x, y], state, changes) do + filter([x, y, 0], state, changes) end - def filter([x, y, offset]) do + def filter([x, y, offset], _state, _changes) do filter_impl(x, y, offset) end - def filter_impl(x, y, offset \\ 0) do + def filter_impl(x, y, offset \\ 0) + + def filter_impl(x, c, offset) when is_integer(c) do + remove(x, c + offset) + :passive + end + + def filter_impl(x, y, offset) do cond do fixed?(x) -> remove(y, plus(min(x), -offset)) + :passive fixed?(y) -> remove(x, plus(min(y), offset)) + :passive true -> :stable end end + + @impl true + def failed?([x, y, offset], _state) do + fixed?(x) && fixed?(y) && min(x) == plus(min(y), offset) + end + + @impl true + def entailed?([x, y, offset], _state) do + ## x != y holds on the condition below + fixed?(x) && fixed?(y) && min(x) != plus(min(y), offset) + end end diff --git a/lib/solver/constraints/propagators/or.ex b/lib/solver/constraints/propagators/or.ex new file mode 100644 index 00000000..2cee3cca --- /dev/null +++ b/lib/solver/constraints/propagators/or.ex @@ -0,0 +1,100 @@ +defmodule CPSolver.Propagator.Or do + use CPSolver.Propagator + + @moduledoc """ + The propagator for 'or' constraint. + Takes the list of boolean variables. + Constraints to have at least one variable to be resolved to true. + """ + + @impl true + def variables(args) do + Enum.map(args, fn x_el -> set_propagate_on(x_el, :fixed) end) + end + + @impl true + def arguments(args) do + Arrays.new(args, implementation: Aja.Vector) + end + + @impl true + def filter(all_vars, nil, changes) do + case initial_reduction(all_vars) do + :resolved -> + :passive + + unfixed -> + (MapSet.size(unfixed) == 0 && fail()) || + filter(all_vars, %{unfixed: unfixed}, changes) + end + end + + def filter(all_vars, %{unfixed: unfixed} = _state, changes) when map_size(changes) == 0 do + Enum.reduce_while(unfixed, unfixed, fn idx, unfixed_acc -> + var = Arrays.get(all_vars, idx) + if fixed?(var) do + if min(var) == 1 do + {:halt, :resolved} + else + {:cont, MapSet.delete(unfixed_acc, idx)} + end + else + {:cont, unfixed_acc} + end + end) + |> finalize(all_vars) + end + + def filter(all_vars, %{unfixed: unfixed} = _state, changes) do + Enum.reduce_while(changes, unfixed, fn {var_index, :fixed}, unfixed_acc -> + if MapSet.member?(unfixed_acc, var_index) do + if min(Arrays.get(all_vars, var_index)) == 1 do + {:halt, :resolved} + else + {:cont, MapSet.delete(unfixed_acc, var_index)} + end + else + {:cont, unfixed_acc} + end + end) + |> finalize(all_vars) + end + + defp initial_reduction(all_vars) do + Enum.reduce_while(0..(Arrays.size(all_vars) - 1), MapSet.new(), fn var_idx, candidates_acc -> + var = Arrays.get(all_vars, var_idx) + + if fixed?(var) do + case min(var) do + 0 -> {:cont, candidates_acc} + 1 -> {:halt, :resolved} + _other_value -> throw(:not_boolean) + end + else + {:cont, MapSet.put(candidates_acc, var_idx)} + end + end) + end + + defp finalize(filtering_result, all_vars) do + case filtering_result do + :resolved -> + :passive + + unfixed -> + case MapSet.size(unfixed) do + 0 -> fail() + 1 -> + last_var_idx = MapSet.to_list(unfixed) |> List.first() + fix(Arrays.get(all_vars, last_var_idx), 1) + :passive + _more -> + {:state, %{unfixed: unfixed}} + end + end + end + + defp fail() do + throw(:fail) + end +end diff --git a/lib/solver/constraints/propagators/reified.ex b/lib/solver/constraints/propagators/reified.ex new file mode 100644 index 00000000..68ec6f1d --- /dev/null +++ b/lib/solver/constraints/propagators/reified.ex @@ -0,0 +1,202 @@ +defmodule CPSolver.Propagator.Reified do + use CPSolver.Propagator + + alias CPSolver.BooleanVariable, as: BoolVar + alias CPSolver.Propagator + + @moduledoc """ + The propagator for reification constraints. + + Full reification: + + 1. If b is fixed to 1, the propagator for the reification reduces to a propagator for C. + 2. If b is fixed to 0, the propagator for the reification reduces to a propagator for opposite(C). + 3. If a propagator for C would realize that the C would be entailed, the propagator for the reification fixes b to 1 and ceases to exist. + 4. If a propagator for C would realize that the C would fail, the propagator for the reification fixes x b to 0 and ceases to exist. + + Half-reification: + + Rules 2 and 3 of full reification. + + Inverse implication: + + Rules 1 and 4 of full reification. + """ + def new(propagators, b_var, mode) when mode in [:full, :half, :inverse_half] do + new([propagators, b_var, mode]) + end + + @impl true + def variables([propagators, b_var, _mode]) do + Enum.reduce( + propagators, + [set_propagate_on(b_var, :fixed)], + fn p, acc -> acc ++ Propagator.variables(p) end + ) + |> Enum.uniq() + end + + @impl true + def filter(args, nil, changes) do + filter(args, initial_state(args), changes) + end + + def filter( + [_propagators, b_var, mode] = _args, + %{active_propagators: active_propagators} = _state, + changes + ) do + filter_impl(mode, b_var, active_propagators, changes) + end + + @impl true + def bind(%{args: [propagators, b_var, mode] = _args, state: state} = propagator, source, var_field) do + bound_propagators = Enum.map(propagators, fn p -> Propagator.bind(p, source, var_field) end) + + Map.put(propagator, :args, [ + bound_propagators, + Propagator.bind_to_variable(b_var, source, var_field), + mode + ]) + |> Map.put(:state, %{active_propagators: bound_propagators, b_idx: state[:b_idx]}) + end + + defp actions() do + %{ + :full => [ + &propagate/2, + &propagate_negative/2, + &terminate_false/1, + &terminate_true/1 + ], + :half => [nil, &propagate_negative/2, nil, &terminate_true/1], + :inverse_half => [&propagate/2, nil, &terminate_false/1, nil] + } + end + + ## Callbacks for reified + defp propagate(propagators, incoming_changes) do + res = + Enum.reduce_while(propagators, [], fn p, active_propagators_acc -> + case Propagator.filter(p, changes: incoming_changes) do + :fail -> + {:halt, :fail} + + %{active?: active?} -> + {:cont, (active? && [p | active_propagators_acc]) || active_propagators_acc} + + :stable -> + {:cont, [p | active_propagators_acc]} + end + end) + + cond do + res == :fail -> :fail + Enum.empty?(res) -> :passive + true -> {:state, %{active_propagators: res}} + end + end + + defp propagate_negative(propagators, changes) do + propagators + |> opposite_propagators() + |> propagate(changes) + end + + defp terminate_true(b_var) do + terminate_propagator(b_var, true) + end + + defp terminate_false(b_var) do + terminate_propagator(b_var, false) + end + + defp terminate_propagator(b_var, bool) do + (bool && fix(b_var, 1)) || fix(b_var, 0) + :passive + end + + defp filter_impl(mode, b_var, propagators, changes) do + [propagate_action, propagate_negative_action, fix_to_false_action, fix_to_true_action] = + Map.get(actions(), mode) + + cond do + BoolVar.true?(b_var) -> + propagate_action && propagate_action.(propagators, changes) && active_state(propagators) + + BoolVar.false?(b_var) -> + propagate_negative_action && propagate_negative_action.(propagators, changes) && + active_state(propagators) + + true -> + ## Control variable is not fixed + case check_propagators(propagators, changes) do + :fail -> + fix_to_false_action && fix_to_false_action.(b_var) && active_state(propagators) + + :entailed -> + fix_to_true_action && fix_to_true_action.(b_var) && active_state(propagators) + + active_propagators -> + active_state(active_propagators) + end + end + end + + defp initial_state([propagators, _b_var, _mode] = args) do + %{active_propagators: propagators, b_idx: length(variables(args)) - 1} + end + + defp check_propagators(propagators, _incoming_changes) do + propagators + |> Enum.reduce_while( + [], + fn p, active_propagators_acc -> + cond do + Propagator.failed?(p) -> + {:halt, :fail} + + Propagator.entailed?(p) -> + {:cont, active_propagators_acc} + + true -> + {:cont, [p | active_propagators_acc]} + end + end + ) + |> case do + :fail -> + :fail + + active_propagators -> + (Enum.empty?(active_propagators) && :entailed) || active_propagators + end + end + + defp opposite_propagators(propagators) do + Enum.map(propagators, fn p -> opposite(p) end) + end + + ## Opposite propagators + alias CPSolver.Propagator.{Equal, NotEqual, Less, LessOrEqual, Absolute, AbsoluteNotEqual} + + defp opposite(%{mod: Equal} = p) do + %{p | mod: NotEqual} + end + + defp opposite(%{mod: NotEqual} = p) do + %{p | mod: Equal} + end + + defp opposite(%{mod: LessOrEqual, args: [x, y, offset]} = p) do + %{p | mod: Less, args: [y, x, -offset]} + end + + defp opposite(%{mod: Absolute} = p) do + %{p | mod: AbsoluteNotEqual} + end + + defp active_state(propagators) do + {:state, %{active?: true, active_propagators: propagators}} + end +end diff --git a/lib/solver/constraints/propagators/sum.ex b/lib/solver/constraints/propagators/sum.ex index eeb140f5..7f9e3f43 100644 --- a/lib/solver/constraints/propagators/sum.ex +++ b/lib/solver/constraints/propagators/sum.ex @@ -11,6 +11,11 @@ defmodule CPSolver.Propagator.Sum do new([minus(y) | x]) end + @impl true + def arguments(args) do + Arrays.new(args, implementation: Aja.Vector) + end + defp initial_state(args) do {sum_fixed, unfixed_vars} = args @@ -32,16 +37,11 @@ defmodule CPSolver.Propagator.Sum do end @impl true - def filter(args) do - filter(args, initial_state(args)) + def filter(all_vars, nil, changes) do + filter(all_vars, initial_state(all_vars), changes) end - def filter(args, nil) do - filter(args, initial_state(args)) - end - - @impl true - def filter(all_vars, %{sum_fixed: sum_fixed, unfixed_ids: unfixed_ids} = _state) do + def filter(all_vars, %{sum_fixed: sum_fixed, unfixed_ids: unfixed_ids} = _state, _changes) do {unfixed_vars, updated_unfixed_ids, new_sum} = update_unfixed(all_vars, unfixed_ids) updated_sum = sum_fixed + new_sum {sum_min, sum_max} = sum_min_max(updated_sum, unfixed_vars) @@ -54,7 +54,7 @@ defmodule CPSolver.Propagator.Sum do defp update_unfixed(all_vars, unfixed_ids) do Enum.reduce(unfixed_ids, {[], unfixed_ids, 0}, fn pos, {unfixed_acc, ids_acc, sum_acc} -> - var = Enum.at(all_vars, pos) + var = Arrays.get(all_vars, pos) (fixed?(var) && {unfixed_acc, MapSet.delete(ids_acc, pos), sum_acc + min(var)}) || {[var | unfixed_acc], ids_acc, sum_acc} diff --git a/lib/solver/constraints/reified.ex b/lib/solver/constraints/reified.ex new file mode 100644 index 00000000..20c9d7a6 --- /dev/null +++ b/lib/solver/constraints/reified.ex @@ -0,0 +1,63 @@ +defmodule CPSolver.Constraint.Reified do + @moduledoc """ + Reified (equivalence) constraint. + Extends constraint C to constraint R(C, b), + where 'b' is a boolean variable, and + C holds iff b is fixed to true + """ + use CPSolver.Constraint + alias CPSolver.Propagator.Reified, as: ReifPropagator + + def new(constraint, b) do + new([constraint, b]) + end + + @impl true + def propagators([constraint, b]) do + [reified_propagator(constraint, b, :full)] + end + + def reified_propagator(constraint, b, mode) when mode in [:full, :half, :inverse_half] do + ReifPropagator.new(Constraint.constraint_to_propagators(constraint), b, mode) + end +end + +defmodule CPSolver.Constraint.HalfReified do + @moduledoc """ + Half-reified (implication) constraint. + Extends constraint C to constraint R(C, b), + where 'b' is a boolean variable, and + b is fixed to true if C holds + """ + use CPSolver.Constraint + alias CPSolver.Constraint.Reified + + def new(constraint, b) do + new([constraint, b]) + end + + @impl true + def propagators([constraint, b]) do + [Reified.reified_propagator(constraint, b, :half)] + end +end + +defmodule CPSolver.Constraint.InverseHalfReified do + @moduledoc """ + Inverse half-reified (inverse implication) constraint. + Extends constraint C to constraint R(C, b), + where 'b' is a boolean variable, and + C holds if b is fixed to true. + """ + use CPSolver.Constraint + alias CPSolver.Constraint.Reified + + def new(constraint, b) do + new([constraint, b]) + end + + @impl true + def propagators([constraint, b]) do + [Reified.reified_propagator(constraint, b, :inverse_half)] + end +end diff --git a/lib/solver/constraints/sum.ex b/lib/solver/constraints/sum.ex index 54208901..249be5fd 100644 --- a/lib/solver/constraints/sum.ex +++ b/lib/solver/constraints/sum.ex @@ -1,14 +1,33 @@ defmodule CPSolver.Constraint.Sum do use CPSolver.Constraint alias CPSolver.Propagator.Sum, as: SumPropagator + alias CPSolver.Variable.View.Factory + alias CPSolver.IntVariable, as: Variable @spec new(Variable.variable_or_view(), [Variable.variable_or_view()]) :: Constraint.t() + + def new(c, x) when is_integer(c) do + new(Variable.new(c), x) + end + def new(y, x) do new([y | x]) end @impl true def propagators([y | x]) do - [SumPropagator.new(y, x)] + # Separate constants and variables + {constant, vars} = + List.foldr(x, {0, []}, fn arg, {constant_acc, vars_acc} -> + (is_integer(arg) && {constant_acc + arg, vars_acc}) || + {constant_acc, [arg | vars_acc]} + end) + + ## Adjust sum (variable or constant) + y_arg = + (is_integer(y) && Variable.new(y - constant)) || + Factory.inc(y, -constant) + + [SumPropagator.new(y_arg, vars)] end end diff --git a/lib/solver/core/constraint.ex b/lib/solver/core/constraint.ex index 67727044..2ec05635 100644 --- a/lib/solver/core/constraint.ex +++ b/lib/solver/core/constraint.ex @@ -1,10 +1,10 @@ defmodule CPSolver.Constraint do - alias CPSolver.Variable alias CPSolver.Variable.Interface + alias CPSolver.Propagator @callback new(args :: list()) :: Constraint.t() @callback propagators(args :: list()) :: [atom()] - @callback variables(args :: list()) :: [Variable.t()] + @callback arguments(args :: list()) :: list() defmacro __using__(_) do quote do @@ -13,18 +13,19 @@ defmodule CPSolver.Constraint do alias CPSolver.Common def new(args) do - Constraint.new(__MODULE__, args) + Constraint.new(__MODULE__, arguments(args)) end - def variables(args) do + def arguments(args) do args end - defoverridable variables: 1 + defoverridable new: 1, arguments: 1 end end def new(constraint_impl, args) do + Enum.empty?(args) && throw({constraint_impl, :no_args}) || {constraint_impl, args} end @@ -37,49 +38,20 @@ defmodule CPSolver.Constraint do constraint_to_propagators({constraint_mod, args}) end - def extract_variables({_mod, args}) do - Enum.flat_map(args, fn arg -> - var = Interface.variable(arg) - (var && [var]) || [] - end) + def post(constraint) when is_tuple(constraint) do + propagators = constraint_to_propagators(constraint) + Enum.map(propagators, fn p -> Propagator.filter(p) end) end -end - -defmodule CPSolver.Constraint.Factory do - alias CPSolver.Constraint.{Sum, Element, Element2D} - alias CPSolver.IntVariable, as: Variable - alias CPSolver.Variable.Interface - alias CPSolver.DefaultDomain, as: Domain - def element(array, x, opts \\ []) do - domain = array - y = Variable.new(domain, name: Keyword.get(opts, :name, make_ref())) - {y, Element.new(array, x, y)} - end - - def element2d(array2d, x, y, opts \\ []) do - domain = array2d |> List.flatten() - z = Variable.new(domain, name: Keyword.get(opts, :name, make_ref())) - {z, Element2D.new([array2d, x, y, z])} - end - - def sum(vars, opts \\ []) do - domain = - case opts[:domain] do - nil -> - {domain_min, domain_max} = - Enum.reduce(vars, {0, 0}, fn var, {min_acc, max_acc} -> - domain = Interface.domain(var) |> Domain.to_list() - {min_acc + Enum.min(domain), max_acc + Enum.max(domain)} - end) - - domain_min..domain_max - - d -> - d - end - - sum_var = Variable.new(domain, name: Keyword.get(opts, :name, make_ref())) - {sum_var, Sum.new(sum_var, vars)} + def extract_variables(constraint) do + constraint + |> constraint_to_propagators() + |> Enum.map(fn p -> + p + |> Propagator.variables() + |> Enum.map(fn var -> Interface.variable(var) end) + end) + |> List.flatten() + |> Enum.uniq() end end diff --git a/lib/solver/core/propagator/constraint_graph.ex b/lib/solver/core/propagator/constraint_graph.ex index 7022e83f..a362f0ab 100644 --- a/lib/solver/core/propagator/constraint_graph.ex +++ b/lib/solver/core/propagator/constraint_graph.ex @@ -2,47 +2,28 @@ defmodule CPSolver.Propagator.ConstraintGraph do @moduledoc """ The constraint graph connects propagators and their variables. The edge between a propagator and a variable represents a notification - the propagator receives upon varable's domain change. + the propagator receives upon variable's domain change. """ alias CPSolver.Propagator alias CPSolver.Variable.Interface alias CPSolver.DefaultDomain, as: Domain - @spec create([Propagator.t()] | %{reference() => Propagator.t()}) :: Graph.t() + @spec create([Propagator.t()]) :: Graph.t() def create(propagators) when is_list(propagators) do - propagators - |> Enum.map(fn p -> {p.id, p} end) - |> Map.new() - |> create() - end - - def create(propagators) when is_map(propagators) do - Enum.reduce(propagators, Graph.new(), fn {propagator_id, - %{mod: propagator_mod, args: args} = p}, - acc -> - args - |> propagator_mod.variables() - |> Enum.reduce(acc, fn var, acc2 -> - Graph.add_vertex(acc2, propagator_vertex(propagator_id)) - |> Graph.add_edge({:variable, Interface.id(var)}, propagator_vertex(propagator_id), - label: get_propagate_on(var) - ) - end) - |> Graph.label_vertex(propagator_vertex(propagator_id), p) + Enum.reduce(propagators, Graph.new(), fn p, graph_acc -> + add_propagator(graph_acc, p) end) end - def get_propagator_ids(constraint_graph, variable_id, filter_fun) - when is_function(filter_fun) do + def propagators_by_variable(constraint_graph, variable_id, reduce_fun) + when is_function(reduce_fun, 2) do constraint_graph - |> Graph.edges({:variable, variable_id}) - |> Enum.flat_map(fn edge -> - if filter_fun.(edge) do - {:propagator, p_id} = edge.v2 - [p_id] - else - [] - end + |> Graph.edges(variable_vertex(variable_id)) + |> Enum.reduce(Map.new(), fn edge, acc -> + {:propagator, p_id} = edge.v2 + + ((p_data = reduce_fun.(p_id, edge.label)) && p_data && Map.put(acc, p_id, p_data)) || + acc end) end @@ -52,18 +33,61 @@ defmodule CPSolver.Propagator.ConstraintGraph do variable_id, domain_change ) do - get_propagator_ids(constraint_graph, variable_id, fn edge -> domain_change in edge.label end) + propagators_by_variable(constraint_graph, variable_id, fn p_id, propagator_variable_edge -> + domain_change in propagator_variable_edge.propagate_on && + get_propagator_data( + propagator_variable_edge, + domain_change, + get_propagator(constraint_graph, p_id) + ) + end) + end + + defp get_propagator_data(_edge, domain_change, propagator) do + %{ + domain_change: domain_change, + propagator: propagator + } end def has_variable?(graph, variable_id) do - Graph.has_vertex?(graph, {:variable, variable_id}) + Graph.has_vertex?(graph, variable_vertex(variable_id)) end - def get_propagator(%Graph{} = graph, propagator_id) do - case Graph.vertex_labels(graph, propagator_vertex(propagator_id)) do - [] -> nil - [p] -> p - end + def add_variable(graph, variable) do + Graph.add_vertex(graph, variable_vertex(variable.id), [variable]) + end + + def add_propagator(graph, propagator) do + propagator_vertex = propagator_vertex(propagator.id) + + propagator + |> Propagator.variables() + |> Enum.reduce(graph, fn var, graph_acc -> + interface_var = Interface.variable(var) + + graph_acc + |> add_variable(interface_var) + |> Graph.add_vertex(propagator_vertex) + |> then(fn graph -> + (Interface.domain(interface_var) |> Domain.fixed?() && graph) || + Graph.add_edge(graph, variable_vertex(interface_var.id), propagator_vertex, + label: %{ + propagate_on: get_propagate_on(var), + variable_name: interface_var.name + } + ) + end) + end) + |> Graph.label_vertex(propagator_vertex, propagator) + end + + def get_propagator(%Graph{} = graph, {:propagator, _propagator_id} = vertex) do + get_label(graph, vertex) + end + + def get_propagator(graph, propagator_id) do + get_propagator(graph, propagator_vertex(propagator_id)) end def update_propagator( @@ -77,6 +101,10 @@ defmodule CPSolver.Propagator.ConstraintGraph do |> Map.put(:vertex_labels, Map.put(labels, identifier.(vertex), [propagator])) end + def variable_vertex(variable_id) do + {:variable, variable_id} + end + def propagator_vertex(propagator_id) do {:propagator, propagator_id} end @@ -85,30 +113,54 @@ defmodule CPSolver.Propagator.ConstraintGraph do remove_vertex(graph, propagator_vertex(propagator_id)) end - ## Remove variable and all propagators that are isolated points as a result of variable removal + def remove_edge(graph, var_id, propagator_id) do + Graph.delete_edge(graph, variable_vertex(var_id), propagator_vertex(propagator_id)) + end + + def entailed_propagator?(graph, propagator) do + Enum.empty?(Graph.neighbors(graph, propagator_vertex(propagator.id))) + end + + def get_variable(%Graph{} = graph, {:variable, _variable_id} = vertex) do + get_label(graph, vertex) + end + + def get_variable(graph, variable_id) do + get_variable(graph, variable_vertex(variable_id)) + end + + ## Remove variable def remove_variable(graph, variable_id) do - remove_vertex(graph, {:variable, variable_id}) + remove_vertex(graph, variable_vertex(variable_id)) + end + + def disconnect_variable(graph, variable_id) do + Graph.delete_edges(graph, Graph.edges(graph, variable_vertex(variable_id))) end - ### Remove fixed variables and update propagators with variable domains. + ### This is called on creation of new space. + ### + ### Stop notifications from fixed variables and update propagators with variable domains. ### Returns updated graph and a list of propagators bound to variable domains def update(graph, vars) do - ## Remove fixed variables - {g1, propagators, variable_map} = - Enum.reduce(vars, {graph, [], Map.new()}, fn %{domain: domain} = v, - {graph_acc, propagators_acc, variables_acc} -> - {if Domain.fixed?(domain) do - remove_variable(graph_acc, v.id) - else - graph_acc - end, propagators_acc ++ get_propagator_ids(graph_acc, v.id, fn _ -> true end), - Map.put(variables_acc, v.id, v)} + {updated_var_graph, propagators} = + Enum.reduce(vars, {graph, MapSet.new()}, fn %{id: var_id} = v, + {graph_acc, propagators_acc} -> + {update_variable(graph_acc, var_id, v), + MapSet.union( + propagators_acc, + MapSet.new( + Map.keys(propagators_by_variable(graph, Interface.id(v), fn _p_id, edge -> edge end)) + ) + )} end) ## Update domains - Enum.reduce(propagators, {g1, []}, fn p_id, {graph_acc, p_acc} -> - get_propagator(graph_acc, p_id) - |> Propagator.bind_to_variables(variable_map, :domain) + propagators + |> Enum.reduce({updated_var_graph, []}, fn p_id, {graph_acc, p_acc} -> + graph_acc + |> get_propagator(p_id) + |> Propagator.bind(updated_var_graph, :domain) |> then(fn bound_p -> { update_propagator(graph_acc, p_id, bound_p), @@ -123,6 +175,23 @@ defmodule CPSolver.Propagator.ConstraintGraph do end defp get_propagate_on(variable) do - Map.get(variable, :propagate_on, Propagator.to_domain_events(:fixed)) + Map.get(variable, :propagate_on) || Propagator.to_domain_events(:fixed) + end + + defp get_label(%Graph{} = graph, vertex) do + case Graph.vertex_labels(graph, vertex) do + [] -> nil + [p] -> p + end + end + + def update_variable( + %Graph{vertex_labels: labels, vertex_identifier: identifier} = graph, + var_id, + variable + ) do + vertex = variable_vertex(var_id) + + Map.put(graph, :vertex_labels, Map.put(labels, identifier.(vertex), [variable])) end end diff --git a/lib/solver/core/propagator/propagator.ex b/lib/solver/core/propagator/propagator.ex index 7ffa75c8..2367eae2 100644 --- a/lib/solver/core/propagator/propagator.ex +++ b/lib/solver/core/propagator/propagator.ex @@ -1,19 +1,27 @@ defmodule CPSolver.Propagator do @type propagator_event :: :domain_change | :bound_change | :min_change | :max_change | :fixed - @callback new(args :: list()) :: Propagator.t() - @callback update(Propagator.t(), changes: any()) :: Propagator.t() - @callback filter(args :: list()) :: map() | :stable | :fail | propagator_event() - @callback filter(args :: list(), state :: map() | nil) :: - map() | :stable | :fail | propagator_event() + @callback reset(args :: list(), state :: map()) :: map() | nil + @callback reset(args :: list(), state :: map(), opts :: Keyword.t()) :: map() | nil + @callback bind(Propagator.t(), source :: any(), variable_field :: atom()) :: Propagator.t() + @callback filter(args :: list(), state :: map(), changes :: map()) :: + {:state, map()} | :stable | :fail | propagator_event() + @callback entailed?(Propagator.t(), state :: map() | nil) :: boolean() + @callback failed?(Propagator.t(), state :: map() | nil) :: boolean() + @callback variables(args :: list()) :: list() + @callback arguments(args :: list()) :: Arrays.t() alias CPSolver.Variable + alias CPSolver.Variable.Interface alias CPSolver.Variable.View alias CPSolver.Propagator.Variable, as: PropagatorVariable alias CPSolver.DefaultDomain, as: Domain - alias CPSolver.Variable.Interface - alias CPSolver.ConstraintStore + alias CPSolver.Propagator.ConstraintGraph + alias CPSolver.Utils.TupleArray + alias CPSolver.Utils + + require Logger defmacro __using__(_) do quote do @@ -25,22 +33,44 @@ defmodule CPSolver.Propagator do @behaviour Propagator def new(args) do - Propagator.new(__MODULE__, args) + Propagator.new(__MODULE__, arguments(args)) + end + + def arguments(args) do + args + end + + def reset(args, state, _opts) do + reset(args, state) + end + + def reset(_args, state) do + state end - def update(propagator, _changes) do - propagator + def bind(%{args: args} = propagator, source, var_field) do + Map.put(propagator, :args, Propagator.bind_to_variables(args, source, var_field)) end - def filter(args, _propagator_state) do - filter(args) + def entailed?(args, propagator_state) do + false + end + + def failed?(args, _propagator_state) do + false end def variables(args) do Propagator.default_variables_impl(args) end - defoverridable variables: 1, update: 2, new: 1, filter: 2 + defoverridable arguments: 1, + variables: 1, + reset: 2, + reset: 3, + bind: 3, + failed?: 2, + entailed?: 2 end end @@ -50,10 +80,7 @@ defmodule CPSolver.Propagator do def default_variables_impl(args) do args - |> Enum.filter(fn - %Variable{} -> true - _ -> false - end) + |> Enum.reject(fn arg -> is_constant_arg(arg) end) end def new(mod, args, opts \\ []) do @@ -64,53 +91,110 @@ defmodule CPSolver.Propagator do id: id, name: name, mod: mod, - args: args + args: args, + state: nil, + variable_positions: + mod.variables(Enum.to_list(args)) + |> Enum.with_index(0) + |> Map.new(fn {var, pos} -> + {Interface.id(var), pos} + end) } end - def update(%{mod: mod} = propagator, changes) do - try do - mod.update(propagator, changes) - catch - :fail -> - :fail - end + def variables(%{mod: mod, args: args} = _propagator) do + mod_variables = mod.variables(Enum.to_list(args)) + + mod_variables + |> Enum.with_index() + |> Enum.map(fn {var, idx} -> Map.put(var, :arg_position, idx) end) end - def filter(%{mod: mod, args: args} = propagator, opts \\ []) do + def reset(%{mod: mod, args: args} = propagator, opts \\ []) do + Map.put(propagator, :state, mod.reset(args, Map.get(propagator, :state), opts)) + end + + def bind(%{mod: mod} = propagator, source, var_field \\ :domain) do + mod.bind(propagator, source, var_field) + end + + def dry_run(%{args: args} = propagator, opts \\ []) do + staged_propagator = %{propagator | args: copy_args(args)} + {staged_propagator, filter(staged_propagator, opts)} + end + + def filter(%{mod: mod, args: args, variable_positions: positions_map} = propagator, opts \\ []) do PropagatorVariable.reset_variable_ops() - store = Keyword.get(opts, :store) state = propagator[:state] - ConstraintStore.set_store(store) + + ## Propagation changes + ## The propagation may reshedule the filtering and pass the changes that woke + ## the propagator. + incoming_changes = + case Keyword.get(opts, :changes) do + nil -> + %{} + + var_changes -> + Enum.reduce(var_changes, Map.new(), fn {var_id, domain_change}, positional_changes_acc -> + position = is_integer(var_id) && var_id || Map.get(positions_map, var_id) + position && Map.put(positional_changes_acc, position, domain_change) + || positional_changes_acc + end) + end + + ## We will reset the state if required. + ## Reset will be forced when the space starts propagation. + reset? = Keyword.get(opts, :reset?, false) try do - mod.filter(args, state) + state = (reset? && mod.reset(args, state, opts)) || state + + case mod.filter(args, state, incoming_changes) do + :fail -> + :fail + + :stable -> + :stable + + result -> + get_filter_changes(result) + end catch + :error, error -> + {:filter_error, {mod, error}} + |> tap(fn _ -> Logger.error(%{mod: mod, error: error, stacktrace: __STACKTRACE__}) end) + :fail -> :fail - else - :fail -> - :fail + end + |> tap(fn result -> + case Keyword.get(opts, :debug) do + debug_fun when is_function(debug_fun) -> + debug_fun.(propagator, Keyword.drop(opts, [:debug]), result) - :stable -> - :stable + nil -> + nil + end + end) + end - result -> - get_filter_changes(result) - end + ## Check if propagator is entailed (i.e., all variables are fixed) + def entailed?(%{mod: mod, args: args} = propagator) do + mod.entailed?(args, propagator[:state]) end - def find_variable(args, var_id) do - Enum.find(args, fn arg -> Interface.id(arg) == var_id end) + def failed?(%{mod: mod, args: args} = propagator) do + mod.failed?(args, propagator[:state]) end ## How propagator events map to domain events def to_domain_events(:domain_change) do - [:domain_change, :min_change, :max_change, :fixed] + [:domain_change | to_domain_events(:bound_change)] end def to_domain_events(:bound_change) do - [:min_change, :max_change, :fixed] + [:min_change, :max_change, :bound_change, :fixed] end def to_domain_events(:min_change) do @@ -121,7 +205,7 @@ defmodule CPSolver.Propagator do [:max_change, :fixed] end - def to_domain_events(:fixed) do + def to_domain_events(_fixed) do [:fixed] end @@ -138,31 +222,99 @@ defmodule CPSolver.Propagator do defp get_filter_changes({:state, state}) do get_filter_changes(true) |> Map.put(:state, state) + |> Map.put(:active?, Map.get(state, :active?, true)) end defp get_filter_changes(result) do get_filter_changes(result != :passive) end - def bind_to_variables(propagator, indexed_variables, var_field) do - bound_args = - propagator.args - |> Enum.map(fn arg -> bind_to_variable(arg, indexed_variables, var_field) end) - - Map.put(propagator, :args, bound_args) + def bind_to_variables(args, variable_source, var_field) do + arg_map(args, fn arg -> + bind_to_variable(arg, variable_source, var_field) + end) end - defp bind_to_variable(%Variable{id: id} = var, indexed_variables, var_field) do - field_value = Map.get(indexed_variables, id) |> Map.get(var_field) - Map.put(var, var_field, field_value) + def bind_to_variable(%Variable{id: id} = propagator_var, variable_source, var_field) do + source_var = get_variable(variable_source, id) + Map.put(propagator_var, var_field, Map.get(source_var, var_field)) end - defp bind_to_variable(%View{variable: variable} = view, indexed_variables, var_field) do - bound_var = bind_to_variable(variable, indexed_variables, var_field) + def bind_to_variable(%View{variable: variable} = view, variable_source, var_field) do + bound_var = bind_to_variable(variable, variable_source, var_field) Map.put(view, :variable, bound_var) end - defp bind_to_variable(const, _indexed_variables, _var_field) do + def bind_to_variable(const, _variable_source, _var_field) do const end + + defp get_variable(%Graph{} = constraint_graph, var_id) do + ConstraintGraph.get_variable(constraint_graph, var_id) + end + + defp get_variable(variable_source, var_id) when is_map(variable_source) do + Map.get(variable_source, var_id) + end + + defp copy_variable(%Variable{domain: domain} = var) do + %{var | domain: Domain.copy(domain)} + end + + defp copy_variable(%View{variable: variable} = view) do + Map.put(view, :variable, copy_variable(variable)) + end + + def is_constant_arg(%Variable{} = _arg) do + false + end + + def is_constant_arg(%View{} = _arg) do + false + end + + def is_constant_arg(_other) do + true + end + + def arg_at(args, pos) when is_tuple(args) do + TupleArray.at(args, pos) + end + + def arg_at(args, pos) do + Arrays.get(args, pos) + end + + def arg_map(args, mapper) when is_function(mapper) and is_list(args) do + Enum.map(args, mapper) + end + + def arg_map(args, mapper) when is_function(mapper) and is_tuple(args) do + TupleArray.map(args, mapper) + end + + def arg_map(args, mapper) when is_function(mapper) do + Arrays.map(args, mapper) + end + + def args_to_list(args) when is_tuple(args) do + Tuple.to_list(args) + end + + def args_to_list(args) do + args + end + + def domain_values(%{args: args} = _p) do + arg_map(args, fn arg -> + (is_constant_arg(arg) && arg) || {Interface.variable(arg).name, Utils.domain_values(arg)} + end) + end + + defp copy_args(args) do + arg_map(args, fn arg -> + (is_constant_arg(arg) && arg) || + copy_variable(arg) + end) + end end diff --git a/lib/solver/core/propagator/propagator_variable.ex b/lib/solver/core/propagator/propagator_variable.ex index 5e6fccc7..407e034f 100644 --- a/lib/solver/core/propagator/propagator_variable.ex +++ b/lib/solver/core/propagator/propagator_variable.ex @@ -3,6 +3,7 @@ defmodule CPSolver.Propagator.Variable do alias CPSolver.Variable.View alias CPSolver.Variable.Interface.ThrowIfFails, as: Interface alias CPSolver.Propagator + import CPSolver.Common @propagator_events Propagator.propagator_events() @domain_events CPSolver.Common.domain_events() @@ -66,9 +67,14 @@ defmodule CPSolver.Propagator.Variable do defp save_op(var, domain_change) when domain_change in @domain_events do current_changes = ((changes = get_variable_ops()) && changes) || Map.new() + {_, updated_changes} = + Map.get_and_update(current_changes, Interface.id(var), fn current_var_change -> + {current_var_change, stronger_domain_change(current_var_change, domain_change)} + end) + Process.put( @variable_op_results_key, - Map.put(current_changes, Interface.id(var), domain_change) + updated_changes ) end diff --git a/lib/solver/core/shared.ex b/lib/solver/core/shared.ex index 6b892b6d..c08519ae 100644 --- a/lib/solver/core/shared.ex +++ b/lib/solver/core/shared.ex @@ -5,7 +5,7 @@ defmodule CPSolver.Shared do def init_shared_data(opts) do distributed = Keyword.get(opts, :distributed, false) - max_space_threads = Keyword.get(opts, :max_space_threads) + space_threads = Keyword.get(opts, :space_threads) %{ caller: self(), @@ -19,7 +19,7 @@ defmodule CPSolver.Shared do active_nodes: :ets.new(__MODULE__, [:set, :public, read_concurrency: true, write_concurrency: false]), complete_flag: init_complete_flag(), - space_thread_counters: init_space_thread_counters(max_space_threads), + space_thread_counters: init_space_thread_counters(space_threads), times: init_times(), distributed: distributed } @@ -86,11 +86,11 @@ defmodule CPSolver.Shared do ## The value is 2-element (:counters) array ## First element is a thread counter, 2nd is the max number of ## space processes allowed to run simultaneously on a given node. - defp init_space_thread_counters(max_space_threads, nodes \\ [Node.self() | Node.list()]) do + defp init_space_thread_counters(space_threads, nodes \\ [Node.self() | Node.list()]) do Map.new(nodes, fn node -> ref = :counters.new(2, [:atomics]) :counters.put(ref, 1, 0) - :counters.put(ref, 2, max_space_threads) + :counters.put(ref, 2, space_threads) {node, ref} end) |> then(fn node_thread_counters -> diff --git a/lib/solver/core/solver.ex b/lib/solver/core/solver.ex index c4ca6d89..81665998 100644 --- a/lib/solver/core/solver.ex +++ b/lib/solver/core/solver.ex @@ -27,7 +27,7 @@ defmodule CPSolver do shared_data = Shared.init_shared_data( - max_space_threads: opts[:max_space_threads], + space_threads: opts[:space_threads], distributed: opts[:distributed] ) |> Map.put(:sync_mode, opts[:sync_mode]) @@ -105,7 +105,8 @@ defmodule CPSolver do status(statistics(solver), objective_value(solver), complete?(solver)) end - defp status(%{active_node_count: 0, solution_count: 0}, _objective_value, true) do + defp status(%{active_node_count: active_node_count, solution_count: 0}, _objective_value, true) + when active_node_count <= 1 do :unsatisfiable end @@ -113,8 +114,14 @@ defmodule CPSolver do (objective_value && {:optimal, objective: objective_value}) || :all_solutions end - defp status(%{active_node_count: active_nodes}, objective_value, true) when active_nodes > 0 do - (objective_value && {:satisfied, objective: objective_value}) || :satisfied + defp status( + %{active_node_count: active_nodes, solution_count: solution_count}, + objective_value, + true + ) + when active_nodes > 0 do + (objective_value && {:satisfied, objective: objective_value}) || + (solution_count > 0 && :satisfied) || :unknown end defp status( @@ -268,7 +275,7 @@ defmodule CPSolver do Enum.reduce(constraints, [], fn constraint, acc -> acc ++ Enum.map(Constraint.constraint_to_propagators(constraint), fn p -> - Propagator.bind_to_variables(p, indexed_variables, :index) + Propagator.bind(p, indexed_variables, :index) end) end) diff --git a/lib/solver/domain/bitvector_domain.ex b/lib/solver/domain/bitvector_domain.ex index 3d754c6d..8b4a116a 100644 --- a/lib/solver/domain/bitvector_domain.ex +++ b/lib/solver/domain/bitvector_domain.ex @@ -1,8 +1,11 @@ defmodule CPSolver.BitVectorDomain do import Bitwise + @failure_value (1 <<< 64) - 1 + @max64_value 1 <<< 64 + def new([]) do - throw(:empty_domain) + fail() end def new(value) when is_integer(value) do @@ -13,7 +16,7 @@ defmodule CPSolver.BitVectorDomain do new([domain]) end - def new({{:bit_vector, _size, _ref} = _bitmap, _offset} = domain) do + def new({{:bit_vector, _ref} = _bitmap, _offset} = domain) do domain end @@ -23,198 +26,449 @@ defmodule CPSolver.BitVectorDomain do bv = :bit_vector.new(domain_size) Enum.each(domain, fn idx -> :bit_vector.set(bv, idx + offset) end) + PackedMinMax.set_min(0, 0) + |> PackedMinMax.set_max(Enum.max(domain) + offset) + |> then(fn min_max -> set_min_max(bv, min_max) end) + {bv, offset} end - def map(domain, mapper_fun) when is_function(mapper_fun) do - to_list(domain, mapper_fun) + def copy({{:bit_vector, ref} = bit_vector, offset} = _domain) do + %{ + min_addr: %{block: current_min_block}, + max_addr: %{block: current_max_block} + } = get_bound_addrs(bit_vector) + + new_atomics_size = current_max_block + 1 + new_atomics_ref = :atomics.new(new_atomics_size, [{:signed, false}]) + + Enum.each( + current_min_block..current_max_block, + fn block_idx -> + block_val = :atomics.get(ref, block_idx) + :atomics.put(new_atomics_ref, block_idx, block_val) + end + ) + + new_bit_vector = {:bit_vector, new_atomics_ref} + set_min_max(new_bit_vector, get_min_max_impl(bit_vector) |> elem(1)) + {new_bit_vector, offset} end - def to_list(domain, mapper_fun \\ &Function.identity/1) do - Enum.reduce(min(domain)..max(domain), [], fn i, acc -> - (contains?(domain, i) && [mapper_fun.(i) | acc]) || acc - end) + def map(domain, mapper_fun) when is_function(mapper_fun) do + to_list(domain, mapper_fun) end - def size({{:bit_vector, _size, ref} = bit_vector, _offset}) do - Enum.reduce(1..last_index(bit_vector), 0, fn idx, acc -> + def to_list( + {{:bit_vector, ref} = bit_vector, offset} = domain, + mapper_fun \\ &Function.identity/1 + ) do + %{ + min_addr: %{block: current_min_block, offset: _min_offset}, + max_addr: %{block: current_max_block, offset: _max_offset} + } = get_bound_addrs(bit_vector) + + mapped_lb = mapper_fun.(min(domain)) + mapped_ub = mapper_fun.(max(domain)) + + ## Note: this will only work for monotonic mapper functions. + ## We don't have non-monotonic mappers for the moment. + ## + ## Adjust bounds + {lb, ub} = (mapped_lb <= mapped_ub && {mapped_lb, mapped_ub}) || {mapped_ub, mapped_lb} + + Enum.reduce(current_min_block..current_max_block, MapSet.new(), fn idx, acc -> n = :atomics.get(ref, idx) - (n == 0 && acc) || - acc + (for(<>, do: bit) |> Enum.sum()) + if n == 0 do + acc + else + MapSet.union( + acc, + bit_positions(n, fn val -> {lb, ub, mapper_fun.(val + 64 * (idx - 1) - offset)} end) + ) + end end) end - def fixed?(domain) do - size(domain) == 1 + def fixed?({bit_vector, _offset} = _domain) do + {current_min_max, _min_max_idx, current_min, current_max} = get_min_max(bit_vector) + current_max == current_min && current_min_max != @failure_value end - def fail?(domain) do - size(domain) == 0 + def failed?({:bit_vector, _ref} = bit_vector) do + failed?(elem(get_min_max_impl(bit_vector), 1)) end - def min({{:bit_vector, _zero_based_max, atomics_ref} = bit_vector, offset}) do - ## Skip to a first non-zero element of atomics + def failed?({bit_vector, _offset} = _domain) do + failed?(bit_vector) + end - min_value = - Enum.reduce_while(1..last_index(bit_vector), nil, fn idx, _acc -> - case :atomics.get(atomics_ref, idx) do - 0 -> {:cont, nil} - non_zero_block -> {:halt, (idx - 1) * 64 + lsb(non_zero_block) - offset} - end - end) + def failed?(min_max_value) when is_integer(min_max_value) do + min_max_value == @failure_value + end - (min_value && min_value) || :fail + def min({bit_vector, offset} = _domain) do + get_min(bit_vector) - offset end - def max({{:bit_vector, _zero_based_max, atomics_ref} = bit_vector, offset}) do - ## Skip to a last non-zero element of atomics - max_value = - Enum.reduce_while(1..last_index(bit_vector) |> Enum.reverse(), nil, fn idx, _acc -> - case :atomics.get(atomics_ref, idx) do - 0 -> {:cont, nil} - non_zero_block -> {:halt, (idx - 1) * 64 + msb(non_zero_block) - offset} - end - end) + def max({bit_vector, offset} = _domain) do + get_max(bit_vector) - offset + end + + def size({{:bit_vector, ref} = bit_vector, _offset}) do + %{ + min_addr: %{block: current_min_block, offset: min_offset}, + max_addr: %{block: current_max_block, offset: max_offset} + } = get_bound_addrs(bit_vector) - (max_value && max_value) || :fail + Enum.reduce(current_min_block..current_max_block, 0, fn idx, acc -> + n = :atomics.get(ref, idx) + + if n == 0 do + acc + else + n1 = (idx == current_min_block && n >>> min_offset) || n + n2 = (idx == current_max_block && ((1 <<< (max_offset + 1)) - 1 &&& n1)) || n1 + acc + bit_count(n2) + end + end) end - def contains?({{:bit_vector, zero_based_max, _ref} = bit_vector, offset}, value) do + def contains?({{:bit_vector, _ref} = bit_vector, offset}, value) do + {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) vector_value = value + offset + contains?(bit_vector, vector_value, min_value, max_value) + end - vector_value >= 0 && vector_value < zero_based_max && + def contains?(bit_vector, vector_value, min_value, max_value) do + vector_value >= min_value && vector_value <= max_value && :bit_vector.get(bit_vector, vector_value) == 1 end - def remove({bitmap, offset} = domain, value) do + def fix({bit_vector, offset} = _domain, value) do + min_max_info = + {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) + + vector_value = value + offset + + if contains?(bit_vector, vector_value, min_value, max_value) do + set_fixed(bit_vector, value + offset, min_max_info) + else + fail(bit_vector) + end + end + + def remove({bit_vector, offset} = domain, value) do + {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) + vector_value = value + offset + + cond do + ## No value in the domain, do nothing + contains?(bit_vector, vector_value, min_value, max_value) -> + domain_change = + cond do + min_value == max_value && vector_value == min_value -> + ## Fixed value: fail on removing attempt + fail(bit_vector) + + min_value == vector_value -> + tighten_min(bit_vector, min_value, max_value) + + max_value == vector_value -> + tighten_max(bit_vector, max_value, min_value) + + true -> + :domain_change + end + + {domain_change, domain} + |> tap(fn _ -> :bit_vector.clear(bit_vector, vector_value) end) + + true -> + :no_change + end + end + + def removeAbove({bit_vector, offset} = domain, value) do + {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) + vector_value = value + offset + cond do - !contains?(domain, value) -> + vector_value >= max_value -> :no_change + vector_value < min_value -> + fail(bit_vector) + true -> - min? = min(domain) == value - max? = max(domain) == value - - cond do - ## Attempt to remove fixed value - min? && max? -> - :fail - - true -> - vector_value = value + offset - {:bit_vector.clear(bitmap, vector_value), offset} - ## What kind of domain change happened? - domain_change = - cond do - fixed?(domain) -> :fixed - min? -> :min_change - max? -> :max_change - true -> :domain_change - end + ## The value is strictly less than max + domain_change = tighten_max(bit_vector, vector_value + 1, min_value) - {domain_change, domain} - end + {domain_change, domain} end end - def removeAbove({{:bit_vector, _zero_based_max, ref} = bit_vector, offset} = domain, value) do + def removeBelow({bit_vector, offset} = domain, value) do + {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) + vector_value = value + offset + cond do - value >= max(domain) -> + vector_value <= min_value -> :no_change - value < min(domain) -> - :fail + vector_value > max_value -> + fail(bit_vector) true -> - vector_value = value + offset - block_index = block_index(vector_value) - last_index = last_index(bit_vector) - ## Clear up all blocks that follow the block the value is in - last_index > block_index && - Enum.each((block_index + 1)..last_index, fn idx -> :atomics.put(ref, idx, 0) end) - - block_value = :atomics.get(ref, block_index) - ## Find position for the value within the block - pos = rem(vector_value, 64) - # mask = (:math.pow(2, pos + 1) - 1) |> floor() - mask = (1 <<< (pos + 1)) - 1 - ## Remove all significant bits in the block above the value position - # msb = msb(block_value) - # shift = msb - pos - new_value = block_value &&& mask - :atomics.put(ref, block_index, new_value) - - domain_change = - cond do - fail?(domain) -> :fail - fixed?(domain) -> :fixed - true -> :max_change - end + ## The value is strictly greater than min + domain_change = tighten_min(bit_vector, vector_value - 1, max_value) {domain_change, domain} end end - def removeBelow({{:bit_vector, _zero_based_max, ref} = _bit_vector, offset} = domain, value) do + def raw({{:bit_vector, ref} = _bit_vector, offset} = _domain) do + %{ + offset: offset, + content: Enum.map(1..:atomics.info(ref).size, fn i -> :atomics.get(ref, i) end) + } + end + + ## Last 2 bytes of bit_vector are min and max + def last_index({:bit_vector, ref} = _bit_vector) do + :atomics.info(ref).size - 1 + end + + defp set_min_max({:bit_vector, ref} = bit_vector, min_max) do + bit_vector + |> min_max_index() + |> tap(fn idx -> + :atomics.put(ref, idx, min_max) + end) + end + + def get_min(bit_vector) do + get_min_max(bit_vector) |> elem(2) + end + + def get_max(bit_vector) do + get_min_max(bit_vector) |> elem(3) + end + + defp min_max_index(bit_vector) do + last_index(bit_vector) + 1 + end + + def get_min_max(bit_vector) do + get_min_max_impl(bit_vector) + |> then(fn {min_max_index, min_max} -> + min_max == @failure_value && fail(bit_vector) + {min_max, min_max_index, PackedMinMax.get_min(min_max), PackedMinMax.get_max(min_max)} + end) + end + + defp get_min_max_impl({:bit_vector, ref} = bit_vector) do + min_max_index = min_max_index(bit_vector) + {min_max_index, :atomics.get(ref, min_max_index)} + end + + def set_min(bit_vector, new_min) do + set_min(bit_vector, new_min, get_min_max(bit_vector)) + end + + def set_min({:bit_vector, ref} = bit_vector, new_min, min_max_info) do + {current_min_max, min_max_idx, current_min, current_max} = min_max_info + cond do - value <= min(domain) -> - :no_change + new_min > current_max -> + ## Inconsistency + fail(bit_vector) - value > max(domain) -> - :fail + new_min != current_min && current_min == current_max -> + ## Attempt to re-fix + fail(bit_vector) true -> - vector_value = value + offset - block_index = block_index(vector_value) - ## Clear up all blocks on the left of the block the value is in - block_index > 1 && - Enum.each(1..(block_index - 1), fn idx -> :atomics.put(ref, idx, 0) end) - - block_value = :atomics.get(ref, block_index) - ## Find position for the value within the block - pos = rem(vector_value, 64) - msb = msb(block_value) - mask = ((1 <<< msb) - 1) <<< pos - ## Remove all significant bits in the block below the value position - new_value = block_value &&& mask - :atomics.put(ref, block_index, new_value) + ## Min change + min_max_value = PackedMinMax.set_min(current_min_max, new_min) + + case :atomics.compare_exchange(ref, min_max_idx, current_min_max, min_max_value) do + :ok -> + cond do + new_min == current_max -> :fixed + new_min <= current_min -> :no_change + true -> :min_change + end + + changed_by_other_thread -> + min2 = PackedMinMax.get_min(changed_by_other_thread) + max2 = PackedMinMax.get_max(changed_by_other_thread) + set_min(bit_vector, new_min, {changed_by_other_thread, min_max_idx, min2, max2}) + end + end + end - domain_change = - cond do - fail?(domain) -> :fail - fixed?(domain) -> :fixed - true -> :min_change - end + def set_max(bit_vector, new_max) do + set_max(bit_vector, new_max, get_min_max(bit_vector)) + end - {domain_change, domain} + def set_max({:bit_vector, ref} = bit_vector, new_max, min_max_info) do + {current_min_max, min_max_idx, current_min, current_max} = min_max_info + + cond do + new_max < current_min -> + ## Inconsistency + fail(bit_vector) + + new_max != current_max && current_min == current_max -> + ## Attempt to re-fix + fail(bit_vector) + + true -> + ## Max change + min_max_value = PackedMinMax.set_max(current_min_max, new_max) + + case :atomics.compare_exchange(ref, min_max_idx, current_min_max, min_max_value) do + :ok -> + cond do + new_max == current_min -> :fixed + new_max >= current_max -> :no_change + true -> :max_change + end + + changed_by_other_thread -> + min2 = PackedMinMax.get_min(changed_by_other_thread) + max2 = PackedMinMax.get_max(changed_by_other_thread) + set_max(bit_vector, new_max, {changed_by_other_thread, min_max_idx, min2, max2}) + end end end - def fix(domain, value) do - if contains?(domain, value) do - {:fixed, new(value)} + def set_fixed({:bit_vector, ref} = bit_vector, fixed_value, min_max_info) do + {current_min_max, min_max_idx, current_min, current_max} = min_max_info + + if fixed_value != current_max && current_min == current_max do + ## Attempt to re-fix + fail(bit_vector) else - :fail + min_max_value = PackedMinMax.set_min(0, fixed_value) |> PackedMinMax.set_max(fixed_value) + + case :atomics.compare_exchange(ref, min_max_idx, current_min_max, min_max_value) do + :ok -> + :fixed + + changed_by_other_thread -> + min2 = PackedMinMax.get_min(changed_by_other_thread) + max2 = PackedMinMax.get_max(changed_by_other_thread) + set_fixed(bit_vector, fixed_value, {changed_by_other_thread, min_max_idx, min2, max2}) + end end end + ## Update (cached) min, if necessary + defp tighten_min( + {:bit_vector, atomics_ref} = bit_vector, + starting_at, + max_value + ) do + {current_max_block, _} = vector_address(max_value) + {rightmost_block, position_in_block} = vector_address(starting_at + 1) + ## Find a new min (on the right of the current one) + min_value = + Enum.reduce_while(rightmost_block..current_max_block, false, fn idx, min_block_empty? -> + case :atomics.get(atomics_ref, idx) do + 0 -> + {:cont, min_block_empty?} + + non_zero_block -> + block_lsb = + if min_block_empty? do + lsb(non_zero_block) + else + ## Reset all bits in the block to the left of the position + shift = position_in_block + lsb(non_zero_block >>> shift <<< shift) + end + + (block_lsb && + {:halt, (idx - 1) * 64 + block_lsb}) || {:cont, true} + end + end) + + (is_integer(min_value) && set_min(bit_vector, min_value)) || fail(bit_vector) + end + + ## Update (cached) max + defp tighten_max( + {:bit_vector, atomics_ref} = bit_vector, + starting_at, + min_value + ) do + {current_min_block_idx, _} = vector_address(min_value) + {leftmost_block_idx, position_in_block} = vector_address(starting_at - 1) + ## Find a new max (on the left of the current one) + ## + + max_value = + Enum.reduce_while( + leftmost_block_idx..current_min_block_idx, + false, + fn idx, max_block_empty? -> + case :atomics.get(atomics_ref, idx) do + 0 -> + {:cont, max_block_empty?} + + non_zero_block -> + block_msb = + if max_block_empty? do + msb(non_zero_block) + else + ## Reset all bits in the block to the right of the position + mask = (1 <<< (position_in_block + 1)) - 1 + msb(non_zero_block &&& mask) + end + + (block_msb && + {:halt, (idx - 1) * 64 + block_msb}) || {:cont, true} + end + end + ) + + (is_integer(max_value) && set_max(bit_vector, max_value)) || fail(bit_vector) + end + + defp fail(bit_vector \\ nil) do + bit_vector && set_min_max(bit_vector, @failure_value) + throw(:fail) + end + + def get_bound_addrs(bit_vector) do + {_, _, current_min, current_max} = get_min_max(bit_vector) + {current_min_block, current_min_offset} = vector_address(current_min) + {current_max_block, current_max_offset} = vector_address(current_max) + + %{ + min_addr: %{block: current_min_block, offset: current_min_offset}, + max_addr: %{block: current_max_block, offset: current_max_offset} + } + end + ## Find the index of atomics where the n-value resides - def block_index(n) do + defp block_index(n) do div(n, 64) + 1 end - ## Last 2 bytes of bit_vector are min and max - def last_index({:bit_vector, _zero_based_max, ref} = _bit_vector) do - :atomics.info(ref).size - 2 + defp vector_address(n) do + {block_index(n), rem(n, 64)} end ## Find least significant bit - def lsb(0) do + defp lsb(0) do nil end - def lsb(n) do + defp lsb(n) do lsb(n, 0) end @@ -227,11 +481,11 @@ defmodule CPSolver.BitVectorDomain do lsb(n >>> 1, idx + 1) end - def msb(0) do + defp msb(0) do nil end - def msb(n) do + defp msb(n) do msb = floor(:math.log2(n)) ## Check if there is no precision loss. ## We really want to throw away the fraction part even if it may @@ -242,4 +496,45 @@ defmodule CPSolver.BitVectorDomain do msb end end + + def bit_count_iter(n) do + for <>, reduce: 0 do + acc -> acc + bit + end + end + + def bit_count(0) do + 0 + end + + def bit_count(n) do + n = (n &&& 0x5555555555555555) + (n >>> 1 &&& 0x5555555555555555) + n = (n &&& 0x3333333333333333) + (n >>> 2 &&& 0x3333333333333333) + n = (n &&& 0x0F0F0F0F0F0F0F0F) + (n >>> 4 &&& 0x0F0F0F0F0F0F0F0F) + n = (n &&& 0x00FF00FF00FF00FF) + (n >>> 8 &&& 0x00FF00FF00FF00FF) + n = (n &&& 0x0000FFFF0000FFFF) + (n >>> 16 &&& 0x0000FFFF0000FFFF) + (n &&& 0x00000000FFFFFFFF) + (n >>> 32 &&& 0x00000000FFFFFFFF) + end + + def bit_positions(n, mapper) do + bit_positions(n, 1, 0, mapper, MapSet.new()) + end + + def bit_positions(_n, @max64_value, _iteration, _mapper, positions) do + positions + end + + def bit_positions(n, shift, iteration, mapper, positions) do + acc = + ((n &&& shift) > 0 && + ( + {lb, ub, new_value} = mapper.(iteration) + + (new_value >= lb && new_value <= ub && + MapSet.put(positions, new_value)) || positions + )) || + positions + + bit_positions(n, shift <<< 1, iteration + 1, mapper, acc) + end end diff --git a/lib/solver/domain/bitvector_domain_v2.ex b/lib/solver/domain/bitvector_domain_v2.ex deleted file mode 100644 index 1c180424..00000000 --- a/lib/solver/domain/bitvector_domain_v2.ex +++ /dev/null @@ -1,481 +0,0 @@ -defmodule CPSolver.BitVectorDomain.V2 do - import Bitwise - - @max_value (1 <<< 64) - 1 - - def new([]) do - fail() - end - - def new(value) when is_integer(value) do - new([value]) - end - - def new(domain) when is_integer(domain) do - new([domain]) - end - - def new({{:bit_vector, _size, _ref} = _bitmap, _offset} = domain) do - domain - end - - def new(domain) do - offset = -Enum.min(domain) - domain_size = Enum.max(domain) + offset + 1 - bv = :bit_vector.new(domain_size) - Enum.each(domain, fn idx -> :bit_vector.set(bv, idx + offset) end) - - PackedMinMax.set_min(0, 0) - |> PackedMinMax.set_max(Enum.max(domain) + offset) - |> then(fn min_max -> set_min_max(bv, min_max) end) - - {bv, offset} - end - - def copy({{:bit_vector, _size, ref} = bit_vector, offset} = domain) do - %{ - min_addr: %{block: current_min_block}, - max_addr: %{block: current_max_block} - } = get_bound_addrs(bit_vector) - - new_atomics_size = current_max_block + 1 - new_atomics_ref = :atomics.new(new_atomics_size, [{:signed, false}]) - - Enum.each( - current_min_block..current_max_block, - fn block_idx -> - block_val = :atomics.get(ref, block_idx) - :atomics.put(new_atomics_ref, block_idx, block_val) - end - ) - - new_bit_vector = {:bit_vector, size(domain), new_atomics_ref} - set_min_max(new_bit_vector, get_min_max_impl(bit_vector) |> elem(1)) - {new_bit_vector, offset} - end - - def map(domain, mapper_fun) when is_function(mapper_fun) do - to_list(domain, mapper_fun) - end - - def to_list({bit_vector, offset} = _domain, mapper_fun \\ &Function.identity/1) do - {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) - - Enum.reduce(min_value..max_value, [], fn i, acc -> - (contains?(bit_vector, i, min_value, max_value) && [mapper_fun.(i - offset) | acc]) || acc - end) - end - - def fixed?({bit_vector, _offset} = _domain) do - {current_min_max, _min_max_idx, current_min, current_max} = get_min_max(bit_vector) - current_max == current_min && current_min_max != @max_value - end - - def failed?({bit_vector, _offset} = _domain) do - failed?(bit_vector) - end - - def failed?({:bit_vector, _size, _ref} = bit_vector) do - failed?(elem(get_min_max_impl(bit_vector), 1)) - end - - def failed?(min_max_value) when is_integer(min_max_value) do - min_max_value == @max_value || - PackedMinMax.get_min(min_max_value) > PackedMinMax.get_max(min_max_value) - end - - def min({bit_vector, offset} = _domain) do - get_min(bit_vector) - offset - end - - def max({bit_vector, offset} = _domain) do - get_max(bit_vector) - offset - end - - def size({{:bit_vector, _size, ref} = bit_vector, _offset}) do - %{ - min_addr: %{block: current_min_block, offset: min_offset}, - max_addr: %{block: current_max_block, offset: max_offset} - } = get_bound_addrs(bit_vector) - - Enum.reduce(current_min_block..current_max_block, 0, fn idx, acc -> - n = :atomics.get(ref, idx) - - if n == 0 do - acc - else - n1 = (idx == current_min_block && n >>> min_offset) || n - n2 = (idx == current_max_block && ((1 <<< (max_offset + 1)) - 1 &&& n1)) || n1 - acc + bit_count(n2) - end - end) - end - - def contains?({{:bit_vector, _zero_based_max, _ref} = bit_vector, offset}, value) do - {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) - vector_value = value + offset - contains?(bit_vector, vector_value, min_value, max_value) - end - - def contains?(bit_vector, vector_value, min_value, max_value) do - vector_value >= min_value && vector_value <= max_value && - :bit_vector.get(bit_vector, vector_value) == 1 - end - - def fix({bit_vector, offset} = _domain, value) do - min_max_info = - {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) - - vector_value = value + offset - - if contains?(bit_vector, vector_value, min_value, max_value) do - set_fixed(bit_vector, value + offset, min_max_info) - else - fail(bit_vector) - end - end - - def remove({bit_vector, offset} = domain, value) do - {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) - vector_value = value + offset - - cond do - ## No value in the domain, do nothing - !contains?(bit_vector, vector_value, min_value, max_value) -> - :no_change - - true -> - domain_change = - cond do - min_value == max_value && vector_value == min_value -> - ## Fixed value: fail on removing attempt - fail(bit_vector) - - min_value == vector_value -> - tighten_min(bit_vector, min_value, max_value) - - max_value == vector_value -> - tighten_max(bit_vector, max_value, min_value) - - true -> - :domain_change - end - - {domain_change, domain} - |> tap(fn _ -> :bit_vector.clear(bit_vector, vector_value) end) - end - end - - def removeAbove({bit_vector, offset} = domain, value) do - {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) - vector_value = value + offset - - cond do - vector_value >= max_value -> - :no_change - - vector_value < min_value -> - fail(bit_vector) - - true -> - ## The value is strictly less than max - domain_change = tighten_max(bit_vector, vector_value + 1, min_value) - - {domain_change, domain} - end - end - - def removeBelow({bit_vector, offset} = domain, value) do - {_current_min_max, _min_max_idx, min_value, max_value} = get_min_max(bit_vector) - vector_value = value + offset - - cond do - vector_value <= min_value -> - :no_change - - vector_value > max_value -> - fail(bit_vector) - - true -> - ## The value is strictly greater than min - domain_change = tighten_min(bit_vector, vector_value - 1, max_value) - - {domain_change, domain} - end - end - - def raw({{:bit_vector, _, ref} = _bit_vector, offset} = _domain) do - %{ - offset: offset, - content: Enum.map(1..:atomics.info(ref).size, fn i -> :atomics.get(ref, i) end) - } - end - - ## Last 2 bytes of bit_vector are min and max - def last_index({:bit_vector, _zero_based_max, ref} = _bit_vector) do - :atomics.info(ref).size - 1 - end - - defp set_min_max({:bit_vector, _, ref} = bit_vector, min_max) do - bit_vector - |> min_max_index() - |> tap(fn idx -> - :atomics.put(ref, idx, min_max) - end) - end - - def get_min(bit_vector) do - get_min_max(bit_vector) |> elem(2) - end - - def get_max(bit_vector) do - get_min_max(bit_vector) |> elem(3) - end - - defp min_max_index(bit_vector) do - last_index(bit_vector) + 1 - end - - def get_min_max(bit_vector) do - get_min_max_impl(bit_vector) - |> then(fn {min_max_index, min_max} -> - min_max == @max_value && fail(bit_vector) - {min_max, min_max_index, PackedMinMax.get_min(min_max), PackedMinMax.get_max(min_max)} - end) - end - - defp get_min_max_impl({:bit_vector, _zero_based_max, ref} = bit_vector) do - min_max_index = min_max_index(bit_vector) - {min_max_index, :atomics.get(ref, min_max_index)} - end - - def set_min(bit_vector, new_min) do - set_min(bit_vector, new_min, get_min_max(bit_vector)) - end - - def set_min({:bit_vector, _zero_based_max, ref} = bit_vector, new_min, min_max_info) do - {current_min_max, min_max_idx, current_min, current_max} = min_max_info - - cond do - new_min > current_max -> - ## Inconsistency - fail(bit_vector) - - new_min != current_min && current_min == current_max -> - ## Attempt to re-fix - fail(bit_vector) - - true -> - ## Min change - min_max_value = PackedMinMax.set_min(current_min_max, new_min) - - case :atomics.compare_exchange(ref, min_max_idx, current_min_max, min_max_value) do - :ok -> - cond do - new_min == current_max -> :fixed - new_min <= current_min -> :no_change - true -> :min_change - end - - changed_by_other_thread -> - min2 = PackedMinMax.get_min(changed_by_other_thread) - max2 = PackedMinMax.get_max(changed_by_other_thread) - set_min(bit_vector, new_min, {changed_by_other_thread, min_max_idx, min2, max2}) - end - end - end - - def set_max(bit_vector, new_max) do - set_max(bit_vector, new_max, get_min_max(bit_vector)) - end - - def set_max({:bit_vector, _zero_based_max, ref} = bit_vector, new_max, min_max_info) do - {current_min_max, min_max_idx, current_min, current_max} = min_max_info - - cond do - new_max < current_min -> - ## Inconsistency - fail(bit_vector) - - new_max != current_max && current_min == current_max -> - ## Attempt to re-fix - fail(bit_vector) - - true -> - ## Max change - min_max_value = PackedMinMax.set_max(current_min_max, new_max) - - case :atomics.compare_exchange(ref, min_max_idx, current_min_max, min_max_value) do - :ok -> - cond do - new_max == current_min -> :fixed - new_max >= current_max -> :no_change - true -> :max_change - end - - changed_by_other_thread -> - min2 = PackedMinMax.get_min(changed_by_other_thread) - max2 = PackedMinMax.get_max(changed_by_other_thread) - set_max(bit_vector, new_max, {changed_by_other_thread, min_max_idx, min2, max2}) - end - end - end - - def set_fixed({:bit_vector, _zero_based_max, ref} = bit_vector, fixed_value, min_max_info) do - {current_min_max, min_max_idx, current_min, current_max} = min_max_info - - if fixed_value != current_max && current_min == current_max do - ## Attempt to re-fix - fail(bit_vector) - else - min_max_value = PackedMinMax.set_min(0, fixed_value) |> PackedMinMax.set_max(fixed_value) - - case :atomics.compare_exchange(ref, min_max_idx, current_min_max, min_max_value) do - :ok -> - :fixed - - changed_by_other_thread -> - min2 = PackedMinMax.get_min(changed_by_other_thread) - max2 = PackedMinMax.get_max(changed_by_other_thread) - set_fixed(bit_vector, fixed_value, {changed_by_other_thread, min_max_idx, min2, max2}) - end - end - end - - ## Update (cached) min, if necessary - defp tighten_min( - {:bit_vector, _zero_based_max, atomics_ref} = bit_vector, - starting_at, - max_value - ) do - {current_max_block, _} = vector_address(max_value) - {rightmost_block, position_in_block} = vector_address(starting_at + 1) - ## Find a new min (on the right of the current one) - min_value = - Enum.reduce_while(rightmost_block..current_max_block, false, fn idx, min_block_empty? -> - case :atomics.get(atomics_ref, idx) do - 0 -> - {:cont, min_block_empty?} - - non_zero_block -> - block_lsb = - if min_block_empty? do - lsb(non_zero_block) - else - ## Reset all bits above the position - shift = position_in_block - lsb(non_zero_block >>> shift <<< shift) - end - - (block_lsb && - {:halt, (idx - 1) * 64 + block_lsb}) || {:cont, true} - end - end) - - (is_integer(min_value) && set_min(bit_vector, min_value)) || fail(bit_vector) - end - - ## Update (cached) max - defp tighten_max( - {:bit_vector, _zero_based_max, atomics_ref} = bit_vector, - starting_at, - min_value - ) do - {current_min_block_idx, _} = vector_address(min_value) - {leftmost_block_idx, position_in_block} = vector_address(starting_at - 1) - ## Find a new max (on the left of the current one) - ## - - max_value = - Enum.reduce_while( - leftmost_block_idx..current_min_block_idx, - false, - fn idx, max_block_empty? -> - case :atomics.get(atomics_ref, idx) do - 0 -> - {:cont, max_block_empty?} - - non_zero_block -> - block_msb = - if max_block_empty? do - msb(non_zero_block) - else - ## Reset all bits above the position - mask = (1 <<< (position_in_block + 1)) - 1 - msb(non_zero_block &&& mask) - end - - (block_msb && - {:halt, (idx - 1) * 64 + block_msb}) || {:cont, true} - end - end - ) - - (is_integer(max_value) && set_max(bit_vector, max_value)) || fail(bit_vector) - end - - defp fail(bit_vector \\ nil) do - bit_vector && set_min_max(bit_vector, @max_value) - throw(:fail) - end - - def get_bound_addrs(bit_vector) do - {_, _, current_min, current_max} = get_min_max(bit_vector) - {current_min_block, current_min_offset} = vector_address(current_min) - {current_max_block, current_max_offset} = vector_address(current_max) - - %{ - min_addr: %{block: current_min_block, offset: current_min_offset}, - max_addr: %{block: current_max_block, offset: current_max_offset} - } - end - - ## Find the index of atomics where the n-value resides - defp block_index(n) do - div(n, 64) + 1 - end - - defp vector_address(n) do - {block_index(n), rem(n, 64)} - end - - ## Find least significant bit - defp lsb(0) do - nil - end - - defp lsb(n) do - lsb(n, 0) - end - - defp lsb(1, idx) do - idx - end - - defp lsb(n, idx) do - ((n &&& 1) == 1 && idx) || - lsb(n >>> 1, idx + 1) - end - - defp msb(0) do - nil - end - - defp msb(n) do - msb = floor(:math.log2(n)) - ## Check if there is no precision loss. - ## We really want to throw away the fraction part even if it may - ## get very close to 1. - if floor(:math.pow(2, msb)) > n do - msb - 1 - else - msb - end - end - - defp bit_count(n) do - for <>, reduce: 0 do - acc -> acc + bit - end - end -end diff --git a/lib/solver/domain/default_domain.ex b/lib/solver/domain/default_domain.ex index 5e0e13ff..4ece9754 100644 --- a/lib/solver/domain/default_domain.ex +++ b/lib/solver/domain/default_domain.ex @@ -1,5 +1,5 @@ defmodule CPSolver.DefaultDomain do - alias CPSolver.BitVectorDomain.V2, as: Domain + alias CPSolver.BitVectorDomain, as: Domain defdelegate new(values), to: Domain @@ -36,10 +36,14 @@ defmodule CPSolver.DefaultDomain do end def to_list(arg) when is_integer(arg) do - [arg] + MapSet.new([arg]) end def to_list(arg) when is_list(arg) do + MapSet.new(arg) + end + + def to_list(%MapSet{} = arg) do arg end @@ -51,8 +55,12 @@ defmodule CPSolver.DefaultDomain do fixed end + def copy(values) when is_list(values) do + (Enum.empty?(values) && fail()) || Domain.new(values) + end + def copy(domain) do - Domain.copy(domain) + (fixed?(domain) && min(domain)) || Domain.copy(domain) end def size(fixed) when is_integer(fixed) do @@ -67,6 +75,10 @@ defmodule CPSolver.DefaultDomain do true end + def fixed?(values) when is_list(values) do + length(values) == 1 + end + def fixed?(domain) do Domain.fixed?(domain) end diff --git a/lib/solver/model/model.ex b/lib/solver/model/model.ex index d27714fd..57b7e91e 100644 --- a/lib/solver/model/model.ex +++ b/lib/solver/model/model.ex @@ -1,6 +1,6 @@ defmodule CPSolver.Model do alias CPSolver.Constraint - alias CPSolver.Variable + alias CPSolver.IntVariable, as: Variable alias CPSolver.Variable.Interface alias CPSolver.Objective @@ -16,6 +16,7 @@ defmodule CPSolver.Model do } def new(variables, constraints, opts \\ []) do + constraints = normalize_constraints(constraints) {all_variables, objective} = init_model(variables, constraints, opts[:objective]) %__MODULE__{ @@ -29,7 +30,12 @@ defmodule CPSolver.Model do end def init_model(variables, constraints, objective) do - variable_map = Map.new(variables, fn v -> {Interface.id(v), Interface.variable(v)} end) + safe_variables = Enum.map(variables, fn v -> + is_integer(v) && Variable.new(v) || v + end) + + variable_map = Map.new(safe_variables, fn v -> + {Interface.id(v), Interface.variable(v)} end) ## Additional variables may come from constraint definitions ## (example: LessOrEqual constraint, where the second argument is a constant value). @@ -39,7 +45,7 @@ defmodule CPSolver.Model do |> extract_variables_from_constraints() |> Enum.reject(fn c_var -> Map.has_key?(variable_map, c_var.id) end) - (variables ++ additional_variables) + (safe_variables ++ additional_variables) |> Enum.with_index(1) |> Enum.map_reduce(objective, fn {var, idx}, obj_acc -> { @@ -58,5 +64,16 @@ defmodule CPSolver.Model do constraints |> Enum.map(&Constraint.extract_variables/1) |> List.flatten() + |> Enum.uniq_by(fn var -> Map.get(var, :id) end) + end + + ~S""" + Transform list of constraints of different types + into the list of plain constraints. + For now just flatten (but maybe more in the future + for factory-constructed constraints etc.) + """ + defp normalize_constraints(constraints) do + List.flatten(constraints) end end diff --git a/lib/solver/objective/objective_propagator.ex b/lib/solver/objective/objective_propagator.ex index 26229634..0d9b3256 100644 --- a/lib/solver/objective/objective_propagator.ex +++ b/lib/solver/objective/objective_propagator.ex @@ -12,7 +12,7 @@ defmodule CPSolver.Objective.Propagator do end @impl true - def filter([x, bound_handle | _]) do + def filter([x, bound_handle | _], _state, _changes) do removeAbove(x, Objective.get_bound(bound_handle)) end end diff --git a/lib/solver/search/domain_partition.ex b/lib/solver/search/domain_partition.ex deleted file mode 100644 index 5b315af9..00000000 --- a/lib/solver/search/domain_partition.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule CPSolver.Search.DomainPartition do - alias CPSolver.DefaultDomain, as: Domain - alias CPSolver.Variable.Interface - - require Logger - - def partition(variable, strategy) when is_function(strategy) do - domain = Interface.domain(variable) - split_domain_by(domain, strategy.(variable)) - end - - def partition(variable, choice) when choice in [:min, :max] do - domain = Interface.domain(variable) - val = apply(Domain, choice, [domain]) - split_domain_by(domain, val) - end - - def by_min(variable) do - partition(variable, :min) - end - - def by_max(variable) do - partition(variable, :max) - end - - def random(variable) do - domain = Interface.domain(variable) - random_val = Domain.to_list(domain) |> Enum.random() - split_domain_by(domain, random_val) - end - - def split_domain_by(domain, value) do - try do - Domain.remove(domain, value) - {:ok, [Domain.new(value), domain]} - rescue - :fail -> - Logger.error( - "Failure on partitioning with value #{inspect(value)}, domain: #{inspect(CPSolver.BitVectorDomain.V2.raw(domain))}" - ) - - throw(:fail) - end - end -end diff --git a/lib/solver/search/strategy.ex b/lib/solver/search/strategy.ex index 53dcd3ee..059e7c48 100644 --- a/lib/solver/search/strategy.ex +++ b/lib/solver/search/strategy.ex @@ -1,7 +1,12 @@ defmodule CPSolver.Search.Strategy do - alias CPSolver.Search.DomainPartition alias CPSolver.Variable.Interface alias CPSolver.Search.VariableSelector.FirstFail + alias CPSolver.DefaultDomain, as: Domain + # alias CPSolver.Constraint.{Equal, NotEqual} + + alias CPSolver.Search.ValueSelector.{Min, Max, Random} + + require Logger def default_strategy() do { @@ -22,15 +27,15 @@ defmodule CPSolver.Search.Strategy do end def shortcut(:indomain_min) do - &DomainPartition.by_min/1 + Min end def shortcut(:indomain_max) do - &DomainPartition.by_max/1 + Max end def shortcut(:indomain_random) do - &DomainPartition.random/1 + Random end def select_variable(variables, variable_choice) when is_atom(variable_choice) do @@ -46,12 +51,12 @@ defmodule CPSolver.Search.Strategy do end) end - def partition(variable, value_choice) when is_atom(value_choice) do - shortcut(value_choice).(variable) + defp partition_impl(variable, value_choice) when is_atom(value_choice) do + shortcut(value_choice).select_value(variable) end - def partition(variable, value_choice) when is_function(value_choice) do - DomainPartition.partition(variable, value_choice) + defp partition_impl(variable, value_choice) when is_function(value_choice) do + value_choice.(variable) end def branch(variables, {variable_choice, partition_strategy}) do @@ -71,6 +76,12 @@ defmodule CPSolver.Search.Strategy do end end + def partition(variable, value_choice) do + variable + |> partition_impl(value_choice) + |> split_domain_by(variable) + end + def all_vars_fixed_exception() do :all_vars_fixed end @@ -84,10 +95,44 @@ defmodule CPSolver.Search.Strategy do end defp variable_partitions(selected_variable, domain_partitions, variables) do - Enum.map(domain_partitions, fn domain -> - Enum.map(variables, fn var -> - (var.id == selected_variable.id && set_domain(var, domain)) || var - end) + Enum.map(domain_partitions, fn {domain, constraint} -> + {Enum.map(variables, fn var -> + domain_copy = + ((var.id == selected_variable.id && domain) || var.domain) + # var.domain + |> Domain.copy() + + set_domain(var, domain_copy) + end), constraint} end) end + + defp split_domain_by(value, variable) do + domain = Interface.domain(variable) + + try do + {remove_changes, _domain} = Domain.remove(domain, value) + + {:ok, + [ + { + Domain.new(value), + %{variable.id => :fixed} + # Equal.new(variable, value) + }, + { + domain, + %{variable.id => remove_changes} + # NotEqual.new(variable, value) + } + ]} + rescue + :fail -> + Logger.error( + "Failure on partitioning with value #{inspect(value)}, domain: #{inspect(CPSolver.BitVectorDomain.raw(domain))}" + ) + + throw(:fail) + end + end end diff --git a/lib/solver/search/value_partition.ex b/lib/solver/search/value_partition.ex deleted file mode 100644 index a450cbc9..00000000 --- a/lib/solver/search/value_partition.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule CPSolver.Search.ValuePartition do - alias CPSolver.Variable - @callback partition(Variable.t()) :: {:ok, [Domain.t() | number()]} | {:error, any()} -end diff --git a/lib/solver/search/first_fail.ex b/lib/solver/search/variable_value/first_fail.ex similarity index 100% rename from lib/solver/search/first_fail.ex rename to lib/solver/search/variable_value/first_fail.ex diff --git a/lib/solver/search/variable_value/indomain_max.ex b/lib/solver/search/variable_value/indomain_max.ex new file mode 100644 index 00000000..0df4dd3c --- /dev/null +++ b/lib/solver/search/variable_value/indomain_max.ex @@ -0,0 +1,9 @@ +defmodule CPSolver.Search.ValueSelector.Max do + @behaviour CPSolver.Search.ValueSelector + alias CPSolver.Variable.Interface + + @impl true + def select_value(variable) do + Interface.max(variable) + end +end diff --git a/lib/solver/search/variable_value/indomain_min.ex b/lib/solver/search/variable_value/indomain_min.ex new file mode 100644 index 00000000..ea9c9cc9 --- /dev/null +++ b/lib/solver/search/variable_value/indomain_min.ex @@ -0,0 +1,9 @@ +defmodule CPSolver.Search.ValueSelector.Min do + @behaviour CPSolver.Search.ValueSelector + alias CPSolver.Variable.Interface + + @impl true + def select_value(variable) do + Interface.min(variable) + end +end diff --git a/lib/solver/search/variable_value/indomain_random.ex b/lib/solver/search/variable_value/indomain_random.ex new file mode 100644 index 00000000..79affd66 --- /dev/null +++ b/lib/solver/search/variable_value/indomain_random.ex @@ -0,0 +1,13 @@ +defmodule CPSolver.Search.ValueSelector.Random do + @behaviour CPSolver.Search.ValueSelector + alias CPSolver.Variable.Interface + alias CPSolver.DefaultDomain, as: Domain + + @impl true + def select_value(variable) do + variable + |> Interface.domain() + |> Domain.to_list() + |> Enum.random() + end +end diff --git a/lib/solver/search/variable_value/input_order.ex b/lib/solver/search/variable_value/input_order.ex new file mode 100644 index 00000000..538f4b6d --- /dev/null +++ b/lib/solver/search/variable_value/input_order.ex @@ -0,0 +1,9 @@ +defmodule CPSolver.Search.VariableSelector.InputOrder do + @behaviour CPSolver.Search.VariableSelector + + @impl true + def select_variable(variables) do + Enum.sort_by(variables, fn %{index: idx} -> idx end) + |> List.first() + end +end diff --git a/lib/solver/search/variable_value/value_selector.ex b/lib/solver/search/variable_value/value_selector.ex new file mode 100644 index 00000000..d8bcbf58 --- /dev/null +++ b/lib/solver/search/variable_value/value_selector.ex @@ -0,0 +1,3 @@ +defmodule CPSolver.Search.ValueSelector do + @callback select_value(Variable.t()) :: integer() +end diff --git a/lib/solver/search/variable_selector.ex b/lib/solver/search/variable_value/variable_selector.ex similarity index 100% rename from lib/solver/search/variable_selector.ex rename to lib/solver/search/variable_value/variable_selector.ex diff --git a/lib/solver/solution/solution.ex b/lib/solver/solution/solution.ex index 5e69654c..3b72e2d7 100644 --- a/lib/solver/solution/solution.ex +++ b/lib/solver/solution/solution.ex @@ -22,7 +22,7 @@ defmodule CPSolver.Solution do end end - def reconcile(solution, variables) do + defp reconcile(solution, variables) do ## We want to present a solution (which is var_name => value map) in order of initial variable names. solution |> Enum.sort_by(fn {var_name, _val} -> diff --git a/lib/solver/space/propagation.ex b/lib/solver/space/propagation.ex index a134e106..897288f5 100644 --- a/lib/solver/space/propagation.ex +++ b/lib/solver/space/propagation.ex @@ -1,79 +1,118 @@ defmodule CPSolver.Space.Propagation do alias CPSolver.Propagator.ConstraintGraph alias CPSolver.Propagator + import CPSolver.Common - def run(propagators, constraint_graph, store) when is_list(propagators) do - propagators - |> run_impl(constraint_graph, store) - |> finalize(propagators, store) + require Logger + + def run(constraint_graph, changes \\ %{}) + + def run(%Graph{} = constraint_graph, changes) do + constraint_graph + |> get_propagators() + |> then(fn propagators -> + run_impl(propagators, constraint_graph, propagator_changes(constraint_graph, changes), + reset?: true + ) + |> finalize(changes) + end) end - defp run_impl(propagators, constraint_graph, store) do - case propagate(propagators, constraint_graph, store) do + defp get_propagators(constraint_graph) do + constraint_graph + |> Graph.vertices() + ## Get %{id => propagator} map + |> Enum.flat_map(fn + {:propagator, p_id} -> + [ConstraintGraph.get_propagator(constraint_graph, p_id)] + + _ -> + [] + end) + end + + defp run_impl(propagators, constraint_graph, domain_changes, opts) do + case propagate(propagators, constraint_graph, domain_changes, opts) do :fail -> :fail - {scheduled_propagators, reduced_graph} -> + {scheduled_propagators, reduced_graph, new_domain_changes} -> (MapSet.size(scheduled_propagators) == 0 && reduced_graph) || - run_impl(scheduled_propagators, reduced_graph, store) + run_impl(scheduled_propagators, reduced_graph, new_domain_changes, reset?: false) end end - @spec propagate(map(), Graph.t(), map()) :: - :fail | {map(), Graph.t()} | {:changes, map()} + def propagate(propagators, graph) do + propagate(propagators, graph, []) + end + + def propagate(propagators, graph, opts) do + propagate(propagators, graph, Map.new(), opts) + end + + @spec propagate(map(), Graph.t(), map(), Keyword.t()) :: + :fail | {map(), Graph.t(), map()} @doc """ A single pass of propagation. Produces the list (up to implementation) of propagators scheduled for the next pass. Side effect: modifies the constraint graph. The graph will be modified on every individual Propagator.filter/1, if the latter results in any domain changes. """ - - def propagate(propagators, graph, store) when is_list(propagators) do + def propagate(propagators, graph, domain_changes, opts) when is_list(propagators) do propagators |> Map.new(fn p -> {p.id, p} end) - |> propagate(graph, store) + |> propagate(graph, domain_changes, opts) end - def propagate(%MapSet{} = propagator_ids, graph, store) do + def propagate(%MapSet{} = propagator_ids, graph, propagator_changes, opts) do Map.new(propagator_ids, fn p_id -> {p_id, ConstraintGraph.get_propagator(graph, p_id)} end) - |> propagate(graph, store) + |> propagate(graph, propagator_changes, opts) end - def propagate(propagators, graph, store) when is_map(propagators) do + def propagate(propagators, graph, propagator_changes, opts) when is_map(propagators) do propagators |> reorder() - |> Task.async_stream( - fn {p_id, p} -> - {p_id, Propagator.filter(p, store: store)} - end, - ## TODO: make it an option - ## - max_concurrency: 1 - ) - |> Enum.reduce_while({MapSet.new(), graph}, fn {:ok, {p_id, res}}, {scheduled, g} = _acc -> - case res do - :fail -> - {:halt, :fail} - - :stable -> - {:cont, {unschedule(scheduled, p_id), g}} - - %{changes: nil, active?: active?} -> - {:cont, {unschedule(scheduled, p_id), maybe_remove_propagator(g, p_id, active?)}} - - %{changes: changes} = filtering_results -> - case update_propagator(g, p_id, filtering_results) do - :fail -> - {:halt, :fail} - - updated_graph -> - {updated_graph, scheduled_by_propagator} = - schedule_by_propagator(changes, updated_graph) - - {:cont, {reschedule(scheduled, p_id, scheduled_by_propagator), updated_graph}} - end + |> Enum.reduce_while( + {MapSet.new(), graph, Map.new()}, + fn {p_id, p}, {scheduled_acc, g_acc, changes_acc} = _acc -> + res = + Propagator.filter(p, + reset?: opts[:reset?], + changes: Map.get(propagator_changes, p_id), + constraint_graph: graph + ) + + case res do + {:filter_error, error} -> + throw({:error, {:filter_error, error}}) + + :fail -> + {:halt, :fail} + + :stable -> + {:cont, {unschedule(scheduled_acc, p_id), g_acc, changes_acc}} + + %{changes: no_changes, active?: active?} when no_changes in [nil, %{}] -> + {:cont, + {unschedule(scheduled_acc, p_id), maybe_remove_propagator(g_acc, p_id, active?), + changes_acc}} + + %{changes: new_changes, active?: active?, state: state} -> + {updated_graph, updated_scheduled, updated_changes} = + update_schedule( + scheduled_acc, + changes_acc, + new_changes, + maybe_remove_propagator(g_acc, p_id, active?) + ) + + {:cont, + {updated_scheduled |> unschedule(p_id), + ConstraintGraph.update_propagator(updated_graph, p_id, Map.put(p, :state, state)), + updated_changes}} + end end - end) + ) end ## Note: we do not reschedule a propagator that was the source of domain changes, @@ -81,90 +120,54 @@ defmodule CPSolver.Space.Propagation do ## We will probably introduce the option to be used in propagator implementations ## to signify that the propagator is not idempotent. ## - defp reschedule(current_schedule, p_id, scheduled_by_propagator) do - current_schedule - |> MapSet.union(scheduled_by_propagator) - |> unschedule(p_id) - end - - ## Returns set of propagator ids scheduled as a result of domain changes. - defp schedule_by_propagator(domain_changes, graph) do - {updated_graph, scheduled_propagators} = - domain_changes - |> Enum.reduce({graph, MapSet.new()}, fn {var_id, domain_change}, {g, propagators} -> - {maybe_remove_variable(g, var_id, domain_change), - MapSet.union( - propagators, - MapSet.new(ConstraintGraph.get_propagator_ids(g, var_id, domain_change)) - )} - end) - - {updated_graph, scheduled_propagators} - end + defp update_schedule(current_schedule, current_changes, new_domain_changes, graph) do + {updated_graph, scheduled_propagators, cumulative_domain_changes} = + new_domain_changes + |> Enum.reduce( + {graph, current_schedule, current_changes}, + fn {var_id, domain_change} = change, {g_acc, propagators_acc, changes_acc} -> + propagator_ids = + ConstraintGraph.get_propagator_ids(g_acc, var_id, domain_change) - defp maybe_remove_propagator(graph, _propagator_id, _active?) do - ## TODO: reintroduce removing passive propagators - ## There is a bigger problem as to what to do with unfixed variables - ## when no active propagators left. - # (active? && graph) || ConstraintGraph.remove_propagator(graph, propagator_id) - graph - end + {maybe_remove_variable(g_acc, var_id, domain_change), + MapSet.union( + propagators_acc, + MapSet.new(Map.keys(propagator_ids)) + ), propagator_changes(propagator_ids, change, changes_acc)} + end + ) - ## Update propagator - ## Do not update if passive - defp update_propagator(graph, _propagator_id, %{active?: false} = _filtering_results) do - graph + {updated_graph, scheduled_propagators, cumulative_domain_changes} end - defp update_propagator(graph, propagator_id, %{changes: changes, active?: true, state: state}) do - case ConstraintGraph.get_propagator(graph, propagator_id) do - nil -> - graph - - p -> - Map.put(p, :state, state) - |> Propagator.update(changes) - |> then(fn updated_propagator -> - ConstraintGraph.update_propagator(graph, propagator_id, updated_propagator) - end) - end + ## Remove passive propagator + defp maybe_remove_propagator(graph, propagator_id, active?) do + (active? && graph) || ConstraintGraph.remove_propagator(graph, propagator_id) end - defp finalize(:fail, _propagators, _store) do + defp finalize(:fail, _changes) do :fail end ## At this point, the space is either solved or stable. - defp finalize(%Graph{} = residual_graph, propagators, store) do + defp finalize(%Graph{} = residual_graph, changes) do if Enum.empty?(Graph.edges(residual_graph)) do - (checkpoint(propagators, store) && :solved) || :fail + :solved else - {:stable, remove_entailed_propagators(residual_graph, propagators)} + residual_graph + |> remove_fixed_variables(changes) + |> then(fn g -> + if Enum.empty?(Graph.edges(g)) do + :solved + else + {:stable, g} + end + end) end end - defp checkpoint(propagators, store) do - Enum.reduce_while(propagators, true, fn p, acc -> - case Propagator.filter(p, store: store) do - :fail -> {:halt, false} - _ -> {:cont, acc} - end - end) - end - - defp remove_entailed_propagators(graph, propagators) do - Enum.reduce(propagators, graph, fn p, g -> - p_vertex = ConstraintGraph.propagator_vertex(p.id) - - case Graph.neighbors(g, p_vertex) do - [] -> ConstraintGraph.remove_propagator(g, p.id) - _connected_vars -> g - end - end) - end - defp maybe_remove_variable(graph, var_id, :fixed) do - ConstraintGraph.remove_variable(graph, var_id) + ConstraintGraph.disconnect_variable(graph, var_id) end defp maybe_remove_variable(graph, _var_id, _domain_change) do @@ -175,13 +178,48 @@ defmodule CPSolver.Space.Propagation do MapSet.delete(scheduled_propagators, p_id) end + defp remove_fixed_variables(graph, changes) do + Enum.reduce(changes, graph, fn {var_id, domain_change}, g_acc -> + domain_change == :fixed && + ConstraintGraph.disconnect_variable(g_acc, var_id) + || g_acc + end) + end + ## TODO: possible reordering strategy ## for the next pass. - ## Ideas: put to-be-entailed propagators first, + ## Ideas: + ## - Put to-be-entailed propagators first, ## so if they fail, it'd be early. - ## In general, arrange by the number of fixed variables? + ## - (extension of ^^) Order by the number of fixed variables ## defp reorder(propagators) do propagators end + + defp propagator_changes(%Graph{} = graph, domain_changes) when is_map(domain_changes) do + Enum.reduce(domain_changes, Map.new(), fn {var_id, domain_change} = change, changes_acc -> + graph + |> ConstraintGraph.get_propagator_ids(var_id, domain_change) + |> propagator_changes(change, changes_acc) + end) + end + + defp propagator_changes(propagator_ids, {var_id, domain_change} = _change, changes_acc) do + Enum.reduce( + propagator_ids, + changes_acc, + fn {p_id, _p_data}, acc -> + Map.update(acc, p_id, Map.new(%{var_id => domain_change}), fn var_map -> + current_var_change = Map.get(var_map, var_id) + + Map.put( + var_map, + var_id, + stronger_domain_change(current_var_change, domain_change) + ) + end) + end + ) + end end diff --git a/lib/solver/space/space.ex b/lib/solver/space/space.ex index 3ac03274..9cec8544 100644 --- a/lib/solver/space/space.ex +++ b/lib/solver/space/space.ex @@ -11,11 +11,13 @@ defmodule CPSolver.Space do alias CPSolver.Search.Strategy, as: Search alias CPSolver.Solution, as: Solution alias CPSolver.Propagator.ConstraintGraph + alias CPSolver.Propagator alias CPSolver.Space.Propagation alias CPSolver.Objective alias CPSolver.Shared alias CPSolver.Distributed + alias CPSolver.Utils require Logger @@ -26,7 +28,7 @@ defmodule CPSolver.Space do store_impl: CPSolver.ConstraintStore.default_store(), solution_handler: Solution.default_handler(), search: Search.default_strategy(), - max_space_threads: 8, + space_threads: :erlang.system_info(:logical_processors), postpone: false, distributed: false ] @@ -150,7 +152,10 @@ defmodule CPSolver.Space do space: self() ) - {constraint_graph, bound_propagators} = ConstraintGraph.update(graph, space_variables) + {constraint_graph, bound_propagators} = + graph + # |> apply_branch_constraint(space_opts[:branch_constraint]) + |> ConstraintGraph.update(space_variables) space_data = data @@ -160,6 +165,7 @@ defmodule CPSolver.Space do |> Map.put(:constraint_graph, constraint_graph) |> Map.put(:objective, update_objective(space_opts[:objective], space_variables)) |> Map.put(:propagators, bound_propagators) + |> Map.put(:changes, Keyword.get(space_opts, :branch_constraint, %{})) (space_opts[:postpone] && {:ok, space_data}) || {:ok, space_data, {:continue, :propagate}} @@ -183,25 +189,30 @@ defmodule CPSolver.Space do defp propagate( %{ - propagators: propagators, constraint_graph: constraint_graph, - store: store + changes: changes } = data ) do - case Propagation.run(propagators, constraint_graph, store) do - :fail -> - handle_failure(data) - - :solved -> - handle_solved(data) - - {:stable, reduced_constraint_graph} -> - %{ - data - | constraint_graph: reduced_constraint_graph - } - |> handle_stable() + try do + case Propagation.run(constraint_graph, changes) do + :fail -> + handle_failure(data) + + :solved -> + handle_solved(data) + + {:stable, reduced_constraint_graph} -> + Map.put( + data, + :constraint_graph, + reduced_constraint_graph + ) + |> handle_stable() + end + catch + {:error, error} -> + handle_error(error, data) end end @@ -210,26 +221,41 @@ defmodule CPSolver.Space do end defp handle_solved(data) do - data - |> solution() - |> then(fn - :fail -> - shutdown(data, :fail) + if checkpoint(data.propagators, data.constraint_graph) do + maybe_tighten_objective_bound(data[:objective]) - solution -> - maybe_tighten_objective_bound(data[:objective]) - Solution.run_handler(solution, data.opts[:solution_handler]) - shutdown(data, :solved) - end) + ## Generate solutions and run them through solution handler. + solutions(data) + + shutdown(data, :solved) + else + handle_failure(data) + end end - defp solution(%{variables: variables, store: store} = _data) do - Enum.reduce_while(variables, Map.new(), fn var, acc -> - case ConstraintStore.get(store, var, :min) do - :fail -> {:halt, :fail} - val -> {:cont, Map.put(acc, var.name, val)} - end - end) + defp handle_error(exception, data) do + Logger.error(inspect(exception)) + Shared.set_complete(shared(data)) + shutdown(data, :error) + end + + defp solutions(%{variables: variables} = data) do + try do + Enum.map(variables, fn var -> + Interface.domain(var) |> Domain.to_list() + end) + |> Utils.lazy_cartesian(fn values -> + Enum.reduce(values, {0, Map.new()}, fn val, {idx_acc, map_acc} -> + {idx_acc + 1, Map.put(map_acc, Arrays.get(variables, idx_acc).name, val)} + end) + |> elem(1) + |> Solution.run_handler(data.opts[:solution_handler]) + ## Stop producing solutions if the solving is complete + |> tap(fn _ -> CPSolver.complete?(shared(data)) && throw(:complete) end) + end) + catch + :complete -> :complete + end end defp maybe_tighten_objective_bound(nil) do @@ -245,15 +271,57 @@ defmodule CPSolver.Space do end defp update_objective(%{variable: variable} = objective, variables) do + updated_var = update_domain(variable, variables) + Map.put(objective, :variable, updated_var) + # objective + end + + defp update_domain(variable, space_variables) do var_domain = - Enum.at(variables, Interface.variable(variable).index - 1) + Arrays.get(space_variables, Interface.variable(variable).index - 1) |> Interface.domain() - updated_var = Interface.update(variable, :domain, var_domain) - Map.put(objective, :variable, updated_var) - # objective + Interface.update(variable, :domain, var_domain) end + def checkpoint(propagators, constraint_graph) do + Enum.reduce_while(propagators, true, fn p, acc -> + case Propagator.filter(p, reset?: true, constraint_graph: constraint_graph) do + :fail -> {:halt, false} + _ -> {:cont, acc} + end + end) + end + + + # defp add_branch_constraint(constraint_graph, nil) do + # constraint_graph + # end + + # defp add_branch_constraint(constraint_graph, _constraint) do + # [] + # constraint + # |> Constraint.constraint_to_propagators() + # |> IO.inspect() + # |> Enum.reduce(constraint_graph, fn propagator, graph_acc -> ConstraintGraph.add_propagator(graph_acc, propagator) end) + # constraint_graph + # |> tap(fn -> constraint.() end) + # end + + # defp add_branch_constraint(constraint_graph, nil) do + # constraint_graph + # end + + # defp add_branch_constraint(constraint_graph, _constraint) do + # [] + # constraint + # |> Constraint.constraint_to_propagators() + # |> IO.inspect() + # |> Enum.reduce(constraint_graph, fn propagator, graph_acc -> ConstraintGraph.add_propagator(graph_acc, propagator) end) + # constraint_graph + # |> tap(fn -> constraint.() end) + # end + defp handle_stable(data) do try do distribute(data) @@ -266,16 +334,21 @@ defmodule CPSolver.Space do def distribute( %{ opts: opts, - variables: variables + variables: variables, + constraint_graph: _graph } = data ) do ## The search strategy branches off the existing variables. ## Each branch is a list of variables to use by a child space branches = Search.branch(variables, opts[:search]) - Enum.take_while(branches, fn variable_copies -> + Enum.take_while(branches, fn {branch_variables, constraint} -> !CPSolver.complete?(shared(data)) && - spawn_space(data |> Map.put(:variables, variable_copies)) + spawn_space( + data + |> Map.put(:variables, branch_variables) + |> put_in([:opts, :branch_constraint], constraint) + ) end) shutdown(data, :distribute) diff --git a/lib/solver/store/store.ex b/lib/solver/store/store.ex index ef71d274..ec70608e 100644 --- a/lib/solver/store/store.ex +++ b/lib/solver/store/store.ex @@ -6,7 +6,6 @@ defmodule CPSolver.ConstraintStore do """ ################# alias CPSolver.{Common, Variable} - alias CPSolver.DefaultDomain, as: Domain require Logger @@ -86,11 +85,6 @@ defmodule CPSolver.ConstraintStore do def create_store(variables, opts \\ []) def create_store(variables, opts) do - variables = - Enum.map(variables, fn %{domain: d} = var -> - Map.put(var, :domain, (Domain.fixed?(d) && Domain.min(d)) || Domain.copy(d)) - end) - opts = Keyword.merge(default_store_opts(), opts) space = Keyword.get(opts, :space) store_impl = Keyword.get(opts, :store_impl) @@ -104,13 +98,17 @@ defmodule CPSolver.ConstraintStore do {:ok, variables - |> Enum.with_index(1) - |> Enum.map(fn {var, index} = _indexed_var -> - var - |> Map.put(:index, index) - |> Map.put(:name, var.name) - |> Map.put(:store, store) - end), store} + |> Enum.reduce({Arrays.new([], implementation: Aja.Vector), 1}, fn var, + {vars_acc, idx_acc} -> + updated_var = + var + |> Map.put(:index, idx_acc) + |> Map.put(:name, var.name) + |> Map.put(:store, store) + + {Arrays.append(vars_acc, updated_var), idx_acc + 1} + end) + |> elem(0), store} |> tap(fn _ -> set_store(store) end) end diff --git a/lib/solver/store/vector_store.ex b/lib/solver/store/vector_store.ex new file mode 100644 index 00000000..8af9ef56 --- /dev/null +++ b/lib/solver/store/vector_store.ex @@ -0,0 +1,106 @@ +defmodule CPSolver.Store.Vector do + alias CPSolver.DefaultDomain, as: Domain + + use CPSolver.ConstraintStore + + @impl true + def create(variables, opts \\ []) + + def create(variables, _opts) when is_list(variables) do + {:ok, + Enum.reduce(variables, {Arrays.new([], implementation: Aja.Vector), 1}, fn var, + {vector_acc, + idx_acc} -> + {Arrays.append(vector_acc, Map.put(var, :index, idx_acc)), idx_acc + 1} + end) + |> elem(0)} + end + + def create(variables, _opts) do + {:ok, variables} + end + + @impl true + def dispose(_store, _vars) do + :ok + end + + @impl true + def get(store, variable, operation, args \\ []) do + handle_request(:get, store, variable, operation, args) + end + + @impl true + def update_domain(store, variable, operation, args) do + handle_request(:update, store, variable, operation, args) + end + + @impl true + def on_change(_store, _variable, _change) do + :ok + end + + @impl true + def on_fix(_store, _variable, _value) do + :ok + end + + @impl true + def on_no_change(_store, _variable) do + :ok + end + + @impl true + def on_fail(_store, _variable) do + :ok + end + + defp update_variable_domain( + _table, + _variable, + _domain, + event + ) do + event + end + + @impl true + def domain(vector, variable) do + vector + |> lookup(variable) + |> Map.get(:domain) + end + + def lookup(vector, %{index: index} = _variable) do + lookup(vector, index) + end + + def lookup(vector, index) do + Arrays.get(vector, index - 1) + end + + defp handle_request(kind, vector, var_id, operation, args) do + variable = lookup(vector, var_id) + handle_request_impl(kind, vector, variable, operation, args) + end + + def handle_request_impl(:get, _vector, %{domain: domain} = _variable, operation, args) do + apply(Domain, operation, [domain | args]) + end + + def handle_request_impl(:update, vector, %{domain: domain} = variable, operation, args) do + case apply(Domain, operation, [domain | args]) do + :fail -> + update_variable_domain(vector, variable, :fail, :fail) + + :no_change -> + :no_change + + :fixed -> + update_variable_domain(vector, variable, domain, :fixed) + + {domain_change, new_domain} -> + update_variable_domain(vector, variable, new_domain, domain_change) + end + end +end diff --git a/lib/solver/variables/api_impl.ex b/lib/solver/variables/api_impl.ex index 651e6f1a..bd506693 100644 --- a/lib/solver/variables/api_impl.ex +++ b/lib/solver/variables/api_impl.ex @@ -2,6 +2,7 @@ alias CPSolver.Variable.Interface alias CPSolver.Variable alias CPSolver.Variable.View +alias CPSolver.DefaultDomain, as: Domain defimpl Interface, for: Variable do def id(var), do: var.id @@ -44,24 +45,41 @@ defimpl Interface, for: View do end end +defimpl Interface, for: Integer do + def variable(_any), do: nil + def id(_val), do: nil + defdelegate map(val, mapper), to: Domain + def domain(val), do: val + defdelegate size(val), to: Domain + defdelegate min(val), to: Domain + defdelegate max(val), to: Domain + defdelegate fixed?(val), to: Domain + defdelegate contains?(val, value), to: Domain + defdelegate remove(val, remove_val), to: Domain + defdelegate removeAbove(val, removeAbove), to: Domain + defdelegate removeBelow(val, removeBelow), to: Domain + defdelegate fix(value, fixed_value), to: Domain + def update(val, _field, _value), do: val +end + defimpl Interface, for: Any do def variable(_any), do: nil - def id(var), do: not_supported(:id, var) - def map(var, _value), do: var - def domain(var), do: not_supported(:domain, var) - def size(var), do: not_supported(:size, var) - def min(var), do: not_supported(:min, var) - def max(var), do: not_supported(:max, var) - def fixed?(var), do: not_supported(:fixed?, var) - def contains?(var, _val), do: not_supported(:contains, var) - def remove(var, _val), do: not_supported(:remove, var) - def removeAbove(var, _val), do: not_supported(:removeAbove, var) - def removeBelow(var, _val), do: not_supported(:removeBelow, var) - def fix(var, _val), do: not_supported(:fix, var) - def update(var, _field, _value), do: not_supported(:update, var) + def id(non_var), do: not_supported(:id, non_var) + def map(non_var, _value), do: non_var + def domain(non_var), do: not_supported(:domain, non_var) + def size(non_var), do: not_supported(:size, non_var) + def min(non_var), do: not_supported(:min, non_var) + def max(non_var), do: not_supported(:max, non_var) + def fixed?(_), do: true + def contains?(non_var, _val), do: not_supported(:contains, non_var) + def remove(non_var, _val), do: not_supported(:remove, non_var) + def removeAbove(non_var, _val), do: not_supported(:removeAbove, non_var) + def removeBelow(non_var, _val), do: not_supported(:removeBelow, non_var) + def fix(non_var, _val), do: not_supported(:fix, non_var) + def update(non_var, _field, _value), do: not_supported(:update, non_var) - defp not_supported(var, op) do - throw({:operation_not_supported, op, for: var}) + defp not_supported(non_var, op) do + throw({:operation_not_supported, op, for: non_var}) end end diff --git a/lib/solver/variables/bool_variable.ex b/lib/solver/variables/bool_variable.ex new file mode 100644 index 00000000..89104d14 --- /dev/null +++ b/lib/solver/variables/bool_variable.ex @@ -0,0 +1,28 @@ +defmodule CPSolver.BooleanVariable do + alias CPSolver.IntVariable + alias CPSolver.Variable.Interface + + def new(opts \\ []) do + IntVariable.new(0..1, opts) + end + + def set_false(var) do + Interface.fix(var, 0) + end + + def set_true(var) do + Interface.fix(var, 1) + end + + def true?(var) do + fixed?(var, 1) + end + + def false?(var) do + fixed?(var, 0) + end + + def fixed?(var, val) do + Interface.fixed?(var) && Interface.min(var) == val + end +end diff --git a/lib/solver/variables/int_variable.ex b/lib/solver/variables/int_variable.ex index dbd92a05..d661ca27 100644 --- a/lib/solver/variables/int_variable.ex +++ b/lib/solver/variables/int_variable.ex @@ -13,4 +13,8 @@ defmodule CPSolver.IntVariable do defdelegate removeAbove(var, val), to: Variable defdelegate removeBelow(var, val), to: Variable defdelegate fix(var, val), to: Variable + + def to_variable(arg) do + (is_integer(arg) && new(arg)) || arg + end end diff --git a/lib/solver/variables/variable.ex b/lib/solver/variables/variable.ex index ea4e1d31..1e6ebf2b 100644 --- a/lib/solver/variables/variable.ex +++ b/lib/solver/variables/variable.ex @@ -92,23 +92,26 @@ defmodule CPSolver.Variable do store_op(op, variable, value) end - defp store_op(op, %{store: store} = variable, value) + defp store_op(op, %{store: store, domain: domain} = variable, value) when op in [:remove, :removeAbove, :removeBelow, :fix] do - ConstraintStore.update(store, variable, op, [value]) + (domain && apply(Domain, op, [domain, value]) |> normalize_update_result()) || + ConstraintStore.update(store, variable, op, [value]) end - defp store_op(op, %{store: store} = variable, value) + defp store_op(op, %{store: store, domain: domain} = variable, value) when op in [:contains?] do - ConstraintStore.get(store, variable, op, [value]) + (domain && Domain.contains?(domain, value)) || + ConstraintStore.get(store, variable, op, [value]) end defp store_op(op, %View{variable: variable}) do store_op(op, variable) end - defp store_op(op, %{store: store} = variable) + defp store_op(op, %{store: store, domain: domain} = variable) when op in [:size, :fixed?, :min, :max] do - ConstraintStore.get(store, variable, op) + (domain && apply(Domain, op, [domain])) || + ConstraintStore.get(store, variable, op) end defp store_op(:domain, %{store: nil, domain: domain}) when not is_nil(domain) do @@ -118,4 +121,9 @@ defmodule CPSolver.Variable do defp store_op(:domain, %{store: store} = variable) do ConstraintStore.domain(store, variable) end + + defp normalize_update_result({change, _}), do: change + + defp normalize_update_result(change), do: change + end diff --git a/lib/solver/variables/view.ex b/lib/solver/variables/view.ex index dc63068a..6276744c 100644 --- a/lib/solver/variables/view.ex +++ b/lib/solver/variables/view.ex @@ -1,4 +1,8 @@ defmodule CPSolver.Variable.View do + @moduledoc """ + View is a variable with attached `mapper` function. + `mapper` is a bijection of the domain of original variable to the domain of the view + """ alias CPSolver.Variable alias CPSolver.DefaultDomain, as: Domain alias __MODULE__, as: View @@ -15,15 +19,23 @@ defmodule CPSolver.Variable.View do returns nil if there is no mapping. """ @spec new(Variable.t(), neg_integer() | pos_integer(), integer()) :: View.t() - def new(variable, a, b) do - mapper_fun = fn + def new(variable, a, b) when is_struct(variable, Variable) do + %View{variable: variable, mapper: make_mapper_fun(a, b)} + end + + def new(%{mapper: mapper} = view, a, b) when is_struct(view, View) do + Map.put(view, :mapper, chained_mapper(a, b, mapper)) + end + + defp make_mapper_fun(a, b) do + fn ## Given value from variable domain, returns mapped value from view domain value when is_integer(value) -> a * value + b ## Given value from view domain, returns mapped value from variable domain, ## or nil, if no mapping exists. - {value, :reverse} when is_integer(value) -> + {value, :inverse} when is_integer(value) -> (rem(value - b, a) == 0 && div(value - b, a)) || nil ## (Used by removeAbove and removeBelow operations) @@ -44,9 +56,16 @@ defmodule CPSolver.Variable.View do ## Used by min and max to decide if the operation has to be flipped :flip? -> a < 0 + + :get_params -> + {a, b} end + end - %View{variable: variable, mapper: mapper_fun} + defp chained_mapper(a, b, mapper) + when is_integer(a) and is_integer(b) and is_function(mapper) do + {current_a, current_b} = mapper.(:get_params) + make_mapper_fun(a * current_a, a * current_b + b) end def domain(%{mapper: mapper_fun, variable: variable} = _view) do @@ -73,17 +92,17 @@ defmodule CPSolver.Variable.View do end def contains?(%{mapper: mapper_fun, variable: variable} = _view, value) do - source_value = mapper_fun.({value, :reverse}) + source_value = mapper_fun.({value, :inverse}) source_value && Variable.contains?(variable, source_value) end def remove(%{mapper: mapper_fun, variable: variable} = _view, value) do - source_value = mapper_fun.({value, :reverse}) + source_value = mapper_fun.({value, :inverse}) (source_value && Variable.remove(variable, source_value)) || :no_change end def fix(%{mapper: mapper_fun, variable: variable} = _view, value) do - source_value = mapper_fun.({value, :reverse}) + source_value = mapper_fun.({value, :inverse}) (source_value && Variable.fix(variable, source_value)) || :fail end @@ -100,24 +119,31 @@ end defmodule CPSolver.Variable.View.Factory do import CPSolver.Variable.View - alias CPSolver.Variable alias CPSolver.IntVariable - def minus(%Variable{} = var) do + def minus(var) do mul(var, -1) end - def mul(%Variable{} = var, coefficient) do + def mul(var, coefficient) do linear(var, coefficient, 0) end + def inc(var, c) when is_integer(c) do + linear(var, 1, c) + end + def linear(_var, 0, offset) do IntVariable.new(offset) end - def linear(%Variable{} = var, coefficient, offset) + def linear(var, coefficient, offset) when is_integer(coefficient) and is_integer(offset) do new(var, coefficient, offset) end + + def negation(var) do + linear(var, -1, 1) + end end diff --git a/lib/utils/tuple_array.ex b/lib/utils/tuple_array.ex new file mode 100644 index 00000000..9c60821f --- /dev/null +++ b/lib/utils/tuple_array.ex @@ -0,0 +1,38 @@ +defmodule CPSolver.Utils.TupleArray do + def new(values) when is_list(values) do + Enum.reduce(values, {}, fn + val, acc when is_list(val) -> + Tuple.append(acc, new(val)) + + val, acc -> + Tuple.append(acc, val) + end) + end + + def at(tuple_array, idx) when is_integer(idx) do + (idx >= 0 && tuple_size(tuple_array) - 1 >= idx && + elem(tuple_array, idx)) || + nil + end + + def at(tuple_array, []) do + tuple_array + end + + def at(tuple_array, [idx | rest]) do + at(tuple_array, idx) + |> then(fn sub_arr -> (sub_arr && at(sub_arr, rest)) || nil end) + end + + def map(tuple_array, mapper) when is_function(mapper) do + Enum.reduce(0..(tuple_size(tuple_array) - 1), {}, fn idx, acc -> + Tuple.append(acc, mapper.(elem(tuple_array, idx))) + end) + end + + def reduce(tuple_array, initial_value, reducer) when is_function(reducer) do + Enum.reduce(0..(tuple_size(tuple_array) - 1), initial_value, fn idx, acc -> + reducer.(elem(tuple_array, idx), acc) + end) + end +end diff --git a/lib/utils/utils.ex b/lib/utils/utils.ex index 1e56cb4e..a61e0027 100644 --- a/lib/utils/utils.ex +++ b/lib/utils/utils.ex @@ -1,4 +1,7 @@ defmodule CPSolver.Utils do + alias CPSolver.Variable.Interface + alias CPSolver.DefaultDomain, as: Domain + def on_primary_node?(arg) when is_reference(arg) or is_pid(arg) or is_port(arg) do Node.self() == node(arg) end @@ -19,4 +22,44 @@ defmodule CPSolver.Utils do end end end + + ## Cartesian product of list of lists + def cartesian([h]) do + for i <- h do + [i] + end + end + + def cartesian([h | t] = _values, handler \\ nil) do + for i <- h, j <- cartesian(t) do + [i | j] + |> tap(fn res -> handler && handler.(res) end) + end + end + + def lazy_cartesian(lists, callback \\ &Function.identity/1) do + lazy_cartesian(lists, callback, []) + end + + def lazy_cartesian([head | rest] = _lists, callback, values) do + Enum.map(head, fn i -> + more_values = [i | values] + if !Enum.empty?(rest) do + lazy_cartesian(rest, callback, more_values) + else + callback && callback.(Enum.reverse(more_values)) + end + end) + + end + + + + + + def domain_values(variable_or_view) do + variable_or_view + |> Interface.domain() + |> Domain.to_list() + end end diff --git a/livebooks/fixpoint.livemd b/livebooks/fixpoint.livemd index 27cb158d..7f153dcf 100644 --- a/livebooks/fixpoint.livemd +++ b/livebooks/fixpoint.livemd @@ -2,9 +2,9 @@ ```elixir Mix.install([ - # {:fixpoint, path: "/Users/bokner/projects/cpsolver"}, - :fixpoint, - {:kino, "~> 0.10.0"} + {:fixpoint, path: "/Users/bokner/projects/cpsolver"}, + # :fixpoint, + :kino ]) Logger.configure(level: :notice) @@ -162,28 +162,43 @@ To make it more concrete, we will solve some combinatorial problems using ***Fix ```elixir alias CPSolver.Examples.Queens n = 8 +timeout = 180_000 {:ok, res} = - CPSolver.solve_sync(Queens.model(n), - search: {:first_fail, :indomain_random}, + CPSolver.solve_sync( + Queens.model(n, + symmetry: :half_symmetry + ), + search: {:input_order, :indomain_random}, stop_on: {:max_solutions, 1}, - timeout: 60_000 + timeout: timeout, + space_threads: 4 ) -IO.puts("Solved in #{div(res.statistics.elapsed_time, 1000)} \u33b3") - if res.status == :unsatisfiable do IO.puts(IO.ANSI.red() <> "There is no solution for n = #{n}!" <> IO.ANSI.reset()) else - solution = hd(res.solutions) + solution = List.first(res.solutions) - IO.puts( - (Queens.check_solution(solution) && - IO.ANSI.green() <> - "Solution is correct!") || IO.ANSI.red() <> "Solution is wrong!" <> IO.ANSI.reset() - ) + if solution do + IO.puts( + ((Queens.check_solution(solution) && + IO.ANSI.green() <> + "Solution is correct!") || IO.ANSI.red() <> "Solution is wrong!") <> IO.ANSI.reset() + ) + + IO.puts("Solved in #{div(res.statistics.elapsed_time, 1000)} \u33b3") + else + IO.puts("No solution was found within #{timeout} \u33b3") + end + + view_limit = 20 - RenderHTML.render_nqueens(solution) + if n <= view_limit do + RenderHTML.render_nqueens(solution) + else + IO.puts("Chess board will only be shown for n <= #{view_limit}") + end end ``` @@ -197,7 +212,8 @@ The instance below is taken from http://www.tellmehowto.net/sudoku/veryhardsudok ```elixir alias CPSolver.Examples.Sudoku -hard_puzzle = Sudoku.puzzles().hard9x9 +puzzles = Sudoku.puzzles() +hard_puzzle = puzzles.hard9x9 RenderHTML.render_sudoku(hard_puzzle) ``` @@ -206,7 +222,7 @@ RenderHTML.render_sudoku(hard_puzzle) ```elixir {:ok, res} = CPSolver.solve_sync(Sudoku.model(hard_puzzle), - max_space_threads: 12, + space_threads: 8, stop_on: {:max_solutions, 1}, search: {:first_fail, :indomain_random} ) @@ -312,8 +328,7 @@ So the encoding of the rules does not require any programming except wiring rule ```elixir alias CPSolver.Examples.Reindeers -{:ok, res} = CPSolver.solve_sync(Reindeers.model()) -IO.puts("Solved in #{div(res.statistics.elapsed_time, 1000)} \u33b3") +{:ok, res} = Reindeers.solve() ``` ## SEND + MORE = MONEY @@ -397,8 +412,6 @@ sorted_by_value = Enum.sort_by(items, fn {_name, _weight, value} -> value end, : {[n | n_acc], [w | w_acc], [v | v_acc]} end) -model = Knapsack.model(values, weights, capacity) - :ok ``` @@ -429,11 +442,12 @@ first before deciding not to. Overall, our search strategy translates to "pack the items with higher values first". ```elixir +model = Knapsack.model(values, weights, capacity) search_strategy = {:input_order, :indomain_max} {:ok, res} = CPSolver.solve_sync(model, - max_space_threads: 8, + space_threads: 8, timeout: 1_000, search: search_strategy ) @@ -466,6 +480,8 @@ IO.puts("Total weight: #{IO.ANSI.red()}#{total_weight}/#{capacity}#{IO.ANSI.rese IO.puts("Solved in: #{div(res.statistics.elapsed_time, 1000)} \u33b3") ``` +## Benchmarking (local) + ```elixir IEx.Helpers.c("scripts/sudoku_benchmark.exs") ``` @@ -477,8 +493,12 @@ alias CPSolver.Examples.Sudoku res = SudokuBenchmark.run("data/sudoku/hardest", 100, 12, 30_000) |> Enum.map(fn s -> - !(s.solutions |> List.first() |> then(fn sol -> sol && Sudoku.check_solution(sol) end)) && - Logger.error("Wrong solution!") + s.solutions + |> List.first() + |> tap(fn sol -> + (sol && Sudoku.check_solution(sol) && Logger.notice("OK")) || + Logger.error("Wrong solution!") + end) s.statistics.elapsed_time end) diff --git a/livebooks/xkcd_np.livemd b/livebooks/xkcd_np.livemd index 111581c1..532c5b89 100644 --- a/livebooks/xkcd_np.livemd +++ b/livebooks/xkcd_np.livemd @@ -2,8 +2,7 @@ ```elixir Mix.install([ - {:fixpoint, path: "/Users/bokner/projects/cpsolver"}, - # :fixpoint, + :fixpoint, {:kino, "~> 0.10.0"} ]) @@ -127,11 +126,10 @@ defmodule XKCD.NP.Appetizers do Enum.zip(quantities, appetizers) |> Enum.map(fn {q_var, {_name, price}} -> mul(q_var, price) end) - total_price = Variable.new(total, name: "total_price") Model.new( quantities, - [Sum.new(total_price, priced_quantities)] + [Sum.new(total, priced_quantities)] ) end @@ -139,7 +137,7 @@ defmodule XKCD.NP.Appetizers do (Enum.map_join(solver_results.solutions, "\n OR \n", fn sol -> sol |> Enum.zip(solver_results.variables) - |> Enum.reject(fn {q, _name} -> q == 0 end) + |> Enum.reject(fn {q, name} -> q == 0 || is_reference(name) end) |> Enum.map_join(", ", fn {q, name} -> IO.ANSI.red() <> "#{name} : #{IO.ANSI.blue()}#{q}" end) @@ -177,12 +175,12 @@ import ServingTablesHelpers tables = ["Table1", "Table2", "Table3", "Table4", "Table5", "Table6", "Table7"] -table_coordinates = [{4, 7}, {5, 5}, {7, 2}, {1, 5}, {1, 1}, {8, 6}, {11, 2}] -# [{2, 2}, {2, 10}, {4, 6}, {6, 12}, {8, 4}, {8, 8}, {12, 4}, {12, 8}] +table_coordinates = [{4, 7}, {5, 5}, {7, 2}, {1, 5}, {1, 1}, {8, 4}, {11, 5}] + distances = distances_from_coordinates(table_coordinates) model = TSP.model(distances) -{:ok, result} = CPSolver.solve_sync(model, search: TSP.search(model)) +{:ok, result} = CPSolver.solve_sync(model, search: TSP.search(model), space_threads: 8) optimal_solution = result.solutions |> List.last() @@ -192,6 +190,9 @@ visualize_route(optimal_route, distances, tables, table_coordinates) ``` ```elixir -{result.objective, result.statistics.elapsed_time, result.solutions, optimal_solution} -# TSP.check_solution(optimal_solution, model) +result.solutions +``` + +```elixir +{result.statistics.elapsed_time, optimal_route} ``` diff --git a/mix.exs b/mix.exs index 6f192390..3db55ad2 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule CPSolver.MixProject do def project do [ app: :fixpoint, - version: "0.8.21", + version: "0.8.48", elixir: "~> 1.15", start_permanent: Mix.env() == :prod, deps: deps(), @@ -27,6 +27,8 @@ defmodule CPSolver.MixProject do defp deps do [ {:libgraph, "~> 0.16.0"}, + {:arrays, "~> 2.1"}, + {:arrays_aja, "~> 0.2.0"}, {:math, "~> 0.7.0", only: :test}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:dialyxir, "~> 1.2", only: [:dev], runtime: false}, @@ -57,8 +59,9 @@ defmodule CPSolver.MixProject do # These are the default files included in the package files: ~w(lib src test data .formatter.exs mix.exs README* LICENSE* ), + exclude_patterns: ["misc/**", "scripts/**", "**/._exs"], licenses: ["MIT"], - links: %{"GitHub" => "https://github.com/bokner/cpsolver"} + links: %{"GitHub" => "https://github.com/bokner/fixpoint"} ] end end diff --git a/mix.lock b/mix.lock index d7d61fec..410d3d00 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,28 @@ %{ - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "aja": {:hex, :aja, "0.6.5", "e780fce3de247d86bb25097726021c5c53ebe383300290e26c61e4d36bfe85e8", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "cd377ac20f487dd7987be13772c37eb2f42ab038c0654714a1ecb3607f9e8590"}, + "arrays": {:hex, :arrays, "2.1.1", "4c495cddeb5d31814a739e79db42d012e62ec033902597b7b930c221cc8a3fca", [:mix], [{:extractable, "~> 1.0", [hex: :extractable, repo: "hexpm", optional: false]}, {:fun_land, "~> 0.10.0", [hex: :fun_land, repo: "hexpm", optional: true]}, {:insertable, "~> 1.0", [hex: :insertable, repo: "hexpm", optional: false]}], "hexpm", "f6c03081303e3240267e048f44f821305100ae824030208131b30ae5a681f33a"}, + "arrays_aja": {:hex, :arrays_aja, "0.2.0", "36331c03b022a6c8e76fe7aa64d7bb9a3ff2ce9dfd3b510ca03515a4fe8201f6", [:mix], [{:aja, "~> 0.6.0", [hex: :aja, repo: "hexpm", optional: false]}, {:arrays, "~> 2.0", [hex: :arrays, repo: "hexpm", optional: false]}], "hexpm", "89f5b01904ca0ca10bc44da6c827fb3f5dabaf0e5a026f33a822f60bc8291ac5"}, + "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, + "currying": {:hex, :currying, "1.0.3", "6d036c86c38663a795858c9e744e406a62d7e5a66243a7da15326e731cdcd672", [:mix], [], "hexpm", "7b9b8cd644ecac635f0abf6267ca46d497e73144a5882449a843461a014f1154"}, + "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "erlang_term": {:hex, :erlang_term, "2.0.7", "8fc08465432667e1c8d00b75dafa42e97b053c2c44aaa140ac0915c160d78bd7", [:rebar3], [], "hexpm", "29b2b5cd47df681e42f0f11ad4680f2bb7db2f7b871b074dcf3fba75219be9f9"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "ex_united": {:hex, :ex_united, "0.1.5", "48941f1ad77414b7d41e56b39fa82f0205cec2b99c48471fc2c4116cb21962c4", [:mix], [], "hexpm", "4aaec070d88b083c5de324922754609eb369f1643a841bb3b057c06fb152544e"}, + "extractable": {:hex, :extractable, "1.0.1", "2cf2b213cbda82eb578c821de48fa313cddfa7573b256bce4306248f56b44509", [:mix], [{:type_check, "~> 0.8", [hex: :type_check, repo: "hexpm", optional: false]}], "hexpm", "820e9db7550a62c7d63b760a8da51ab2341ba425563955db005baadcb403b046"}, + "fun_land": {:hex, :fun_land, "0.10.0", "edd9cc1907961048c7e87c85f9868956d22857557d2d52ee8e0d3997587e33a8", [:mix], [{:currying, "~> 1.0", [hex: :currying, repo: "hexpm", optional: false]}, {:numbers, "~> 5.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "0b52048e9de50b65805a7ed9699562fb16d9f253b5c708329b6d8e45eb3d5eb7"}, "global_flags": {:hex, :global_flags, "1.0.0", "ee6b864979a1fb38d1fbc67838565644baf632212bce864adca21042df036433", [:rebar3], [], "hexpm", "85d944cecd0f8f96b20ce70b5b16ebccedfcd25e744376b131e89ce61ba93176"}, + "insertable": {:hex, :insertable, "1.0.0", "879c7023b5491bbb694dff78422c94faded616fda963e529916343bc6e35162f", [:mix], [{:type_check, "~> 0.8", [hex: :type_check, repo: "hexpm", optional: false]}], "hexpm", "4b3f60169221a1ace67fdfe8b0d3c35708c098e55df549d4a374b9a7b0255af4"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "local_cluster": {:hex, :local_cluster, "1.2.1", "8eab3b8a387680f0872eacfb1a8bd5a91cb1d4d61256eec6a655b07ac7030c73", [:mix], [{:global_flags, "~> 1.0", [hex: :global_flags, repo: "hexpm", optional: false]}], "hexpm", "aae80c9bc92c911cb0be085fdeea2a9f5b88f81b6bec2ff1fec244bb0acc232c"}, - "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "math": {:hex, :math, "0.7.0", "12af548c3892abf939a2e242216c3e7cbfb65b9b2fe0d872d05c6fb609f8127b", [:mix], [], "hexpm", "7987af97a0c6b58ad9db43eb5252a49fc1dfe1f6d98f17da9282e297f594ebc2"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "redbug_clone": {:hex, :redbug_clone, "2.0.10", "786c766accaa660089760025faf06a6acb4e3517f2c6fc9a34d698fe38217c64", [:rebar3], [], "hexpm", "7ad11db975aa5547b81101a6628bb9defb46e7f274861fbd37fd8d85dc03f6ee"}, "replbug": {:hex, :replbug, "1.0.2", "5dae87d9839d9821ad5452a7c29f39259c92f7d561f3e5f8dfff930528ece47f", [:mix], [{:erlang_term, "~> 2.0", [hex: :erlang_term, repo: "hexpm", optional: false]}, {:redbug_clone, "~> 2.0.10", [hex: :redbug_clone, repo: "hexpm", optional: false]}], "hexpm", "8e0d8066f2cd776f3efdd51918152ab54c4bbf1f975d6e8d25ff3db0de5ed2e6"}, + "type_check": {:hex, :type_check, "0.13.5", "b41ce95808546e9913d7d56d0886ae8298c4298c9ff596fd7987c5000a2d2316", [:mix], [{:credo, "~> 1.5", [hex: :credo, repo: "hexpm", optional: true]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "6196910e3cec69b4e2cec71f8d9f243532aeeb37924a86e8256bc5ee05b3fd00"}, } diff --git a/papers/gac-alldifferent.pdf b/papers/gac-alldifferent.pdf new file mode 100644 index 00000000..7102295e Binary files /dev/null and b/papers/gac-alldifferent.pdf differ diff --git a/scripts/bitvector_debug.exs b/scripts/bitvector_debug.exs new file mode 100644 index 00000000..9f33e369 --- /dev/null +++ b/scripts/bitvector_debug.exs @@ -0,0 +1,25 @@ +defmodule DebugBitVector do + alias CPSolver.BitVectorDomain, as: Domain + + def build_domain(data) do + ref = :atomics.new(length(data.raw.content), [{:signed, false}]) + + Enum.each(Enum.with_index(data.raw.content, 1), fn {val, idx} -> + :atomics.put(ref, idx, val) + end) + + bit_vector = {:bit_vector, data.raw.offset, ref} + domain = {bit_vector, data.raw.offset} + end +end + +data = %{ + max: 76, + min: 14, + raw: %{offset: -11, content: [4_503_599_644_147_720, 2, 279_172_874_243]}, + size: 4, + remove: 76, + values: [76, 63, 35, 14], + failed?: false, + fixed?: false +} diff --git a/scripts/constraint.exs b/scripts/constraint.exs new file mode 100644 index 00000000..eef78efc --- /dev/null +++ b/scripts/constraint.exs @@ -0,0 +1,11 @@ +alias CPSolver.Constraint +alias CPSolver.Constraint.Less +alias CPSolver.IntVariable, as: Variable +alias CPSolver.ConstraintStore + +x = Variable.new(1..2, name: "x") +y = Variable.new(1..2, name: "y") + +{:ok, [x_var, y_var] = bound_vars, store} = ConstraintStore.create_store([x, y]) + +Constraint.post(Less.new(x_var, y_var)) diff --git a/scripts/debug_circuit.exs b/scripts/debug_circuit.exs new file mode 100644 index 00000000..08a4de51 --- /dev/null +++ b/scripts/debug_circuit.exs @@ -0,0 +1,26 @@ +defmodule DebugCircuit do + alias CPSolver.ConstraintStore + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Variable.Interface + alias CPSolver.DefaultDomain, as: Domain + alias CPSolver.Propagator + alias CPSolver.Propagator.Circuit + + def run(domains) do + end + + def filter(domains) do + domains + |> make_propagator() + |> Propagator.filter() + end + + def make_propagator(domains) do + variables = + Enum.map(Enum.with_index(domains), fn {d, idx} -> Variable.new(d, name: "x#{idx}") end) + + {:ok, x_vars, _store} = ConstraintStore.create_store(variables) + + Circuit.new(x_vars) + end +end diff --git a/scripts/fwc_script_debug.exs b/scripts/fwc_script_debug.exs new file mode 100644 index 00000000..0e355c23 --- /dev/null +++ b/scripts/fwc_script_debug.exs @@ -0,0 +1,152 @@ +defmodule FWCDebug do + # alias CPSolver.ConstraintStore + alias CPSolver.IntVariable, as: Variable + # alias CPSolver.Variable.Interface + # alias CPSolver.Propagator + alias CPSolver.Constraint + # alias CPSolver.Propagator.AllDifferent.FWC + alias CPSolver.Constraint.AllDifferent.FWC, as: FWC_Constraint + alias CPSolver.Model + import CPSolver.Variable.View.Factory + + # {:ok, x_vars, _store} = ConstraintStore.create_store(x) + + ## Initial state + ## + # row_propagator = FWC.new(x_vars) + + # indexed_q = Enum.with_index(x_vars, 1) + + # diagonal_down = Enum.map(indexed_q, fn {var, idx} -> linear(var, 1, -idx) end) + # diagonal_down_propagator = FWC.new(diagonal_down) + + # diagonal_up = Enum.map(indexed_q, fn {var, idx} -> linear(var, 1, idx) end) + # diagonal_up_propagator = FWC.new(diagonal_up) + + # propagators = [row_propagator, diagonal_down_propagator, diagonal_up_propagator] + # order = Enum.shuffle(1..3) + + # filtering_results = + # Enum.map(order, fn i -> Enum.at(propagators, i-1) end) |> Enum.map(fn p -> Propagator.filter(p) end) + + # Enum.map(x_vars, fn v -> Interface.domain(v) end) + def debug(x) do + row_constraint = Constraint.new(FWC_Constraint, x) + indexed_x = Enum.with_index(x, 1) + diagonal_down_views = Enum.map(indexed_x, fn {var, idx} -> linear(var, 1, idx) end) + diagonal_down_constraint = Constraint.new(FWC_Constraint, diagonal_down_views) + diagonal_up_views = Enum.map(indexed_x, fn {var, idx} -> linear(var, 1, -idx) end) + + diagonal_up_constraint = Constraint.new(FWC_Constraint, diagonal_up_views) + + order = Enum.shuffle(1..3) + constraint_names = [:row, :down, :up] + + all_constraints = [row_constraint, diagonal_down_constraint, diagonal_up_constraint] + + model = + Model.new( + x, + Enum.map(order, fn ord -> Enum.at(all_constraints, ord - 1) end) + ) + + {:ok, res} = CPSolver.solve_sync(model) + res |> Map.put(:order, Enum.zip(order, constraint_names)) + end + + def trace(x, patterns) do + Replbug.start(patterns, + time: :timer.seconds(10), + msgs: 100_000, + max_queue: 100_000, + silent: true + ) + + Process.sleep(50) + res = debug(x) + Process.sleep(100) + traces = Replbug.stop() + res.status != :unsatisfiable && %{result: res, traces: traces} + end + + def trace(x, patterns, n) do + Enum.reduce_while(1..n, nil, fn _, _acc -> + res = trace(x, patterns) + (res && {:halt, res}) || {:cont, nil} + end) + end +end + +""" +x = +Enum.map( + [ + {"row1", 5}, + {"row2", 1}, + {"row3", [4]}, + {"row4", [4, 6]}, + {"row5", [2, 3, 6]}, + {"row6", [2, 3, 4, 6]} + ], + fn {name, d} -> + Variable.new(d, name: name) + end +) + +patterns = ["CPSolver.Propagator.filter/_"] + +FWCDebug.trace(x, patterns, 100) +""" + +###################### +## Propagation +###################### +""" +alias CPSolver.Propagator.AllDifferent.FWC +alias CPSolver.Variable.Interface +import CPSolver.Variable.View.Factory + +variables = + Enum.map( + [ + {"row1", 5}, + {"row2", 1}, + {"row3", 4}, + {"row4", [4, 6]}, + {"row5", [2, 3, 6]}, + {"row6", [2, 3, 4, 6]} + ], + fn {name, d} -> + Variable.new(d, name: name) + end + ) + +{:ok, x_vars, store} = + ConstraintStore.create_store(variables) + +row_propagator = Propagator.new(FWC, x_vars, name: "row") + +indexed_q = Enum.with_index(x_vars, 1) + +diagonal_down = Enum.map(indexed_q, fn {var, idx} -> linear(var, 1, idx) end) +diagonal_down_propagator = Propagator.new(FWC, diagonal_down, name: "down") + +diagonal_up = Enum.map(indexed_q, fn {var, idx} -> linear(var, 1, -idx) end) +diagonal_up_propagator = Propagator.new(FWC, diagonal_up, name: "up") + +propagators = [row_propagator, diagonal_up_propagator, diagonal_down_propagator] +graph = ConstraintGraph.create(propagators) + +## 3 propagators and 6 variables +assert Graph.num_vertices(graph) == 9 + +{scheduled_propagators, reduced_graph} = Propagation.propagate(propagators, graph, store) +assert MapSet.size(scheduled_propagators) == 1 +assert MapSet.member?(scheduled_propagators, row_propagator.id) + +assert ConstraintGraph.get_propagator(reduced_graph, row_propagator.id) + |> get_in([:state, :unfixed_vars]) == %{} + +assert Enum.all?(x_vars, fn var -> Interface.fixed?(var) end) +assert Graph.num_vertices(reduced_graph) == 6 +""" diff --git a/scripts/sudoku_benchmark.exs b/scripts/sudoku_benchmark.exs new file mode 100644 index 00000000..02b3f725 --- /dev/null +++ b/scripts/sudoku_benchmark.exs @@ -0,0 +1,41 @@ +defmodule SudokuBenchmark do + alias CPSolver.Examples.Sudoku + + require Logger + + def run(instance_file, n, space_threads, timeout) do + instances = File.read!(instance_file) |> String.split("\n") |> Enum.take(n) + + Enum.map( + instances |> Enum.with_index(1), + fn {instance, idx} -> + {:ok, res} = + CPSolver.solve_sync(Sudoku.model(instance), + stop_on: {:max_solutions, 1}, + space_threads: space_threads, + timeout: timeout, + search: {:first_fail, :indomain_random} + ) + + res + |> tap(fn res -> IO.puts("#{idx}: #{div(res.statistics.elapsed_time, 1000)} ms") end) + end + ) + end + + def stats(instance_file, n, space_threads, timeout) do + run(instance_file, n, space_threads, timeout) + |> Enum.map(fn s -> + !(s.solutions |> hd |> Sudoku.check_solution()) && + Logger.error("Wrong solution!") + + s.statistics.elapsed_time + end) + |> Enum.sort() + end +end + +instance_file = "data/sudoku/clue17.txt" +n = 100 +space_threads = 16 +timeout = :timer.seconds(30) diff --git a/scripts/sudoku_rosetta.exs b/scripts/sudoku_rosetta.exs new file mode 100644 index 00000000..ab51269e --- /dev/null +++ b/scripts/sudoku_rosetta.exs @@ -0,0 +1,201 @@ +defmodule SudokuRosetta do + def display(grid), do: for(y <- 1..9, do: display_row(y, grid)) + + def start(knowns), do: Enum.into(knowns, Map.new()) + + def solve(grid) do + sure = solve_all_sure(grid) + solve_unsure(potentials(sure), sure) + end + + def task(knowns) do + IO.puts("start") + start = start(knowns) + display(start) + IO.puts("solved") + solved = solve(start) + display(solved) + IO.puts("") + end + + defp bt(grid), do: bt_reject(is_not_allowed(grid), grid) + + defp bt_accept(true, board), do: throw({:ok, board}) + defp bt_accept(false, grid), do: bt_loop(potentials_one_position(grid), grid) + + defp bt_loop({position, values}, grid), do: for(x <- values, do: bt(Map.put(grid, position, x))) + + defp bt_reject(true, _grid), do: :backtrack + defp bt_reject(false, grid), do: bt_accept(is_all_correct(grid), grid) + + defp display_row(row, grid) do + for x <- [1, 4, 7], do: display_row_group(x, row, grid) + display_row_nl(row) + end + + defp display_row_group(start, row, grid) do + Enum.each(start..(start + 2), &IO.write(" #{Map.get(grid, {&1, row}, ".")}")) + IO.write(" ") + end + + defp display_row_nl(n) when n in [3, 6, 9], do: IO.puts("\n") + defp display_row_nl(_n), do: IO.puts("") + + defp is_all_correct(grid), do: map_size(grid) == 81 + + defp is_not_allowed(grid) do + is_not_allowed_rows(grid) or is_not_allowed_columns(grid) or is_not_allowed_groups(grid) + end + + defp is_not_allowed_columns(grid), + do: values_all_columns(grid) |> Enum.any?(&is_not_allowed_values/1) + + defp is_not_allowed_groups(grid), + do: values_all_groups(grid) |> Enum.any?(&is_not_allowed_values/1) + + defp is_not_allowed_rows(grid), do: values_all_rows(grid) |> Enum.any?(&is_not_allowed_values/1) + + defp is_not_allowed_values(values), do: length(values) != length(Enum.uniq(values)) + + defp group_positions({x, y}) do + for colum <- group_positions_close(x), row <- group_positions_close(y), do: {colum, row} + end + + defp group_positions_close(n) when n < 4, do: [1, 2, 3] + defp group_positions_close(n) when n < 7, do: [4, 5, 6] + defp group_positions_close(_n), do: [7, 8, 9] + + defp positions_not_in_grid(grid) do + keys = Map.keys(grid) + for x <- 1..9, y <- 1..9, {x, y} not in keys, do: {x, y} + end + + defp potentials_one_position(grid) do + Enum.min_by(potentials(grid), fn {_position, values} -> length(values) end) + end + + defp potentials(grid), + do: List.flatten(for x <- positions_not_in_grid(grid), do: potentials(x, grid)) + + defp potentials(position, grid) do + useds = potentials_used_values(position, grid) + {position, Enum.to_list(1..9) -- useds} + end + + defp potentials_used_values({x, y}, grid) do + row_values = for(row <- 1..9, row != x, do: {row, y}) |> potentials_values(grid) + column_values = for(column <- 1..9, column != y, do: {x, column}) |> potentials_values(grid) + group_values = (group_positions({x, y}) -- [{x, y}]) |> potentials_values(grid) + row_values ++ column_values ++ group_values + end + + defp potentials_values(keys, grid) do + for x <- keys, val = grid[x], do: val + end + + defp values_all_columns(grid) do + for x <- 1..9, do: for(y <- 1..9, do: {x, y}) |> potentials_values(grid) + end + + defp values_all_groups(grid) do + [[g1, g2, g3], [g4, g5, g6], [g7, g8, g9]] = + for x <- [1, 4, 7], do: values_all_groups(x, grid) + + [g1, g2, g3, g4, g5, g6, g7, g8, g9] + end + + defp values_all_groups(x, grid) do + for x_offset <- x..(x + 2), do: values_all_groups(x, x_offset, grid) + end + + defp values_all_groups(_x, x_offset, grid) do + for(y_offset <- group_positions_close(x_offset), do: {x_offset, y_offset}) + |> potentials_values(grid) + end + + defp values_all_rows(grid) do + for y <- 1..9, do: for(x <- 1..9, do: {x, y}) |> potentials_values(grid) + end + + defp solve_all_sure(grid), do: solve_all_sure(solve_all_sure_values(grid), grid) + + defp solve_all_sure([], grid), do: grid + + defp solve_all_sure(sures, grid) do + solve_all_sure(Enum.reduce(sures, grid, &solve_all_sure_store/2)) + end + + defp solve_all_sure_values(grid), + do: for({position, [value]} <- potentials(grid), do: {position, value}) + + defp solve_all_sure_store({position, value}, acc), do: Map.put(acc, position, value) + + defp solve_unsure([], grid), do: grid + + defp solve_unsure(_potentials, grid) do + try do + bt(grid) + catch + {:ok, board} -> board + end + end +end + +simple = [ + {{1, 1}, 3}, + {{2, 1}, 9}, + {{3, 1}, 4}, + {{6, 1}, 2}, + {{7, 1}, 6}, + {{8, 1}, 7}, + {{4, 2}, 3}, + {{7, 2}, 4}, + {{1, 3}, 5}, + {{4, 3}, 6}, + {{5, 3}, 9}, + {{8, 3}, 2}, + {{2, 4}, 4}, + {{3, 4}, 5}, + {{7, 4}, 9}, + {{1, 5}, 6}, + {{9, 5}, 7}, + {{3, 6}, 7}, + {{7, 6}, 5}, + {{8, 6}, 8}, + {{2, 7}, 1}, + {{5, 7}, 6}, + {{6, 7}, 7}, + {{9, 7}, 8}, + {{3, 8}, 9}, + {{6, 8}, 8}, + {{2, 9}, 2}, + {{3, 9}, 6}, + {{4, 9}, 4}, + {{7, 9}, 7}, + {{8, 9}, 3}, + {{9, 9}, 5} +] + +Sudoku.task(simple) + +difficult = [ + {{6, 2}, 3}, + {{8, 2}, 8}, + {{9, 2}, 5}, + {{3, 3}, 1}, + {{5, 3}, 2}, + {{4, 4}, 5}, + {{6, 4}, 7}, + {{3, 5}, 4}, + {{7, 5}, 1}, + {{2, 6}, 9}, + {{1, 7}, 5}, + {{8, 7}, 7}, + {{9, 7}, 3}, + {{3, 8}, 2}, + {{5, 8}, 1}, + {{5, 9}, 4}, + {{9, 9}, 9} +] + +Sudoku.task(difficult) diff --git a/scripts/sum_debug.exs b/scripts/sum_debug.exs new file mode 100644 index 00000000..dee5f521 --- /dev/null +++ b/scripts/sum_debug.exs @@ -0,0 +1,47 @@ +defmodule SumDebug do + alias CPSolver.ConstraintStore + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Propagator + alias CPSolver.Propagator.Sum + alias CPSolver.Constraint.Sum, as: SumConstraint + alias CPSolver.Model + + def debug(y_domain, x_domains) do + y = Variable.new(y_domain, name: "Y") + + xs = + Enum.map(x_domains |> Enum.with_index(1), fn {d, idx} -> + Variable.new(d, name: "x.#{idx}") + end) + + {:ok, [y_var | x_vars] = bound_vars, store} = ConstraintStore.create_store([y | xs]) + + # Enum.each(bound_vars, fn v -> IO.inspect("#{v.name} => #{inspect v.id}, #{min(v)}..#{max(v)}") end) + # assert :stable == + sum_propagator = Sum.new(y_var, x_vars) + IO.inspect(sum_propagator.args) + + Propagator.filter(sum_propagator, store: store) + end + + def debug_constraint(y_domain, x_domains) do + y = Variable.new(y_domain, name: "Y") + + xs = + Enum.map(x_domains |> Enum.with_index(1), fn {d, idx} -> + Variable.new(d, name: "x.#{idx}") + end) + + sum_constraint = SumConstraint.new(y, xs) + + model = + Model.new( + [y | xs], + [sum_constraint] + ) + + {:ok, _res} = CPSolver.solve_sync(model) + end +end + +SumDebug.debug_constraint(1..55, [1..10, 1..50]) diff --git a/src/bit_vector.erl b/src/bit_vector.erl index 4b238ad3..99296f28 100644 --- a/src/bit_vector.erl +++ b/src/bit_vector.erl @@ -10,11 +10,11 @@ new(Size) -> atomics:put(Atomics, Words + 1, 0), %% Set 'min_max' to lowest possible - {?MODULE, Size, Atomics}. + {?MODULE, Atomics}. -get({?MODULE, _Size, Aref}, Bix) -> +get({?MODULE, Aref}, Bix) -> Wix = (Bix div 64) + 1, Mask = (1 bsl (Bix rem 64)), case atomics:get(Aref, Wix) band Mask of @@ -22,20 +22,21 @@ get({?MODULE, _Size, Aref}, Bix) -> Mask -> 1 end. -set({?MODULE, _Size, Aref}, Bix) -> +set({?MODULE, Aref}, Bix) -> Mask = (1 bsl (Bix rem 64)), update(Aref, Bix, fun(Word) -> Word bor Mask end). -clear({?MODULE, _Size, Aref}, Bix) -> +clear({?MODULE, Aref}, Bix) -> Mask = bnot (1 bsl (Bix rem 64)), update(Aref, Bix, fun(Word) -> Word band Mask end). -flip({?MODULE, _Size, Aref}, Bix) -> +flip({?MODULE, Aref}, Bix) -> Mask = (1 bsl (Bix rem 64)), update(Aref, Bix, fun(Word) -> Word bxor Mask end). -print({?MODULE, Size, _Aref} = BV) -> - print(BV, Size-1). +print({?MODULE, Aref} = BV) -> + #{size := Size} = atomics:info(Aref), + print(BV, Size). print(BV, 0) -> io:format("~B~n",[get(BV, 0)]); print(BV, Slot) -> @@ -47,10 +48,9 @@ update(Aref, Bix, Fun) -> update_loop(Aref, Wix, Fun, atomics:get(Aref, Wix)). update_loop(Aref, Wix, Fun, Current) -> - atomics:put(Aref, Wix, Fun(Current)). - % case atomics:compare_exchange(Aref, Wix, Expected, Fun(Expected)) of - % ok -> - % ok; - % Was -> - % update_loop(Aref, Wix, Fun, Was) - % end. \ No newline at end of file + case atomics:compare_exchange(Aref, Wix, Current, Fun(Current)) of + ok -> + ok; + Was -> + update_loop(Aref, Wix, Fun, Was) + end. \ No newline at end of file diff --git a/test/constraints/abs_test.exs b/test/constraints/abs_test.exs new file mode 100644 index 00000000..65747feb --- /dev/null +++ b/test/constraints/abs_test.exs @@ -0,0 +1,59 @@ +defmodule CPSolverTest.Constraint.Absolute do + use ExUnit.Case, async: false + + describe "Absolute" do + alias CPSolver.Constraint.Absolute + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Variable.Interface + alias CPSolver.Model + alias CPSolver.Constraint.Factory, as: ConstraintFactory + + ~c""" + MiniZinc model (for verification): + + var -2..2: x; + var -2..2: y; + constraint y = abs(x); + + """ + + test "`Absolute` functionality" do + x = Variable.new(-2..2, name: "x") + y = Variable.new(-2..2, name: "y") + + model = Model.new([x, y], [Absolute.new(x, y)]) + + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 5 + assert check_solutions(res) + end + + test "inconsistency" do + x = Variable.new(0, name: "x") + y = Variable.new(1, name: "y") + + model = Model.new([x, y], [Absolute.new(x, y)]) + {:ok, res} = CPSolver.solve_sync(model) + assert res.status == :unsatisfiable + end + + test "factory" do + x = Variable.new(-2..2) + + {abs_var, abs_constraint} = ConstraintFactory.absolute(x) + + assert Interface.min(abs_var) == 0 + assert Interface.max(abs_var) == 2 + + model = Model.new([x], [abs_constraint]) + + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 5 + assert check_solutions(res) + end + + defp check_solutions(result) do + Enum.all?(result.solutions, fn [x_val, y_val] -> y_val == abs(x_val) end) + end + end +end diff --git a/test/constraints/all_different_test.exs b/test/constraints/all_different_binary_test.exs similarity index 94% rename from test/constraints/all_different_test.exs rename to test/constraints/all_different_binary_test.exs index 894627c3..98e629b2 100644 --- a/test/constraints/all_different_test.exs +++ b/test/constraints/all_different_binary_test.exs @@ -3,7 +3,7 @@ defmodule CPSolverTest.Constraint.AllDifferent do describe "AllDifferent" do alias CPSolver.Propagator.NotEqual, as: PropagatorNotEqual - alias CPSolver.Constraint.AllDifferent + alias CPSolver.Constraint.AllDifferent.Binary, as: AllDifferent alias CPSolver.IntVariable alias CPSolver.Constraint alias CPSolver.Model diff --git a/test/constraints/all_different_fwc_test.exs b/test/constraints/all_different_fwc_test.exs index cd70be5a..89df8ffd 100644 --- a/test/constraints/all_different_fwc_test.exs +++ b/test/constraints/all_different_fwc_test.exs @@ -8,18 +8,26 @@ defmodule CPSolverTest.Constraint.AllDifferent.FWC do alias CPSolver.Model import CPSolver.Variable.View.Factory + test "all fixed" do + variables = Enum.map(1..5, fn i -> IntVariable.new(i) end) + model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)]) + {:ok, result} = CPSolver.solve_sync(model) + + assert hd(result.solutions) == [1, 2, 3, 4, 5] + assert result.statistics.solution_count == 1 + end + test "produces all possible permutations" do domain = 1..3 variables = Enum.map(1..3, fn _ -> IntVariable.new(domain) end) model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)]) - {:ok, solver} = CPSolver.solve(model) + {:ok, result} = CPSolver.solve_sync(model, timeout: 100) - Process.sleep(100) - assert CPSolver.statistics(solver).solution_count == 6 + assert result.statistics.solution_count == 6 - assert CPSolver.solutions(solver) |> Enum.sort() == [ + assert result.solutions |> Enum.sort() == [ [1, 2, 3], [1, 3, 2], [2, 1, 3], @@ -29,6 +37,24 @@ defmodule CPSolverTest.Constraint.AllDifferent.FWC do ] end + test "unsatisfiable (duplicates)" do + variables = Enum.map(1..3, fn _ -> IntVariable.new(1) end) + model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)]) + + {:ok, result} = CPSolver.solve_sync(model, timeout: 1000) + + assert result.status == :unsatisfiable + end + + test "unsatisfiable(pigeonhole)" do + variables = Enum.map(1..4, fn _ -> IntVariable.new(1..3) end) + model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)]) + + {:ok, result} = CPSolver.solve_sync(model) + + assert result.status == :unsatisfiable + end + test "views in variable list" do n = 3 variables = Enum.map(1..n, fn i -> IntVariable.new(1..n, name: "row#{i}") end) diff --git a/test/constraints/arithmetics_test.exs b/test/constraints/arithmetics_test.exs new file mode 100644 index 00000000..593caf67 --- /dev/null +++ b/test/constraints/arithmetics_test.exs @@ -0,0 +1,55 @@ +defmodule CPSolverTest.Constraint.Arithmetics do + use ExUnit.Case, async: false + + describe "Arithmetics" do + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Constraint.Factory, as: ConstraintFactory + alias CPSolver.Variable.View.Factory, as: ViewFactory + alias CPSolver.Model + alias CPSolver.Constraint.Equal + + test "add 2 variables" do + {:ok, res} = setup_and_solve(:add_variable) + + assert Enum.all?(res.solutions, fn [x_val, y_val, sum_val] -> + x_val + y_val == sum_val + end) + end + + test "subtract 2 variables" do + {:ok, res} = setup_and_solve(:subtract_variable) + + assert Enum.all?(res.solutions, fn [x_val, y_val, sum_val] -> + x_val - y_val == sum_val + end) + end + + test "add variable and constant" do + x_domain = 1..10 + c = 3 + x = Variable.new(x_domain, name: "x") + model = Model.new([x], [Equal.new(ViewFactory.inc(x, c), 10)]) + {:ok, res} = CPSolver.solve_sync(model) + + assert length(res.solutions) == 1 + assert hd(hd(res.solutions)) == 10 - c + end + + defp setup_and_solve(constraint_kind) + when constraint_kind in [:add_variable, :subtract_variable] do + x_domain = 0..2 + y_domain = 0..2 + x = Variable.new(x_domain, name: "x") + y = Variable.new(y_domain, name: "y") + + {_sum_var, constraint} = + case constraint_kind do + :add_variable -> ConstraintFactory.add(x, y) + :subtract_variable -> ConstraintFactory.subtract(x, y) + end + + model = Model.new([x, y], [constraint]) + {:ok, _res} = CPSolver.solve_sync(model) + end + end +end diff --git a/test/constraints/count_test.exs b/test/constraints/count_test.exs new file mode 100644 index 00000000..6c84275b --- /dev/null +++ b/test/constraints/count_test.exs @@ -0,0 +1,44 @@ +defmodule CPSolverTest.Constraint.Count do + use ExUnit.Case, async: false + + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Model + alias CPSolver.Constraint.Factory, as: ConstraintFactory + + describe "Count constraint" do + + test "`count` functionality" do + ~S""" + MiniZinc: + + var -5..5: c; + var 0..10: y; + array[1..5] of var 1..3: arr; + + constraint count_eq(arr, y, c); + """ + + c = Variable.new(-5..5, name: "count") + y = Variable.new(0..10, name: "value") + array = Enum.map(1..5, fn i -> Variable.new(1..3, name: "arr#{i}") end) + + model = Model.new([array, y, c] |> List.flatten(), ConstraintFactory.count(array, y, c) |> List.flatten()) + + {:ok, result} = CPSolver.solve_sync(model) + + assert result.statistics.solution_count == 2673 + assert_count(result.solutions, length(array)) + end + + defp assert_count(solutions, array_len) do + assert Enum.all?(solutions, fn solution -> + arr = Enum.take(solution, array_len) + value = Enum.at(solution, array_len) + c = Enum.at(solution, array_len + 1) + Enum.count(arr, fn el -> el == value end) == c + end) + end + + end + +end diff --git a/test/constraints/element_test.exs b/test/constraints/element_test.exs index ec2c8cd6..e3e8b461 100644 --- a/test/constraints/element_test.exs +++ b/test/constraints/element_test.exs @@ -1,10 +1,13 @@ defmodule CPSolverTest.Constraint.Element do use ExUnit.Case, async: false - describe "Element" do - alias CPSolver.Constraint.Element - alias CPSolver.IntVariable, as: Variable - alias CPSolver.Model + alias CPSolver.IntVariable, as: Variable + alias CPSolver.DefaultDomain, as: Domain + alias CPSolver.Model + alias CPSolver.Constraint.Factory, as: ConstraintFactory + + describe "Element (constant array)" do + alias CPSolver.Constraint.{Element, Element2D} test "`element` functionality" do y = Variable.new(-3..10) @@ -13,14 +16,215 @@ defmodule CPSolverTest.Constraint.Element do model = Model.new([y, z], [Element.new(t, y, z)]) - {:ok, solver} = CPSolver.solve(model) + {:ok, result} = CPSolver.solve_sync(model) + + assert result.statistics.solution_count == 5 + assert_element(result.solutions, t) + end + + test "`element2d functionality" do + x = Variable.new(-2..40, name: "x") + y = Variable.new(-3..10, name: "y") + z = Variable.new(2..40, name: "z") + + t = [ + [9, 8, 7, 5, 6], + [9, 1, 5, 2, 8], + [8, 3, 1, 4, 9], + [9, 1, 2, 8, 6] + ] + + model = Model.new([x, y, z], [Element2D.new(t, x, y, z)]) + + {:ok, result} = CPSolver.solve_sync(model) + refute Enum.empty?(result.solutions) + assert_element2d(result.solutions, t) + end + + test "`element` factory function" do + x_var = Variable.new(-20..40) + t = [9, 8, 7, 5, 6] + + {y_var, element_constraint} = ConstraintFactory.element(t, x_var) + ## domain of generated variable corresponds to content of t + assert Domain.to_list(y_var.domain) |> Enum.sort() == Enum.sort(t) + ## Create and run model with generated constraint + model = Model.new([x_var, y_var], [element_constraint]) + + {:ok, result} = CPSolver.solve_sync(model) + assert_element(result.solutions, t) + end + + test "`element2d` factory function" do + x_var = Variable.new(-2..40, name: "x") + y_var = Variable.new(-3..10, name: "y") + + t = [ + [9, 8, 7, 5, 6], + [9, 1, 5, 2, 8], + [8, 3, 1, 4, 9], + [9, 1, 2, 8, 6] + ] - Process.sleep(100) - assert CPSolver.statistics(solver).solution_count == 5 + {z_var, element2d_constraint} = ConstraintFactory.element2d(t, x_var, y_var) + ## domain of generated variable corresponds to content of t (all unique values) + assert Domain.to_list(z_var.domain) |> Enum.sort() == + t |> List.flatten() |> Enum.uniq() |> Enum.sort() - assert Enum.all?(CPSolver.solutions(solver), fn [y_value, z_value, _] -> + model = Model.new([x_var, y_var, z_var], [element2d_constraint]) + + {:ok, result} = CPSolver.solve_sync(model) + assert_element2d(result.solutions, t) + end + + ## Constraint check: t[y] = z + ## Note: last variable is a placeholder (0) + ## This is to maintain compatibility with element2D + ## Placeholder will be eliminated in upcoming versions. + ## + defp assert_element(solutions, t) do + assert Enum.all?(solutions, fn [y_value, z_value | _placeholder] -> Enum.at(t, y_value) == z_value end) end + + defp assert_element2d(solutions, t) do + assert Enum.all?(solutions, fn [x, y, z | _rest] -> + Enum.at(t, x) |> Enum.at(y) == z + end) + end + end + + describe "Element (variable array)" do + alias CPSolver.Constraint.ElementVar + + test "inconsistency (index domain outside of index set of the array)" do + index_var = Variable.new(6..10, name: "index") + value_var = Variable.new(-20..40, name: "value") + + array_var = + Enum.map(Enum.with_index([9, 8, 7, 5, 6], 1), fn {val, idx} -> + Variable.new(val, name: "T#{idx}") + end) + + element_constraint = ElementVar.new(array_var, index_var, value_var) + model = Model.new([index_var, value_var | array_var], [element_constraint]) + {:ok, res} = CPSolver.solve_sync(model) + + assert res.status == :unsatisfiable + end + + test "`element with fixed values in variable array" do + index_var = Variable.new(-3..10, name: "index") + value_var = Variable.new(-20..40, name: "value") + + array_values = [9, 8, 7, 5, 6] + # Enum.map(Enum.with_index([9, 8, 7, 5, 6], 1), fn {val, idx} -> + # Variable.new(val, name: "T#{idx}") + # end) + + element_constraint = ElementVar.new(array_values, index_var, value_var) + model = Model.new([index_var, value_var | array_values], [element_constraint]) + {:ok, res} = CPSolver.solve_sync(model) + + assert res.statistics.solution_count == 5 + + assert Enum.all?(res.solutions, fn [idx, val | array_vals] -> + Enum.at(array_vals, idx) == val + end) + end + + test "element with non-fixed domains in the array" do + index_var = Variable.new(-5..2, name: "idx") + value_var = Variable.new(-2..2, name: "value") + array_var = Enum.map(0..4, fn idx -> Variable.new(-1..1, name: "A#{idx}") end) + element_constraint = ElementVar.new(array_var, index_var, value_var) + model = Model.new([index_var, value_var | array_var], [element_constraint]) + {:ok, res} = CPSolver.solve_sync(model) + + ~S""" + Verified by MiniZinc model: + var -5..2: idx; + var -2..2: value; + array[0..4] of var -1..1: arr; + constraint arr[idx] = value; + """ + + assert res.statistics.solution_count == 729 + + assert Enum.all?(res.solutions, fn [idx, val | array_vals] -> + Enum.at(array_vals, idx) == val + end) + end + + test "enumerated index" do + index_var = Variable.new([-5, 0, 2], name: "idx") + value_var = Variable.new(-2..2, name: "value") + array_var = Enum.map(0..4, fn idx -> Variable.new(-1..1, name: "A#{idx}") end) + element_constraint = ElementVar.new(array_var, index_var, value_var) + model = Model.new([index_var, value_var | array_var], [element_constraint]) + {:ok, res} = CPSolver.solve_sync(model) + + ~S""" + Verified by MiniZinc model: + var {-5, 0, 2}: idx; + var -10..10: value; + array[0..4] of var -1..1: arr; + + constraint arr[idx] = value; + """ + + assert res.statistics.solution_count == 486 + + assert Enum.all?(res.solutions, fn [idx, val | array_vals] -> + Enum.at(array_vals, idx) == val + end) + end + end + + test "variable 2d array" do + ~S""" + MiniZinc: + + array[1..2, 1..2] of var 0..3: arr2d; + var 0..4: x; + var 0..2: y; + var 1..10: z; + + constraint arr2d[x, y] = z; + """ + + arr2d = + for i <- 1..2 do + for j <- 1..2 do + Variable.new(0..3, name: "arr(#{i},#{j})") + end + end + + x = Variable.new(0..4, name: "x") + y = Variable.new(0..2, name: "y") + z = Variable.new(1..10, name: "z") + + model = + Model.new( + [x, y, z, arr2d] |> List.flatten(), + ConstraintFactory.element2d_var(arr2d, x, y, z) + ) + + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 768 + + assert_element2d_var(res.solutions, length(arr2d), length(hd(arr2d))) + end + + defp assert_element2d_var(solutions, row_num, col_num) do + assert Enum.all?( + solutions, + fn solution -> + [x, y, z | rest] = solution + arr_solution = Enum.take(rest, row_num * col_num) + Enum.at(arr_solution, x * col_num + y) == z + end + ) end end diff --git a/test/constraints/equal_test.exs b/test/constraints/equal_test.exs new file mode 100644 index 00000000..5ae57114 --- /dev/null +++ b/test/constraints/equal_test.exs @@ -0,0 +1,40 @@ +defmodule CPSolverTest.Constraint.Equal do + use ExUnit.Case, async: false + + describe "Equal" do + alias CPSolver.Constraint.Equal + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Model + + test "equal, 2 variables" do + x = Variable.new(0..1, name: "x") + y = Variable.new(0..1, name: "y") + z = Variable.new(2..4, name: "z") + satisfiable_model = Model.new([x, y], [Equal.new([x, y])]) + {:ok, res} = CPSolver.solve_sync(satisfiable_model) + + assert length(res.solutions) == 2 + assert Enum.all?(res.solutions, fn [x_val, y_val] -> x_val == y_val end) + + unsatisfiable_model = Model.new([x, y, z], [Equal.new([x, y]), Equal.new([y, z])]) + {:ok, res} = CPSolver.solve_sync(unsatisfiable_model) + assert res.status == :unsatisfiable + end + + test "variable and constant" do + x = Variable.new(0..4) + satisfiable_value = 3 + equal_constraint = Equal.new(x, satisfiable_value) + satisfiable_model = Model.new([x], [equal_constraint]) + {:ok, res} = CPSolver.solve_sync(satisfiable_model) + assert length(res.solutions) == 1 + assert hd(hd(res.solutions)) == satisfiable_value + + unsatisfiable_value = -1 + equal_constraint = Equal.new(x, unsatisfiable_value) + unsatisfiable_model = Model.new([x], [equal_constraint]) + {:ok, res} = CPSolver.solve_sync(unsatisfiable_model) + assert res.status == :unsatisfiable + end + end +end diff --git a/test/constraints/less_or_equal_test.exs b/test/constraints/less_or_equal_test.exs index 6bac895e..d5ab0c28 100644 --- a/test/constraints/less_or_equal_test.exs +++ b/test/constraints/less_or_equal_test.exs @@ -2,7 +2,7 @@ defmodule CPSolverTest.Constraint.LessOrEqual do use ExUnit.Case, async: false describe "LessOrEqual" do - alias CPSolver.Constraint.LessOrEqual + alias CPSolver.Constraint.{Less, LessOrEqual} alias CPSolver.IntVariable, as: Variable alias CPSolver.Constraint alias CPSolver.Model @@ -26,5 +26,14 @@ defmodule CPSolverTest.Constraint.LessOrEqual do assert length(res.solutions) == 3 assert Enum.all?(res.solutions, fn [x_val, _] -> x_val <= upper_bound end) end + + test "Less" do + x = Variable.new(1) + y = Variable.new(1) + less_constraint = Less.new(x, y) + model = Model.new([x, y], [less_constraint]) + {:ok, res} = CPSolver.solve_sync(model) + assert res.status == :unsatisfiable + end end end diff --git a/test/constraints/modulo_test.exs b/test/constraints/modulo_test.exs new file mode 100644 index 00000000..5139cba6 --- /dev/null +++ b/test/constraints/modulo_test.exs @@ -0,0 +1,51 @@ +defmodule CPSolverTest.Constraint.Modulo do + use ExUnit.Case, async: false + + describe "Modulo" do + alias CPSolver.Constraint.Modulo + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Variable.Interface + alias CPSolver.Model + alias CPSolver.Constraint.Factory, as: ConstraintFactory + + ~c""" + MiniZinc model (for verification): + + var -2..2: x; + var -2..2: y; + var -100..100: m; + constraint m = x mod y; + + """ + + test "`modulo` functionality" do + x = Variable.new(-2..2, name: "x") + y = Variable.new(-2..2, name: "y") + m = Variable.new(-100..100, name: "m") + + model = Model.new([x, y, m], [Modulo.new(m, x, y)]) + + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 20 + assert check_solutions(res) + end + + test "Factory.mod/2,3" do + x = Variable.new(-100..100, name: "x") + y = Variable.new(-7..7, name: "y") + {mod_var, mod_constraint} = ConstraintFactory.mod(x, y) + assert Interface.min(mod_var) == -6 + assert Interface.max(mod_var) == 6 + + model = Model.new([x, y], [mod_constraint]) + {:ok, res} = CPSolver.solve_sync(model) + ## Verification against MiniZinc count + assert res.statistics.solution_count == 2814 + assert check_solutions(res) + end + + defp check_solutions(result) do + Enum.all?(result.solutions, fn [x_val, y_val, m_val] -> rem(x_val, y_val) == m_val end) + end + end +end diff --git a/test/constraints/or_test.exs b/test/constraints/or_test.exs new file mode 100644 index 00000000..06ada389 --- /dev/null +++ b/test/constraints/or_test.exs @@ -0,0 +1,56 @@ +defmodule CPSolverTest.Constraint.Or do + use ExUnit.Case, async: false + + alias CPSolver.BooleanVariable + alias CPSolver.Model + alias CPSolver.Constraint.Or + + describe "Or constraint" do + test "`or` functionality" do + bool_vars = Enum.map(1..4, fn i -> BooleanVariable.new(name: "b#{i}") end) + or_constraint = Or.new(bool_vars) + + model = Model.new(bool_vars, [or_constraint]) + + {:ok, result} = CPSolver.solve_sync(model) + + assert result.statistics.solution_count == 15 + assert_or(result.solutions, length(bool_vars)) + end + + test "inconsistency (all-false)" do + bool_vars = List.duplicate(0, 4) + + or_constraint = Or.new(bool_vars) + + model = Model.new(bool_vars, [or_constraint]) + + {:ok, result} = CPSolver.solve_sync(model) + + assert result.status == :unsatisfiable + end + + test "peformance" do + n = 1000 + bool_vars = Enum.map(1..n, fn i -> BooleanVariable.new(name: "b#{i}") end) + or_constraint = Or.new(bool_vars) + + model = Model.new(bool_vars, [or_constraint]) + + {:ok, res} = CPSolver.solve_sync(model, stop_on: {:max_solutions, 1}, + search: {:first_fail, :indomain_max}, + space_threads: 1) + assert res.statistics.solution_count >= 1 + ## Arbitrary elapsed time, the main point it shouldn't be too big + assert res.statistics.elapsed_time < 250_000 + end + + + defp assert_or(solutions, array_len) do + assert Enum.all?(solutions, fn solution -> + arr = Enum.take(solution, array_len) + Enum.sum(arr) > 0 + end) + end + end +end diff --git a/test/constraints/reified_test.exs b/test/constraints/reified_test.exs new file mode 100644 index 00000000..b54babba --- /dev/null +++ b/test/constraints/reified_test.exs @@ -0,0 +1,247 @@ +defmodule CPSolverTest.Constraint.Reified do + use ExUnit.Case, async: false + + describe "Reification" do + alias CPSolver.Constraint.{Reified, HalfReified, InverseHalfReified} + alias CPSolver.Constraint.{Equal, NotEqual, LessOrEqual, Less, Absolute} + alias CPSolver.IntVariable + alias CPSolver.BooleanVariable + alias CPSolver.Model + + test "equivalence: (x `relation` y) <-> b" do + ~c""" + MiniZinc model (for verification): + var 0..1: x; + var 0..1: y; + + var bool: b; + + constraint x <= y <-> b; + + Solutions: + x = 1; y = 1; b = true; + + x = 0; y = 1; b = true; + + x = 1; y = 0; b = false; + + x = 0; y = 0; b = true; + + """ + + x_domain = 0..1 + y_domain = 0..1 + + for p <- [LessOrEqual, Less, Equal, NotEqual, Absolute] do + model1 = make_model(x_domain, y_domain, p, Reified) + {:ok, res} = CPSolver.solve_sync(model1) + assert res.statistics.solution_count == num_sols(p, Reified) + assert Enum.all?(res.solutions, fn s -> check_solution(s, p, Reified) end) + + ## The order of variables doesn't matter + model2 = make_model(x_domain, y_domain, p, Reified, fn [x, y, b] -> [b, x, y] end) + {:ok, res} = CPSolver.solve_sync(model2) + assert res.statistics.solution_count == num_sols(p, Reified) + + assert Enum.all?(res.solutions, fn [b_value, x_value, y_value] = s -> + check_solution([x_value, y_value, b_value], p, Reified) + end) + end + end + + test "implication (half-reification): (x `relation` y) -> b" do + ~c""" + Minizinc model: + var 0..1: x; + var 0..1: y; + + var bool: b; + + constraint x <= y -> b; + """ + + x_domain = 0..1 + y_domain = 0..1 + + for p <- [LessOrEqual, Less, Equal, NotEqual, Absolute] do + model = make_model(x_domain, y_domain, p, HalfReified) + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == num_sols(p, HalfReified) + assert Enum.all?(res.solutions, fn s -> check_solution(s, p, HalfReified) end) + end + end + + test "inverse implication (inverse half-reification): (x `relation` y) <- b" do + ~c""" + Minizinc model: + var 0..1: x; + var 0..1: y; + + var bool: b; + + constraint x <= y <- b; + """ + + x_domain = 0..1 + y_domain = 0..1 + + for p <- [LessOrEqual, Less, Equal, NotEqual, Absolute] do + model = make_model(x_domain, y_domain, p, InverseHalfReified) + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == num_sols(p, InverseHalfReified) + assert Enum.all?(res.solutions, fn s -> check_solution(s, p, InverseHalfReified) end) + end + end + + test "Absolute, reified (both negatives and positives in domains)" do + x_domain = -1..1 + y_domain = -1..1 + + for {mode, expected_num_sols} <- [ + {Reified, 9}, + {HalfReified, 15}, + {InverseHalfReified, 12} + ] do + model = make_model(x_domain, y_domain, Absolute, mode) + {:ok, res} = CPSolver.solve_sync(model) + assert Enum.all?(res.solutions, fn s -> check_solution(s, Absolute, mode) end) + assert res.statistics.solution_count == expected_num_sols + end + end + + defp make_model( + x_domain, + y_domain, + constraint_mod, + reif_impl, + order_fun \\ &Function.identity/1 + ) do + x = IntVariable.new(x_domain, name: "x") + y = IntVariable.new(y_domain, name: "y") + b = BooleanVariable.new(name: "b") + le_constraint = constraint_mod.new(x, y) + Model.new(order_fun.([x, y, b]), [reif_impl.new(le_constraint, b)]) + end + + defp check_solution([x, y, b] = _solution, constraint_impl, reification_mod) do + checker = Map.get(constraint_data(), constraint_impl)[:check_fun] + + case reification_mod do + Reified -> (checker.(x, y) && b == 1) || b == 0 + HalfReified -> !checker.(x, y) || b == 1 + InverseHalfReified -> checker.(x, y) || b == 0 + end + end + + defp num_sols(constraint_impl, reification_mod) do + get_in(constraint_data(), [constraint_impl, :num_sols, reification_mod]) + end + + defp constraint_data() do + %{ + LessOrEqual => %{ + check_fun: fn x, y -> x <= y end, + num_sols: %{Reified => 4, HalfReified => 5, InverseHalfReified => 7} + }, + Less => %{ + check_fun: fn x, y -> x < y end, + num_sols: %{Reified => 4, HalfReified => 7, InverseHalfReified => 5} + }, + Equal => %{ + check_fun: fn x, y -> x == y end, + num_sols: %{Reified => 4, HalfReified => 6, InverseHalfReified => 6} + }, + NotEqual => %{ + check_fun: fn x, y -> x != y end, + num_sols: %{Reified => 4, HalfReified => 6, InverseHalfReified => 6} + }, + Absolute => %{ + check_fun: fn x, y -> abs(x) == y end, + num_sols: %{Reified => 4, HalfReified => 6, InverseHalfReified => 6} + } + } + end + end + + describe "Factory (implication, equivalence, inverse implication)" do + alias CPSolver.Constraint.Factory + alias CPSolver.IntVariable + alias CPSolver.Model + alias CPSolver.Constraint.{LessOrEqual} + + test "equivalence" do + model = build_model(1..2, 1..2, 1..2, LessOrEqual, :equiv) + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 4 + + assert Enum.all?(res.solutions, fn [x, y, z | _rest] -> + x <= y && y <= z + end) + end + + test "implication" do + model = build_model(1..2, 1..2, 1..2, LessOrEqual, :impl) + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 6 + + assert Enum.all?(res.solutions, fn [x, y, z | _rest] -> + x > y || y <= z + end) + end + + test "inverse implication" do + model = build_model(1..2, 1..2, 1..2, LessOrEqual, :inverse_impl) + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 6 + + assert Enum.all?(res.solutions, fn [x, y, z | _rest] -> + x <= y || y > z + end) + end + + test "ignoring temporary variables" do + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Constraint.Less + x = Variable.new(1..3, name: "x") + y = Variable.new(4..6, name: "y") + c1 = 1 + c2 = 5 + + ~S""" + Minizinc: + var 1..3: x; + var 4..6: y; + + constraint (1 < x) -> (5 < y); + """ + + impl_model = Factory.impl(Less.new(c1, x), Less.new(c2, y)) + model = Model.new([], impl_model.constraints) + {:ok, res} = CPSolver.solve_sync(model) + assert res.statistics.solution_count == 5 + ## Positions of variables can be arbitrary, if omitted in the model description + x_pos = Enum.find_index(res.variables, fn name -> name == "x" end) + y_pos = Enum.find_index(res.variables, fn name -> name == "y" end) + assert Enum.all?(res.solutions, fn solution -> + x = Enum.at(solution, x_pos) + y = Enum.at(solution, y_pos) + x >= 1 || y > 5 + end) + + + end + + defp build_model(x_domain, y_domain, z_domain, constraint, kind) do + x_var = IntVariable.new(x_domain, name: "x") + y_var = IntVariable.new(y_domain, name: "y") + z_var = IntVariable.new(z_domain, name: "z") + + %{constraints: constraints, derived_variables: tmp_vars} = + apply(Factory, kind, [constraint.new([x_var, y_var]), constraint.new([y_var, z_var])]) + Model.new( + [x_var, y_var, z_var] ++ tmp_vars, + constraints + ) + end + end +end diff --git a/test/constraints/sum_test.exs b/test/constraints/sum_test.exs index 4557ab89..f79cfaa2 100644 --- a/test/constraints/sum_test.exs +++ b/test/constraints/sum_test.exs @@ -11,7 +11,7 @@ defmodule CPSolverTest.Constraint.Sum do y = Variable.new(0..1, name: "y") z = Variable.new(0..1, name: "z") - {_sum_var, sum_constraint} = Factory.sum([x, y, z], name: "sum") + {_sum_var, sum_constraint} = Factory.sum([x, y, z]) model = Model.new([x, y, z], [sum_constraint]) {:ok, res} = CPSolver.solve_sync(model) @@ -22,5 +22,25 @@ defmodule CPSolverTest.Constraint.Sum do Enum.sum(Enum.take(s, length(s) - 1)) == List.last(s) end) end + + test "sum (mixed arguments)" do + c1 = 3 + c2 = -4 + c3 = 5 + + x = Variable.new(0..1, name: "x") + y = Variable.new(0..1, name: "y") + z = Variable.new(0..1, name: "z") + {_sum_var, sum_constraint} = Factory.sum([x, c1, y, c2, c3, z]) + + model = Model.new([x, y, z], [sum_constraint]) + {:ok, res} = CPSolver.solve_sync(model) + + assert 8 == length(res.solutions) + + assert Enum.all?(res.solutions, fn s -> + Enum.sum(Enum.take(s, length(s) - 1)) + c1 + c2 + c3 == List.last(s) + end) + end end end diff --git a/test/domain/mutable_domain_test.exs b/test/domain/mutable_domain_test.exs index d347a374..28854d54 100644 --- a/test/domain/mutable_domain_test.exs +++ b/test/domain/mutable_domain_test.exs @@ -1,8 +1,8 @@ defmodule CPSolverTest.MutableDomain do use ExUnit.Case - describe "Default domain" do - alias CPSolver.BitVectorDomain.V2, as: Domain + describe "Mutable domain" do + alias CPSolver.BitVectorDomain, as: Domain test "creates domain from integer range and list" do assert catch_throw(Domain.new([])) == :fail @@ -157,6 +157,55 @@ defmodule CPSolverTest.MutableDomain do assert_domain(domain, values1) end + @tag :slow + test "Concurrent removal of values (threads remove distinct values)" do + ## + values = 1..100_000 + domain = Domain.new(values) + + Task.async_stream( + values, + fn val -> + try do + Domain.remove(domain, val) + catch + _ -> + :ok + end + end, + max_concurrency: 8 + ) + |> Enum.to_list() + + assert Domain.failed?(domain) + end + + test "Concurrent removal of values (multiple threads remove shared values)" do + ## + n_values = 3 + values = 1..n_values + domain = Domain.new(values) + + Task.async_stream( + 1..2, + fn _thread_id -> + try do + ## Keep one random value, remove the rest + Enum.each(Enum.take(values, n_values - 1), fn val -> + Domain.remove(domain, val) + end) + catch + _ -> + :failed + end + end, + max_concurrency: 8 + ) + |> Enum.to_list() + + assert Domain.fixed?(domain) + end + defp build_domain(data) do ref = :atomics.new(length(data.raw.content), [{:signed, false}]) @@ -164,12 +213,12 @@ defmodule CPSolverTest.MutableDomain do :atomics.put(ref, idx, val) end) - bit_vector = {:bit_vector, data.raw.offset, ref} + bit_vector = {:bit_vector, ref} _domain = {bit_vector, data.raw.offset} end defp assert_domain(domain, values) do - assert Domain.to_list(domain) == values + assert Domain.to_list(domain) |> Enum.sort() == values |> Enum.sort() assert Domain.size(domain) == length(values) assert Domain.min(domain) == Enum.min(values) assert Domain.max(domain) == Enum.max(values) @@ -190,7 +239,7 @@ defmodule CPSolverTest.MutableDomain do assert Domain.min(1) == 1 assert Domain.max(1) == 1 - assert Domain.to_list(1) == [1] + assert Domain.to_list(1) == MapSet.new([1]) assert Domain.map(3, fn x -> 2 * x end) == [6] assert Domain.copy(1) == 1 diff --git a/test/examples/graph_coloring_test.exs b/test/examples/graph_coloring_test.exs index c003649e..0b2b57ce 100644 --- a/test/examples/graph_coloring_test.exs +++ b/test/examples/graph_coloring_test.exs @@ -31,8 +31,9 @@ defmodule CPSolverTest.Examples.GraphColoring do test_graph("gc_15_30_1", 36) end - test "gc_15_30_2" do - test_graph("gc_15_30_2", 21408, timeout: 2000, trials: 1) + @tag :slow + test "gc_15_30_5" do + test_graph("gc_15_30_5", 34848, timeout: 2000, trials: 1) end test "gc_15_30_3" do diff --git a/test/examples/quadratic_assignment_test.exs b/test/examples/quadratic_assignment_test.exs index 87b84a36..e89a926f 100644 --- a/test/examples/quadratic_assignment_test.exs +++ b/test/examples/quadratic_assignment_test.exs @@ -18,7 +18,8 @@ defmodule CPSolverTest.Examples.QAP do [2, 1, 4, 0] ] - {:ok, results} = CPSolver.solve_sync(QAP.model(distances, weights)) + qap_model = QAP.model(distances, weights) + {:ok, results} = CPSolver.solve_sync(qap_model) assert Enum.all?(results.solutions, fn solution -> QAP.check_solution(solution, distances, weights) @@ -27,7 +28,7 @@ defmodule CPSolverTest.Examples.QAP do optimal_solution = List.last(results.solutions) objective_variable_index = - Enum.find_index(results.variables, fn name -> name == "total_cost" end) + Enum.find_index(results.variables, fn name -> name == qap_model.extra.total_cost_var_id end) assert results.objective == Enum.at(optimal_solution, objective_variable_index) end diff --git a/test/examples/queens_test.exs b/test/examples/queens_test.exs index 50bdcaca..9ad521ca 100644 --- a/test/examples/queens_test.exs +++ b/test/examples/queens_test.exs @@ -31,7 +31,7 @@ defmodule CPSolverTest.Examples.Queens do end test "50 Queens" do - test_queens(50, 1, timeout: 500, trials: 2, stop_on: {:max_solutions, 1}) + test_queens(50, 1, timeout: 500, trials: 2, space_threads: 4, stop_on: {:max_solutions, 1}) end defp test_queens(n, expected_solutions, opts \\ []) do @@ -39,7 +39,7 @@ defmodule CPSolverTest.Examples.Queens do Keyword.merge([timeout: 100, trials: 10], opts) Enum.each(1..opts[:trials], fn i -> - {:ok, result} = CPSolver.solve_sync(Queens.model(n), timeout: opts[:timeout]) + {:ok, result} = CPSolver.solve_sync(Queens.model(n), opts) Enum.each(result.solutions, &assert_solution/1) solution_count = result.statistics.solution_count diff --git a/test/examples/sat_test.exs b/test/examples/sat_test.exs new file mode 100644 index 00000000..f80fd07b --- /dev/null +++ b/test/examples/sat_test.exs @@ -0,0 +1,71 @@ +defmodule CPSolverTest.Examples.SatSolver do + @moduledoc """ + + Most test cases are borrowed from: + https://github.com/ash-project/simple_sat/blob/main/test/simple_sat_test.exs + + """ + + use ExUnit.Case + + alias CPSolver.Examples.SatSolver + + test "simple unsatisfiable" do + assert_unsatisfiable([[1], [-1]]) + end + + test "slightly more complex unsatisfiable" do + assert_unsatisfiable([[1, 2], [-1, -2], [1], [2]]) + end + + test "single variable" do + assert [1] = SatSolver.solve([[1]]) + end + + test "three variables" do + assert_satisfiable([[1, 3], [2], [1, -2, 3]]) + end + + test "many single-variable clauses" do + assert_satisfiable([[7], [-8], [6], [-5], [-4], [-3], [2], [-1]]) + end + + test "bigger instance" do + clauses = [ + [1], + [-3], + [-7], + [6], + [-5], + [-4], + [3, 2], + [1, 2], + [-7, -6, 5, 4, 3, -1, -2] + ] + assert_satisfiable(clauses) + end + + test "voting (https://github.com/bitwalker/picosat_elixir/blob/main/README.md#example)" do + assert MapSet.new([-2, 1, 3]) == SatSolver.solve([ + [1, 2, -3], + [2, 3], + [-2], + [-1, 3] + ]) |> SatSolver.to_cnf() + end + + @tag :slow + test "2 instances (50 vars, 218 clauses) from Dimacs" do + assert_satisfiable(:sat50_218) + assert_unsatisfiable(:unsat50_218) + end + + defp assert_satisfiable(clauses) do + solution = SatSolver.solve(clauses) + assert SatSolver.check_solution(solution, clauses) + end + + defp assert_unsatisfiable(clauses) do + assert :unsatisfiable == SatSolver.solve(clauses) + end +end diff --git a/test/examples/stable_marriage_test.exs b/test/examples/stable_marriage_test.exs new file mode 100644 index 00000000..476ec9c2 --- /dev/null +++ b/test/examples/stable_marriage_test.exs @@ -0,0 +1,14 @@ +defmodule CPSolverTest.Examples.StableMarriage do + use ExUnit.Case + + alias CPSolver.Examples.StableMarriage + + test "Instance: Van Hentenryck (OPL)" do + + {:ok, result} = CPSolver.solve_sync(StableMarriage.model(:van_hentenryck)) + assert result.statistics.solution_count == 3 + assert Enum.all?(result.solutions, + fn solution -> StableMarriage.check_solution(solution, :van_hentenryck) + end) + end +end diff --git a/test/examples/tsp_test.exs b/test/examples/tsp_test.exs index 281a9afe..223f67d2 100644 --- a/test/examples/tsp_test.exs +++ b/test/examples/tsp_test.exs @@ -21,25 +21,25 @@ defmodule CPSolverTest.Examples.TSP do end test "7 cities, optimality" do - model = TSP.model("data/tsp/tsp_7.txt") - {:ok, result} = CPSolver.solve_sync(model) + tsp_7_instance = "data/tsp/tsp_7.txt" + model = TSP.model(tsp_7_instance) + {:ok, result} = TSP.run(tsp_7_instance) assert Enum.all?(result.solutions, fn sol -> TSP.check_solution(sol, model) end) assert result.status == {:optimal, [objective: 56]} end - test "15 cities, first few solutions" do + test "15 cities, optimality" do model = TSP.model("data/tsp/tsp_15.txt") {:ok, result} = CPSolver.solve_sync(model, - stop_on: {:max_solutions, 3}, timeout: 5_000, - max_space_threads: 12 + space_threads: 8 ) assert Enum.all?(result.solutions, fn sol -> TSP.check_solution(sol, model) end) - assert length(result.solutions) >= 3 + assert result.status == {:optimal, [objective: 291]} end end diff --git a/test/model/model_test.exs b/test/model/model_test.exs index d1001752..acafe0ff 100644 --- a/test/model/model_test.exs +++ b/test/model/model_test.exs @@ -26,7 +26,7 @@ defmodule CpSolverTest.Model do objective: Objective.minimize(z) ) - ## Additional variable z is pulled from the Sum constraints + ## Additional variable z is pulled from the Sum constraints assert length(model.variables) == 3 ## All variables are indexed starting from 1 assert Enum.all?(Enum.with_index(model.variables, 1), fn {var, idx} -> var.index == idx end) diff --git a/test/objective/objective_test.exs b/test/objective/objective_test.exs index 15885a7b..b49578ca 100644 --- a/test/objective/objective_test.exs +++ b/test/objective/objective_test.exs @@ -4,8 +4,8 @@ defmodule CpSolverTest.Objective do alias CPSolver.Model alias CPSolver.Objective alias CPSolver.Propagator - alias CPSolver.ConstraintStore alias CPSolver.Variable.Interface + import CPSolver.Test.Helpers describe "Objective API" do test "low-level operations" do @@ -37,15 +37,15 @@ defmodule CpSolverTest.Objective do end test "Propagation and tightening" do - {:ok, [objective_variable] = _bound_vars, store} = - ConstraintStore.create_store([Variable.new(1..10)]) + {:ok, [objective_variable], _store} = + create_store([Variable.new(1..10)]) min_objective = %{propagator: min_propagator, bound_handle: min_handle} = Objective.minimize(objective_variable) assert %{changes: nil, active?: true, state: nil} == - Propagator.filter(min_propagator, store: store) + Propagator.filter(min_propagator) ## Tighten the bound (this will set the bound to objective_variable.max() - 1) Objective.tighten(min_objective) diff --git a/test/propagators/abs_test.exs b/test/propagators/abs_test.exs new file mode 100644 index 00000000..a68f35be --- /dev/null +++ b/test/propagators/abs_test.exs @@ -0,0 +1,43 @@ +defmodule CPSolverTest.Propagator.Absolute do + use ExUnit.Case + import CPSolver.Test.Helpers + + describe "Propagator filtering" do + alias CPSolver.IntVariable, as: Variable + alias CPSolver.Variable.Interface + alias CPSolver.Propagator + alias CPSolver.Propagator.Absolute + + test "filtering, initial call" do + x = -1..10 + y = -5..5 + variables = Enum.map([x, y], fn d -> Variable.new(d) end) + + {:ok, [x_var, y_var] = bound_vars, _store} = create_store(variables) + + p = Absolute.new(bound_vars) + _res = Propagator.filter(p) + ## y has negative values removed + assert Interface.min(y_var) >= 0 + ## min(y) = min(|x|) + assert Interface.min(y_var) == 0 + assert Interface.max(y_var) == 5 + ## max(y) is now min(max(|x|), max(y) (i.e. din't change in this case) + assert Interface.max(y_var) == Enum.max(y) + ## domain of x is adjusted to domain of y + assert Interface.min(x_var) == -1 + assert Interface.max(x_var) == 5 + end + + test "inconsistency, if domains y and |x| do not intersect" do + x = 1..10 + y = 11..20 + variables = Enum.map([x, y], fn d -> Variable.new(d) end) + + {:ok, bound_vars, _store} = create_store(variables) + p = Absolute.new(bound_vars) + + assert :fail = Propagator.filter(p) + end + end +end diff --git a/test/propagators/all_different_fwc_test.exs b/test/propagators/all_different_fwc_test.exs index 1deb5679..1d6a0447 100644 --- a/test/propagators/all_different_fwc_test.exs +++ b/test/propagators/all_different_fwc_test.exs @@ -7,6 +7,14 @@ defmodule CPSolverTest.Propagator.AllDifferent.FWC do alias CPSolver.Variable.Interface alias CPSolver.Propagator alias CPSolver.Propagator.AllDifferent.FWC + import CPSolver.Test.Helpers + + test "unsatisfiable" do + x = Enum.map([2, 1, 1, 3], fn val -> Variable.new(val) end) + {:ok, x_vars, _store} = ConstraintStore.create_store(x) + fwc_propagator = FWC.new(x_vars) + assert :fail == Propagator.filter(fwc_propagator) + end test "maintains the list of unfixed variables" do x = @@ -14,7 +22,7 @@ defmodule CPSolverTest.Propagator.AllDifferent.FWC do Variable.new(d, name: name) end) - {:ok, x_vars, _store} = ConstraintStore.create_store(x) + {:ok, x_vars, _store} = create_store(x) [x1_var, x2_var, x3_var, _x4_var, _x5_var] = x_vars @@ -23,23 +31,20 @@ defmodule CPSolverTest.Propagator.AllDifferent.FWC do fwc_propagator = FWC.new(x_vars) filtering_results = Propagator.filter(fwc_propagator) - %{unfixed_vars: unfixed_vars} = filtering_results.state - ## x1, x2 and x3 should be in unfixed_vars list - assert length(unfixed_vars) == 3 - + # IO.inspect(filtering_results) ## The values of fixed variables (namely, 4 and 5) have been removed from unfixed variables assert Enum.all?([x1_var, x2_var, x3_var], fn var -> Interface.max(var) == 3 end) ## Fixing one of the variables will remove the value it's fixed to from other variables :fixed = Interface.fix(x1_var, 3) - fwc_propagator_step2 = Map.put(fwc_propagator, :state, filtering_results.state) - filtering_results2 = Propagator.filter(fwc_propagator_step2) - - %{unfixed_vars: updated_unfixed_vars} = filtering_results2.state + ## Emulate changes that would come from the propagation process. + ## x1 is at position 0 in the propagator arguments + changes = %{0 => :fixed} - ## x1 had been fixed, and so is now removed from unfixed vars - assert length(updated_unfixed_vars) == 2 + fwc_propagator_step2 = Map.put(fwc_propagator, :state, filtering_results.state) + _filtering_results2 = Propagator.filter(fwc_propagator_step2, changes: changes) + # IO.inspect(filtering_results2) assert Interface.min(x2_var) == 1 && Interface.max(x2_var) == 2 assert Interface.min(x3_var) == 0 && Interface.max(x3_var) == 2 @@ -55,7 +60,7 @@ defmodule CPSolverTest.Propagator.AllDifferent.FWC do Variable.new(d, name: name) end) - {:ok, x_vars, _store} = ConstraintStore.create_store(x) + {:ok, x_vars, _store} = create_store(x) fwc_propagator = FWC.new(x_vars) %{changes: changes} = Propagator.filter(fwc_propagator) diff --git a/test/propagators/circuit_test.exs b/test/propagators/circuit_test.exs index 70d91bb7..4c4eace4 100644 --- a/test/propagators/circuit_test.exs +++ b/test/propagators/circuit_test.exs @@ -44,7 +44,7 @@ defmodule CPSolverTest.Propagator.Circuit do test "filtering" do domains = [0..2, 0..2, 0..2] propagator = make_propagator(domains) - [x0, x1, x2] = propagator.args + [x0, x1, x2] = Arrays.to_list(propagator.args) res1 = Propagator.filter(propagator) assert_initial_reduction(propagator) @@ -80,7 +80,7 @@ defmodule CPSolverTest.Propagator.Circuit do end defp assert_initial_reduction(propagator) do - n = length(propagator.args) + n = Arrays.size(propagator.args) assert Enum.all?( Enum.with_index(propagator.args), diff --git a/test/propagators/constraint_graph_test.exs b/test/propagators/constraint_graph_test.exs index d2a6d47c..0d72c311 100644 --- a/test/propagators/constraint_graph_test.exs +++ b/test/propagators/constraint_graph_test.exs @@ -4,9 +4,12 @@ defmodule CPSolverTest.Propagator.ConstraintGraph do describe "Propagator graph" do alias CPSolver.Propagator.ConstraintGraph - alias CPSolver.Constraint.AllDifferent + alias CPSolver.Constraint.AllDifferent.Binary, as: AllDifferent alias CPSolver.Constraint + alias CPSolver.Propagator alias CPSolver.IntVariable, as: Variable + alias CPSolver.Variable.Interface + alias CPSolver.DefaultDomain, as: Domain test "Build graph from AllDifferent constraint" do graph = build_graph(AllDifferent, 3) @@ -15,7 +18,9 @@ defmodule CPSolverTest.Propagator.ConstraintGraph do ## Edges: 2 per each propagator assert length(Graph.edges(graph)) == 6 ## All edges are labeled with :fixed - Enum.all?(Graph.edges(graph), fn edge -> assert edge.label == [:fixed] end) + Enum.all?(Graph.edges(graph), fn edge -> assert edge.label.propagate_on == [:fixed] end) + ## Make sure the propagators are properly bound to their variables + assert_propagator_domains(graph, 1..3) end test "Get propagators for the given variable and domain event" do @@ -30,7 +35,7 @@ defmodule CPSolverTest.Propagator.ConstraintGraph do ## For each variable, there are 2 propagators listening to ':fixed' domain change Enum.all?(variables, fn var_id -> - assert length(ConstraintGraph.get_propagator_ids(graph, var_id, :fixed)) == 2 + assert map_size(ConstraintGraph.get_propagator_ids(graph, var_id, :fixed)) == 2 end) end @@ -38,12 +43,8 @@ defmodule CPSolverTest.Propagator.ConstraintGraph do graph = build_graph(AllDifferent, 3) variables = - [v1, _v2, _v3] = - Graph.vertices(graph) - |> Enum.flat_map(fn - {:variable, v} -> [v] - _ -> [] - end) + [v1, _v2, _v3] = get_variable_ids(graph) + assert graph |> ConstraintGraph.remove_variable(v1) |> Graph.edges() |> length == 4 @@ -54,12 +55,64 @@ defmodule CPSolverTest.Propagator.ConstraintGraph do |> Graph.edges() == [] end + test "Update graph variables" do + graph = build_graph(AllDifferent, 3) + graph_variables = get_variables(graph) + + new_variables = Enum.map(graph_variables, fn v -> + Variable.copy(v) |> tap(fn c -> Variable.remove(c, 3) end) end) + + {updated_graph, _bound_propagators} = ConstraintGraph.update(graph, new_variables) + ## The domains fo variables in the graph should be updated with domains of new variables + assert Enum.all?(get_variables(updated_graph), + fn var -> + Domain.to_list(var.domain) == MapSet.new([1,2]) + end) + + ## The propagators should be bound to new variables + assert_propagator_domains(updated_graph, 1..2) + end + defp build_graph(constraint_impl, n) do domain = 1..n - variables = Enum.map(1..n, fn _ -> Variable.new(domain) end) - constraint = {constraint_impl, variables} + variables = Enum.map(1..n, fn _i -> Variable.new(domain) end) + constraint = Constraint.new(constraint_impl, variables) propagators = Constraint.constraint_to_propagators(constraint) ConstraintGraph.create(propagators) end + + defp get_variable_ids(graph) do + Graph.vertices(graph) + |> Enum.flat_map(fn + {:variable, v} -> [v] + _ -> [] + end) + end + + defp get_variables(graph) do + graph + |> get_variable_ids() + |> Enum.map(fn var_id -> ConstraintGraph.get_variable(graph, var_id) end) + end + + defp assert_propagator_domains(graph, domain) do + propagators = Enum.flat_map(Graph.vertices(graph), + fn {:propagator, p_id} -> + [ + ConstraintGraph.get_propagator(graph, p_id) + |> Propagator.bind(graph, :domain) + ] + _ -> [] + end) + + assert length(propagators) == 3 + assert Enum.all?(propagators, + fn p -> + + Enum.all?(p.args, fn arg -> + is_integer(arg) || Interface.domain(arg) |> Domain.to_list() + == MapSet.new(domain) end) + end) + end end end diff --git a/test/propagators/element2d_test.exs b/test/propagators/element2d_test.exs index 8a347796..de0c77a6 100644 --- a/test/propagators/element2d_test.exs +++ b/test/propagators/element2d_test.exs @@ -2,11 +2,11 @@ defmodule CPSolverTest.Propagator.Element2D do use ExUnit.Case describe "Propagator filtering" do - alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable alias CPSolver.Variable.Interface alias CPSolver.Propagator alias CPSolver.Propagator.Element2D + import CPSolver.Test.Helpers test "filtering" do x = -2..40 @@ -22,7 +22,8 @@ defmodule CPSolverTest.Propagator.Element2D do variables = Enum.map([x, y, z], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var, z_var] = _bound_vars, _store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = create_store(variables) + [x_var, y_var, z_var] = bound_vars propagator = Element2D.new(t, x_var, y_var, z_var) @@ -75,7 +76,8 @@ defmodule CPSolverTest.Propagator.Element2D do variables = Enum.map([x, y, z], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var, z_var], _store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = create_store(variables) + [x_var, y_var, z_var] = bound_vars ## The propagator will fail. ## D(x) = 1..2 implies filtering to {1} (because T is 2x2, and it's a 0-based index) ## This leaves only the second row for the values of z, which is inconsistent with D(z). @@ -97,7 +99,8 @@ defmodule CPSolverTest.Propagator.Element2D do variables = Enum.map([x, y, z], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var, z_var] = _bound_vars, _store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = create_store(variables) + [x_var, y_var, z_var] = bound_vars propagator = Element2D.new(t, x_var, y_var, z_var) diff --git a/test/propagators/less_or_equal_test.exs b/test/propagators/less_or_equal_test.exs index aad7bd42..7a653647 100644 --- a/test/propagators/less_or_equal_test.exs +++ b/test/propagators/less_or_equal_test.exs @@ -2,10 +2,10 @@ defmodule CPSolverTest.Propagator.LessOrEqual do use ExUnit.Case describe "Propagator filtering" do - alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable alias CPSolver.Propagator - alias CPSolver.Propagator.LessOrEqual + alias CPSolver.Propagator.{Less, LessOrEqual} + import CPSolver.Test.Helpers test "filtering" do ## Both vars are unfixed @@ -13,8 +13,10 @@ defmodule CPSolverTest.Propagator.LessOrEqual do y = -5..5 variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var] = bound_vars, _store} = ConstraintStore.create_store(variables) - %{changes: changes} = Propagator.filter(LessOrEqual.new(bound_vars)) + {:ok, bound_vars, _store} = create_store(variables) + vars = [x_var, y_var] = bound_vars + + %{changes: changes} = Propagator.filter(LessOrEqual.new(vars)) assert Map.get(changes, x_var.id) == :max_change assert Map.get(changes, y_var.id) == :min_change assert 0 == Variable.min(x_var) @@ -30,20 +32,20 @@ defmodule CPSolverTest.Propagator.LessOrEqual do ## Inconsistency: no solution to x <= y variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, bound_vars, _store} = ConstraintStore.create_store(variables, space: nil) + {:ok, bound_vars, _store} = create_store(variables) ## The propagator will fail on one of the variables - assert catch_throw(LessOrEqual.filter(bound_vars)) == :fail + assert Propagator.filter(LessOrEqual.new(bound_vars)) == :fail end test "offset" do x = 0..10 y = -10..0 variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var], _store} = ConstraintStore.create_store(variables) - + {:ok, bound_vars, _store} = create_store(variables) + [x_var, y_var] = bound_vars # (x <= y + 5) offset = 5 - LessOrEqual.filter([x_var, y_var, offset]) + Propagator.filter(LessOrEqual.new([x_var, y_var, offset])) ## The domain of (y+5) variable is -5..5 assert 0 == Variable.min(x_var) assert 5 == Variable.max(x_var) @@ -56,16 +58,29 @@ defmodule CPSolverTest.Propagator.LessOrEqual do y = 2..4 variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var], _store} = ConstraintStore.create_store(variables) - refute :passive == LessOrEqual.filter([x_var, y_var]) + {:ok, bound_vars, _store} = create_store(variables) + [x_var, y_var] = bound_vars + + refute %{active: false} == Propagator.filter(LessOrEqual.new([x_var, y_var])) ## Cut domain of x so it intersects with domain of y in exactly one point Variable.removeAbove(x_var, 2) - {:state, state} = LessOrEqual.filter([x_var, y_var]) - refute state.active? + result = Propagator.filter(LessOrEqual.new([x_var, y_var])) + refute result.active? ## Cut domain of x so it does not intersect with domain of y Variable.remove(x_var, 2) - assert :passive == LessOrEqual.filter([x_var, y_var], state) + assert %{active?: false} = Propagator.filter(LessOrEqual.new([x_var, y_var])) + end + + test "Less" do + x = 1 + y = 1 + + [x_var, y_var] = Enum.map([x, y], fn d -> Variable.new(d) end) + + less_propagator = Propagator.new(Less, [x_var, y_var]) + + assert Propagator.filter(less_propagator) == :fail end end end diff --git a/test/propagators/modulo_test.exs b/test/propagators/modulo_test.exs new file mode 100644 index 00000000..8c5e636d --- /dev/null +++ b/test/propagators/modulo_test.exs @@ -0,0 +1,104 @@ +defmodule CPSolverTest.Propagator.Modulo do + use ExUnit.Case + import CPSolver.Test.Helpers + + describe "Propagator filtering" do + alias CPSolver.IntVariable, as: Variable + alias CPSolver.DefaultDomain, as: Domain + alias CPSolver.Variable.Interface + alias CPSolver.Propagator + alias CPSolver.Propagator.Modulo + + test "filtering, initial call" do + ## Both vars are unfixed + x = 1..10 + y = -5..5 + m = -10..10 + variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) + + {:ok, [m_var, _x_var, y_var] = bound_vars, _store} = create_store(variables) + # before filtering + assert Interface.contains?(y_var, 0) + assert Interface.min(m_var) == -10 + + p = Modulo.new(bound_vars) + _res = Propagator.filter(p) + ## y has 0 removed + refute Interface.contains?(y_var, 0) + + ## Nothing is fixed + refute Enum.any?(bound_vars, fn var -> Interface.fixed?(var) end) + end + + test "filtering, dividend and divisor fixed" do + x = -7 + y = 3 + m = -10..10 + variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) + + {:ok, [m_var, _x_var, _y_var] = bound_vars, _store} = create_store(variables) + p = Modulo.new(bound_vars) + res = Propagator.filter(p) + + assert res.changes == %{m_var.id => :fixed} + + ## Modulo is fixed to x % y + ## Dividend and modulo have the same sign + assert Interface.fixed?(m_var) && Interface.min(m_var) == rem(x, y) + end + + test "filtering, modulo and dividend fixed" do + x = -10 + y = -100..100 + m = -2 + variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) + + {:ok, [_m_var, _x_var, y_var] = bound_vars, _store} = create_store(variables) + p = Modulo.new(bound_vars) + res = Propagator.filter(p) + refute res == :fail + ## All values in domain of y satisfy x % y = m + assert Enum.all?(Domain.to_list(y_var.domain), fn y_val -> + rem(x, y_val) == m + end) + end + + test "filtering, modulo and divider fixed" do + x = -100..100 + y = -10 + m = -2 + variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) + + {:ok, [_m_var, x_var, _y_var] = bound_vars, _store} = create_store(variables) + p = Modulo.new(bound_vars) + res = Propagator.filter(p) + refute res == :fail + ## All values in domain of x satisfy x % y = m + assert Enum.all?(Domain.to_list(x_var.domain), fn x_val -> rem(x_val, y) == m end) + end + + test "inconsistency, if modulo and dividend are fixed to values of different sign" do + x = 10 + y = -100..100 + m = -2 + variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) + + {:ok, bound_vars, _store} = create_store(variables) + p = Modulo.new(bound_vars) + + assert :fail = Propagator.filter(p) + end + + test "inconsistency, if every modulo value has a different sign with every divident value" do + m = 1..10 + y = -100..100 + x = -10..-1 + variables = Enum.map([m, x, y], fn d -> Variable.new(d) end) + + {:ok, bound_vars, _store} = create_store(variables) + p = Modulo.new(bound_vars) + + assert :fail = Propagator.filter(p) + end + end +end diff --git a/test/propagators/not_equal_test.exs b/test/propagators/not_equal_test.exs index ca75e231..a26bd10a 100644 --- a/test/propagators/not_equal_test.exs +++ b/test/propagators/not_equal_test.exs @@ -2,10 +2,11 @@ defmodule CPSolverTest.Propagator.NotEqual do use ExUnit.Case describe "Propagator filtering" do - alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable alias CPSolver.Propagator.Variable, as: PropagatorVariable + alias CPSolver.Propagator alias CPSolver.Propagator.NotEqual + import CPSolver.Test.Helpers test "propagation events" do x = 1..10 @@ -20,14 +21,14 @@ defmodule CPSolverTest.Propagator.NotEqual do y = -5..5 variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, bound_vars, _store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = create_store(variables) assert :stable == reset_and_filter(bound_vars) refute PropagatorVariable.get_variable_ops() [x_var, y_var] = bound_vars ## Fix one of vars assert :fixed = Variable.fix(x_var, 5) - assert :max_change == reset_and_filter(bound_vars) + assert %{active?: false} = reset_and_filter(bound_vars) assert PropagatorVariable.get_variable_ops() == %{y_var.id => :max_change} ## The filtering should have removed '5' from y_var @@ -36,7 +37,7 @@ defmodule CPSolverTest.Propagator.NotEqual do ## Fix second var and filter again assert :fixed == Variable.fix(y_var, 4) - assert :no_change == reset_and_filter(bound_vars) + assert %{active?: false} = reset_and_filter(bound_vars) refute PropagatorVariable.get_variable_ops() ## Make sure filtering doesn't fail on further calls refute Enum.any?( @@ -45,7 +46,7 @@ defmodule CPSolverTest.Propagator.NotEqual do ) ## Consequent filtering does not trigger domain change events - assert :no_change == reset_and_filter(bound_vars) + assert %{active?: false} = reset_and_filter(bound_vars) end test "inconsistency" do @@ -53,8 +54,8 @@ defmodule CPSolverTest.Propagator.NotEqual do y = 0..0 variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, bound_vars, _store} = ConstraintStore.create_store(variables, space: nil) - assert catch_throw(NotEqual.filter(bound_vars)) == :fail + {:ok, bound_vars, _store} = create_store(variables) + assert Propagator.filter(NotEqual.new(bound_vars)) == :fail assert PropagatorVariable.get_variable_ops() == nil end @@ -62,24 +63,24 @@ defmodule CPSolverTest.Propagator.NotEqual do x = 5..5 y = -5..10 variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var], _store} = ConstraintStore.create_store(variables) - + {:ok, bound_vars, _store} = create_store(variables) + [x_var, y_var] = bound_vars assert Variable.contains?(y_var, 0) # (x != y + 5) offset = 5 - NotEqual.filter([x_var, y_var, offset]) + Propagator.filter(NotEqual.new([x_var, y_var, offset])) refute Variable.contains?(y_var, 0) # (x != y - 5) offset = -5 assert Variable.contains?(y_var, 10) - NotEqual.filter([x_var, y_var, offset]) + Propagator.filter(NotEqual.new([x_var, y_var, offset])) refute Variable.contains?(y_var, 10) end defp reset_and_filter(args) do PropagatorVariable.reset_variable_ops() - NotEqual.filter(args) + Propagator.filter(NotEqual.new(args)) end end end diff --git a/test/propagators/propagator_test.exs b/test/propagators/propagator_test.exs index 9ed45fb9..673f8219 100644 --- a/test/propagators/propagator_test.exs +++ b/test/propagators/propagator_test.exs @@ -3,9 +3,11 @@ defmodule CPSolverTest.Propagator do describe "Propagator general" do alias CPSolver.IntVariable, as: Variable + alias CPSolver.Variable.Interface alias CPSolver.Propagator.{NotEqual, LessOrEqual} alias CPSolver.ConstraintStore alias CPSolver.Propagator + import CPSolver.Test.Helpers import CPSolver.Variable.View.Factory @@ -28,7 +30,7 @@ defmodule CPSolverTest.Propagator do assert Variable.fixed?(x_bound) propagator = NotEqual.new(bound_variables) - assert %{changes: %{y_bound.id => :fixed}, active?: true, state: nil} == + assert %{changes: %{y_bound.id => :fixed}, active?: false, state: nil} == Propagator.filter(propagator) assert ConstraintStore.get(store, y_bound, :fixed?) @@ -39,8 +41,10 @@ defmodule CPSolverTest.Propagator do y = 0..10 variables = Enum.map([x, y], fn d -> Variable.new(d) end) - {:ok, [x_var, y_var] = _bound_vars, _store} = - ConstraintStore.create_store(variables, space: nil) + {:ok, bound_vars, _store} = + create_store(variables) + + [x_var, y_var] = bound_vars ## Make 'minus' view minus_y_view = minus(y_var) @@ -50,9 +54,64 @@ defmodule CPSolverTest.Propagator do assert :fail = Propagator.filter(LessOrEqual.new(x_var, minus_y_view)) end + test "dry run (reduction)" do + # `dry_run` option tests the result of the propagator filtering, + # but does not change space variables + %{bound_variables: bound_variables} = + setup_store([1..1, 1..2]) + + [x_bound, y_bound] = bound_variables + + assert Variable.fixed?(x_bound) + refute Variable.fixed?(y_bound) + + propagator = NotEqual.new(bound_variables) + + ## Dry-run first + {_p_copy, dry_run_result} = Propagator.dry_run(propagator) + assert dry_run_result == %{changes: %{y_bound.id => :fixed}, active?: false, state: nil} + + # Store variables didn't change + assert Variable.fixed?(x_bound) + refute Variable.fixed?(y_bound) + + ## Real run now + real_run_result = Propagator.filter(propagator) + ## The results of dry run vs. real run + assert dry_run_result == real_run_result + + ## Variables are fixed, as expected + assert Variable.fixed?(x_bound) + assert Variable.fixed?(y_bound) + end + + test "dry run (inconsistency, view)" do + x = 1..10 + y = 0..10 + variables = Enum.map([x, y], fn d -> Variable.new(d) end) + + {:ok, bound_vars, _store} = + create_store(variables) + + [x_var, y_var] = bound_vars + + minus_y_view = minus(y_var) + {_p_copy, res} = Propagator.dry_run(LessOrEqual.new(x_var, minus_y_view)) + + ## Should fail, because `minus` view turns `y` domain to -10..0 + assert res == :fail + ## ...but the domains of variables stay intact + assert 10 == Interface.size(x_var) && (11 = Interface.size(y_var)) + + ## Now, filter for real + assert :fail == Propagator.filter(LessOrEqual.new(x_var, minus_y_view)) + ## At least one variable is now in :fail state + assert catch_throw(10 == Interface.size(x_var) && (11 = Interface.size(y_var))) == :fail + end + defp setup_store(domains) do variables = Enum.map(domains, fn d -> Variable.new(d) end) - {:ok, bound_variables, store} = ConstraintStore.create_store(variables) + {:ok, bound_variables, store} = create_store(variables) %{variables: variables, bound_variables: bound_variables, store: store} end end diff --git a/test/propagators/sum_test.exs b/test/propagators/sum_test.exs index e99d96ed..0c6a0546 100644 --- a/test/propagators/sum_test.exs +++ b/test/propagators/sum_test.exs @@ -2,12 +2,12 @@ defmodule CPSolverTest.Propagator.Sum do use ExUnit.Case describe "Propagator filtering" do - alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable alias CPSolver.Propagator alias CPSolver.Propagator.Sum import CPSolver.Variable.Interface import CPSolver.Variable.View.Factory + import CPSolver.Test.Helpers test "The domain bounds of 'sum' variable are reduced to the sum of bounds of the summands" do y = Variable.new(-100..100, name: "y") @@ -17,9 +17,10 @@ defmodule CPSolverTest.Propagator.Sum do Variable.new(d, name: name) end) - {:ok, [y_var | x_vars] = _bound_vars, store} = ConstraintStore.create_store([y | x]) + {:ok, bound_vars, _store} = create_store([y | x]) + [y_var | x_vars] = bound_vars - Propagator.filter(Sum.new(y_var, x_vars), store: store) + Propagator.filter(Sum.new(y_var, x_vars)) assert 1 == min(y_var) assert 15 == max(y_var) @@ -33,13 +34,15 @@ defmodule CPSolverTest.Propagator.Sum do Variable.new(d, name: name) end) - {:ok, [y_var | x_vars] = _bound_vars, store} = ConstraintStore.create_store([y | x]) + {:ok, bound_vars, _store} = create_store([y | x]) + + [y_var | x_vars] = bound_vars [x1_var, _x2_var, _x3_var] = x_vars sum_propagator = Sum.new(y_var, x_vars) - Propagator.filter(sum_propagator, store: store) + Propagator.filter(sum_propagator) assert -3 == min(x1_var) assert 0 == min(y_var) @@ -54,13 +57,14 @@ defmodule CPSolverTest.Propagator.Sum do Variable.new(d, name: name) end) - {:ok, [y_var | x_vars] = _bound_vars, store} = ConstraintStore.create_store([y | x]) + {:ok, bound_vars, _store} = create_store([y | x]) + [y_var | x_vars] = bound_vars [x1_var, _x2_var, x3_var] = x_vars sum_propagator = Sum.new(y_var, x_vars) - Propagator.filter(sum_propagator, store: store) + Propagator.filter(sum_propagator) assert 4 == max(x1_var) assert 3 == min(x1_var) @@ -73,10 +77,12 @@ defmodule CPSolverTest.Propagator.Sum do x1 = Variable.new(0..4, name: "x1") x2 = Variable.new(0..5, name: "x2") - {:ok, [y_var, x1_var, x2_var] = _bound_vars, store} = - ConstraintStore.create_store([y, x1, x2]) + {:ok, bound_vars, _store} = + create_store([y, x1, x2]) + + [y_var, x1_var, x2_var] = bound_vars - assert :fail == Propagator.filter(Sum.new(y_var, [x1_var, x2_var]), store: store) + assert :fail == Propagator.filter(Sum.new(y_var, [x1_var, x2_var])) end test "when summands are views" do @@ -84,8 +90,10 @@ defmodule CPSolverTest.Propagator.Sum do x1 = Variable.new(0..2, name: "x1") x2 = Variable.new(1..2, name: "x2") - {:ok, [y_var, x1_var, x2_var] = _bound_vars, store} = - ConstraintStore.create_store([y, x1, x2]) + {:ok, bound_vars, store} = + create_store([y, x1, x2]) + + [y_var, x1_var, x2_var] = bound_vars refute :fail == Propagator.filter(Sum.new(y_var, [mul(x1_var, 10), mul(x2_var, 20)]), store: store) diff --git a/test/search/search_strategy_test.exs b/test/search/search_strategy_test.exs index af68519e..beda54a0 100644 --- a/test/search/search_strategy_test.exs +++ b/test/search/search_strategy_test.exs @@ -2,10 +2,10 @@ defmodule CPSolverTest.Search.FirstFail do use ExUnit.Case describe "First-fail search strategy" do - alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable alias CPSolver.DefaultDomain, as: Domain alias CPSolver.Search.Strategy, as: SearchStrategy + import CPSolver.Test.Helpers test ":first_fail and :indomain_min" do v0_values = 0..0 @@ -17,7 +17,7 @@ defmodule CPSolverTest.Search.FirstFail do values = [v0_values, v1_values, v2_values, v3_values, v4_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, bound_vars, _store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = create_store(variables) # first_fail chooses unfixed variable selected_variable = SearchStrategy.select_variable(bound_vars, :first_fail) @@ -25,7 +25,7 @@ defmodule CPSolverTest.Search.FirstFail do assert selected_variable.id == v2_var.id # indomain_min splits domain of selected variable into min and the rest of the domain - {:ok, [min_value_partition, no_min_partition]} = + {:ok, [{min_value_partition, _equal_constraint}, {no_min_partition, _not_equal_constraint}]} = SearchStrategy.partition(selected_variable, :indomain_min) min_value = Domain.min(min_value_partition) @@ -43,7 +43,7 @@ defmodule CPSolverTest.Search.FirstFail do values = [v0_values, v1_values, v2_values, v3_values, v4_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, _bound_vars, _store} = ConstraintStore.create_store(variables) + {:ok, _bound_vars, _store} = create_store(variables) assert catch_throw(SearchStrategy.select_variable(variables, :first_fail)) == SearchStrategy.all_vars_fixed_exception() @@ -59,21 +59,24 @@ defmodule CPSolverTest.Search.FirstFail do values = [v0_values, v1_values, v2_values, v3_values, v4_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, bound_vars, _store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = create_store(variables) [b_left, b_right] = branches = SearchStrategy.branch(bound_vars, {:first_fail, :indomain_min}) refute b_left == b_right ## Each branch has the same number of variables, as the original list of vars - assert Enum.all?(branches, fn branch -> length(branch) == length(variables) end) + assert Enum.all?(branches, fn {branch, _constraint} -> + length(branch) == length(variables) + end) + ## Left branch contains v2 variable fixed at 0 - assert Enum.at(b_left, 2) + assert Enum.at(b_left |> elem(0), 2) |> Map.get(:domain) |> then(fn domain -> Domain.size(domain) == 1 && Domain.min(domain) == 0 end) ## Right branch contains v2 variable with 0 removed - refute Enum.at(b_right, 2) |> Map.get(:domain) |> Domain.contains?(0) + refute Enum.at(b_right |> elem(0), 2) |> Map.get(:domain) |> Domain.contains?(0) end end end diff --git a/test/solver/cpsolver_test.exs b/test/solver/cpsolver_test.exs index 51ca3691..d76bfaa4 100644 --- a/test/solver/cpsolver_test.exs +++ b/test/solver/cpsolver_test.exs @@ -25,24 +25,26 @@ defmodule CpSolverTest do [NotEqual.new(x, y)] ) - {:ok, solver} = CPSolver.solve(model) + {:ok, res} = CPSolver.solve_sync(model) - Process.sleep(100) - - assert CPSolver.statistics(solver).failure_count == 0 + assert res.statistics.failure_count == 0 ## Note: there are 2 "first fail" distributions: - ## 1. Variable 'x' triggers distribution into 2 spaces - (x: 1, y: [0, 1]) and (x: 2, y: [0, 1])). + ## 1. Choice of variable 'x' triggers distribution into 2 spaces - (x: 1, y: [0, 1]) and (x: 2, y: [0, 1])). ## 2. First space produces solution (x: 1, y: 0) ## 3. Second space triggers distribution into 2 spaces - (x: 2, y: 0) and (x: 2, y: 1) ## 4. These 2 spaces produce remaining solutions. - ## 5. There have been 5 spaces - top one, and 4 as described above, which corresponds - ## to 5 nodes. - assert CPSolver.statistics(solver).node_count == 5 - assert CPSolver.statistics(solver).solution_count == 3 + # + ## Note 2: for the second space, the child spaces are not being created anymore, + ## as NotEqual is passive in that space (x: 2, y: [0, 1]). + ## So the solutions here are deducted by cartesian product of domains, which gives + ## solutions (x: 2, y: 0) and (x: 2, y: 1). + ## Finally, we have only 3 nodes: top one, and two child spaces (p.2, p.3). + ## + assert res.statistics.node_count == 3 + assert res.statistics.solution_count == 3 solutions = - solver - |> CPSolver.solutions() + res.solutions |> Enum.sort_by(fn [x, y] -> x + y end) assert solutions == [[1, 0], [2, 0], [2, 1]] @@ -100,6 +102,8 @@ defmodule CpSolverTest do end ) + File.close(@solution_handler_test_file) + solutions_from_file = @solution_handler_test_file |> File.read!() diff --git a/test/solver/distributed_test.exs b/test/solver/distributed_test.exs index c64c8495..c07bf099 100644 --- a/test/solver/distributed_test.exs +++ b/test/solver/distributed_test.exs @@ -2,6 +2,8 @@ defmodule CPSolverTest.Distributed do use ExUnit.Case alias CPSolver.Examples.Sudoku + @moduletag :slow + setup do # {:ok, spawned} = ExUnited.spawn([:test_worker1, :test_worker2, :test_worker3]) :ok = LocalCluster.start() diff --git a/test/solver/shared_test.exs b/test/solver/shared_test.exs index 24da5f61..2ecd888b 100644 --- a/test/solver/shared_test.exs +++ b/test/solver/shared_test.exs @@ -5,7 +5,7 @@ defmodule CPSolverTest.Shared do test "space thread checkins/checkouts" do max_threads = 3 - shared = Shared.init_shared_data(max_space_threads: max_threads) + shared = Shared.init_shared_data(space_threads: max_threads) ## No threads were checked out refute Shared.checkin_space_thread(shared) assert Enum.all?(1..max_threads, fn _ -> Shared.checkout_space_thread(shared) end) diff --git a/test/space/space_propagation_test.exs b/test/space/space_propagation_test.exs index 8fc91db6..00b310a1 100644 --- a/test/space/space_propagation_test.exs +++ b/test/space/space_propagation_test.exs @@ -2,87 +2,46 @@ defmodule CPSolverTest.SpacePropagation do use ExUnit.Case alias CPSolver.IntVariable, as: Variable + alias CPSolver.DefaultDomain, as: Domain alias CPSolver.Propagator.NotEqual - alias CPSolver.ConstraintStore alias CPSolver.Space.Propagation alias CPSolver.Propagator alias CPSolver.Propagator.ConstraintGraph + import CPSolver.Test.Helpers test "Propagation on stable space" do %{ - propagators: propagators, - variables: [_x, y, _z] = variables, + propagators: _propagators, + variables: [x, y, z] = _variables, constraint_graph: graph, store: store } = stable_setup() - {:stable, constraint_graph} = Propagation.run(propagators, graph, store) - assert Graph.num_vertices(constraint_graph) == 3 - - assert [y] == - Enum.filter(variables, fn var -> - Graph.has_vertex?(constraint_graph, {:variable, var.id}) - end) + :solved = Propagation.run(graph, store) - ## In stable state, variables referenced in constraint graph are unfixed. + assert Variable.fixed?(x) && Variable.fixed?(z) + ## Check not_equal(x, z) + assert Variable.min(x) != Variable.min(z) refute Variable.fixed?(y) - propagators_from_graph = - Enum.flat_map( - Graph.vertices(constraint_graph), - fn - {:propagator, id} -> [ConstraintGraph.get_propagator(constraint_graph, id)] - _ -> [] - end - ) - - assert length(propagators_from_graph) == 2 - - propagator_vars_in_graph = - Enum.map(propagators_from_graph, fn %{mod: NotEqual, args: vars} = _v -> - Enum.map(vars, fn v -> v.name end) - end) - - ## Both propagators in constraint graph have "y" variable - assert Enum.all?(propagator_vars_in_graph, fn vars -> "y" in vars end) + ## All values of reduced domain of 'y' participate in proper solutions. + assert Enum.all?(Variable.domain(y) |> Domain.to_list(), fn y_value -> + y_value != Variable.min(x) && y_value != Variable.min(z) + end) end test "Propagation on solvable space" do - %{propagators: propagators, variables: variables, constraint_graph: graph, store: store} = + %{variables: variables, constraint_graph: graph, store: store} = solved_setup() refute Enum.all?(variables, fn var -> Variable.fixed?(var) end) - assert :solved == Propagation.run(propagators, graph, store) + assert :solved == Propagation.run(graph, store) assert Enum.all?(variables, fn var -> Variable.fixed?(var) end) end test "Propagation on failed space" do - %{propagators: propagators, constraint_graph: graph, store: store} = fail_setup() - assert :fail == Propagation.run(propagators, graph, store) - end - - test "Propagation pass" do - x = 1..1 - y = 1..2 - z = 1..3 - %{propagators: propagators, constraint_graph: graph, store: store} = space_setup(x, y, z) - {scheduled_propagators, reduced_graph} = Propagation.propagate(propagators, graph, store) - - ## Propagators are not being rescheduled - ## as a result of their own filtering (idempotency). - ## - ## Only NotEqual(y, z) is rescheduled. - ## Explanation: - ## - NotEqual(x, y) changes y => schedules NotEqual(y,z); - ## - NotEqual(x, z) changes z => schedules NotEqual(y,z); - ## - NotEqual(y, z) changes z and/or y (if not called first) as a result of it's own filtering. - ## So, at no point NotEqual(x, y) and NotEqual(x, z) are being rescheduled. - - [not_equal_y_z_reference] = MapSet.to_list(scheduled_propagators) - not_equal_y_z = ConstraintGraph.get_propagator(reduced_graph, not_equal_y_z_reference) - assert not_equal_y_z.mod == NotEqual - - assert not_equal_y_z.name == "y != z" + %{constraint_graph: graph, store: store} = fail_setup() + assert :fail == Propagation.run(graph, store) end defp stable_setup() do @@ -113,8 +72,10 @@ defmodule CPSolverTest.SpacePropagation do variables = Enum.map([{x, "x"}, {y, "y"}, {z, "z"}], fn {d, name} -> Variable.new(d, name: name) end) - {:ok, [x_var, y_var, z_var] = bound_vars, store} = - ConstraintStore.create_store(variables) + {:ok, bound_vars, store} = + create_store(variables) + + bound_vars = [x_var, y_var, z_var] = bound_vars propagators = Enum.map( diff --git a/test/store/store_test.exs b/test/store/store_test.exs index ab576ca6..9c913e97 100644 --- a/test/store/store_test.exs +++ b/test/store/store_test.exs @@ -4,6 +4,7 @@ defmodule CPSolverTest.Store do describe "Store" do alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable + import CPSolver.Test.Helpers test "create variables in the space" do v1_values = 1..10 @@ -11,7 +12,7 @@ defmodule CPSolverTest.Store do values = [v1_values, v2_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, bound_vars, _store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = create_store(variables) ## Bound vars have space and ids assigned assert Enum.all?(bound_vars, fn var -> var end) end @@ -23,7 +24,7 @@ defmodule CPSolverTest.Store do values = [v1_values, v2_values, v3_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, bound_vars, store} = ConstraintStore.create_store(variables) + {:ok, bound_vars, store} = create_store(variables) # Min assert Enum.all?(Enum.zip(bound_vars, values), fn {var, vals} -> ConstraintStore.get(store, var, :min) == Enum.min(vals) @@ -57,7 +58,7 @@ defmodule CPSolverTest.Store do values = [v1_values, v2_values, v3_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, bound_vars, store} = ConstraintStore.create_store(variables, space: nil) + {:ok, bound_vars, store} = create_store(variables) [v1, v2, v3] = bound_vars # remove diff --git a/test/test_helper.exs b/test/test_helper.exs index c7598b23..0c919bf3 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -5,4 +5,9 @@ defmodule CPSolver.Test.Helpers do def number_of_occurences(string, pattern) do string |> String.split(pattern) |> length() |> Kernel.-(1) end + + def create_store(variables) do + {:ok, bound_vars, store} = CPSolver.ConstraintStore.create_store(variables) + {:ok, Arrays.to_list(bound_vars), store} + end end diff --git a/test/variables/interface_test.exs b/test/variables/interface_test.exs index af28d5c0..5a2a9f0f 100644 --- a/test/variables/interface_test.exs +++ b/test/variables/interface_test.exs @@ -2,12 +2,12 @@ defmodule CPSolverTest.Variable.Interface do use ExUnit.Case describe "Views" do - alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable alias CPSolver.DefaultDomain, as: Domain alias CPSolver.Variable.Interface import CPSolver.Variable.View.Factory + import CPSolver.Test.Helpers test "view vs variable" do v1_values = 1..10 @@ -15,8 +15,10 @@ defmodule CPSolverTest.Variable.Interface do values = [v1_values, v2_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, [var1, _var2] = bound_vars, _store} = - ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = + create_store(variables) + + [var1, _var2] = bound_vars [view1, view2] = Enum.map(bound_vars, fn var -> minus(var) end) @@ -68,12 +70,12 @@ defmodule CPSolverTest.Variable.Interface do ## Fix and Fixed? ## - assert Interface.domain(var1) |> Domain.to_list() |> Enum.sort() == [2, 3, 4, 5] + assert Interface.domain(var1) |> Domain.to_list() == MapSet.new([2, 3, 4, 5]) assert :fixed == Interface.fix(var1, 2) assert Interface.fixed?(var1) assert :fail == catch_throw(Interface.fix(var1, 1)) - assert Interface.domain(view2) == [-5, -4, -3, -2] + assert Interface.domain(view2) == MapSet.new([-5, -4, -3, -2]) assert :fixed == Interface.fix(view2, -2) assert Interface.fixed?(view2) assert :fail == catch_throw(Interface.fix(view2, 1)) diff --git a/test/variables/view_test.exs b/test/variables/view_test.exs index 41303dbd..88433908 100644 --- a/test/variables/view_test.exs +++ b/test/variables/view_test.exs @@ -4,11 +4,12 @@ defmodule CPSolverTest.Variable.View do alias CPSolver.DefaultDomain, as: Domain describe "Views" do - alias CPSolver.ConstraintStore alias CPSolver.IntVariable, as: Variable + alias CPSolver.BooleanVariable alias CPSolver.Variable.View alias CPSolver.Variable.Interface import CPSolver.Variable.View.Factory + import CPSolver.Test.Helpers test "'minus' view" do v1_values = 1..10 @@ -18,9 +19,10 @@ defmodule CPSolverTest.Variable.View do values = [v1_values, v2_values, v3_values, v4_values] variables = Enum.map(values, fn d -> Variable.new(d) end) - {:ok, [source_var, var2, _var3, _var4] = bound_vars, _store} = - ConstraintStore.create_store(variables) + {:ok, bound_vars, _store} = + create_store(variables) + [source_var, var2, _var3, _var4] = bound_vars views = [view1, view2, view3, view4] = Enum.map(bound_vars, fn var -> minus(var) end) ## Domains of variables that back up views do not change assert Variable.min(source_var) == 1 @@ -66,11 +68,11 @@ defmodule CPSolverTest.Variable.View do assert -10 == View.min(view1) ## Remove below - assert Domain.to_list(View.domain(view1)) == [-10, -9, -8, -7, -6, -4] + assert Domain.to_list(View.domain(view1)) == MapSet.new([-10, -9, -8, -7, -6, -4]) ## Same as for removeAbove, :max_change reflects the domain change for the variable, ## and not the view. assert :max_change == View.removeBelow(view1, -7) - assert Domain.to_list(View.domain(view1)) == [-7, -6, -4] + assert Domain.to_list(View.domain(view1)) == MapSet.new([-7, -6, -4]) assert :fixed == View.removeBelow(view1, -4) assert -4 == View.min(view1) @@ -84,8 +86,8 @@ defmodule CPSolverTest.Variable.View do test "'mul' view" do domain = 1..10 - {:ok, [source_var] = _bound_vars, _store} = - ConstraintStore.create_store([Variable.new(domain)]) + {:ok, [source_var], _store} = + create_store([Variable.new(domain)]) view1 = mul(source_var, 1) view2 = mul(source_var, 10) @@ -121,9 +123,44 @@ defmodule CPSolverTest.Variable.View do assert View.fixed?(view1) && View.fixed?(view3) && Variable.fixed?(source_var) end + test "'not' view" do + bool_var1 = BooleanVariable.new() + bool_var2 = BooleanVariable.new() + {:ok, _, _store} = + create_store([bool_var1, bool_var2]) + + not_view1 = negation(bool_var1) + not_view2 = negation(bool_var2) + + refute Interface.fixed?(not_view1) + refute Interface.fixed?(not_view2) + + Interface.fix(bool_var1, 1) + assert Interface.fixed?(not_view1) + assert Interface.min(not_view1) == 0 + + Interface.fix(bool_var2, 0) + assert Interface.fixed?(not_view2) + assert Interface.min(not_view2) == 1 + end + + test "chained views" do + domain = 1..10 + + {:ok, [source_var], _store} = + create_store([Variable.new(domain)]) + + view1 = minus(source_var) + view2 = minus(view1) + view3 = mul(view2, 10) + + assert Interface.min(source_var) == Interface.min(view2) + assert Interface.min(source_var) * 10 == Interface.min(view3) + end + test "remove value that falls in the hole" do - {:ok, [x] = _bound_vars, _store} = - ConstraintStore.create_store([Variable.new(0..5, name: "x")]) + {:ok, [x], _store} = + create_store([Variable.new(0..5, name: "x")]) y_plus = mul(x, 20) y_minus = mul(x, -20) @@ -155,7 +192,10 @@ defmodule CPSolverTest.Variable.View do end defp compare_domains(d1, d2, map_fun) do - Enum.zip(Domain.to_list(d1) |> Enum.sort(:desc), Domain.to_list(d2) |> Enum.sort(:asc)) + Enum.zip( + Domain.to_list(d1) |> Enum.sort(:desc), + Domain.to_list(d2) |> Enum.sort(:asc) + ) |> Enum.all?(fn {val1, val2} -> val2 == map_fun.(val1) end) end end