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