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
data/lib/winston/constraint.rb
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
module Winston
|
|
2
2
|
class Constraint
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
attr_reader :variables, :predicate, :allow_nil, :global
|
|
5
|
+
|
|
6
|
+
def initialize(variables: nil, predicate: nil, allow_nil: false)
|
|
7
|
+
@variables = [variables].flatten.compact
|
|
6
8
|
@predicate = predicate
|
|
9
|
+
@allow_nil = allow_nil
|
|
7
10
|
@global = @variables.empty?
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def elegible_for?(changed_var, assignments)
|
|
11
|
-
|
|
14
|
+
global || (variables.include?(changed_var) && has_required_values?(assignments))
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def validate(assignments)
|
|
15
|
-
return false unless
|
|
16
|
-
|
|
18
|
+
return false unless predicate
|
|
19
|
+
predicate.call(*values_at(assignments), assignments)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def values_at(assignments)
|
|
25
|
+
assignments.values_at(*variables)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def has_required_values?(assignments)
|
|
29
|
+
allow_nil || variables.all? { |v| assignments.key?(v) }
|
|
17
30
|
end
|
|
18
31
|
end
|
|
19
32
|
end
|
|
@@ -2,7 +2,8 @@ module Winston
|
|
|
2
2
|
module Constraints
|
|
3
3
|
class AllDifferent < Winston::Constraint
|
|
4
4
|
def validate(assignments)
|
|
5
|
-
assignments.values
|
|
5
|
+
values = global ? assignments.values : values_at(assignments).compact
|
|
6
|
+
values.uniq.size == values.size
|
|
6
7
|
end
|
|
7
8
|
end
|
|
8
9
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Winston
|
|
2
|
+
module Constraints
|
|
3
|
+
class NotInList < Winston::Constraint
|
|
4
|
+
|
|
5
|
+
attr_reader :list
|
|
6
|
+
|
|
7
|
+
def initialize(variables: nil, allow_nil: false, list: [])
|
|
8
|
+
super(variables: variables, allow_nil: allow_nil)
|
|
9
|
+
@list = list
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def validate(assignments)
|
|
13
|
+
values = global ? assignments.values : values_at(assignments)
|
|
14
|
+
!values.any? { |v| list.include?(v) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/winston/csp.rb
CHANGED
|
@@ -8,8 +8,12 @@ module Winston
|
|
|
8
8
|
@constraints = []
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def solve(solver =
|
|
12
|
-
|
|
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)
|
|
@@ -17,8 +21,8 @@ module Winston
|
|
|
17
21
|
variables[name] = Variable.new(name, value: value, domain: domain)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
def add_constraint(*variables, constraint: nil, &block)
|
|
21
|
-
constraint ||= Constraint.new(variables, block)
|
|
24
|
+
def add_constraint(*variables, constraint: nil, allow_nil: false, &block)
|
|
25
|
+
constraint ||= Constraint.new(variables: variables, allow_nil: allow_nil, predicate: block)
|
|
22
26
|
constraints << constraint
|
|
23
27
|
end
|
|
24
28
|
|
|
@@ -29,6 +33,13 @@ module Winston
|
|
|
29
33
|
true
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
def domain_for(variable_name)
|
|
37
|
+
variable = variables[variable_name]
|
|
38
|
+
return [] if variable.nil? || variable.domain.nil?
|
|
39
|
+
|
|
40
|
+
variable.domain.values
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
private
|
|
33
44
|
|
|
34
45
|
def var_assignments
|
|
@@ -37,5 +48,29 @@ module Winston
|
|
|
37
48
|
assignments
|
|
38
49
|
end
|
|
39
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
|
|
40
75
|
end
|
|
41
76
|
end
|
data/lib/winston/dsl.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Winston
|
|
2
|
+
class DSL
|
|
3
|
+
attr_reader :csp
|
|
4
|
+
|
|
5
|
+
def initialize(csp = CSP.new)
|
|
6
|
+
@csp = csp
|
|
7
|
+
@domains = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def domain(name, values)
|
|
11
|
+
@domains[name] = values
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def var(name, domain: nil, value: nil, &block)
|
|
15
|
+
resolved_domain = resolve_domain(domain)
|
|
16
|
+
csp.add_variable(name, value: value, domain: resolved_domain, &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def constraint(*variables, allow_nil: false, &block)
|
|
20
|
+
csp.add_constraint(*variables, allow_nil: allow_nil, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def use_constraint(name, *variables, allow_nil: false, **options)
|
|
24
|
+
factory = constraint_factory_for(name)
|
|
25
|
+
constraint = factory.call(variables, allow_nil, **options)
|
|
26
|
+
csp.add_constraint(constraint: constraint)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def resolve_domain(domain)
|
|
32
|
+
return domain unless domain.is_a?(Symbol)
|
|
33
|
+
return @domains[domain] if @domains.key?(domain)
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, "Unknown domain :#{domain}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def constraint_factory_for(name)
|
|
39
|
+
registry = Winston.constraint_registry
|
|
40
|
+
return registry[name] if registry.key?(name)
|
|
41
|
+
|
|
42
|
+
raise ArgumentError, "Unknown constraint :#{name}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.constraint_registry
|
|
47
|
+
@constraint_registry ||= {
|
|
48
|
+
all_different: lambda do |variables, allow_nil, **options|
|
|
49
|
+
Winston::Constraints::AllDifferent.new(variables: variables, allow_nil: allow_nil, **options)
|
|
50
|
+
end,
|
|
51
|
+
not_in_list: lambda do |variables, allow_nil, **options|
|
|
52
|
+
Winston::Constraints::NotInList.new(variables: variables, allow_nil: allow_nil, **options)
|
|
53
|
+
end
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.register_constraint(name, factory = nil, &block)
|
|
58
|
+
factory ||= block
|
|
59
|
+
raise ArgumentError, "Constraint factory required for :#{name}" unless factory
|
|
60
|
+
|
|
61
|
+
constraint_registry[name] = factory
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.define(&block)
|
|
65
|
+
builder = DSL.new
|
|
66
|
+
builder.instance_eval(&block) if block
|
|
67
|
+
builder.csp
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Winston
|
|
2
|
+
module Heuristics
|
|
3
|
+
def self.mrv
|
|
4
|
+
lambda do |vars, assignments, csp|
|
|
5
|
+
vars.min_by { |var| remaining_values(var, assignments, csp).size }
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.lcv
|
|
10
|
+
lambda do |values, var, assignments, csp|
|
|
11
|
+
other_vars = vars_without(var, assignments, csp)
|
|
12
|
+
scored = values.map do |value|
|
|
13
|
+
score = other_vars.sum do |other|
|
|
14
|
+
csp.domain_for(other.name).count do |other_value|
|
|
15
|
+
csp.validate(other.name, assignments.merge(var.name => value, other.name => other_value))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
[value, score]
|
|
19
|
+
end
|
|
20
|
+
scored.sort_by { |(_, score)| -score }.map(&:first)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.in_order
|
|
25
|
+
->(values, _var, _assignments, _csp) { values }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.forward_checking
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.remaining_values(var, assignments, csp)
|
|
33
|
+
csp.domain_for(var.name).select do |value|
|
|
34
|
+
csp.validate(var.name, assignments.merge(var.name => value))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.vars_without(var, assignments, csp)
|
|
39
|
+
csp.variables.reject { |k, _| assignments.include?(k) || k == var.name }.each_value.to_a
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_class_method :remaining_values, :vars_without
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Winston
|
|
2
|
+
module Solvers
|
|
3
|
+
class Backtrack
|
|
4
|
+
def initialize(csp, variable_strategy: :first, value_strategy: :in_order, forward_checking: false)
|
|
5
|
+
@csp = csp
|
|
6
|
+
@variable_strategy = variable_strategy
|
|
7
|
+
@value_strategy = value_strategy
|
|
8
|
+
@forward_checking = forward_checking
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def search(assignments = {})
|
|
12
|
+
return assignments if complete?(assignments)
|
|
13
|
+
var = select_unassigned_variable(assignments)
|
|
14
|
+
domain_values(var, assignments).each do |value|
|
|
15
|
+
assigned = assignments.merge(var.name => value)
|
|
16
|
+
if valid?(var.name, assigned)
|
|
17
|
+
result = search(assigned)
|
|
18
|
+
return result if result
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :csp, :variable_strategy, :value_strategy, :forward_checking
|
|
27
|
+
|
|
28
|
+
def complete?(assignments)
|
|
29
|
+
assignments.size == csp.variables.size
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def valid?(changed, assignments)
|
|
33
|
+
return false unless csp.validate(changed, assignments)
|
|
34
|
+
return true unless forward_checking
|
|
35
|
+
|
|
36
|
+
forward_check(assignments)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def select_unassigned_variable(assignments)
|
|
40
|
+
vars = unassigned_variables(assignments)
|
|
41
|
+
return vars.first if variable_strategy == :first
|
|
42
|
+
return variable_strategy.call(vars, assignments, csp) if variable_strategy.respond_to?(:call)
|
|
43
|
+
return vars.min_by { |var| remaining_values(var, assignments).size } if variable_strategy == :mrv
|
|
44
|
+
|
|
45
|
+
vars.first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def domain_values(var, assignments)
|
|
49
|
+
values = remaining_values(var, assignments)
|
|
50
|
+
return values if value_strategy == :in_order
|
|
51
|
+
return value_strategy.call(values, var, assignments, csp) if value_strategy.respond_to?(:call)
|
|
52
|
+
return order_lcv(values, var, assignments) if value_strategy == :lcv
|
|
53
|
+
|
|
54
|
+
values
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def remaining_values(var, assignments)
|
|
58
|
+
csp.domain_for(var.name).select do |value|
|
|
59
|
+
csp.validate(var.name, assignments.merge(var.name => value))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def order_lcv(values, var, assignments)
|
|
64
|
+
other_vars = unassigned_variables(assignments).reject { |v| v.name == var.name }
|
|
65
|
+
scored = values.map do |value|
|
|
66
|
+
score = other_vars.sum do |other|
|
|
67
|
+
csp.domain_for(other.name).count do |other_value|
|
|
68
|
+
csp.validate(other.name, assignments.merge(var.name => value, other.name => other_value))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
[value, score]
|
|
72
|
+
end
|
|
73
|
+
scored.sort_by { |(_, score)| -score }.map(&:first)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unassigned_variables(assignments)
|
|
77
|
+
csp.variables.reject { |k, _| assignments.include?(k) }.each_value.to_a
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def forward_check(assignments)
|
|
81
|
+
unassigned_variables(assignments).all? do |var|
|
|
82
|
+
csp.domain_for(var.name).any? do |value|
|
|
83
|
+
csp.validate(var.name, assignments.merge(var.name => value))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Backtrack = Solvers::Backtrack
|
|
91
|
+
end
|