restructure project + unit tests
This commit is contained in:
+4
-1
@@ -1,2 +1,5 @@
|
||||
*.kate-swp
|
||||
|
||||
.venv/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.egg-info/
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
## anf.py
|
||||
# boolean-tools
|
||||
|
||||
## install
|
||||
```
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## unit tests
|
||||
```
|
||||
pip install pytest
|
||||
pytest
|
||||
```
|
||||
|
||||
|
||||
### anf.py
|
||||
|
||||
Takes a boolean expression with 'not' (!) 'and' (AND) and 'or' (OR) in input and outputs its simplified ANF form (with XOR (⊕) and AND (⸱)).
|
||||
|
||||
#### run:
|
||||
```
|
||||
anf
|
||||
```
|
||||
|
||||
#### example:
|
||||
|
||||
input:
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "boolean-tools"
|
||||
version = "0.1.0"
|
||||
description = "Boolean tools"
|
||||
authors = [{name = "Sam HADOW"}]
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[project.scripts]
|
||||
anf = "anf.cli:main"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
@@ -0,0 +1,6 @@
|
||||
from .parser import Parser, tokenize
|
||||
from .printer import format_poly
|
||||
|
||||
def convert(expr: str) -> str:
|
||||
poly = Parser(tokenize(expr)).parse()
|
||||
return format_poly(poly)
|
||||
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from . import convert
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) > 1:
|
||||
expr = " ".join(sys.argv[1:])
|
||||
else:
|
||||
expr = input("Enter formula: ").strip()
|
||||
|
||||
print(convert(expr))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import FrozenSet
|
||||
|
||||
Monomial = FrozenSet[str]
|
||||
Poly = set[Monomial]
|
||||
|
||||
|
||||
def zero() -> Poly:
|
||||
return set()
|
||||
|
||||
|
||||
def one() -> Poly:
|
||||
return {frozenset()}
|
||||
|
||||
|
||||
def var(name: str) -> Poly:
|
||||
return {frozenset([name])}
|
||||
|
||||
|
||||
def poly_xor(a: Poly, b: Poly) -> Poly:
|
||||
return a ^ b
|
||||
|
||||
|
||||
def poly_and(a: Poly, b: Poly) -> Poly:
|
||||
out: Poly = set()
|
||||
for ma in a:
|
||||
for mb in b:
|
||||
m = frozenset(set(ma) | set(mb))
|
||||
if m in out:
|
||||
out.remove(m)
|
||||
else:
|
||||
out.add(m)
|
||||
return out
|
||||
|
||||
|
||||
def poly_not(a: Poly) -> Poly:
|
||||
return poly_xor(a, one())
|
||||
+3
-120
@@ -1,68 +1,9 @@
|
||||
#!/bin/python3
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import FrozenSet, Iterable, List, Union
|
||||
from typing import List
|
||||
|
||||
import sys
|
||||
|
||||
#####################
|
||||
# ANF representation over GF(2)
|
||||
#####################
|
||||
#
|
||||
# polynomial: set of monomials.
|
||||
# monomial: frozenset of variable names.
|
||||
#
|
||||
# Examples:
|
||||
# 0 -> set()
|
||||
# 1 -> {frozenset()}
|
||||
# X3 -> {frozenset({"X3"})}
|
||||
# X3 XOR X4 -> {{"X3"}, {"X4"}}
|
||||
# X3 AND X4 -> {{"X3","X4"}}
|
||||
|
||||
|
||||
Monomial = FrozenSet[str]
|
||||
Poly = set[Monomial]
|
||||
|
||||
|
||||
def zero() -> Poly:
|
||||
return set()
|
||||
|
||||
|
||||
def one() -> Poly:
|
||||
return {frozenset()}
|
||||
|
||||
|
||||
def var(name: str) -> Poly:
|
||||
return {frozenset([name])}
|
||||
|
||||
|
||||
def poly_xor(a: Poly, b: Poly) -> Poly:
|
||||
# symmetric difference = XOR over GF(2)
|
||||
return a ^ b
|
||||
|
||||
|
||||
def poly_and(a: Poly, b: Poly) -> Poly:
|
||||
out: Poly = set()
|
||||
for ma in a:
|
||||
for mb in b:
|
||||
m = frozenset(set(ma) | set(mb))
|
||||
if m in out:
|
||||
out.remove(m)
|
||||
else:
|
||||
out.add(m)
|
||||
return out
|
||||
|
||||
|
||||
def poly_not(a: Poly) -> Poly:
|
||||
# NOT a = a XOR 1
|
||||
return poly_xor(a, one())
|
||||
|
||||
|
||||
#####################
|
||||
# Tokenizer
|
||||
#####################
|
||||
from .core import Poly, poly_and, poly_not, poly_xor, one, var, zero
|
||||
|
||||
TOKEN_RE = re.compile(
|
||||
r"""
|
||||
@@ -79,6 +20,7 @@ TOKEN_RE = re.compile(
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def tokenize(s: str) -> List[str]:
|
||||
tokens = TOKEN_RE.findall(s)
|
||||
stripped = re.sub(r"\s+", "", s)
|
||||
@@ -87,22 +29,6 @@ def tokenize(s: str) -> List[str]:
|
||||
return tokens
|
||||
|
||||
|
||||
#####################
|
||||
# Parser
|
||||
#####################
|
||||
# priotity order:
|
||||
# !
|
||||
# AND
|
||||
# OR
|
||||
#
|
||||
# Grammar:
|
||||
# expr := or_expr
|
||||
# or_expr := and_expr ("OR" and_expr)*
|
||||
# and_expr := not_expr ("AND" not_expr)*
|
||||
# not_expr := "!" not_expr | atom
|
||||
# atom := VAR | 0 | 1 | "(" expr ")"
|
||||
|
||||
|
||||
class Parser:
|
||||
def __init__(self, tokens: List[str]):
|
||||
self.tokens = tokens
|
||||
@@ -172,46 +98,3 @@ class Parser:
|
||||
return var(tok)
|
||||
|
||||
raise ValueError(f"Unexpected token: {tok!r}")
|
||||
|
||||
#####################
|
||||
# CLI PRETTY PRINT
|
||||
#####################
|
||||
|
||||
def monomial_key(m: Monomial):
|
||||
return (len(m), tuple(sorted(m)))
|
||||
|
||||
|
||||
def format_monomial(m: Monomial) -> str:
|
||||
if len(m) == 0:
|
||||
return "1"
|
||||
if len(m) == 1:
|
||||
return next(iter(m))
|
||||
parts = sorted(m, key=lambda x: (len(x), x))
|
||||
return "(" + "⸱".join(parts) + ")"
|
||||
|
||||
|
||||
def format_poly(p: Poly) -> str:
|
||||
if not p:
|
||||
return "0"
|
||||
|
||||
monos = sorted(p, key=monomial_key)
|
||||
return "⊕".join(format_monomial(m) for m in monos)
|
||||
|
||||
|
||||
def convert(expr: str) -> str:
|
||||
tokens = tokenize(expr)
|
||||
poly = Parser(tokens).parse()
|
||||
return format_poly(poly)
|
||||
|
||||
#####################
|
||||
# CLI
|
||||
#####################
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
expr = " ".join(sys.argv[1:])
|
||||
else:
|
||||
expr = input("Enter formula: ").strip()
|
||||
|
||||
print(convert(expr))
|
||||
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import Monomial, Poly
|
||||
|
||||
|
||||
def monomial_key(m: Monomial):
|
||||
return (len(m), tuple(sorted(m)))
|
||||
|
||||
|
||||
def format_monomial(m: Monomial) -> str:
|
||||
if len(m) == 0:
|
||||
return "1"
|
||||
if len(m) == 1:
|
||||
return next(iter(m))
|
||||
parts = sorted(m, key=lambda x: (len(x), x))
|
||||
return "(" + "⸱".join(parts) + ")"
|
||||
|
||||
|
||||
def format_poly(p: Poly) -> str:
|
||||
if not p:
|
||||
return "0"
|
||||
|
||||
monos = sorted(p, key=monomial_key)
|
||||
return "⊕".join(format_monomial(m) for m in monos)
|
||||
@@ -0,0 +1,90 @@
|
||||
import itertools
|
||||
|
||||
from anf import convert
|
||||
from anf.parser import Parser, tokenize
|
||||
|
||||
|
||||
def parse(expr):
|
||||
return Parser(tokenize(expr)).parse()
|
||||
|
||||
|
||||
def eval_poly(poly, env):
|
||||
res = False
|
||||
for mono in poly:
|
||||
term = True
|
||||
for v in mono:
|
||||
term &= env.get(v, False)
|
||||
res ^= term
|
||||
return res
|
||||
|
||||
|
||||
def eval_expr(expr, env):
|
||||
py = expr
|
||||
py = py.replace("!", " not ")
|
||||
py = py.replace("AND", " and ")
|
||||
py = py.replace("OR", " or ")
|
||||
py = py.replace("+", " or ")
|
||||
py = py.replace("*", " and ")
|
||||
return eval(py, {"__builtins__": {}}, env)
|
||||
|
||||
|
||||
def equivalent(expr):
|
||||
poly = parse(expr)
|
||||
vars_ = sorted({t for t in tokenize(expr) if t.startswith("X")})
|
||||
|
||||
for values in itertools.product([False, True], repeat=len(vars_)):
|
||||
env = dict(zip(vars_, values))
|
||||
assert eval_expr(expr, env) == eval_poly(poly, env)
|
||||
|
||||
|
||||
# ------------------------
|
||||
# Tests
|
||||
# ------------------------
|
||||
|
||||
def test_basic_or():
|
||||
assert equivalent("X1 OR X2") is None
|
||||
|
||||
|
||||
def test_basic_and():
|
||||
assert equivalent("X1 AND X2") is None
|
||||
|
||||
|
||||
def test_not():
|
||||
assert equivalent("!X1") is None
|
||||
|
||||
|
||||
def test_plus_star_syntax():
|
||||
assert equivalent("X1 * (X2 + !X3)") is None
|
||||
|
||||
|
||||
def test_idempotent():
|
||||
assert convert("X1 OR X1") == "X1"
|
||||
|
||||
|
||||
def test_contradiction():
|
||||
assert convert("X1 AND !X1") == "0"
|
||||
|
||||
|
||||
def test_big_formula():
|
||||
expr = (
|
||||
"!X11 AND (!X3 AND !X4 OR X3 AND X4 OR X3 AND !X4 AND X12) OR X11 AND !X12 AND (X3 OR X4)"
|
||||
)
|
||||
|
||||
expected = (
|
||||
"1⊕X11⊕X3⊕X4⊕(X3⸱X12)⊕(X4⸱X11⸱X12)⊕(X3⸱X4⸱X11)⊕(X3⸱X4⸱X12)"
|
||||
)
|
||||
|
||||
assert convert(expr) == expected
|
||||
|
||||
|
||||
def test_equivalence_suite():
|
||||
formulas = [
|
||||
"X1 OR X2",
|
||||
"X1 AND !X2",
|
||||
"!(X1 OR X2)",
|
||||
"(X1 + X2) * (!X1 + X3)",
|
||||
"!X1 AND (!X2 OR X3) OR X1 AND X2",
|
||||
]
|
||||
|
||||
for f in formulas:
|
||||
equivalent(f)
|
||||
Reference in New Issue
Block a user