boolean-algebra-engine 0.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.
- api/__init__.py +0 -0
- api/routes.py +386 -0
- boolean_algebra_engine-0.1.0.dist-info/METADATA +213 -0
- boolean_algebra_engine-0.1.0.dist-info/RECORD +19 -0
- boolean_algebra_engine-0.1.0.dist-info/WHEEL +5 -0
- boolean_algebra_engine-0.1.0.dist-info/entry_points.txt +2 -0
- boolean_algebra_engine-0.1.0.dist-info/licenses/LICENSE +674 -0
- boolean_algebra_engine-0.1.0.dist-info/top_level.txt +5 -0
- cli/__init__.py +0 -0
- cli/main.py +411 -0
- core/__init__.py +0 -0
- core/evaluator.py +86 -0
- core/models.py +96 -0
- core/parser.py +73 -0
- core/synthesizer.py +167 -0
- mcp_server/__init__.py +0 -0
- mcp_server/server.py +247 -0
- nl/__init__.py +0 -0
- nl/nl.py +449 -0
core/synthesizer.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/synthesizer.py — minimal expression synthesis via Quine-McCluskey.
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
synthesize(truth_table) → (str, PerformanceMetrics)
|
|
6
|
+
|
|
7
|
+
The inverse of evaluate(): given a TruthTable, returns the minimal sum-of-
|
|
8
|
+
products boolean expression that produces it. Uses the Quine-McCluskey
|
|
9
|
+
algorithm: iteratively merge minterms that differ by one bit to find prime
|
|
10
|
+
implicants, then select the minimum cover using essential prime implicants
|
|
11
|
+
first, greedy cover for the remainder.
|
|
12
|
+
|
|
13
|
+
Special cases:
|
|
14
|
+
No minterms (contradiction) → '0'
|
|
15
|
+
All minterms (tautology) → '1'
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import time
|
|
19
|
+
import tracemalloc
|
|
20
|
+
from .models import TruthTable, PerformanceMetrics
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _can_merge(a: str, b: str) -> tuple[bool, str]:
|
|
24
|
+
"""Return (True, merged) if two implicants differ in exactly one non-dash bit."""
|
|
25
|
+
diff_count = 0
|
|
26
|
+
diff_pos = -1
|
|
27
|
+
for i, (x, y) in enumerate(zip(a, b)):
|
|
28
|
+
if x != y:
|
|
29
|
+
if x == '-' or y == '-':
|
|
30
|
+
return False, ''
|
|
31
|
+
diff_count += 1
|
|
32
|
+
diff_pos = i
|
|
33
|
+
if diff_count > 1:
|
|
34
|
+
return False, ''
|
|
35
|
+
if diff_count == 1:
|
|
36
|
+
result = list(a)
|
|
37
|
+
result[diff_pos] = '-'
|
|
38
|
+
return True, ''.join(result)
|
|
39
|
+
return False, ''
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _covers(implicant: str, minterm: int, n: int) -> bool:
|
|
43
|
+
"""Return True if an implicant pattern covers a given minterm index."""
|
|
44
|
+
bits = format(minterm, f'0{n}b')
|
|
45
|
+
return all(p == '-' or p == b for p, b in zip(implicant, bits))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _minimum_cover(prime_implicants: list[str], minterms: list[int], n: int) -> list[str]:
|
|
49
|
+
"""Select the minimum set of prime implicants that covers all minterms."""
|
|
50
|
+
coverage = {pi: {m for m in minterms if _covers(pi, m, n)} for pi in prime_implicants}
|
|
51
|
+
selected = []
|
|
52
|
+
covered = set()
|
|
53
|
+
|
|
54
|
+
for m in minterms:
|
|
55
|
+
covering = [pi for pi in prime_implicants if m in coverage[pi]]
|
|
56
|
+
if len(covering) == 1 and covering[0] not in selected:
|
|
57
|
+
selected.append(covering[0])
|
|
58
|
+
covered |= coverage[covering[0]]
|
|
59
|
+
|
|
60
|
+
remaining = set(minterms) - covered
|
|
61
|
+
while remaining:
|
|
62
|
+
best = max(
|
|
63
|
+
(pi for pi in prime_implicants if pi not in selected),
|
|
64
|
+
key=lambda pi: len(coverage[pi] & remaining),
|
|
65
|
+
default=None,
|
|
66
|
+
)
|
|
67
|
+
if best is None:
|
|
68
|
+
break
|
|
69
|
+
selected.append(best)
|
|
70
|
+
covered |= coverage[best]
|
|
71
|
+
remaining -= coverage[best]
|
|
72
|
+
|
|
73
|
+
return selected
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _pi_to_expr(pi: str, variables: list[str]) -> str:
|
|
77
|
+
"""Convert a prime implicant bit pattern to a boolean product term string."""
|
|
78
|
+
terms = []
|
|
79
|
+
for bit, var in zip(pi, variables):
|
|
80
|
+
if bit == '1':
|
|
81
|
+
terms.append(var)
|
|
82
|
+
elif bit == '0':
|
|
83
|
+
terms.append(f'!{var}')
|
|
84
|
+
return '.'.join(terms) if terms else '1'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def synthesize(truth_table: TruthTable) -> tuple[str, PerformanceMetrics]:
|
|
88
|
+
"""
|
|
89
|
+
Synthesize the minimal boolean expression for a given truth table.
|
|
90
|
+
|
|
91
|
+
Uses Quine-McCluskey to find all prime implicants, then selects the
|
|
92
|
+
minimum cover — essential prime implicants first, greedy for the rest.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
truth_table: A TruthTable returned by evaluate().
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
(expression, PerformanceMetrics) — minimal SOP expression string
|
|
99
|
+
and timing/memory data. Returns '0' for contradictions, '1' for
|
|
100
|
+
tautologies.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
table, _ = evaluate('A.B+A.!B')
|
|
104
|
+
expr, metrics = synthesize(table)
|
|
105
|
+
print(expr) # 'A'
|
|
106
|
+
print(metrics.prime_implicant_count) # 1
|
|
107
|
+
"""
|
|
108
|
+
minterms = truth_table.minterms
|
|
109
|
+
variables = truth_table.variables
|
|
110
|
+
n = len(variables)
|
|
111
|
+
|
|
112
|
+
tracemalloc.start()
|
|
113
|
+
t_start = time.perf_counter()
|
|
114
|
+
|
|
115
|
+
if not minterms:
|
|
116
|
+
synth_time_ms = (time.perf_counter() - t_start) * 1000
|
|
117
|
+
_, peak = tracemalloc.get_traced_memory()
|
|
118
|
+
tracemalloc.stop()
|
|
119
|
+
return '0', PerformanceMetrics(synth_time_ms=round(synth_time_ms, 4), peak_memory_bytes=peak, prime_implicant_count=0)
|
|
120
|
+
|
|
121
|
+
if len(minterms) == 2 ** n:
|
|
122
|
+
synth_time_ms = (time.perf_counter() - t_start) * 1000
|
|
123
|
+
_, peak = tracemalloc.get_traced_memory()
|
|
124
|
+
tracemalloc.stop()
|
|
125
|
+
return '1', PerformanceMetrics(synth_time_ms=round(synth_time_ms, 4), peak_memory_bytes=peak, prime_implicant_count=0)
|
|
126
|
+
|
|
127
|
+
current: dict[str, set[int]] = {format(m, f'0{n}b'): {m} for m in minterms}
|
|
128
|
+
prime_implicants: list[str] = []
|
|
129
|
+
|
|
130
|
+
while True:
|
|
131
|
+
next_round: dict[str, set[int]] = {}
|
|
132
|
+
used: set[str] = set()
|
|
133
|
+
items = list(current.items())
|
|
134
|
+
|
|
135
|
+
for i in range(len(items)):
|
|
136
|
+
for j in range(i + 1, len(items)):
|
|
137
|
+
a, a_cov = items[i]
|
|
138
|
+
b, b_cov = items[j]
|
|
139
|
+
ok, merged = _can_merge(a, b)
|
|
140
|
+
if ok:
|
|
141
|
+
if merged not in next_round:
|
|
142
|
+
next_round[merged] = set()
|
|
143
|
+
next_round[merged] |= a_cov | b_cov
|
|
144
|
+
used.add(a)
|
|
145
|
+
used.add(b)
|
|
146
|
+
|
|
147
|
+
for term in current:
|
|
148
|
+
if term not in used:
|
|
149
|
+
prime_implicants.append(term)
|
|
150
|
+
|
|
151
|
+
if not next_round:
|
|
152
|
+
break
|
|
153
|
+
current = next_round
|
|
154
|
+
|
|
155
|
+
selected = _minimum_cover(prime_implicants, minterms, n)
|
|
156
|
+
expr = '+'.join(_pi_to_expr(pi, variables) for pi in selected)
|
|
157
|
+
|
|
158
|
+
synth_time_ms = (time.perf_counter() - t_start) * 1000
|
|
159
|
+
_, peak = tracemalloc.get_traced_memory()
|
|
160
|
+
tracemalloc.stop()
|
|
161
|
+
|
|
162
|
+
metrics = PerformanceMetrics(
|
|
163
|
+
synth_time_ms=round(synth_time_ms, 4),
|
|
164
|
+
peak_memory_bytes=peak,
|
|
165
|
+
prime_implicant_count=len(prime_implicants),
|
|
166
|
+
)
|
|
167
|
+
return expr, metrics
|
mcp_server/__init__.py
ADDED
|
File without changes
|
mcp_server/server.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mcp_server/server.py — MCP server wrapping the boolean algebra engine.
|
|
3
|
+
|
|
4
|
+
Exposes five tools Claude can call mid-conversation:
|
|
5
|
+
|
|
6
|
+
evaluate expression → full truth table + metrics
|
|
7
|
+
simplify expression → minimal equivalent expression
|
|
8
|
+
equivalent expr1, expr2 → bool (same truth table?)
|
|
9
|
+
satisfiable expression → bool (any row outputs 1?)
|
|
10
|
+
check_contradiction expression → bool (no row outputs 1?)
|
|
11
|
+
|
|
12
|
+
Run with:
|
|
13
|
+
python3.11 -m mcp_server.server
|
|
14
|
+
or via the MCP CLI:
|
|
15
|
+
mcp dev mcp_server/server.py
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
# Allow running from project root: `python3.11 -m mcp_server.server`
|
|
23
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
24
|
+
|
|
25
|
+
from mcp.server.fastmcp import FastMCP
|
|
26
|
+
from core.evaluator import evaluate as _evaluate
|
|
27
|
+
from core.synthesizer import synthesize as _synthesize
|
|
28
|
+
|
|
29
|
+
mcp = FastMCP(
|
|
30
|
+
"boolean-algebra-engine",
|
|
31
|
+
instructions=(
|
|
32
|
+
"Tools for evaluating boolean algebra expressions. "
|
|
33
|
+
"Use these to verify logic exactly — do not predict boolean results yourself. "
|
|
34
|
+
"Variables must be uppercase letters A-Z. "
|
|
35
|
+
"Operators: ! (NOT), . (AND), ^ (XOR), + (OR). "
|
|
36
|
+
"Parentheses override precedence."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def evaluate(expression: str) -> dict:
|
|
43
|
+
"""
|
|
44
|
+
Evaluate a boolean expression and return its full truth table.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
expression: Boolean expression using variables A-Z and operators ! . ^ +
|
|
48
|
+
Example: "A.(B+C)", "!(A.B)", "A^B"
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary with keys: expression, variables, rows (list of input/output dicts),
|
|
52
|
+
satisfiable, tautology, minterms, maxterms, eval_time_ms, rows_evaluated.
|
|
53
|
+
"""
|
|
54
|
+
table, metrics = _evaluate(expression)
|
|
55
|
+
return {
|
|
56
|
+
"expression": table.expression,
|
|
57
|
+
"variables": table.variables,
|
|
58
|
+
"rows": [{**row.inputs, "output": row.output} for row in table.rows],
|
|
59
|
+
"satisfiable": table.satisfiable,
|
|
60
|
+
"tautology": table.tautology,
|
|
61
|
+
"minterms": table.minterms,
|
|
62
|
+
"maxterms": table.maxterms,
|
|
63
|
+
"eval_time_ms": metrics.eval_time_ms,
|
|
64
|
+
"rows_evaluated": metrics.rows_evaluated,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def simplify(expression: str) -> dict:
|
|
70
|
+
"""
|
|
71
|
+
Simplify a boolean expression to its minimal sum-of-products form.
|
|
72
|
+
|
|
73
|
+
Useful for finding redundant conditions, dead branches, or the canonical
|
|
74
|
+
form of a complex expression.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
expression: Boolean expression using variables A-Z and operators ! . ^ +
|
|
78
|
+
Example: "A.B+A.!B" simplifies to "A"
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dictionary with keys: original, minimal, changed (bool),
|
|
82
|
+
prime_implicant_count, synth_time_ms.
|
|
83
|
+
"""
|
|
84
|
+
table, _ = _evaluate(expression)
|
|
85
|
+
minimal, metrics = _synthesize(table)
|
|
86
|
+
return {
|
|
87
|
+
"original": expression,
|
|
88
|
+
"minimal": minimal,
|
|
89
|
+
"changed": minimal != expression,
|
|
90
|
+
"prime_implicant_count": metrics.prime_implicant_count,
|
|
91
|
+
"synth_time_ms": metrics.synth_time_ms,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
def equivalent(expression1: str, expression2: str) -> dict:
|
|
97
|
+
"""
|
|
98
|
+
Check whether two boolean expressions are logically equivalent.
|
|
99
|
+
|
|
100
|
+
Two expressions are equivalent if they produce identical output columns
|
|
101
|
+
for all possible input combinations.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
expression1: First boolean expression.
|
|
105
|
+
expression2: Second boolean expression.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary with keys: equivalent (bool), expression1, expression2,
|
|
109
|
+
and on mismatch: differing_rows (list of input combos where they differ).
|
|
110
|
+
"""
|
|
111
|
+
t1, _ = _evaluate(expression1)
|
|
112
|
+
t2, _ = _evaluate(expression2)
|
|
113
|
+
|
|
114
|
+
vars1 = set(t1.variables)
|
|
115
|
+
vars2 = set(t2.variables)
|
|
116
|
+
all_vars = sorted(vars1 | vars2)
|
|
117
|
+
|
|
118
|
+
# Re-evaluate both over the union variable set for fair comparison
|
|
119
|
+
from core.parser import get_variables
|
|
120
|
+
combined_expr1 = expression1
|
|
121
|
+
combined_expr2 = expression2
|
|
122
|
+
|
|
123
|
+
# Evaluate over shared variable space
|
|
124
|
+
n = len(all_vars)
|
|
125
|
+
differing = []
|
|
126
|
+
for i in range(2 ** n):
|
|
127
|
+
values = {var: (i >> (n - 1 - j)) & 1 for j, var in enumerate(all_vars)}
|
|
128
|
+
# Filter to each expression's variables
|
|
129
|
+
from core.evaluator import _evaluate_prefix
|
|
130
|
+
from core.parser import infix_to_prefix
|
|
131
|
+
p1 = infix_to_prefix(expression1)
|
|
132
|
+
p2 = infix_to_prefix(expression2)
|
|
133
|
+
v1 = {k: v for k, v in values.items() if k in vars1}
|
|
134
|
+
v2 = {k: v for k, v in values.items() if k in vars2}
|
|
135
|
+
out1 = _evaluate_prefix(p1, v1)
|
|
136
|
+
out2 = _evaluate_prefix(p2, v2)
|
|
137
|
+
if out1 != out2:
|
|
138
|
+
differing.append({**values, expression1: out1, expression2: out2})
|
|
139
|
+
|
|
140
|
+
result: dict = {
|
|
141
|
+
"equivalent": len(differing) == 0,
|
|
142
|
+
"expression1": expression1,
|
|
143
|
+
"expression2": expression2,
|
|
144
|
+
}
|
|
145
|
+
if differing:
|
|
146
|
+
result["differing_rows"] = differing[:10] # cap at 10 for readability
|
|
147
|
+
result["total_differing"] = len(differing)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@mcp.tool()
|
|
152
|
+
def satisfiable(expression: str) -> dict:
|
|
153
|
+
"""
|
|
154
|
+
Check whether a boolean expression is satisfiable.
|
|
155
|
+
|
|
156
|
+
A satisfiable expression has at least one input combination that outputs 1.
|
|
157
|
+
An unsatisfiable expression is a contradiction (always 0).
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
expression: Boolean expression. Example: "A.!A" is unsatisfiable.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary with keys: satisfiable (bool), expression,
|
|
164
|
+
and if satisfiable: example (first input combo that satisfies it).
|
|
165
|
+
"""
|
|
166
|
+
table, _ = _evaluate(expression)
|
|
167
|
+
result: dict = {
|
|
168
|
+
"satisfiable": table.satisfiable,
|
|
169
|
+
"expression": expression,
|
|
170
|
+
}
|
|
171
|
+
if table.satisfiable:
|
|
172
|
+
first = table.rows[table.minterms[0]]
|
|
173
|
+
result["example"] = {**first.inputs, "output": first.output}
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@mcp.tool()
|
|
178
|
+
def check_prompt_logic(rules: list[str]) -> dict:
|
|
179
|
+
"""
|
|
180
|
+
Check a set of boolean rules (e.g. from a system prompt) for contradictions,
|
|
181
|
+
tautologies, and pairwise equivalences.
|
|
182
|
+
|
|
183
|
+
Each rule should be a boolean expression using A-Z variables. Use consistent
|
|
184
|
+
variable naming across rules (e.g. A=user_authenticated, B=is_admin).
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
rules: List of boolean expressions representing logical conditions.
|
|
188
|
+
Example: ["A.B", "!A+!B", "A^B"]
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dictionary with per-rule analysis (satisfiable, tautology, minimal form)
|
|
192
|
+
and pairwise checks (equivalent pairs, contradictory pairs).
|
|
193
|
+
"""
|
|
194
|
+
analysis = []
|
|
195
|
+
for rule in rules:
|
|
196
|
+
try:
|
|
197
|
+
table, _ = _evaluate(rule)
|
|
198
|
+
minimal, _ = _synthesize(table)
|
|
199
|
+
analysis.append({
|
|
200
|
+
"rule": rule,
|
|
201
|
+
"satisfiable": table.satisfiable,
|
|
202
|
+
"tautology": table.tautology,
|
|
203
|
+
"contradiction": not table.satisfiable,
|
|
204
|
+
"minimal": minimal,
|
|
205
|
+
"simplified": minimal != rule,
|
|
206
|
+
})
|
|
207
|
+
except ValueError as e:
|
|
208
|
+
analysis.append({"rule": rule, "error": str(e)})
|
|
209
|
+
|
|
210
|
+
# Pairwise equivalence and contradiction checks
|
|
211
|
+
pairs = []
|
|
212
|
+
valid = [a for a in analysis if "error" not in a]
|
|
213
|
+
for i in range(len(valid)):
|
|
214
|
+
for j in range(i + 1, len(valid)):
|
|
215
|
+
r1, r2 = valid[i]["rule"], valid[j]["rule"]
|
|
216
|
+
try:
|
|
217
|
+
eq = equivalent(r1, r2)
|
|
218
|
+
t1, _ = _evaluate(r1)
|
|
219
|
+
t2, _ = _evaluate(r2)
|
|
220
|
+
# Contradiction: rules can never both be true simultaneously
|
|
221
|
+
combined = f"({r1}).({r2})"
|
|
222
|
+
combined_table, _ = _evaluate(combined)
|
|
223
|
+
pairs.append({
|
|
224
|
+
"rule1": r1,
|
|
225
|
+
"rule2": r2,
|
|
226
|
+
"equivalent": eq["equivalent"],
|
|
227
|
+
"can_both_be_true": combined_table.satisfiable,
|
|
228
|
+
"always_conflict": not combined_table.satisfiable,
|
|
229
|
+
})
|
|
230
|
+
except ValueError:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"rules": analysis,
|
|
235
|
+
"pairwise": pairs,
|
|
236
|
+
"summary": {
|
|
237
|
+
"total": len(rules),
|
|
238
|
+
"contradictions": sum(1 for a in analysis if a.get("contradiction")),
|
|
239
|
+
"tautologies": sum(1 for a in analysis if a.get("tautology")),
|
|
240
|
+
"equivalent_pairs": sum(1 for p in pairs if p.get("equivalent")),
|
|
241
|
+
"conflicting_pairs": sum(1 for p in pairs if p.get("always_conflict")),
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
mcp.run(transport="stdio")
|
nl/__init__.py
ADDED
|
File without changes
|