stanlogic 1.1.0__py3-none-any.whl
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.
stanlogic/__init__.py
ADDED
stanlogic/kmapsolver.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
stanclustlogic.py, library to easily solve 2-4 variable kmaps.
|
|
3
|
+
|
|
4
|
+
Developed from kmapsolver.py, kmap_solver_prototype.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Author: Somtochukwu Stanislus Emeka-Onwuneme
|
|
8
|
+
# Publication Date: 8th September, 2025
|
|
9
|
+
# Copyright © 2025 Somtochukwu Stanislus Emeka-Onwuneme
|
|
10
|
+
#---------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
|
|
14
|
+
class KMapSolver:
|
|
15
|
+
def __init__(self, kmap, convention = "vranseic"):
|
|
16
|
+
"""
|
|
17
|
+
Initialize with a given K-map.
|
|
18
|
+
kmap: 2D list with values 0, 1, or 'd'
|
|
19
|
+
"""
|
|
20
|
+
self.kmap = kmap
|
|
21
|
+
self.num_rows = len(kmap)
|
|
22
|
+
self.num_cols = len(kmap[0])
|
|
23
|
+
self.convention = convention.lower().strip()
|
|
24
|
+
|
|
25
|
+
# Determine number of variables (2, 3, or 4)
|
|
26
|
+
size = self.num_rows * self.num_cols
|
|
27
|
+
if size == 4:
|
|
28
|
+
self.num_vars = 2
|
|
29
|
+
self.row_labels = ["0", "1"]
|
|
30
|
+
self.col_labels = ["0", "1"]
|
|
31
|
+
elif size == 8:
|
|
32
|
+
self.num_vars = 3
|
|
33
|
+
self.row_labels = ["0", "1"]
|
|
34
|
+
self.col_labels = ["00", "01", "11", "10"]
|
|
35
|
+
elif size == 16:
|
|
36
|
+
self.num_vars = 4
|
|
37
|
+
self.row_labels = ["00", "01", "11", "10"]
|
|
38
|
+
self.col_labels = ["00", "01", "11", "10"]
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError("K-map must be 2x2 (2 vars), 2x4/4x2 (3 vars), or 4x4 (4 vars)")
|
|
41
|
+
|
|
42
|
+
# Group sizes (powers of 2 only)
|
|
43
|
+
self.group_sizes = [(1,1),(1,2),(2,1),(2,2),
|
|
44
|
+
(1,4),(4,1),(2,4),(4,2),(4,4)]
|
|
45
|
+
|
|
46
|
+
# ---- Optimizations: precompute mapping and prune group sizes ----
|
|
47
|
+
# keep only group sizes that actually fit the K-map
|
|
48
|
+
self.group_sizes = [(h,w) for (h,w) in self.group_sizes if h <= self.num_rows and w <= self.num_cols]
|
|
49
|
+
|
|
50
|
+
# Precompute cell index (minterm/maxterm index) and bit-strings for each cell.
|
|
51
|
+
# Use _cell_to_bits/_cell_to_term (methods are available on the instance)
|
|
52
|
+
self._cell_index = [[self._cell_to_term(r, c) for c in range(self.num_cols)] for r in range(self.num_rows)]
|
|
53
|
+
self._cell_bits = [[self._cell_to_bits(r, c) for c in range(self.num_cols)] for r in range(self.num_rows)]
|
|
54
|
+
# map minterm index -> (r, c)
|
|
55
|
+
self._index_to_rc = {self._cell_index[r][c]: (r, c) for r in range(self.num_rows) for c in range(self.num_cols)}
|
|
56
|
+
|
|
57
|
+
# ---------- Utility functions ---------- #
|
|
58
|
+
def _cell_to_term(self, r, c):
|
|
59
|
+
"""Convert (row, col) to a minterm index."""
|
|
60
|
+
bits = self.col_labels[c] + self.row_labels[r]
|
|
61
|
+
return int(bits, 2)
|
|
62
|
+
|
|
63
|
+
# def _cell_to_maxterm(self, r, c):
|
|
64
|
+
# """Convert (row, col) to a maxterm index."""
|
|
65
|
+
# bits = self.col_labels[c] + self.row_labels[r]
|
|
66
|
+
# return int(bits, 2)
|
|
67
|
+
|
|
68
|
+
def _cell_to_bits(self, r, c):
|
|
69
|
+
"""Return the bit string representing (row, col) based on the convention."""
|
|
70
|
+
if self.convention == "mano_kime":
|
|
71
|
+
# Mano-Kime → rows = x1x2, cols = x3x4
|
|
72
|
+
bits = self.row_labels[r] + self.col_labels[c]
|
|
73
|
+
else:
|
|
74
|
+
# Vranesic → rows = x3x4, cols = x1x2
|
|
75
|
+
bits = self.col_labels[c] + self.row_labels[r]
|
|
76
|
+
return bits
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_group_coords(self, r, c, h, w):
|
|
80
|
+
"""Get wrapped coordinates for a group starting at (r,c)."""
|
|
81
|
+
return [((r+i) % self.num_rows, (c+j) % self.num_cols)
|
|
82
|
+
for i in range(h) for j in range(w)]
|
|
83
|
+
|
|
84
|
+
# ---------- Group finding ---------- #
|
|
85
|
+
def find_all_groups(self, allow_dontcare=False):
|
|
86
|
+
"""Find all groups of 1’s (and optionally 'd'). Return integer bitmasks for groups."""
|
|
87
|
+
groups = set()
|
|
88
|
+
for r in range(self.num_rows):
|
|
89
|
+
for c in range(self.num_cols):
|
|
90
|
+
for h, w in self.group_sizes:
|
|
91
|
+
coords = self._get_group_coords(r, c, h, w)
|
|
92
|
+
|
|
93
|
+
if allow_dontcare:
|
|
94
|
+
if all(self.kmap[rr][cc] in (1, 'd') for rr, cc in coords) and \
|
|
95
|
+
any(self.kmap[rr][cc] == 1 for rr, cc in coords):
|
|
96
|
+
mask = 0
|
|
97
|
+
for rr, cc in coords:
|
|
98
|
+
mask |= 1 << self._cell_index[rr][cc]
|
|
99
|
+
groups.add(mask)
|
|
100
|
+
else:
|
|
101
|
+
if all(self.kmap[rr][cc] == 1 for rr, cc in coords):
|
|
102
|
+
mask = 0
|
|
103
|
+
for rr, cc in coords:
|
|
104
|
+
mask |= 1 << self._cell_index[rr][cc]
|
|
105
|
+
groups.add(mask)
|
|
106
|
+
return list(groups)
|
|
107
|
+
|
|
108
|
+
def find_all_groups_pos(self, allow_dontcare=False):
|
|
109
|
+
"""Find all groups of 0's (and optionally 'd') for POS. Return integer bitmasks."""
|
|
110
|
+
groups = set()
|
|
111
|
+
for r in range(self.num_rows):
|
|
112
|
+
for c in range(self.num_cols):
|
|
113
|
+
for h, w in self.group_sizes:
|
|
114
|
+
coords = self._get_group_coords(r, c, h, w)
|
|
115
|
+
|
|
116
|
+
if allow_dontcare:
|
|
117
|
+
if all(self.kmap[rr][cc] in (0, 'd') for rr, cc in coords) and \
|
|
118
|
+
any(self.kmap[rr][cc] == 0 for rr, cc in coords):
|
|
119
|
+
mask = 0
|
|
120
|
+
for rr, cc in coords:
|
|
121
|
+
mask |= 1 << self._cell_index[rr][cc]
|
|
122
|
+
groups.add(mask)
|
|
123
|
+
else:
|
|
124
|
+
if all(self.kmap[rr][cc] == 0 for rr, cc in coords):
|
|
125
|
+
mask = 0
|
|
126
|
+
for rr, cc in coords:
|
|
127
|
+
mask |= 1 << self._cell_index[rr][cc]
|
|
128
|
+
groups.add(mask)
|
|
129
|
+
return list(groups)
|
|
130
|
+
|
|
131
|
+
def filter_prime_implicants(self, groups):
|
|
132
|
+
"""Remove groups that are strict subsets of others. groups are integer masks."""
|
|
133
|
+
primes = []
|
|
134
|
+
# sort by bit_count descending to allow early pruning (larger groups first)
|
|
135
|
+
groups_sorted = sorted(groups, key=lambda g: g.bit_count(), reverse=True)
|
|
136
|
+
for i, g in enumerate(groups_sorted):
|
|
137
|
+
is_subset = False
|
|
138
|
+
for other in groups_sorted:
|
|
139
|
+
if other == g:
|
|
140
|
+
continue
|
|
141
|
+
# if g is subset of other
|
|
142
|
+
if (g & other) == g:
|
|
143
|
+
is_subset = True
|
|
144
|
+
break
|
|
145
|
+
if not is_subset:
|
|
146
|
+
primes.append(g)
|
|
147
|
+
return primes
|
|
148
|
+
|
|
149
|
+
# ---------- Boolean simplification ---------- #
|
|
150
|
+
def _simplify_group_bits(self, bits_list):
|
|
151
|
+
bits = list(bits_list[0])
|
|
152
|
+
for b in bits_list[1:]:
|
|
153
|
+
for i in range(self.num_vars):
|
|
154
|
+
if bits[i] != b[i]:
|
|
155
|
+
bits[i] = '-'
|
|
156
|
+
|
|
157
|
+
vars_ = [f"x{i+1}" for i in range(self.num_vars)]
|
|
158
|
+
term = []
|
|
159
|
+
for i, b in enumerate(bits):
|
|
160
|
+
if b == '0':
|
|
161
|
+
term.append(vars_[i] + "'")
|
|
162
|
+
elif b == '1':
|
|
163
|
+
term.append(vars_[i])
|
|
164
|
+
return "".join(term)
|
|
165
|
+
|
|
166
|
+
def _simplify_group_bits_pos(self, bits_list):
|
|
167
|
+
"""Convert group bits to POS term."""
|
|
168
|
+
bits = list(bits_list[0])
|
|
169
|
+
for b in bits_list[1:]:
|
|
170
|
+
for i in range(self.num_vars):
|
|
171
|
+
if bits[i] != b[i]:
|
|
172
|
+
bits[i] = '-'
|
|
173
|
+
|
|
174
|
+
vars_ = [f"x{i+1}" for i in range(self.num_vars)]
|
|
175
|
+
term = []
|
|
176
|
+
for i, b in enumerate(bits):
|
|
177
|
+
if b == '1':
|
|
178
|
+
term.append(vars_[i] + "'")
|
|
179
|
+
elif b == '0':
|
|
180
|
+
term.append(vars_[i])
|
|
181
|
+
return "(" + " + ".join(term) + ")"
|
|
182
|
+
|
|
183
|
+
# ---------- Main minimization ---------- #
|
|
184
|
+
def minimize(self, form='sop'):
|
|
185
|
+
"""
|
|
186
|
+
Minimize the K-map expression.
|
|
187
|
+
Args:
|
|
188
|
+
form: 'sop' for Sum of Products or 'pos' for Product of Sums
|
|
189
|
+
Returns:
|
|
190
|
+
tuple: (list of terms, string expression)
|
|
191
|
+
"""
|
|
192
|
+
if form.lower() not in ['sop', 'pos']:
|
|
193
|
+
raise ValueError("form must be either 'sop' or 'pos'")
|
|
194
|
+
|
|
195
|
+
target_val = 0 if form.lower() == 'pos' else 1
|
|
196
|
+
|
|
197
|
+
# Choose appropriate group-finding and simplifier
|
|
198
|
+
if form.lower() == 'pos':
|
|
199
|
+
groups = self.find_all_groups_pos(allow_dontcare=True)
|
|
200
|
+
simplify_method = self._simplify_group_bits_pos
|
|
201
|
+
join_operator = " * "
|
|
202
|
+
else:
|
|
203
|
+
groups = self.find_all_groups(allow_dontcare=True)
|
|
204
|
+
simplify_method = self._simplify_group_bits
|
|
205
|
+
join_operator = " + "
|
|
206
|
+
|
|
207
|
+
# Step 2: reduce to prime implicants (groups are bitmasks)
|
|
208
|
+
prime_groups = self.filter_prime_implicants(groups)
|
|
209
|
+
|
|
210
|
+
# Step 3: compute terms and covers (use bitmasks for covers)
|
|
211
|
+
prime_terms = []
|
|
212
|
+
prime_covers = []
|
|
213
|
+
for gmask in prime_groups:
|
|
214
|
+
# collect only actual minterms (target_val) covered by this group
|
|
215
|
+
cover_mask = 0
|
|
216
|
+
bits_list = []
|
|
217
|
+
temp = gmask
|
|
218
|
+
while temp:
|
|
219
|
+
low = temp & -temp
|
|
220
|
+
idx = low.bit_length() - 1
|
|
221
|
+
temp -= low
|
|
222
|
+
r, c = self._index_to_rc[idx]
|
|
223
|
+
if self.kmap[r][c] == target_val:
|
|
224
|
+
cover_mask |= 1 << idx
|
|
225
|
+
# include bits for simplification (include don't-cares as they are part of the group)
|
|
226
|
+
bits_list.append(self._cell_bits[r][c])
|
|
227
|
+
if cover_mask == 0:
|
|
228
|
+
continue
|
|
229
|
+
term_str = simplify_method(bits_list)
|
|
230
|
+
prime_terms.append(term_str)
|
|
231
|
+
prime_covers.append(cover_mask)
|
|
232
|
+
|
|
233
|
+
# Step 4: build prime implicant chart using masks
|
|
234
|
+
minterm_to_primes = defaultdict(list)
|
|
235
|
+
all_minterms_mask = 0
|
|
236
|
+
for p_idx, cover in enumerate(prime_covers):
|
|
237
|
+
all_minterms_mask |= cover
|
|
238
|
+
temp = cover
|
|
239
|
+
while temp:
|
|
240
|
+
low = temp & -temp
|
|
241
|
+
idx = low.bit_length() - 1
|
|
242
|
+
temp -= low
|
|
243
|
+
minterm_to_primes[idx].append(p_idx)
|
|
244
|
+
|
|
245
|
+
# Step 5: essential primes
|
|
246
|
+
essential_indices = set()
|
|
247
|
+
for m, primes in minterm_to_primes.items():
|
|
248
|
+
if len(primes) == 1:
|
|
249
|
+
essential_indices.add(primes[0])
|
|
250
|
+
|
|
251
|
+
essential_terms = [prime_terms[i] for i in sorted(essential_indices)]
|
|
252
|
+
|
|
253
|
+
# Step 6: mark covered by essentials
|
|
254
|
+
covered_mask = 0
|
|
255
|
+
for i in essential_indices:
|
|
256
|
+
covered_mask |= prime_covers[i]
|
|
257
|
+
|
|
258
|
+
# Step 7: greedy covering (bitmask-based)
|
|
259
|
+
remaining_mask = all_minterms_mask & ~covered_mask
|
|
260
|
+
selected = set(essential_indices)
|
|
261
|
+
while remaining_mask:
|
|
262
|
+
best_idx, best_cover_count = None, -1
|
|
263
|
+
for idx in range(len(prime_covers)):
|
|
264
|
+
if idx in selected:
|
|
265
|
+
continue
|
|
266
|
+
cover = prime_covers[idx] & remaining_mask
|
|
267
|
+
count = cover.bit_count()
|
|
268
|
+
if count > best_cover_count:
|
|
269
|
+
best_cover_count = count
|
|
270
|
+
best_idx = idx
|
|
271
|
+
if best_idx is None or best_cover_count == 0:
|
|
272
|
+
break
|
|
273
|
+
selected.add(best_idx)
|
|
274
|
+
covered_mask |= prime_covers[best_idx]
|
|
275
|
+
remaining_mask = all_minterms_mask & ~covered_mask
|
|
276
|
+
|
|
277
|
+
# Step 8: remove redundant terms using masks
|
|
278
|
+
def covers_with_indices(indices):
|
|
279
|
+
mask = 0
|
|
280
|
+
for i in indices:
|
|
281
|
+
mask |= prime_covers[i]
|
|
282
|
+
return mask
|
|
283
|
+
|
|
284
|
+
chosen = set(selected)
|
|
285
|
+
for idx in list(sorted(chosen)):
|
|
286
|
+
trial = chosen - {idx}
|
|
287
|
+
if covers_with_indices(trial) == covers_with_indices(chosen):
|
|
288
|
+
chosen = trial
|
|
289
|
+
|
|
290
|
+
final_terms = [prime_terms[i] for i in sorted(chosen)]
|
|
291
|
+
return final_terms, join_operator.join(final_terms)
|
|
292
|
+
|
|
293
|
+
# ---------- Display ---------- #
|
|
294
|
+
def print_kmap(self):
|
|
295
|
+
"""Pretty print the K-map with headers."""
|
|
296
|
+
print(" " + " ".join(self.col_labels))
|
|
297
|
+
for i, row in enumerate(self.kmap):
|
|
298
|
+
print(f"{self.row_labels[i]} " + " ".join(str(val) for val in row))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stanlogic
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A Boolean simplification and inference library that integrates K-Map simplification and logical inference.
|
|
5
|
+
Author: Stan's Technologies
|
|
6
|
+
Author-email: Somtochukwu Stanislus Emeka-Onwuneme <somtochukwueo@outlook.com>
|
|
7
|
+
License: MIT NC
|
|
8
|
+
Keywords: boolean-algebra,kmap,logic-simplification,inference,symbolic-logic
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
stanlogic/__init__.py,sha256=A3ha2ooaqqTZv6-6XLiR5BN2FJJ7aBbCKfwTSFWQ9wU,128
|
|
2
|
+
stanlogic/kmapsolver.py,sha256=VgB1TsTCvmdN0Ob5CLc80kawIpgcr5IW1L0cODFwtGc,12267
|
|
3
|
+
stanlogic-1.1.0.dist-info/METADATA,sha256=kKPamyyo4q759Wc4cYOVT3rMwFcnWoYDwZ39eVght-8,634
|
|
4
|
+
stanlogic-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
stanlogic-1.1.0.dist-info/top_level.txt,sha256=00rMI81vstSf8bY_jjJl1whHKrWLzPIYz0aQJF3vzu8,10
|
|
6
|
+
stanlogic-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stanlogic
|