winston 0.0.2 → 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: 86cee2ee3ce5ed70188c907b9889a640f7ea6458
4
- data.tar.gz: 6b9601b07bdf710141e1b9894c6b8e7b8389b694
2
+ SHA256:
3
+ metadata.gz: accbe29e2a4b319fb17079fed3b14a6c6516222f546323195f6f6d0c65fc6edc
4
+ data.tar.gz: 151ea768f38489232668bb53e64bde84ec76d9569a84c7c286b04b554f566054
5
5
  SHA512:
6
- metadata.gz: 24362a9745841f0f85b255309412ee32a815ff8da790f5028c74285381908203ed11fc1231b4f1eca5185fce2f3d3d8d7aeeffbe428c0f927aabc5b68a661cf8
7
- data.tar.gz: 90f64cc3d08170211a819d379162f029874010cf7a660d21b21f8fb6113be8dbccadb6b77f9231704328c5c149b24aa115b4f9bf6000731e3a88903b0506b158
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/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ http://github.com/dmnelson/winston
6
6
  CHANGELOG
7
7
  ---------
8
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
+
9
17
  ### 0.0.2
10
18
  * Added NotInList constraint.
11
19
  * Added Sudoku example spec.
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,7 +1,7 @@
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
 
@@ -24,7 +24,7 @@ Or install it yourself as:
24
24
  ## Usage
25
25
 
26
26
  The problem consists of three sets of information: Domain, Variables and Constraints. It will try to determine a value
27
- 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.
28
28
 
29
29
  ```ruby
30
30
  require 'winston'
@@ -44,6 +44,167 @@ csp.solve
44
44
  = { a: 6, b: 4, c: 3 }
45
45
  ```
46
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
+
47
208
  ### Variables and Domain
48
209
 
49
210
  It's possible to preset values for variables, and in that case the problem would not try to determine values for
@@ -53,7 +214,8 @@ it, but it will take those values into account for validating the constraints.
53
214
  csp.add_variable "my_var", value: "predefined value"
54
215
  ```
55
216
 
56
- 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.
57
219
 
58
220
  ```ruby
59
221
  csp.add_variable("other_var") { |var_name, csp| [:a, :b, :c ] }
@@ -63,8 +225,8 @@ csp.add_variable("other_var", domain: proc { |var_name, csp| [:a, :b, :c ] })
63
225
 
64
226
  ### Constraints
65
227
 
66
- Constraints can be set for specific variables and would be evaluated only when all those variables are set and one
67
- 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.
68
230
 
69
231
  ```ruby
70
232
  csp.add_constraint(:a) { |a| a > 0 } # positive value
@@ -80,11 +242,11 @@ end
80
242
  # hash with all current assignments
81
243
 
82
244
  csp.add_constraint do |assignments|
83
- 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
84
246
  end
85
247
  ```
86
248
 
87
- 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.
88
250
 
89
251
  ```ruby
90
252
  csp.add_constraint constraint: MyConstraint.new(...)
@@ -92,7 +254,7 @@ csp.add_constraint constraint: MyConstraint.new(...)
92
254
  csp.add_constraint constraint: Winston::Constraints::AllDifferent.new # built-in constraint, checks if all values are different from each other
93
255
  ```
94
256
 
95
- ### Problems without solution
257
+ ### Problems without a solution
96
258
 
97
259
  ```ruby
98
260
  require 'winston'
@@ -110,18 +272,30 @@ csp.solve
110
272
  ```
111
273
 
112
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.
113
- 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
114
- 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.**
115
276
 
116
277
  ### More examples
117
278
 
118
- 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.
119
293
 
120
294
  ## TODOs / Nice-to-haves
121
295
 
122
- - Create a DSL for setting up the problem
123
- - Currently only algorithm to solve the CSP is Backtracking, implement other like Local search, Constraint propagation, ...
124
- - 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
125
299
 
126
300
  ## Contributing
127
301
 
data/Rakefile CHANGED
@@ -5,3 +5,8 @@ task :default do
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
  Rake::Task["spec"].execute
7
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)
data/lib/winston/csp.rb CHANGED
@@ -8,8 +8,12 @@ module Winston
8
8
  @constraints = []
9
9
  end
10
10
 
11
- def solve(solver = Backtrack.new(self))
12
- solver.search(var_assignments)
11
+ def solve(solver = nil, **options)
12
+ initial = var_assignments
13
+ return false unless validate_initial_assignments(initial)
14
+
15
+ solver_instance = build_solver(solver, options)
16
+ solver_instance.search(initial)
13
17
  end
14
18
 
15
19
  def add_variable(name, value: nil, domain: nil, &block)
@@ -44,5 +48,29 @@ module Winston
44
48
  assignments
45
49
  end
46
50
  end
51
+
52
+ def build_solver(solver, options)
53
+ return solver if solver && !solver.is_a?(Symbol)
54
+
55
+ case solver
56
+ when nil, :backtrack
57
+ Solvers::Backtrack.new(self, **options)
58
+ when :mac
59
+ Solvers::MAC.new(self, **options)
60
+ when :min_conflicts
61
+ Solvers::MinConflicts.new(self, **options)
62
+ else
63
+ raise ArgumentError, "Unknown solver :#{solver}"
64
+ end
65
+ end
66
+
67
+ def validate_initial_assignments(assignments)
68
+ constraints.all? do |constraint|
69
+ next constraint.validate(assignments) if constraint.global || constraint.allow_nil
70
+ next true unless constraint.variables.all? { |v| assignments.key?(v) }
71
+
72
+ constraint.validate(assignments)
73
+ end
74
+ end
47
75
  end
48
76
  end