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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "winston"
|
|
2
|
+
|
|
3
|
+
describe "Sudoku Example" do
|
|
4
|
+
let(:csp) { Winston::CSP.new }
|
|
5
|
+
|
|
6
|
+
describe "Unique values for every row, column and quadrant" do
|
|
7
|
+
before do
|
|
8
|
+
domain = [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
9
|
+
|
|
10
|
+
# Adding variables
|
|
11
|
+
1.upto(9).each do |x|
|
|
12
|
+
1.upto(9).each do |j|
|
|
13
|
+
csp.add_variable({ x: x, y: j }, domain: domain)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
variables = csp.variables.keys
|
|
18
|
+
|
|
19
|
+
# add columns and rows constraints
|
|
20
|
+
1.upto(9).each do |i|
|
|
21
|
+
column = variables.select { |name| name[:x] == i }
|
|
22
|
+
row = variables.select { |name| name[:y] == i }
|
|
23
|
+
|
|
24
|
+
csp.add_constraint constraint: Winston::Constraints::AllDifferent.new(variables: row, allow_nil: true)
|
|
25
|
+
csp.add_constraint constraint: Winston::Constraints::AllDifferent.new(variables: column, allow_nil: true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# blocks
|
|
29
|
+
variables.each_slice(3).group_by { |i| (i[0][:y] -1) % 9 }.values.flatten.each_slice(9).each do |vars|
|
|
30
|
+
csp.add_constraint constraint: Winston::Constraints::AllDifferent.new(variables: vars, allow_nil: true)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "should return a valid solution" do
|
|
35
|
+
expect(csp.solve).to eq({
|
|
36
|
+
{:x=>1, :y=>1}=>1,
|
|
37
|
+
{:x=>1, :y=>2}=>2,
|
|
38
|
+
{:x=>1, :y=>3}=>3,
|
|
39
|
+
{:x=>1, :y=>4}=>4,
|
|
40
|
+
{:x=>1, :y=>5}=>5,
|
|
41
|
+
{:x=>1, :y=>6}=>6,
|
|
42
|
+
{:x=>1, :y=>7}=>7,
|
|
43
|
+
{:x=>1, :y=>8}=>8,
|
|
44
|
+
{:x=>1, :y=>9}=>9,
|
|
45
|
+
{:x=>2, :y=>1}=>4,
|
|
46
|
+
{:x=>2, :y=>2}=>5,
|
|
47
|
+
{:x=>2, :y=>3}=>6,
|
|
48
|
+
{:x=>2, :y=>4}=>7,
|
|
49
|
+
{:x=>2, :y=>5}=>8,
|
|
50
|
+
{:x=>2, :y=>6}=>9,
|
|
51
|
+
{:x=>2, :y=>7}=>1,
|
|
52
|
+
{:x=>2, :y=>8}=>2,
|
|
53
|
+
{:x=>2, :y=>9}=>3,
|
|
54
|
+
{:x=>3, :y=>1}=>7,
|
|
55
|
+
{:x=>3, :y=>2}=>8,
|
|
56
|
+
{:x=>3, :y=>3}=>9,
|
|
57
|
+
{:x=>3, :y=>4}=>1,
|
|
58
|
+
{:x=>3, :y=>5}=>2,
|
|
59
|
+
{:x=>3, :y=>6}=>3,
|
|
60
|
+
{:x=>3, :y=>7}=>4,
|
|
61
|
+
{:x=>3, :y=>8}=>5,
|
|
62
|
+
{:x=>3, :y=>9}=>6,
|
|
63
|
+
{:x=>4, :y=>1}=>2,
|
|
64
|
+
{:x=>4, :y=>2}=>1,
|
|
65
|
+
{:x=>4, :y=>3}=>4,
|
|
66
|
+
{:x=>4, :y=>4}=>3,
|
|
67
|
+
{:x=>4, :y=>5}=>6,
|
|
68
|
+
{:x=>4, :y=>6}=>5,
|
|
69
|
+
{:x=>4, :y=>7}=>8,
|
|
70
|
+
{:x=>4, :y=>8}=>9,
|
|
71
|
+
{:x=>4, :y=>9}=>7,
|
|
72
|
+
{:x=>5, :y=>1}=>3,
|
|
73
|
+
{:x=>5, :y=>2}=>6,
|
|
74
|
+
{:x=>5, :y=>3}=>5,
|
|
75
|
+
{:x=>5, :y=>4}=>8,
|
|
76
|
+
{:x=>5, :y=>5}=>9,
|
|
77
|
+
{:x=>5, :y=>6}=>7,
|
|
78
|
+
{:x=>5, :y=>7}=>2,
|
|
79
|
+
{:x=>5, :y=>8}=>1,
|
|
80
|
+
{:x=>5, :y=>9}=>4,
|
|
81
|
+
{:x=>6, :y=>1}=>8,
|
|
82
|
+
{:x=>6, :y=>2}=>9,
|
|
83
|
+
{:x=>6, :y=>3}=>7,
|
|
84
|
+
{:x=>6, :y=>4}=>2,
|
|
85
|
+
{:x=>6, :y=>5}=>1,
|
|
86
|
+
{:x=>6, :y=>6}=>4,
|
|
87
|
+
{:x=>6, :y=>7}=>3,
|
|
88
|
+
{:x=>6, :y=>8}=>6,
|
|
89
|
+
{:x=>6, :y=>9}=>5,
|
|
90
|
+
{:x=>7, :y=>1}=>5,
|
|
91
|
+
{:x=>7, :y=>2}=>3,
|
|
92
|
+
{:x=>7, :y=>3}=>1,
|
|
93
|
+
{:x=>7, :y=>4}=>6,
|
|
94
|
+
{:x=>7, :y=>5}=>4,
|
|
95
|
+
{:x=>7, :y=>6}=>2,
|
|
96
|
+
{:x=>7, :y=>7}=>9,
|
|
97
|
+
{:x=>7, :y=>8}=>7,
|
|
98
|
+
{:x=>7, :y=>9}=>8,
|
|
99
|
+
{:x=>8, :y=>1}=>6,
|
|
100
|
+
{:x=>8, :y=>2}=>4,
|
|
101
|
+
{:x=>8, :y=>3}=>2,
|
|
102
|
+
{:x=>8, :y=>4}=>9,
|
|
103
|
+
{:x=>8, :y=>5}=>7,
|
|
104
|
+
{:x=>8, :y=>6}=>8,
|
|
105
|
+
{:x=>8, :y=>7}=>5,
|
|
106
|
+
{:x=>8, :y=>8}=>3,
|
|
107
|
+
{:x=>8, :y=>9}=>1,
|
|
108
|
+
{:x=>9, :y=>1}=>9,
|
|
109
|
+
{:x=>9, :y=>2}=>7,
|
|
110
|
+
{:x=>9, :y=>3}=>8,
|
|
111
|
+
{:x=>9, :y=>4}=>5,
|
|
112
|
+
{:x=>9, :y=>5}=>3,
|
|
113
|
+
{:x=>9, :y=>6}=>1,
|
|
114
|
+
{:x=>9, :y=>7}=>6,
|
|
115
|
+
{:x=>9, :y=>8}=>4,
|
|
116
|
+
{:x=>9, :y=>9}=>2
|
|
117
|
+
})
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require "winston"
|
|
2
2
|
|
|
3
|
-
describe Winston::Backtrack do
|
|
3
|
+
describe Winston::Solvers::Backtrack do
|
|
4
4
|
let(:csp) { Winston::CSP.new }
|
|
5
5
|
subject { described_class.new(csp) }
|
|
6
6
|
|
|
@@ -44,5 +44,38 @@ describe Winston::Backtrack do
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
context "with heuristics" do
|
|
49
|
+
it "uses MRV to pick the smallest remaining domain first" do
|
|
50
|
+
csp.add_variable :a, domain: [1, 2, 3]
|
|
51
|
+
csp.add_variable :b, domain: [1]
|
|
52
|
+
csp.add_variable :c, domain: [1, 2]
|
|
53
|
+
|
|
54
|
+
solver = described_class.new(csp, variable_strategy: :mrv)
|
|
55
|
+
result = solver.search
|
|
56
|
+
|
|
57
|
+
expect(result.keys).to eq([:b, :c, :a])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "uses LCV to prefer values that leave more options" do
|
|
61
|
+
csp.add_variable :a, domain: [2, 1]
|
|
62
|
+
csp.add_variable :b, domain: [2, 3]
|
|
63
|
+
csp.add_constraint(:a, :b) { |a, b| b > a }
|
|
64
|
+
|
|
65
|
+
solver = described_class.new(csp, value_strategy: :lcv)
|
|
66
|
+
result = solver.search
|
|
67
|
+
|
|
68
|
+
expect(result[:a]).to eq(1)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "supports forward checking" do
|
|
72
|
+
csp.add_variable :a, domain: [1]
|
|
73
|
+
csp.add_variable :b, domain: [1]
|
|
74
|
+
csp.add_constraint(:a, :b) { |a, b| a != b }
|
|
75
|
+
|
|
76
|
+
solver = described_class.new(csp, forward_checking: true)
|
|
77
|
+
expect(solver.search).to be(false)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
47
80
|
end
|
|
48
81
|
end
|
|
@@ -4,7 +4,8 @@ describe Winston::Constraint do
|
|
|
4
4
|
|
|
5
5
|
let(:variables) { nil }
|
|
6
6
|
let(:predicate) { nil }
|
|
7
|
-
|
|
7
|
+
let(:allow_nil) { false }
|
|
8
|
+
subject { described_class.new(variables: variables, predicate: predicate, allow_nil: allow_nil) }
|
|
8
9
|
|
|
9
10
|
describe "#elegible_for?" do
|
|
10
11
|
context "global" do
|
|
@@ -49,6 +50,50 @@ describe Winston::Constraint do
|
|
|
49
50
|
expect(subject.elegible_for?(:a, { b: 2 })).to be(false)
|
|
50
51
|
end
|
|
51
52
|
end
|
|
53
|
+
|
|
54
|
+
context "allowing nil" do
|
|
55
|
+
let(:allow_nil) { true }
|
|
56
|
+
|
|
57
|
+
context "for specific variable" do
|
|
58
|
+
let(:variables) { [:a] }
|
|
59
|
+
|
|
60
|
+
it "should return true when that variable is changed" do
|
|
61
|
+
expect(subject.elegible_for?(:a, { a: nil })).to be(true)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "should return false when that variable isn't the one changed" do
|
|
65
|
+
expect(subject.elegible_for?(:b, { a: 1, b: 2 })).to be(false)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "should return true when that variable is changed but doesn't have a value" do
|
|
69
|
+
expect(subject.elegible_for?(:a, { b: 2 })).to be(true)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context "for multiple variables" do
|
|
74
|
+
let(:variables) { [:a, :b] }
|
|
75
|
+
|
|
76
|
+
it "should return true when one of those variables is changed" do
|
|
77
|
+
expect(subject.elegible_for?(:a, { a: 1, b: nil })).to be(true)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "should return false when one of those variables isn't the one changed" do
|
|
81
|
+
expect(subject.elegible_for?(:c, { a: 1, b: 2 })).to be(false)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "should return true when one of those variables is changed but doesn't have a value for every one of them" do
|
|
85
|
+
expect(subject.elegible_for?(:b, { b: 2 })).to be(true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "should return true when one of those variables is changed but doesn't have a value for every one of them" do
|
|
89
|
+
expect(subject.elegible_for?(:a, { b: 2 })).to be(true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "should return true when one of those variables is changed but doesn't have a value for any of them" do
|
|
93
|
+
expect(subject.elegible_for?(:a, {})).to be(true)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
52
97
|
end
|
|
53
98
|
|
|
54
99
|
describe "#validate" do
|
|
@@ -3,13 +3,6 @@ require "winston"
|
|
|
3
3
|
describe Winston::Constraints::AllDifferent do
|
|
4
4
|
subject { described_class.new }
|
|
5
5
|
|
|
6
|
-
describe "#elegible_for?" do
|
|
7
|
-
it "should be elegible for everything" do
|
|
8
|
-
expect(subject.elegible_for?(:a, {})).to be(true)
|
|
9
|
-
expect(subject.elegible_for?(:b, { a: 1 })).to be(true)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
|
|
13
6
|
describe "#validate" do
|
|
14
7
|
it "should return 'true' when all values are unique" do
|
|
15
8
|
expect(subject.validate(a: 1, b: 2, c: 3, d: 4)).to be(true)
|
|
@@ -18,5 +11,17 @@ describe Winston::Constraints::AllDifferent do
|
|
|
18
11
|
it "should return 'false' when not all values are unique" do
|
|
19
12
|
expect(subject.validate(a: 1, b: 2, c: 2, d: 4)).to be(false)
|
|
20
13
|
end
|
|
14
|
+
|
|
15
|
+
context "for specific variables" do
|
|
16
|
+
subject { described_class.new(variables: [:a, :b, :c]) }
|
|
17
|
+
|
|
18
|
+
it "should return 'true' when all values are unique" do
|
|
19
|
+
expect(subject.validate(a: 1, b: 2, c: 3, d: 3)).to be(true)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "should return 'false' when not all values are unique" do
|
|
23
|
+
expect(subject.validate(a: 1, b: 2, c: 2, d: 4)).to be(false)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
21
26
|
end
|
|
22
27
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "winston"
|
|
2
|
+
|
|
3
|
+
describe Winston::Constraints::NotInList do
|
|
4
|
+
subject { described_class.new(list: [ 4, 5, 6 ]) }
|
|
5
|
+
|
|
6
|
+
describe "#validate" do
|
|
7
|
+
it "should return 'true' none of the values are on the list" do
|
|
8
|
+
expect(subject.validate(a: 1, b: 2, c: 3, d: 3)).to be(true)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "should return 'false' when any of the values are on the list" do
|
|
12
|
+
expect(subject.validate(a: 1, b: 2, c: 2, d: 4)).to be(false)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "should return 'false' when all the values are on the list" do
|
|
16
|
+
expect(subject.validate(a: 4, b: 6, c: 5, d: 4)).to be(false)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context "for specific variables" do
|
|
20
|
+
subject { described_class.new(variables: [:a, :b, :c], list: [ 4, 5, 6 ]) }
|
|
21
|
+
|
|
22
|
+
it "should return 'true' when none of the values are on the list" do
|
|
23
|
+
expect(subject.validate(a: 1, b: 2, c: 3, d: 6)).to be(true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "should return 'false' when any of the values are on the list" do
|
|
27
|
+
expect(subject.validate(a: 1, b: 2, c: 5, d: 3)).to be(false)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "should return 'false' when all the values are on the list" do
|
|
31
|
+
expect(subject.validate(a: 4, b: 6, c: 5, d: 3)).to be(false)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/spec/winston/csp_spec.rb
CHANGED
|
@@ -28,7 +28,7 @@ describe Winston::CSP do
|
|
|
28
28
|
describe "#add_constraint" do
|
|
29
29
|
let(:constraint) { double("Constraint") }
|
|
30
30
|
it "should build a constraint for the given block" do
|
|
31
|
-
expect(Winston::Constraint).to receive(:new).with([:a, :b], an_instance_of(Proc)).once.and_return(constraint)
|
|
31
|
+
expect(Winston::Constraint).to receive(:new).with(variables: [:a, :b], allow_nil: false, predicate: an_instance_of(Proc)).once.and_return(constraint)
|
|
32
32
|
subject.add_constraint(:a, :b) { true }
|
|
33
33
|
expect(subject.constraints).to include(constraint)
|
|
34
34
|
end
|
|
@@ -60,6 +60,25 @@ describe Winston::CSP do
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
describe "#domain_for" do
|
|
64
|
+
before do
|
|
65
|
+
subject.add_variable :a, value: 1
|
|
66
|
+
subject.add_variable :b, domain: [1, 2]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "returns domain values for a given variable" do
|
|
70
|
+
expect(subject.domain_for(:b)).to eq([1, 2])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "returns an empty list when a variable doesn't exist in the problem" do
|
|
74
|
+
expect(subject.domain_for(:c)).to be_empty
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "return an empty list when a variable doesn't have a domain" do
|
|
78
|
+
expect(subject.domain_for(:a)).to be_empty
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
63
82
|
describe "#solve" do
|
|
64
83
|
let(:solver) { double("Solver") }
|
|
65
84
|
before do
|
|
@@ -68,8 +87,32 @@ describe Winston::CSP do
|
|
|
68
87
|
end
|
|
69
88
|
|
|
70
89
|
it "should pass a collection of preset variables to the solver" do
|
|
71
|
-
expect(solver).to receive(:search).with(a: 1)
|
|
90
|
+
expect(solver).to receive(:search).with({ a: 1 })
|
|
72
91
|
subject.solve(solver)
|
|
73
92
|
end
|
|
93
|
+
|
|
94
|
+
it "builds a solver by name with options" do
|
|
95
|
+
subject.add_constraint(:a, :b) { |a, b| a < b }
|
|
96
|
+
result = subject.solve(:backtrack, variable_strategy: :mrv)
|
|
97
|
+
|
|
98
|
+
expect(result).to eq({ a: 1, b: 2 })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "raises for an unknown solver name" do
|
|
102
|
+
expect { subject.solve(:unknown) }.to raise_error(ArgumentError, /Unknown solver/)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "returns false when preset assignments violate a constraint" do
|
|
106
|
+
subject.add_constraint(:a) { |a| a > 2 }
|
|
107
|
+
expect(solver).to_not receive(:search)
|
|
108
|
+
expect(subject.solve(solver)).to be(false)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "returns false when a constraint among preset variables is violated" do
|
|
112
|
+
subject.add_variable :c, value: 2
|
|
113
|
+
subject.add_constraint(:a, :c) { |a, c| a == c }
|
|
114
|
+
expect(solver).to_not receive(:search)
|
|
115
|
+
expect(subject.solve(solver)).to be(false)
|
|
116
|
+
end
|
|
74
117
|
end
|
|
75
118
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require "winston"
|
|
2
|
+
|
|
3
|
+
describe Winston::DSL do
|
|
4
|
+
describe ".define" do
|
|
5
|
+
it "builds a CSP with variables and constraints" do
|
|
6
|
+
csp = Winston.define do
|
|
7
|
+
var :a, domain: [1, 2, 3]
|
|
8
|
+
var :b, domain: [1, 2, 3]
|
|
9
|
+
constraint(:a, :b) { |a, b| a > b }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
expect(csp.solve).to eq({ a: 2, b: 1 })
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "supports named domains" do
|
|
16
|
+
csp = Winston.define do
|
|
17
|
+
domain :digits, [1, 2, 3]
|
|
18
|
+
var :a, domain: :digits
|
|
19
|
+
var :b, domain: :digits
|
|
20
|
+
constraint(:a, :b) { |a, b| a != b }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
result = csp.solve
|
|
24
|
+
expect([1, 2, 3]).to include(result[:a])
|
|
25
|
+
expect([1, 2, 3]).to include(result[:b])
|
|
26
|
+
expect(result[:a]).to_not eq(result[:b])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "uses named constraints via use_constraint" do
|
|
30
|
+
csp = Winston.define do
|
|
31
|
+
var :a, domain: [1, 2]
|
|
32
|
+
var :b, domain: [1, 2]
|
|
33
|
+
use_constraint :all_different, :a, :b
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
expect(csp.solve).to eq({ a: 1, b: 2 })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "passes options to named constraints" do
|
|
40
|
+
csp = Winston.define do
|
|
41
|
+
var :a, domain: [1, 2, 3]
|
|
42
|
+
use_constraint :not_in_list, :a, list: [1, 2]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
expect(csp.solve).to eq({ a: 3 })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe ".register_constraint" do
|
|
50
|
+
it "registers and uses a custom constraint by name" do
|
|
51
|
+
custom = Class.new(Winston::Constraint) do
|
|
52
|
+
def validate(assignments)
|
|
53
|
+
values = values_at(assignments)
|
|
54
|
+
values.all? { |v| v == 2 }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Winston.register_constraint(:all_twos) do |variables, allow_nil, **_options|
|
|
59
|
+
custom.new(variables: variables, allow_nil: allow_nil)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
csp = Winston.define do
|
|
63
|
+
var :a, domain: [1, 2]
|
|
64
|
+
var :b, domain: [1, 2]
|
|
65
|
+
use_constraint :all_twos, :a, :b
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
expect(csp.solve).to eq({ a: 2, b: 2 })
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "winston"
|
|
2
|
+
|
|
3
|
+
describe Winston::Heuristics do
|
|
4
|
+
let(:csp) { Winston::CSP.new }
|
|
5
|
+
|
|
6
|
+
describe ".mrv" do
|
|
7
|
+
it "picks the variable with the fewest remaining values" do
|
|
8
|
+
csp.add_variable :a, domain: [1, 2, 3]
|
|
9
|
+
csp.add_variable :b, domain: [1, 2]
|
|
10
|
+
csp.add_variable :c, domain: [1, 2, 3]
|
|
11
|
+
csp.add_constraint(:a) { |a| a == 1 }
|
|
12
|
+
|
|
13
|
+
vars = csp.variables.each_value.to_a
|
|
14
|
+
chosen = described_class.mrv.call(vars, {}, csp)
|
|
15
|
+
|
|
16
|
+
expect(chosen.name).to eq(:a)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe ".lcv" do
|
|
21
|
+
it "orders values to leave more options for other variables" do
|
|
22
|
+
csp.add_variable :a, domain: [2, 1]
|
|
23
|
+
csp.add_variable :b, domain: [2, 3]
|
|
24
|
+
csp.add_constraint(:a, :b) { |a, b| b > a }
|
|
25
|
+
|
|
26
|
+
values = csp.domain_for(:a)
|
|
27
|
+
ordered = described_class.lcv.call(values, csp.variables[:a], {}, csp)
|
|
28
|
+
|
|
29
|
+
expect(ordered).to eq([1, 2])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe ".in_order" do
|
|
34
|
+
it "returns values as-is" do
|
|
35
|
+
values = [3, 1, 2]
|
|
36
|
+
ordered = described_class.in_order.call(values, nil, {}, csp)
|
|
37
|
+
|
|
38
|
+
expect(ordered).to eq(values)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe ".forward_checking" do
|
|
43
|
+
it "returns true to enable forward checking" do
|
|
44
|
+
expect(described_class.forward_checking).to be(true)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "winston"
|
|
2
|
+
|
|
3
|
+
describe Winston::Solvers::MAC do
|
|
4
|
+
let(:csp) { Winston::CSP.new }
|
|
5
|
+
|
|
6
|
+
def valid_solution?(csp, assignments)
|
|
7
|
+
csp.constraints.all? { |constraint| constraint.validate(assignments) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "solves a simple constraint problem" do
|
|
11
|
+
csp.add_variable :a, domain: [1, 2, 3]
|
|
12
|
+
csp.add_variable :b, domain: [1, 2, 3]
|
|
13
|
+
csp.add_variable :c, domain: [1, 2, 3]
|
|
14
|
+
csp.add_constraint(:a, :b) { |a, b| a > b }
|
|
15
|
+
csp.add_constraint(:b, :c) { |b, c| b > c }
|
|
16
|
+
|
|
17
|
+
solver = described_class.new(csp)
|
|
18
|
+
result = solver.search
|
|
19
|
+
|
|
20
|
+
expect(result).to be_a(Hash)
|
|
21
|
+
expect(valid_solution?(csp, result)).to be(true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "respects preset assignments" do
|
|
25
|
+
csp.add_variable :a, value: 2
|
|
26
|
+
csp.add_variable :b, domain: [1, 2, 3]
|
|
27
|
+
csp.add_constraint(:a, :b) { |a, b| a > b }
|
|
28
|
+
|
|
29
|
+
solver = described_class.new(csp)
|
|
30
|
+
result = solver.search
|
|
31
|
+
|
|
32
|
+
expect(result[:a]).to eq(2)
|
|
33
|
+
expect(valid_solution?(csp, result)).to be(true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "returns false when constraints are impossible" do
|
|
37
|
+
csp.add_variable :a, domain: [1]
|
|
38
|
+
csp.add_variable :b, domain: [1]
|
|
39
|
+
csp.add_constraint(:a, :b) { |a, b| a != b }
|
|
40
|
+
|
|
41
|
+
solver = described_class.new(csp)
|
|
42
|
+
expect(solver.search).to be(false)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "winston"
|
|
2
|
+
|
|
3
|
+
describe Winston::Solvers::MinConflicts do
|
|
4
|
+
let(:csp) { Winston::CSP.new }
|
|
5
|
+
|
|
6
|
+
def valid_solution?(csp, assignments)
|
|
7
|
+
csp.constraints.all? { |constraint| constraint.validate(assignments) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "finds a valid solution for a simple problem" do
|
|
11
|
+
csp.add_variable :a, domain: [1, 2, 3]
|
|
12
|
+
csp.add_variable :b, domain: [1, 2, 3]
|
|
13
|
+
csp.add_variable :c, domain: [1, 2, 3]
|
|
14
|
+
csp.add_constraint(:a, :b) { |a, b| a > b }
|
|
15
|
+
csp.add_constraint(:b, :c) { |b, c| b > c }
|
|
16
|
+
|
|
17
|
+
solver = described_class.new(csp, max_steps: 1_000, random: Random.new(1))
|
|
18
|
+
result = solver.search
|
|
19
|
+
|
|
20
|
+
expect(result).to be_a(Hash)
|
|
21
|
+
expect(valid_solution?(csp, result)).to be(true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "respects preset assignments" do
|
|
25
|
+
csp.add_variable :a, value: 2
|
|
26
|
+
csp.add_variable :b, domain: [1, 2, 3]
|
|
27
|
+
csp.add_constraint(:a, :b) { |a, b| a > b }
|
|
28
|
+
|
|
29
|
+
solver = described_class.new(csp, max_steps: 1_000, random: Random.new(1))
|
|
30
|
+
result = solver.search
|
|
31
|
+
|
|
32
|
+
expect(result[:a]).to eq(2)
|
|
33
|
+
expect(valid_solution?(csp, result)).to be(true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "returns false when no solution is found within max_steps" do
|
|
37
|
+
csp.add_variable :a, domain: [1]
|
|
38
|
+
csp.add_variable :b, domain: [1]
|
|
39
|
+
csp.add_constraint(:a, :b) { |a, b| a != b }
|
|
40
|
+
|
|
41
|
+
solver = described_class.new(csp, max_steps: 10, random: Random.new(1))
|
|
42
|
+
expect(solver.search).to be(false)
|
|
43
|
+
end
|
|
44
|
+
end
|
data/winston.gemspec
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
Gem::Specification.new do |gem|
|
|
2
2
|
gem.name = 'winston'
|
|
3
|
-
gem.version = '0.0
|
|
3
|
+
gem.version = '0.1.0'
|
|
4
4
|
gem.authors = ['David Michael Nelson']
|
|
5
5
|
gem.homepage = 'http://github.com/dmnelson/winston'
|
|
6
6
|
|
|
7
7
|
gem.summary = 'Constraint Satisfaction Problem (CSP) implementation for Ruby'
|
|
8
|
-
gem.description =
|
|
8
|
+
gem.description = 'A small, practical CSP library for Ruby with multiple solvers, heuristics, and a DSL.'
|
|
9
9
|
gem.license = 'MIT'
|
|
10
|
+
gem.required_ruby_version = ">= 3.0"
|
|
10
11
|
|
|
11
12
|
gem.files = `git ls-files`.split($/)
|
|
12
13
|
gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
13
14
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
14
15
|
gem.require_paths = ["lib"]
|
|
15
16
|
|
|
16
|
-
gem.add_development_dependency "bundler", "~>
|
|
17
|
-
gem.add_development_dependency "rake"
|
|
18
|
-
gem.add_development_dependency
|
|
17
|
+
gem.add_development_dependency "bundler", "~> 2.4", ">= 2.4.22"
|
|
18
|
+
gem.add_development_dependency "rake", "~> 13.1"
|
|
19
|
+
gem.add_development_dependency "rspec", "~> 3.13"
|
|
19
20
|
end
|