whatnot 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.
@@ -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