/
inequality_decider.py
243 lines (190 loc) · 8.56 KB
/
inequality_decider.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import itertools, string, operator, collections
from state import State
import networkx as nx
def extract_inequality(prev, next, input):
state = State(prev)
currents = list(reversed(map(lambda i: input[i], state.unstables())))
if next == state.transition(State.left).state:
return ( (-1, currents[0]), (1, currents[1]), (1, prev) )
elif next == state.transition(State.right).state:
return ( (1, currents[0]), (-1, currents[1]), (-1, prev) )
def sortedtuple(term):
return tuple(map(operator.itemgetter(1), sorted(term, key=operator.itemgetter(0), reverse=True)))
def trivially_inconsistent(ineq1, ineq2):
return all(l[0] == -1*r[0] and l[1] == r[1] for l, r in zip(ineq1, ineq2))
def constrained_variable(ineq1, ineq2):
# for these two inequalitites to imply a constraint there have to be exactly two terms of opposite sign
opposites = (not (l[0] == -1*r[0] and l[1] == r[1]) for l, r in zip(ineq1, ineq2))
free_variables = list(itertools.compress(zip(ineq1, ineq2), opposites))
if len(free_variables) != 1:
return
# return format is (A > B)
return sortedtuple(free_variables[0])
def potentially_constrained_variables(ineq1, ineq2):
def invert(ineq):
return sortedtuple(map(lambda t: (-1*t[0], t[1]), ineq))
opposites = (not (l[0] == -1*r[0] and l[1] == r[1]) for l, r in zip(ineq1, ineq2))
free_variables = list(itertools.compress(zip(ineq1, ineq2), opposites))
# for potential constraints, we need exactly one shared variable of opposite sign
# and therefore exactly two free variables
if len(free_variables) != 2:
return
left, right = free_variables
return { invert(left): sortedtuple(right), invert(right): sortedtuple(left) }
def identify_relations(existing, ineq):
trivials = set()
constraints = set()
potential_constraints = collections.defaultdict(set)
for other in existing:
if trivially_inconsistent(other, ineq):
# this is a trivial inconsistency, no need to proceed
trivials.add( (other, ineq) )
else:
# no trivial inconsistency detected
constraint = constrained_variable(ineq, other)
if constraint:
# these two inequalities share two variables in such a way
# that they imply an inequality between two other variables
constraints.add( constraint )
else:
# now check if this relation could imply a constraint if a constraint of the
# previous kind were added
potentials = potentially_constrained_variables(ineq, other)
if potentials:
for k, v in potentials.iteritems():
potential_constraints[k].add(v)
return (trivials, constraints, potential_constraints)
class InequalityStore:
# override this in subclasses
def add(self, ineq):
raise Exception("not implemented")
def add_transition(self, prev, next, input):
self.add(extract_inequality(prev, next, input))
def add_cycle(self, problem, result, cycle):
for input in problem[result]:
for state, to in zip(cycle, cycle[1:] + cycle[:1]):
self.add_transition(state, to, input)
def add_cycle_mapping(self, problem, mapping):
for result, cycle in mapping:
self.add_cycle(problem, result, cycle)
class InequalityPrinter(InequalityStore):
def __init__(self):
self.ineqs = set()
def add(self, ineq):
self.ineqs.add(ineq)
def print_mathematica(self):
def escape(param):
# Mathematica doesnt like I
return param.replace("I", "J")
params = set()
result = set()
for ineq in self.ineqs:
params.add(escape(ineq[0][1]))
params.add(escape(ineq[1][1]))
params.add(escape(ineq[2][1]))
result.add("(%d)*%s + (%d)*%s + (%d)*%s > 0"%(ineq[0][0], escape(ineq[0][1]), ineq[1][0], escape(ineq[1][1]), ineq[2][0], escape(ineq[2][1])))
print "FindInstance[%s, {%s}, Reals]"%(string.join(result, "&&"), string.join(params, ","))
class InequalityDecider(InequalityStore):
def __init__(self):
self.ineqs = set()
self.trivials = set()
self.constraints = set()
self.potential_constraints = collections.defaultdict(set)
def identify_relations(self, ineq):
return identify_relations(self.ineqs, ineq)
def add(self, ineq):
trivials, constraints, potential_constraints = self.identify_relations(ineq)
self.trivials.update(trivials)
self.constraints.update(constraints)
for k, v in potential_constraints.iteritems():
self.potential_constraints[k].update(v)
self.ineqs.add(ineq)
def _construct_graph(self):
result = nx.DiGraph()
# go through each first level constraint, add an edge for it
# then check whether those edges imply a second level constraint
for constraint in self.constraints:
result.add_edge(*constraint)
if constraint in self.potential_constraints:
for high, low in self.potential_constraints[constraint]:
result.add_edge(high, low)
# TODO: check whether the previous step needs to be done recursively
# ie. check for potential constraints that become active because
# of the activation of other potential constraints (are there levels higher than 2??)
return result
def satisfiable(self):
if len(self.trivials) > 0:
return False
return nx.is_directed_acyclic_graph(self._construct_graph())
def freeze(self):
return FrozenDecider(self)
class FrozenDecider:
def __init__(self, parent):
self.parent = parent
self.graph = parent._construct_graph()
def satisfiable_with_ineqs(self, ineqs):
base_ineqs = set(list(self.parent.ineqs)) # to copy
cons = list()
for ineq in ineqs:
cons.append(identify_relations(base_ineqs, ineq))
base_ineqs.add(ineq)
if any(len(t[0]) > 0 for t in cons):
return False
graph = self.graph.copy()
for constraints in map(operator.itemgetter(1), cons):
for constraint in constraints:
graph.add_edge(*constraint)
for potential_constraints in map(operator.itemgetter(2), cons):
for k, constraints in potential_constraints.iteritems():
if k[0] in graph and k[1] in graph and nx.has_path(graph, *k):
for constraint in constraints:
graph.add_edge(*constraint)
return nx.is_directed_acyclic_graph(graph)
def satisfiable_with_transition(self, prev, next, input):
ineq = extract_inequality(prev, next, input)
trivials, constraints, potential_constraints = self.parent.identify_relations(ineq)
if len(trivials) > 0:
return False
graph = self.graph.copy()
for constraint in constraints:
graph.add_edge(*constraint)
for k, constraints in potential_constraints.iteritems():
if k[0] in graph and k[1] in graph and nx.has_path(graph, *k):
for constraint in constraints:
graph.add_edge(*constraint)
return nx.is_directed_acyclic_graph(graph)
def find_possible_transitions(self, in_states, input):
for state in in_states:
for out_state in State.graph[state]:
if self.satisfiable_with_transition(state, out_state, input):
yield (state, out_state)
def build_transition_graph(self, my_cycles, in_states, input):
graph = nx.DiGraph()
for cycle in my_cycles:
for prev, next in zip(cycle, cycle[1:]+cycle[:1]):
graph.add_edge(prev, next)
for prev, next in self.find_possible_transitions(in_states, input):
graph.add_edge(prev, next)
return graph
def potentially_connected(problem_def, cycle_mapping, additionals=[], shortcut=True):
graphs = {}
base_decider = InequalityDecider()
base_decider.add_cycle_mapping(problem_def, cycle_mapping)
for transition in additionals:
base_decider.add_transition(*transition)
decider = base_decider.freeze()
for result, inputs in problem_def.iteritems():
my_cycles = tuple(map(operator.itemgetter(1), filter(lambda a: a[0]==result, cycle_mapping)))
other_cycles = map(operator.itemgetter(1), filter(lambda a: a[0]!=result, cycle_mapping))
all_states = set(map(lambda s: string.join(s, ""), itertools.permutations(["a","a","b","b","c"])))
other_states = all_states-set(itertools.chain(*my_cycles))
for input in inputs:
graph = decider.build_transition_graph(my_cycles, other_states, input)
if shortcut and graph.number_of_edges() == 0:
return False
graphs[(result, input, my_cycles)] = graph
for state in set(itertools.chain(*other_cycles)) - set(itertools.chain(*my_cycles)):
# print state, "->", map(operator.itemgetter(0), my_cycles)
if shortcut and all( nx.has_path(graph, state, cycle[0]) == False for cycle in my_cycles ):
return False
return graphs