restructure project + unit tests
This commit is contained in:
+4
-1
@@ -1,2 +1,5 @@
|
|||||||
*.kate-swp
|
*.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 (⸱)).
|
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:
|
#### example:
|
||||||
|
|
||||||
input:
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from typing import List
|
||||||
from typing import FrozenSet, Iterable, List, Union
|
|
||||||
|
|
||||||
import sys
|
from .core import Poly, poly_and, poly_not, poly_xor, one, var, zero
|
||||||
|
|
||||||
#####################
|
|
||||||
# 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
|
|
||||||
#####################
|
|
||||||
|
|
||||||
TOKEN_RE = re.compile(
|
TOKEN_RE = re.compile(
|
||||||
r"""
|
r"""
|
||||||
@@ -79,6 +20,7 @@ TOKEN_RE = re.compile(
|
|||||||
re.VERBOSE,
|
re.VERBOSE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def tokenize(s: str) -> List[str]:
|
def tokenize(s: str) -> List[str]:
|
||||||
tokens = TOKEN_RE.findall(s)
|
tokens = TOKEN_RE.findall(s)
|
||||||
stripped = re.sub(r"\s+", "", s)
|
stripped = re.sub(r"\s+", "", s)
|
||||||
@@ -87,22 +29,6 @@ def tokenize(s: str) -> List[str]:
|
|||||||
return tokens
|
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:
|
class Parser:
|
||||||
def __init__(self, tokens: List[str]):
|
def __init__(self, tokens: List[str]):
|
||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
@@ -172,46 +98,3 @@ class Parser:
|
|||||||
return var(tok)
|
return var(tok)
|
||||||
|
|
||||||
raise ValueError(f"Unexpected token: {tok!r}")
|
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