whatnot 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,49 @@
1
+ class SolutionEnumerator
2
+ include Enumerable
3
+
4
+ def initialize(solution_interpreter, dimacs="")
5
+ @dimacs = dimacs
6
+ @solution_interpreter = solution_interpreter
7
+ end
8
+
9
+ def next_solution
10
+ infile = "/tmp/SolutionEnumerator-in.txt"
11
+ outfile = "/tmp/SolutionEnumerator-out.txt"
12
+
13
+ File.open(infile, "w+") do |file|
14
+ file << @dimacs
15
+ end
16
+
17
+ `minisat -rnd-init -rnd-seed=#{Time.now.to_i} #{infile} #{outfile} 2>&1 >/dev/null`
18
+
19
+ File.read(outfile)
20
+ end
21
+
22
+ def each
23
+ solution = next_solution()
24
+
25
+ while solution.start_with?("SAT") do
26
+ clean_up_solution!(solution)
27
+
28
+ yield @solution_interpreter.call(solution)
29
+
30
+ @dimacs += ("\n" + inverse_of(solution))
31
+
32
+ solution = next_solution()
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def clean_up_solution!(solution_string)
39
+ solution_string.gsub!(/\A(UN)?SAT\s/, "")
40
+ solution_string.gsub!(/\n\Z/, "")
41
+ end
42
+
43
+ def inverse_of(solution_string)
44
+ solution_string.
45
+ split(" ").
46
+ map { |num| (num.to_i * -1).to_s }.
47
+ join(" ")
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ class Switch
2
+ def self.next_number
3
+ @@next_number ||= 1
4
+ end
5
+
6
+ def self.inc_next_number!
7
+ @@next_number = @@next_number ? @@next_number + 1 : 1
8
+ end
9
+
10
+ def self.all
11
+ @@all ||= {}
12
+ end
13
+
14
+ def self.find(number)
15
+ all[number]
16
+ end
17
+
18
+ def self.add(switch)
19
+ switch.number = next_number
20
+ all[switch.number] = switch
21
+ inc_next_number!
22
+ switch
23
+ end
24
+
25
+ def self.each
26
+ all.values.each do |switch|
27
+ yield switch
28
+ end
29
+ end
30
+
31
+ extend Enumerable
32
+
33
+ def initialize(payload)
34
+ @payload = payload
35
+ Switch.add(self)
36
+ end
37
+
38
+ attr_accessor :number, :payload
39
+ end
@@ -0,0 +1,151 @@
1
+ class SwitchGroup
2
+ attr_accessor :switches, :possibilities
3
+
4
+ def initialize(nums=[], min_on: 1, max_on: 1)
5
+ @min_on = min_on
6
+ @max_on = max_on
7
+ @switches = []
8
+ @possibilities = []
9
+
10
+ if block_given?
11
+ Switch.each do |switch|
12
+ @switches << switch if yield(switch.payload)
13
+ end
14
+ else
15
+ @switches = Switch.all.values_at(*nums)
16
+ end
17
+
18
+ find_all_possibilities!
19
+ end
20
+
21
+ def find_all_possibilities!
22
+ switchmap = Hash[@switches.each_with_index.map { |s, ix| [s.number, ix+1] }]
23
+
24
+ groupdimacs = dimacs.lines.map do |line|
25
+ next if line.start_with?("c") || line.lstrip == ""
26
+
27
+ nums = line.split(" ").map(&:to_i)
28
+
29
+ newnums = []
30
+ nums.each do |n|
31
+ if n > 0
32
+ newnums << switchmap[n]
33
+ elsif n < 0
34
+ newnums << switchmap[n*-1] * -1
35
+ else
36
+ newnums << n
37
+ end
38
+ end
39
+
40
+ newnums.map(&:to_s).join(" ")
41
+ end
42
+
43
+ groupdimacs = groupdimacs.compact.join("\n")
44
+
45
+ groupinterpreter = -> (str) do
46
+ nums = str.split(" ").map(&:to_i)
47
+
48
+ newnums = []
49
+ nums.each do |n|
50
+ if n > 0
51
+ newnums << @switches[n-1].number
52
+ elsif n == 0
53
+ newnums << n
54
+ else
55
+ newnums << @switches[(-1*n)-1].number * -1
56
+ end
57
+ end
58
+
59
+ solution = newnums.map(&:to_s).join(" ")
60
+
61
+ [group_interpret(solution), solution]
62
+ end
63
+
64
+ if groupdimacs.empty?
65
+ # all solutions are possible, so iterate all.
66
+ # can't use minisat here (it won't know how many vars there are).
67
+ possibilities = {}
68
+
69
+ switch_combos = switches.map do |switch|
70
+ n = switch.number
71
+ [n, -1 * n]
72
+ end
73
+
74
+ switch_combos.inject(&:product).each do |product|
75
+ product = product.is_a?(Array) ? product.flatten : [product]
76
+ solution = product.map(&:to_s).join(" ") + " 0"
77
+ possibilities[group_interpret(solution)] = solution
78
+ end
79
+
80
+ @possibilities = possibilities
81
+ else
82
+ @possibilities = Hash[SolutionEnumerator.new(groupinterpreter, groupdimacs).entries]
83
+ end
84
+ end
85
+
86
+ def group_interpret(solution)
87
+ out = {}
88
+
89
+ solution.split(" ").each do |num|
90
+ num = num.to_i
91
+
92
+ if num > 0
93
+ merge_payload!(for_num: num, to_hash: out)
94
+ end
95
+ end
96
+
97
+ out
98
+ end
99
+
100
+ def merge_payload!(for_num: nil, to_hash: nil)
101
+ if for_num.nil? || to_hash.nil?
102
+ raise "left off required kwargs #{"for_num, " unless for_num}#{"to_hash" unless to_hash}"
103
+ end
104
+
105
+ payload = Switch.find(for_num).payload
106
+ payload_key, payload_val = *payload.to_a[0]
107
+
108
+ # manage sets vs. slots
109
+ if @max_on > 1
110
+ payload_val = [payload_val] unless payload_val.is_a?(Array)
111
+
112
+ if to_hash[payload_key]
113
+ to_hash[payload_key] += payload_val
114
+ else
115
+ to_hash[payload_key] = payload_val
116
+ end
117
+
118
+ return to_hash
119
+ end
120
+
121
+ to_hash.merge!(payload)
122
+ end
123
+
124
+ def dimacs
125
+ r = ""
126
+
127
+ r << "c Switches:\n"
128
+ @switches.each do |switch|
129
+ switch_pp = switch.payload.pretty_inspect.lines
130
+ r << "c #{switch.number}: #{switch_pp[0]}"
131
+ switch_pp[1..-1].each do |line|
132
+ r << "c #{line}"
133
+ end
134
+ end
135
+
136
+ if @min_on > 0
137
+ @switches.combination(@switches.size - (@min_on-1)).each do |combo|
138
+ r << combo.map(&:number).map(&:to_s).join(" ") + " 0\n"
139
+ end
140
+ end
141
+
142
+ if @max_on < @switches.size
143
+ @switches.combination(@max_on+1).each do |combo|
144
+ r << combo.map(&:number).map { |s| "-#{s}" }.join(" ") + " 0\n"
145
+ end
146
+ end
147
+
148
+ r << "\n\n"
149
+ r
150
+ end
151
+ end
@@ -0,0 +1,211 @@
1
+ class SwitchInterpreter
2
+ class NameCollisionError < ::RuntimeError
3
+ def initialize(info="")
4
+ super("Already using name #{info}")
5
+ end
6
+ end
7
+
8
+ attr_accessor :set_groups, :slot_groups, :non_interpreted_groups
9
+
10
+ def initialize
11
+ Switch.class_variable_set(:@@all, {})
12
+ Switch.class_variable_set(:@@next_number, 1)
13
+ @slot_groups = {}
14
+ @set_groups = {}
15
+ @non_interpreted_groups = {}
16
+ end
17
+
18
+ def interpreted_groups
19
+ groups = {}
20
+ groups.merge! @slot_groups
21
+ groups.merge! @set_groups
22
+ groups
23
+ end
24
+
25
+ def dimacs
26
+ d = "c SwitchInterpreter\np cnf 1 1\n"
27
+ d << "\n"
28
+
29
+ interpreted_groups.to_a.each do |key, group|
30
+ d << "c ---------------------------\n"
31
+ d << "c INTERPRETED\n"
32
+ d << "c #{group.class} => \n"
33
+ d << "c #{key}\n"
34
+ d << "c \n"
35
+ d << group.dimacs
36
+ end
37
+
38
+ @non_interpreted_groups.to_a.each do |key, group|
39
+ d << "c ---------------------------\n"
40
+ d << "c NON-INTERPRETED\n"
41
+ d << "c #{group.class} => \n"
42
+ d << "c #{key}\n"
43
+ d << "c \n"
44
+ d << group.dimacs
45
+ end
46
+
47
+ d
48
+ end
49
+
50
+ def merge_payload!(for_num: nil, to_hash: nil)
51
+ if for_num.nil? || to_hash.nil?
52
+ raise "left off required kwargs #{"for_num, " unless for_num}#{"to_hash" unless to_hash}"
53
+ end
54
+
55
+ payload = Switch.find(for_num).payload
56
+ payload_key, payload_val = *payload.to_a[0]
57
+
58
+ # manage sets vs. slots
59
+ if @set_groups[payload_key]
60
+ payload_val = [payload_val] unless payload_val.is_a?(Array)
61
+
62
+ if to_hash[payload_key]
63
+ to_hash[payload_key] += payload_val
64
+ else
65
+ to_hash[payload_key] = payload_val
66
+ end
67
+
68
+ return to_hash
69
+ end
70
+
71
+ to_hash.merge!(payload)
72
+ end
73
+
74
+ def create_slot(slotname, slotvalues, allow_empty: true)
75
+ if interpreted_groups[slotname]
76
+ raise NameCollisionError.new(slotname)
77
+ end
78
+
79
+ numbers = []
80
+
81
+ [slotname].product(slotvalues).each do |name, value|
82
+ s = Switch.new({name => value})
83
+ numbers << s.number
84
+ end
85
+
86
+ min_on = allow_empty ? 0 : 1
87
+
88
+ @slot_groups[slotname] = SwitchGroup.new(numbers, min_on: min_on, max_on: 1)
89
+ end
90
+
91
+ def create_set(setname, setvalues, allow_empty: true, max_values: 2)
92
+ if interpreted_groups[setname]
93
+ raise NameCollisionError.new(setname)
94
+ end
95
+
96
+ numbers = []
97
+
98
+ [setname].product(setvalues).each do |name, value|
99
+ s = Switch.new({name => value})
100
+ numbers << s.number
101
+ end
102
+
103
+ min_on = allow_empty ? 0 : 1
104
+
105
+ @set_groups[setname] = SwitchGroup.new(numbers, min_on: min_on, max_on: max_values)
106
+ end
107
+
108
+ def create_mutually_exclusive_slots(slotnames, slotvalues, allow_empty: false)
109
+ begin
110
+ slotnames.each do |slotname|
111
+ create_slot(slotname, slotvalues, allow_empty: allow_empty)
112
+ end
113
+ rescue NameCollisionError
114
+ end
115
+
116
+ slotvalues.each do |slotvalue|
117
+ mutual_exclusion = SwitchGroup.new(nil, min_on: 0, max_on: 1) do |payload|
118
+ k, v = *payload.to_a[0]
119
+ slotnames.include?(k) && slotvalue == v
120
+ end
121
+
122
+ @non_interpreted_groups[slotvalue] = mutual_exclusion
123
+ end
124
+ end
125
+
126
+ def create_mutually_exclusive_sets(slotnames, slotvalues, allow_empty: true, require_complete: true, max_values: 2)
127
+ begin
128
+ slotnames.each do |slotname|
129
+ create_set(slotname, slotvalues, allow_empty: allow_empty, max_values: max_values)
130
+ end
131
+ rescue NameCollisionError
132
+ end
133
+
134
+ slotvalues.each do |slotvalue|
135
+ min_on = require_complete ? 1 : 0
136
+
137
+ mutual_exclusion = SwitchGroup.new(nil, min_on: min_on, max_on: 1) do |payload|
138
+ k, v = *payload.to_a[0]
139
+ slotnames.include?(k) && slotvalue == v
140
+ end
141
+
142
+ @non_interpreted_groups[slotvalue] = mutual_exclusion
143
+ end
144
+ end
145
+
146
+ def create_constraint(*slotnames)
147
+ solutions_to_try = nil
148
+
149
+ slotnames.each do |groupname|
150
+ group = interpreted_groups[groupname]
151
+
152
+ if group.nil?
153
+ group = @non_interpreted_groups[groupname]
154
+ end
155
+
156
+ raise "can't find group #{groupname}" if group.nil?
157
+
158
+ # get possible solutions for each group
159
+ group_solutions = group.possibilities
160
+
161
+ merge_possibilities = -> (p1, p2) do
162
+ Hash[p1.to_a.product(p2.to_a).map { |prod| [prod.map(&:first).inject(&:merge), prod.map(&:last).inject("") { |memo, d| memo + d[0..-2] } + "0"] }]
163
+ end
164
+
165
+ if solutions_to_try
166
+ solutions_to_try = merge_possibilities.call(solutions_to_try, group_solutions)
167
+ else
168
+ solutions_to_try = group_solutions
169
+ end
170
+ end
171
+
172
+ # get source of constraint in user code
173
+ trace = Thread.current.backtrace
174
+ interpreter_lines = []
175
+ trace.each.with_index do |line, ix|
176
+ if line.match(/switch_interpreter.rb/)
177
+ interpreter_lines << ix
178
+ end
179
+ end
180
+ line_of_caller = trace[interpreter_lines.max + 1].split("/").last
181
+ failed_solution_strings = []
182
+
183
+ solutions_to_try.each do |solution, solution_str|
184
+ result = yield(**solution)
185
+
186
+ if !result
187
+ failed_solution_strings << solution_str
188
+ end
189
+ end
190
+
191
+ if constraint_group = @non_interpreted_groups[line_of_caller]
192
+ constraint_group.failed_solutions += failed_solution_strings
193
+ else
194
+ @non_interpreted_groups[line_of_caller] = FailedSolutionSwitchGroup.new(failed_solution_strings)
195
+ end
196
+ end
197
+
198
+ def interpret(solution)
199
+ out = {}
200
+
201
+ solution.split(" ").each do |num|
202
+ num = num.to_i
203
+
204
+ if num > 0
205
+ merge_payload!(for_num: num, to_hash: out)
206
+ end
207
+ end
208
+
209
+ out
210
+ end
211
+ end