whatnot 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.bundle/binstubs/bundler +16 -0
- data/.bundle/binstubs/git-changelog +16 -0
- data/.bundle/binstubs/htmldiff +16 -0
- data/.bundle/binstubs/ldiff +16 -0
- data/.bundle/binstubs/rake +16 -0
- data/.bundle/binstubs/rspec +16 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +20 -0
- data/README.md +58 -0
- data/lib/whatnot.rb +3 -0
- data/lib/whatnot/dimacs_cnf_printer.rb +116 -0
- data/lib/whatnot/dimacs_cnf_var.rb +73 -0
- data/lib/whatnot/failed_solution_switch_group.rb +25 -0
- data/lib/whatnot/solution_enumerator.rb +49 -0
- data/lib/whatnot/switch.rb +39 -0
- data/lib/whatnot/switch_group.rb +151 -0
- data/lib/whatnot/switch_interpreter.rb +211 -0
- data/lib/whatnot/version.rb +3 -0
- data/spec/lib/whatnot/dimacs_cnf_var_spec.rb +65 -0
- data/spec/lib/whatnot/solution_enumerator_spec.rb +47 -0
- data/spec/lib/whatnot/switch_group_spec.rb +67 -0
- data/spec/lib/whatnot/switch_interpreter_spec.rb +188 -0
- data/spec/spec_helper.rb +89 -0
- data/whatnot.gemspec +23 -0
- metadata +99 -0
@@ -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
|