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.
@@ -1,19 +1,32 @@
1
1
  module Winston
2
2
  class Constraint
3
3
 
4
- def initialize(variables = nil, predicate = nil)
5
- @variables = variables || []
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
- @global || (@variables.include?(changed_var) && @variables.all? { |v| assignments.key? v })
14
+ global || (variables.include?(changed_var) && has_required_values?(assignments))
12
15
  end
13
16
 
14
17
  def validate(assignments)
15
- return false unless @predicate
16
- @predicate.call(*assignments.values_at(*@variables), assignments)
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.uniq.size == assignments.size
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 = 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)
@@ -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
@@ -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