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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b136df3119babb39b3950a0cc96b393d3344453e
4
- data.tar.gz: c10c1db2ceda4d4a553b209e7fceb433375eeabd
2
+ SHA256:
3
+ metadata.gz: accbe29e2a4b319fb17079fed3b14a6c6516222f546323195f6f6d0c65fc6edc
4
+ data.tar.gz: 151ea768f38489232668bb53e64bde84ec76d9569a84c7c286b04b554f566054
5
5
  SHA512:
6
- metadata.gz: d47e3a5f91c608f5c5ad49a649bc9aabf9fd29a243ce0efffa89610a2ba1ad37c68ea026cefbf0b42a1fd0574887af20a50820bd357a0c85f5bd8fe5670ea0a9
7
- data.tar.gz: f4a9e5f2c31ef671a50618eb0236d15f59861cf39c2659c980dc34e2e7123f4a84eff5c7c4602220ef0da16ca82eb4541050ffbdeadde0b623601e147b848469
6
+ metadata.gz: aa0872cce4702b580118d1a4741f9c3be0f835ad3dfb484e1087e76c7456bb0e9af854a58056fca30ec83d9c4bd851a9fe79f6ba7f09a3560bdb9c7054c8b40e
7
+ data.tar.gz: da8ff827c889a0e431d1b45a863b9718b4e7e71d7c96db5f69698df499ffb622de63c2da5322e678cc60618caae5a776a67653ef48c0dd30f8a3dfaed1d4272e
data/.gitignore CHANGED
@@ -24,7 +24,6 @@ build/
24
24
  /.bundle/
25
25
  /lib/bundler/man/
26
26
 
27
- Gemfile.lock
28
27
  .ruby-version
29
28
  .ruby-gemset
30
29
 
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.3.6
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
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 though a set of constraints.
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
+ [![Gem Version](https://badge.fury.io/rb/winston.svg)](http://badge.fury.io/rb/winston) [![Build Status](https://travis-ci.org/dmnelson/winston.svg)](https://travis-ci.org/dmnelson/winston) [![Code Climate](https://codeclimate.com/github/dmnelson/winston/badges/gpa.svg)](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 attend all the constraints.
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 would be evaluated only when all those variables are set and one
65
- of them has changed; Or globals, in which case, they'd evaluated for every assignment.
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
- assignmets.values.uniq.size == assignments.keys.size # checks if every value is unique
245
+ assignments.values.uniq.size == assignments.keys.size # checks if every value is unique
82
246
  end
83
247
  ```
84
248
 
85
- Constraint can be also set as their own objects, that's great for reusability.
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'll be recomendable to use of ways to reduce the number of iterations, for example, removing an item from a shared domain
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 `specs/examples` for more usage examples.
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
- - Create a DSL for setting up the problem
121
- - Currently only algorithm to solve the CSP is Backtracking, implement other like Local search, Constraint propagation, ...
122
- - Implement heuristics to improve search time (least constraining value, minimum remaining values,...)
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
@@ -1 +1,12 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default do
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ Rake::Task["spec"].execute
7
+ end
8
+
9
+ desc "Run benchmarks"
10
+ task :bench do
11
+ ruby "bench/run.rb"
12
+ end
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)