-
Notifications
You must be signed in to change notification settings - Fork 0
/
ZebraPuzzle.py
217 lines (192 loc) · 9.18 KB
/
ZebraPuzzle.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
import numpy as np
from IPython.display import display, HTML
# Association of the values used in the data storage matrix
YES = 1
NO = 2
MAYBE = 0
class ZebraPuzzle:
def __init__(self, categories_attributes: dict):
self.categories_attributes: dict = categories_attributes
# Prepare some convenience variables
self.categories: list = list(self.categories_attributes.keys())
self.number_categories: int = len(self.categories)
self.number_attributes: int = len(self.categories_attributes[self.categories[0]])
# Prepare some more convenience variables
self.attributes = []
for i, k in enumerate(self.categories):
for j, n in enumerate(self.categories_attributes[k]):
self.attributes.append(n.lower())
self._tot: int = int(self.number_categories * self.number_attributes)
# Check if each category has same number of attributes
for k in self.categories_attributes:
assert len(self.categories_attributes[k]) == self.number_attributes
# Prepare the data-container
self._data = np.zeros([self._tot, self._tot], dtype=int)
# Initialize the trivial data
assert YES == 1 and NO == 2, "If you want to change the values of YES and NO, please also change the code below accordingly."
su = 2 * np.ones([self.number_attributes, ] * 2) - np.identity(self.number_attributes)
for i in range(self.number_categories):
self._data[i * self.number_attributes:(i + 1) * self.number_attributes,
i * self.number_attributes:(i + 1) * self.number_attributes] = su
def get_category(self, x: int) -> int:
return int(x / self.number_attributes)
def get_category_from_name(self, name: str) -> int:
return self.get_category(self.get_id(name))
def get_id(self, name: str) -> int:
return self.attributes.index(name.lower())
def get_assoc_name(self, a: str, b: str) -> int:
return self.get_assoc(self.get_id(a), self.get_id(b))
def get_assoc(self, a: int, b: int) -> int:
assert a != b, "Requesting the association of a attribute with itself does not make sense."
# assert that a and b are not from the same category
assert self._data[a, b] == self._data[b, a]
return self._data[min(a, b), max(a, b)]
def add_assoc_name(self, name_a: str, name_b: str, assoc: bool):
k_a = self.get_id(name_a)
k_b = self.get_id(name_b)
self.add_assoc(k_a, k_b, assoc)
def add_assoc(self, a: int, b: int, assoc: bool):
v = YES if assoc else NO
assert self.get_assoc(a, b) == MAYBE or self.get_assoc(a, b) == v, \
f"Try to overwrite field {a, b} with value {self.get_assoc(a, b)}."
# In case of a positive assoc, we can already remove quite a few possibilities.
if assoc:
for i in range(self.number_attributes):
self._data[self.get_category(a) * self.number_attributes + i, b] = NO
self._data[a, self.get_category(b) * self.number_attributes + i] = NO
self._data[b, self.get_category(a) * self.number_attributes + i] = NO
self._data[self.get_category(b) * self.number_attributes + i, a] = NO
self._data[a, b] = v
self._data[b, a] = v
def generate_matrix_html(self) -> str:
from IPython.display import display, HTML
# Some css to make the table look nice
table = """
<style type="text/css">
table, th, td {
border: 1px solid black;
margin: 0px;
padding: 0px;
}
th
{
vertical-align: bottom;
text-align: center;
}
th span
{
-ms-writing-mode: tb-rl;
-webkit-writing-mode: vertical-rl;
writing-mode: vertical-rl;
transform: rotate(180deg);
white-space: nowrap;
spacing: 2px;
}
</style>
"""
# Now generate the required HTML
table += '<table>'
# Print categories
table += '<tr><th></th><th></th>'
for ki in range(1, self.number_categories):
k = self.categories[ki]
table += f"<th colspan='{self.number_attributes:d}' style='text-align:center;'>{k}</th><th></th>"
table += "</tr><tr><th></th><th></th>"
# Print attributes
for ki in range(1, self.number_categories):
k = self.categories[ki]
for att in self.categories_attributes[k]:
table += f"<th class='rotate'><span>{att}</span></th>"
table += "<th></th>"
table += "</tr><tr>"
# Print main body
for ki in range(0, self.number_categories - 1):
# Print categories on left
k = self.categories[ki]
table += f"<th rowspan='{self.number_attributes:d}'><span>{k}</span></th>"
for i, att in enumerate(self.categories_attributes[k]):
# Print attributes on left
it = ki * self.number_attributes + i
table += f"<th>{att}</th>"
for kj in range(1, self.number_categories):
for j, att2 in enumerate(self.categories_attributes[k]):
# Print values
jt = kj * self.number_attributes + j
if kj <= ki:
c = "-"
elif self.get_assoc(jt, it) == YES:
c = 'o'
elif self.get_assoc(jt, it) == NO:
c = 'x'
else:
c = '.'
table += f"<td>{c}</td>"
table += f"<td>|</td>"
table += "</tr><tr>"
table += "<td>-</td>" * (2 + (self.number_categories - 1) * (self.number_attributes + 1))
table += "</tr><tr>"
table += '</tr></table>'
table += f"Filled about {int(np.round(self.get_percentage_solved())):d}% of the puzzle."
return table
def display_matrix(self):
# Display output
html_table = HTML(self.generate_matrix_html())
display(html_table)
def _exclude(self, x: int, y: int):
assert self.get_assoc(x, y) == YES
kx = self.get_category(x)
ky = self.get_category(y)
for xi in range(0, self._tot):
if xi != y and self.get_assoc(xi, y) == NO:
if self.get_category(xi) != kx and self.get_category(xi) != ky:
if self.get_assoc(x, xi) == MAYBE:
print(f"By simple logic '{self.attributes[x]}' and '{self.attributes[xi]}'" +
f" are negatively associated.")
self.add_assoc(x, xi, False)
def exclude_all(self):
for i in range(self._tot):
for j in range(i + 1, self._tot):
if self.get_assoc(i, j) == YES:
self._exclude(i, j)
self._exclude(j, i)
def find_solved(self):
for ki in range(self.number_categories):
for kj in range(ki + 1, self.number_categories):
sr, er = ki * self.number_attributes, (ki + 1) * self.number_attributes
for x in range(sr, er):
sc, ec = kj * self.number_attributes, (kj + 1) * self.number_attributes
d = [r == NO for r in self._data[x, sc:ec]]
# print(x, f"{sc}:{ec}", d)
if sum(d) == self.number_attributes - 1:
for y in range(sc, ec):
if self.get_assoc(x, y) == 0:
print(f"By simple logic '{self.attributes[x]}' and '{self.attributes[y]}'" +
f" are positively associated.")
# print(self._data[x, sc:ec])
self.add_assoc(x, y, True)
# Other way around.
# TODO: this is ugly code-duplication. Re-implement this at your leisure.
sc, ec = kj * self.number_attributes, (kj + 1) * self.number_attributes
for y in range(sc, ec):
d = [r == NO for r in self._data[sr:er, y]]
# print(x, f"{sc}:{ec}", d)
if sum(d) == self.number_attributes - 1:
for x in range(sr, er):
if self._data[x, y] == 0:
print(f"By simple logic '{self.attributes[x]}' and '{self.attributes[y]}'" +
f" are positively associated.")
# print(self._data[sc:ec, y])
self.add_assoc(x, y, True)
def iterate_logic(self):
p = 0
while self.get_percentage_solved() > p:
p = self.get_percentage_solved()
self.exclude_all()
self.find_solved()
def get_percentage_solved(self):
# Estimates the percentage to which this puzzle is solved.
n = self._tot
m = self.number_attributes
n_yes = self.number_categories ** 2 * m
init = self.number_categories * (m ** 2 - m) * NO + self.number_categories * m * YES
return 100 * (np.sum(self._data) - init) / (YES * n_yes + NO * (n ** 2 - n_yes) - init)