winston 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +0 -1
- data/.tool-versions +1 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +36 -0
- data/README.md +190 -14
- data/Rakefile +11 -0
- data/bench/run.rb +310 -0
- data/lib/winston/constraint.rb +18 -5
- data/lib/winston/constraints/all_different.rb +2 -1
- data/lib/winston/constraints/not_in_list.rb +18 -0
- data/lib/winston/csp.rb +39 -4
- data/lib/winston/dsl.rb +69 -0
- data/lib/winston/heuristics.rb +44 -0
- data/lib/winston/solvers/backtrack.rb +91 -0
- data/lib/winston/solvers/mac.rb +529 -0
- data/lib/winston/solvers/min_conflicts.rb +125 -0
- data/lib/winston.rb +9 -5
- data/spec/examples/map_coloring_spec.rb +48 -0
- data/spec/examples/sudoku_spec.rb +120 -0
- data/spec/winston/backtrack_spec.rb +34 -1
- data/spec/winston/constraint_spec.rb +46 -1
- data/spec/winston/constraints/all_different_spec.rb +12 -7
- data/spec/winston/constraints/not_in_list_spec.rb +35 -0
- data/spec/winston/csp_spec.rb +45 -2
- data/spec/winston/dsl_spec.rb +71 -0
- data/spec/winston/heuristics_spec.rb +47 -0
- data/spec/winston/mac_spec.rb +44 -0
- data/spec/winston/min_conflicts_spec.rb +44 -0
- data/winston.gemspec +6 -5
- metadata +51 -21
- data/lib/winston/backtrack.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: accbe29e2a4b319fb17079fed3b14a6c6516222f546323195f6f6d0c65fc6edc
|
|
4
|
+
data.tar.gz: 151ea768f38489232668bb53e64bde84ec76d9569a84c7c286b04b554f566054
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa0872cce4702b580118d1a4741f9c3be0f835ad3dfb484e1087e76c7456bb0e9af854a58056fca30ec83d9c4bd851a9fe79f6ba7f09a3560bdb9c7054c8b40e
|
|
7
|
+
data.tar.gz: da8ff827c889a0e431d1b45a863b9718b4e7e71d7c96db5f69698df499ffb622de63c2da5322e678cc60618caae5a776a67653ef48c0dd30f8a3dfaed1d4272e
|
data/.gitignore
CHANGED
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 3.3.6
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Winston
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
http://github.com/dmnelson/winston
|
|
5
|
+
|
|
6
|
+
CHANGELOG
|
|
7
|
+
---------
|
|
8
|
+
|
|
9
|
+
### 0.1.0
|
|
10
|
+
* Added MAC (Maintaining Arc Consistency) solver with GAC for AllDifferent.
|
|
11
|
+
* Added min-conflicts solver.
|
|
12
|
+
* Added heuristics (MRV/LCV) and forward checking support.
|
|
13
|
+
* Added DSL for defining CSPs, named domains, and named constraints.
|
|
14
|
+
* Added benchmark suite and solver selection via `CSP#solve`.
|
|
15
|
+
* Updated gemspec metadata and Ruby version requirement.
|
|
16
|
+
|
|
17
|
+
### 0.0.2
|
|
18
|
+
* Added NotInList constraint.
|
|
19
|
+
* Added Sudoku example spec.
|
|
20
|
+
* Added 'allow_nil' option for constraints, so not all variables are necessarily required.
|
|
21
|
+
* Changed AllDifferent constraint to be restricted to given variables when not global
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
winston (0.0.2)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
diff-lcs (1.6.2)
|
|
10
|
+
rake (13.2.1)
|
|
11
|
+
rspec (3.13.2)
|
|
12
|
+
rspec-core (~> 3.13.0)
|
|
13
|
+
rspec-expectations (~> 3.13.0)
|
|
14
|
+
rspec-mocks (~> 3.13.0)
|
|
15
|
+
rspec-core (3.13.6)
|
|
16
|
+
rspec-support (~> 3.13.0)
|
|
17
|
+
rspec-expectations (3.13.5)
|
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
19
|
+
rspec-support (~> 3.13.0)
|
|
20
|
+
rspec-mocks (3.13.7)
|
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
22
|
+
rspec-support (~> 3.13.0)
|
|
23
|
+
rspec-support (3.13.7)
|
|
24
|
+
|
|
25
|
+
PLATFORMS
|
|
26
|
+
arm64-darwin-24
|
|
27
|
+
ruby
|
|
28
|
+
|
|
29
|
+
DEPENDENCIES
|
|
30
|
+
bundler (>= 2.4.22)
|
|
31
|
+
rake (>= 13.1)
|
|
32
|
+
rspec (>= 3.13)
|
|
33
|
+
winston!
|
|
34
|
+
|
|
35
|
+
BUNDLED WITH
|
|
36
|
+
2.5.22
|
data/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Winston
|
|
2
2
|
|
|
3
3
|
[Constraint Satisfaction Problem](http://en.wikipedia.org/wiki/Constraint_satisfaction_problem) (CSP) implementation for Ruby.
|
|
4
|
-
It provides a useful way to solve problems like resource allocation or planning
|
|
4
|
+
It provides a useful way to solve problems like resource allocation or planning through a set of constraints.
|
|
5
5
|
|
|
6
6
|
The most common example of usage for CSPs is probably the game [Sudoku](http://en.wikipedia.org/wiki/Sudoku).
|
|
7
7
|
|
|
8
|
+
[](http://badge.fury.io/rb/winston) [](https://travis-ci.org/dmnelson/winston) [](https://codeclimate.com/github/dmnelson/winston)
|
|
9
|
+
|
|
8
10
|
## Installation
|
|
9
11
|
|
|
10
12
|
Add this line to your application's Gemfile:
|
|
@@ -22,7 +24,7 @@ Or install it yourself as:
|
|
|
22
24
|
## Usage
|
|
23
25
|
|
|
24
26
|
The problem consists of three sets of information: Domain, Variables and Constraints. It will try to determine a value
|
|
25
|
-
from the given domain for each variable that will
|
|
27
|
+
from the given domain for each variable that will satisfy all the constraints.
|
|
26
28
|
|
|
27
29
|
```ruby
|
|
28
30
|
require 'winston'
|
|
@@ -42,6 +44,167 @@ csp.solve
|
|
|
42
44
|
= { a: 6, b: 4, c: 3 }
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
### Solvers and heuristics
|
|
48
|
+
|
|
49
|
+
The default solver is backtracking, and you can configure it with heuristics to speed up search on harder problems.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
solver = Winston::Solvers::Backtrack.new(
|
|
53
|
+
csp,
|
|
54
|
+
variable_strategy: :mrv,
|
|
55
|
+
value_strategy: :lcv,
|
|
56
|
+
forward_checking: true
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
csp.solve(solver)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You can also pass your own strategies as lambdas:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
custom_var = ->(vars, assignments, csp) { vars.first }
|
|
66
|
+
custom_val = ->(values, var, assignments, csp) { values.reverse }
|
|
67
|
+
|
|
68
|
+
solver = Winston::Solvers::Backtrack.new(
|
|
69
|
+
csp,
|
|
70
|
+
variable_strategy: custom_var,
|
|
71
|
+
value_strategy: custom_val
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Built-in heuristic helpers are also available:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
solver = Winston::Solvers::Backtrack.new(
|
|
79
|
+
csp,
|
|
80
|
+
variable_strategy: Winston::Heuristics.mrv,
|
|
81
|
+
value_strategy: Winston::Heuristics.lcv,
|
|
82
|
+
forward_checking: Winston::Heuristics.forward_checking
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can also use a local-search solver (min-conflicts), which is often fast on large problems:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
solver = Winston::Solvers::MinConflicts.new(csp, max_steps: 10_000)
|
|
90
|
+
csp.solve(solver)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Min-conflicts is not complete, so it may return `false` even if a solution exists.
|
|
94
|
+
|
|
95
|
+
For stronger pruning, you can use MAC (Maintaining Arc Consistency), which enforces arc consistency during search:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
solver = Winston::Solvers::MAC.new(csp, variable_strategy: :mrv, value_strategy: :lcv)
|
|
99
|
+
csp.solve(solver)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
MAC is complete and usually faster than plain backtracking on constrained problems, but can be slower on very small ones.
|
|
103
|
+
|
|
104
|
+
#### Solver selection guide
|
|
105
|
+
|
|
106
|
+
- Use `:backtrack` for small problems or when you want deterministic depth-first search.
|
|
107
|
+
- Use `:mac` for tighter constraints or when backtracking explores too many dead ends.
|
|
108
|
+
- Use `:min_conflicts` for large problems where you want speed and can tolerate incompleteness.
|
|
109
|
+
|
|
110
|
+
### DSL
|
|
111
|
+
|
|
112
|
+
You can build problems using a small DSL:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
csp = Winston.define do
|
|
116
|
+
domain :digits, (1..9).to_a
|
|
117
|
+
|
|
118
|
+
var :a, domain: :digits
|
|
119
|
+
var :b, domain: :digits
|
|
120
|
+
var :c, domain: :digits
|
|
121
|
+
|
|
122
|
+
constraint(:a, :c) { |a, c| a == c * 2 }
|
|
123
|
+
constraint(:a, :b) { |a, b| a > b }
|
|
124
|
+
constraint(:b, :c) { |b, c| b > c }
|
|
125
|
+
constraint(:b) { |b| b.even? }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
csp.solve
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
You can select a solver by name from any CSP instance:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
csp.solve(:backtrack, variable_strategy: :mrv)
|
|
135
|
+
csp.solve(:mac, value_strategy: :lcv)
|
|
136
|
+
csp.solve(:min_conflicts, max_steps: 10_000)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Named solvers:
|
|
140
|
+
- `:backtrack`
|
|
141
|
+
- `:mac`
|
|
142
|
+
- `:min_conflicts`
|
|
143
|
+
|
|
144
|
+
#### DSL Reference
|
|
145
|
+
|
|
146
|
+
`Winston.define { ... }` builds and returns a `Winston::CSP`.
|
|
147
|
+
|
|
148
|
+
DSL methods:
|
|
149
|
+
- `domain :name, values` registers a named domain.
|
|
150
|
+
- `var :name, domain: <values or :name>, value: <preset>, &block` adds a variable.
|
|
151
|
+
- `constraint(*vars, allow_nil: false) { |*values, assignments| ... }` adds a custom constraint.
|
|
152
|
+
- `use_constraint :name, *vars, allow_nil: false, **options` adds a named constraint.
|
|
153
|
+
|
|
154
|
+
Notes:
|
|
155
|
+
- Domains are static. Use constraints for dynamic behavior.
|
|
156
|
+
- `value:` presets a variable and is validated before search starts.
|
|
157
|
+
- `allow_nil: true` lets a constraint run even if some variables are unset.
|
|
158
|
+
|
|
159
|
+
#### Named Domains
|
|
160
|
+
|
|
161
|
+
Named domains reduce repetition and keep variable declarations clean:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
Winston.define do
|
|
165
|
+
domain :digits, (1..9).to_a
|
|
166
|
+
var :a, domain: :digits
|
|
167
|
+
var :b, domain: :digits
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Named Constraints
|
|
172
|
+
|
|
173
|
+
Built-in named constraints:
|
|
174
|
+
- `:all_different`
|
|
175
|
+
- `:not_in_list`
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
Winston.define do
|
|
179
|
+
var :a, domain: [1, 2]
|
|
180
|
+
var :b, domain: [1, 2]
|
|
181
|
+
use_constraint :all_different, :a, :b
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Register custom constraints:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
Winston.register_constraint(:all_twos) do |variables, allow_nil, **_options|
|
|
189
|
+
Class.new(Winston::Constraint) do
|
|
190
|
+
def validate(assignments)
|
|
191
|
+
values = values_at(assignments)
|
|
192
|
+
values.all? { |v| v == 2 }
|
|
193
|
+
end
|
|
194
|
+
end.new(variables: variables, allow_nil: allow_nil)
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use them in the DSL:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
Winston.define do
|
|
202
|
+
var :a, domain: [1, 2]
|
|
203
|
+
var :b, domain: [1, 2]
|
|
204
|
+
use_constraint :all_twos, :a, :b
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
45
208
|
### Variables and Domain
|
|
46
209
|
|
|
47
210
|
It's possible to preset values for variables, and in that case the problem would not try to determine values for
|
|
@@ -51,7 +214,8 @@ it, but it will take those values into account for validating the constraints.
|
|
|
51
214
|
csp.add_variable "my_var", value: "predefined value"
|
|
52
215
|
```
|
|
53
216
|
|
|
54
|
-
And it's also possible to set the domain as `Proc` so it'd be evaluated on-demand.
|
|
217
|
+
And it's also possible to set the domain as `Proc` so it'd be evaluated on-demand. Domains are static and do not
|
|
218
|
+
receive partial assignments; use constraints for dynamic behavior.
|
|
55
219
|
|
|
56
220
|
```ruby
|
|
57
221
|
csp.add_variable("other_var") { |var_name, csp| [:a, :b, :c ] }
|
|
@@ -61,8 +225,8 @@ csp.add_variable("other_var", domain: proc { |var_name, csp| [:a, :b, :c ] })
|
|
|
61
225
|
|
|
62
226
|
### Constraints
|
|
63
227
|
|
|
64
|
-
Constraints can be set for specific variables and
|
|
65
|
-
|
|
228
|
+
Constraints can be set for specific variables and are evaluated based on the active solver strategy. Global
|
|
229
|
+
constraints are evaluated for every assignment; some solvers (like MAC) also use constraints to prune domains.
|
|
66
230
|
|
|
67
231
|
```ruby
|
|
68
232
|
csp.add_constraint(:a) { |a| a > 0 } # positive value
|
|
@@ -78,11 +242,11 @@ end
|
|
|
78
242
|
# hash with all current assignments
|
|
79
243
|
|
|
80
244
|
csp.add_constraint do |assignments|
|
|
81
|
-
|
|
245
|
+
assignments.values.uniq.size == assignments.keys.size # checks if every value is unique
|
|
82
246
|
end
|
|
83
247
|
```
|
|
84
248
|
|
|
85
|
-
|
|
249
|
+
Constraints can also be set as their own objects, which is great for reusability.
|
|
86
250
|
|
|
87
251
|
```ruby
|
|
88
252
|
csp.add_constraint constraint: MyConstraint.new(...)
|
|
@@ -90,7 +254,7 @@ csp.add_constraint constraint: MyConstraint.new(...)
|
|
|
90
254
|
csp.add_constraint constraint: Winston::Constraints::AllDifferent.new # built-in constraint, checks if all values are different from each other
|
|
91
255
|
```
|
|
92
256
|
|
|
93
|
-
### Problems without solution
|
|
257
|
+
### Problems without a solution
|
|
94
258
|
|
|
95
259
|
```ruby
|
|
96
260
|
require 'winston'
|
|
@@ -108,18 +272,30 @@ csp.solve
|
|
|
108
272
|
```
|
|
109
273
|
|
|
110
274
|
**IMPORTANT NOTE: Depending on the number of variables and the size of the domain it can take a long time to test all different possibilities.
|
|
111
|
-
In that case it'
|
|
112
|
-
when it is tested ( A `queue` type of structure, that would `pop` the value on the `each` block).**
|
|
275
|
+
In that case it's recommended to use heuristics or stronger solvers like MAC to reduce the number of iterations.**
|
|
113
276
|
|
|
114
277
|
### More examples
|
|
115
278
|
|
|
116
|
-
Check the folder `
|
|
279
|
+
Check the folder `spec/examples` for more usage examples.
|
|
280
|
+
The `spec/examples/map_coloring_spec.rb` example is a good starting point for small graph problems, and it demonstrates
|
|
281
|
+
using the MAC solver via `csp.solve(:mac, ...)`.
|
|
282
|
+
|
|
283
|
+
### Benchmarks
|
|
284
|
+
|
|
285
|
+
Run benchmarks with:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
rake bench
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The benchmarks live in `bench/run.rb` and compare backtracking, MAC, and min-conflicts on a few sample problems.
|
|
292
|
+
Benchmarks are run with a fixed timeout and number of runs to make results comparable.
|
|
117
293
|
|
|
118
294
|
## TODOs / Nice-to-haves
|
|
119
295
|
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
296
|
+
- Add more named constraints (sum, all_equal, in_range, ...)
|
|
297
|
+
- Add additional inference techniques (backjumping, nogood recording, ...)
|
|
298
|
+
- Add more solver examples and benchmarks
|
|
123
299
|
|
|
124
300
|
## Contributing
|
|
125
301
|
|
data/Rakefile
CHANGED
data/bench/run.rb
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
require "benchmark"
|
|
2
|
+
require "timeout"
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
5
|
+
require "winston"
|
|
6
|
+
|
|
7
|
+
TIMEOUT_SECONDS = 10
|
|
8
|
+
RUNS = 5
|
|
9
|
+
|
|
10
|
+
def timed(label)
|
|
11
|
+
seconds = nil
|
|
12
|
+
status = :ok
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
Timeout.timeout(TIMEOUT_SECONDS) do
|
|
16
|
+
samples = []
|
|
17
|
+
RUNS.times do
|
|
18
|
+
samples << Benchmark.realtime { yield }
|
|
19
|
+
end
|
|
20
|
+
seconds = samples
|
|
21
|
+
end
|
|
22
|
+
rescue Timeout::Error
|
|
23
|
+
status = :timeout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if status == :ok
|
|
27
|
+
avg = seconds.sum / seconds.size
|
|
28
|
+
min = seconds.min
|
|
29
|
+
max = seconds.max
|
|
30
|
+
puts format("%-28s %0.4fs (avg, %0.4f-%0.4f)", label, avg, min, max)
|
|
31
|
+
else
|
|
32
|
+
puts format("%-28s %s", label, "timeout (#{TIMEOUT_SECONDS}s)")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def map_coloring_csp
|
|
37
|
+
Winston.define do
|
|
38
|
+
domain :colors, %i[red green blue]
|
|
39
|
+
|
|
40
|
+
var :western_australia, domain: :colors
|
|
41
|
+
var :northern_territory, domain: :colors
|
|
42
|
+
var :south_australia, domain: :colors
|
|
43
|
+
var :queensland, domain: :colors
|
|
44
|
+
var :new_south_wales, domain: :colors
|
|
45
|
+
var :victoria, domain: :colors
|
|
46
|
+
var :tasmania, domain: :colors
|
|
47
|
+
|
|
48
|
+
constraint(:western_australia, :northern_territory) { |wa, nt| wa != nt }
|
|
49
|
+
constraint(:western_australia, :south_australia) { |wa, sa| wa != sa }
|
|
50
|
+
constraint(:northern_territory, :south_australia) { |nt, sa| nt != sa }
|
|
51
|
+
constraint(:northern_territory, :queensland) { |nt, q| nt != q }
|
|
52
|
+
constraint(:south_australia, :queensland) { |sa, q| sa != q }
|
|
53
|
+
constraint(:south_australia, :new_south_wales) { |sa, nsw| sa != nsw }
|
|
54
|
+
constraint(:south_australia, :victoria) { |sa, v| sa != v }
|
|
55
|
+
constraint(:queensland, :new_south_wales) { |q, nsw| q != nsw }
|
|
56
|
+
constraint(:new_south_wales, :victoria) { |nsw, v| nsw != v }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dense_graph_coloring_csp(nodes: 18, colors: %i[red green blue], edge_probability: 0.6, seed: 42)
|
|
61
|
+
# Dense constraint graph to compare propagation vs backtracking.
|
|
62
|
+
random = Random.new(seed)
|
|
63
|
+
csp = Winston::CSP.new
|
|
64
|
+
nodes.times do |i|
|
|
65
|
+
csp.add_variable(:"v#{i}", domain: colors)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
edges = []
|
|
69
|
+
(0...nodes).to_a.combination(2).each do |i, j|
|
|
70
|
+
next unless random.rand < edge_probability
|
|
71
|
+
edges << [i, j]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
edges.each do |i, j|
|
|
75
|
+
csp.add_constraint(:"v#{i}", :"v#{j}") { |a, b| a != b }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
csp
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def n_queens_csp(n)
|
|
82
|
+
# AllDifferent-heavy problem to showcase GAC propagation.
|
|
83
|
+
csp = Winston::CSP.new
|
|
84
|
+
rows = (0...n).to_a
|
|
85
|
+
rows.each { |row| csp.add_variable(:"row_#{row}", domain: rows) }
|
|
86
|
+
csp.add_constraint constraint: Winston::Constraints::AllDifferent.new(variables: rows.map { |r| :"row_#{r}" })
|
|
87
|
+
|
|
88
|
+
rows.combination(2).each do |i, j|
|
|
89
|
+
csp.add_constraint(:"row_#{i}", :"row_#{j}") do |ci, cj|
|
|
90
|
+
(ci - cj).abs != (i - j)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
csp
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def random_binary_csp(vars: 50, domain: (1..5).to_a, edge_probability: 0.15, allowed_ratio: 0.5, seed: 42)
|
|
97
|
+
# Random binary CSP near phase transition.
|
|
98
|
+
random = Random.new(seed)
|
|
99
|
+
csp = Winston::CSP.new
|
|
100
|
+
vars.times { |i| csp.add_variable(:"x#{i}", domain: domain) }
|
|
101
|
+
|
|
102
|
+
allowed_pairs = {}
|
|
103
|
+
domain.each do |a|
|
|
104
|
+
domain.each do |b|
|
|
105
|
+
allowed_pairs[[a, b]] = random.rand < allowed_ratio
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
(0...vars).to_a.combination(2).each do |i, j|
|
|
110
|
+
next unless random.rand < edge_probability
|
|
111
|
+
csp.add_constraint(:"x#{i}", :"x#{j}") do |x, y|
|
|
112
|
+
allowed_pairs[[x, y]]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
csp
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def hard_binary_csp
|
|
120
|
+
# Larger binary CSP to test search vs propagation overhead.
|
|
121
|
+
random_binary_csp(vars: 80, domain: (1..6).to_a, edge_probability: 0.35, allowed_ratio: 0.35, seed: 1337)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def sudoku_csp
|
|
125
|
+
# Medium puzzle for balanced comparison.
|
|
126
|
+
puzzle = [
|
|
127
|
+
[8, 0, 0, 1, 0, 4, 0, 9, 0],
|
|
128
|
+
[9, 6, 0, 0, 2, 7, 1, 0, 8],
|
|
129
|
+
[3, 4, 1, 6, 0, 0, 7, 5, 0],
|
|
130
|
+
[5, 0, 3, 4, 0, 8, 0, 7, 1],
|
|
131
|
+
[4, 7, 0, 0, 1, 3, 6, 0, 9],
|
|
132
|
+
[6, 1, 8, 9, 0, 2, 0, 3, 0],
|
|
133
|
+
[0, 0, 0, 2, 3, 5, 0, 1, 4],
|
|
134
|
+
[0, 0, 4, 7, 0, 6, 8, 0, 3],
|
|
135
|
+
[0, 0, 9, 8, 4, 1, 5, 0, 7]
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
csp = Winston::CSP.new
|
|
139
|
+
digits = (1..9).to_a
|
|
140
|
+
(0...9).each do |r|
|
|
141
|
+
(0...9).each do |c|
|
|
142
|
+
value = puzzle[r][c]
|
|
143
|
+
name = :"r#{r}c#{c}"
|
|
144
|
+
if value == 0
|
|
145
|
+
csp.add_variable(name, domain: digits)
|
|
146
|
+
else
|
|
147
|
+
csp.add_variable(name, value: value)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
rows = (0...9).map { |r| (0...9).map { |c| :"r#{r}c#{c}" } }
|
|
153
|
+
cols = (0...9).map { |c| (0...9).map { |r| :"r#{r}c#{c}" } }
|
|
154
|
+
boxes = []
|
|
155
|
+
[0, 3, 6].each do |br|
|
|
156
|
+
[0, 3, 6].each do |bc|
|
|
157
|
+
box = []
|
|
158
|
+
(0...3).each do |r|
|
|
159
|
+
(0...3).each do |c|
|
|
160
|
+
box << :"r#{br + r}c#{bc + c}"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
boxes << box
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
(rows + cols + boxes).each do |group|
|
|
168
|
+
group.combination(2).each do |a, b|
|
|
169
|
+
csp.add_constraint(a, b) { |x, y| x != y }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
csp
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def sudoku_hard_csp
|
|
177
|
+
# Harder puzzle with more branching.
|
|
178
|
+
puzzle = [
|
|
179
|
+
[0, 0, 0, 2, 6, 0, 7, 0, 1],
|
|
180
|
+
[6, 8, 0, 0, 7, 0, 0, 9, 0],
|
|
181
|
+
[1, 9, 0, 0, 0, 4, 5, 0, 0],
|
|
182
|
+
[8, 2, 0, 1, 0, 0, 0, 4, 0],
|
|
183
|
+
[0, 0, 4, 6, 0, 2, 9, 0, 0],
|
|
184
|
+
[0, 5, 0, 0, 0, 3, 0, 2, 8],
|
|
185
|
+
[0, 0, 9, 3, 0, 0, 0, 7, 4],
|
|
186
|
+
[0, 4, 0, 0, 5, 0, 0, 3, 6],
|
|
187
|
+
[7, 0, 3, 0, 1, 8, 0, 0, 0]
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
csp = Winston::CSP.new
|
|
191
|
+
digits = (1..9).to_a
|
|
192
|
+
(0...9).each do |r|
|
|
193
|
+
(0...9).each do |c|
|
|
194
|
+
value = puzzle[r][c]
|
|
195
|
+
name = :"r#{r}c#{c}"
|
|
196
|
+
if value == 0
|
|
197
|
+
csp.add_variable(name, domain: digits)
|
|
198
|
+
else
|
|
199
|
+
csp.add_variable(name, value: value)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
rows = (0...9).map { |r| (0...9).map { |c| :"r#{r}c#{c}" } }
|
|
205
|
+
cols = (0...9).map { |c| (0...9).map { |r| :"r#{r}c#{c}" } }
|
|
206
|
+
boxes = []
|
|
207
|
+
[0, 3, 6].each do |br|
|
|
208
|
+
[0, 3, 6].each do |bc|
|
|
209
|
+
box = []
|
|
210
|
+
(0...3).each do |r|
|
|
211
|
+
(0...3).each do |c|
|
|
212
|
+
box << :"r#{br + r}c#{bc + c}"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
boxes << box
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
(rows + cols + boxes).each do |group|
|
|
220
|
+
group.combination(2).each do |a, b|
|
|
221
|
+
csp.add_constraint(a, b) { |x, y| x != y }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
csp
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def sudoku_very_hard_csp
|
|
229
|
+
# Very hard puzzle to stress propagation.
|
|
230
|
+
puzzle = [
|
|
231
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 1],
|
|
232
|
+
[0, 0, 0, 0, 2, 0, 0, 0, 0],
|
|
233
|
+
[0, 0, 1, 0, 0, 9, 0, 0, 0],
|
|
234
|
+
[0, 0, 0, 5, 0, 0, 0, 4, 0],
|
|
235
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
236
|
+
[0, 6, 0, 0, 0, 1, 0, 0, 0],
|
|
237
|
+
[0, 0, 0, 9, 0, 0, 8, 0, 0],
|
|
238
|
+
[0, 0, 0, 0, 7, 0, 0, 0, 0],
|
|
239
|
+
[7, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
csp = Winston::CSP.new
|
|
243
|
+
digits = (1..9).to_a
|
|
244
|
+
(0...9).each do |r|
|
|
245
|
+
(0...9).each do |c|
|
|
246
|
+
value = puzzle[r][c]
|
|
247
|
+
name = :"r#{r}c#{c}"
|
|
248
|
+
if value == 0
|
|
249
|
+
csp.add_variable(name, domain: digits)
|
|
250
|
+
else
|
|
251
|
+
csp.add_variable(name, value: value)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
rows = (0...9).map { |r| (0...9).map { |c| :"r#{r}c#{c}" } }
|
|
257
|
+
cols = (0...9).map { |c| (0...9).map { |r| :"r#{r}c#{c}" } }
|
|
258
|
+
boxes = []
|
|
259
|
+
[0, 3, 6].each do |br|
|
|
260
|
+
[0, 3, 6].each do |bc|
|
|
261
|
+
box = []
|
|
262
|
+
(0...3).each do |r|
|
|
263
|
+
(0...3).each do |c|
|
|
264
|
+
box << :"r#{br + r}c#{bc + c}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
boxes << box
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
(rows + cols + boxes).each do |group|
|
|
272
|
+
group.combination(2).each do |a, b|
|
|
273
|
+
csp.add_constraint(a, b) { |x, y| x != y }
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
csp
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def run_case(name, csp)
|
|
281
|
+
puts "\n#{name}"
|
|
282
|
+
timed("Backtrack") { csp.solve(:backtrack) }
|
|
283
|
+
timed("MAC (default)") { csp.solve(:mac) }
|
|
284
|
+
timed("MAC (mrv)") { csp.solve(:mac, variable_strategy: :mrv) }
|
|
285
|
+
timed("Min-Conflicts") do
|
|
286
|
+
csp.solve(:min_conflicts, max_steps: 10_000, random: Random.new(1))
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
run_case("Map coloring (Australia)", map_coloring_csp)
|
|
291
|
+
run_case(
|
|
292
|
+
"Dense graph coloring (18)",
|
|
293
|
+
dense_graph_coloring_csp(nodes: 18, edge_probability: 0.7, seed: 42)
|
|
294
|
+
)
|
|
295
|
+
run_case(
|
|
296
|
+
"Dense graph coloring (30)",
|
|
297
|
+
dense_graph_coloring_csp(nodes: 30, edge_probability: 0.8, seed: 42)
|
|
298
|
+
)
|
|
299
|
+
run_case(
|
|
300
|
+
"Random binary CSP (50)",
|
|
301
|
+
random_binary_csp(vars: 50, edge_probability: 0.15, allowed_ratio: 0.5, seed: 42)
|
|
302
|
+
)
|
|
303
|
+
run_case(
|
|
304
|
+
"Hard binary CSP (80)",
|
|
305
|
+
hard_binary_csp
|
|
306
|
+
)
|
|
307
|
+
run_case("N-Queens (8)", n_queens_csp(8))
|
|
308
|
+
run_case("Sudoku (medium)", sudoku_csp)
|
|
309
|
+
run_case("Sudoku (hard)", sudoku_hard_csp)
|
|
310
|
+
run_case("Sudoku (very hard)", sudoku_very_hard_csp)
|