From 00dce1b6ba8d89f0aa9dc9f22db4321784378431 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 22 Nov 2025 17:09:38 +0100 Subject: [PATCH 1/3] move boolalg to its own folder --- src/shapepy/bool2d/boolalg.py | 433 ------------------------------- src/shapepy/bool2d/boolean.py | 92 +------ src/shapepy/bool2d/lazy.py | 108 ++++---- src/shapepy/boolalg/__init__.py | 0 src/shapepy/boolalg/converter.py | 156 +++++++++++ src/shapepy/boolalg/simplify.py | 240 +++++++++++++++++ src/shapepy/boolalg/tree.py | 126 +++++++++ tests/bool2d/test_boolalg.py | 381 --------------------------- tests/bool2d/test_empty_whole.py | 1 + tests/boolalg/test_simplify.py | 240 +++++++++++++++++ 10 files changed, 827 insertions(+), 950 deletions(-) delete mode 100644 src/shapepy/bool2d/boolalg.py create mode 100644 src/shapepy/boolalg/__init__.py create mode 100644 src/shapepy/boolalg/converter.py create mode 100644 src/shapepy/boolalg/simplify.py create mode 100644 src/shapepy/boolalg/tree.py delete mode 100644 tests/bool2d/test_boolalg.py create mode 100644 tests/boolalg/test_simplify.py diff --git a/src/shapepy/bool2d/boolalg.py b/src/shapepy/bool2d/boolalg.py deleted file mode 100644 index f51bdfe7..00000000 --- a/src/shapepy/bool2d/boolalg.py +++ /dev/null @@ -1,433 +0,0 @@ -"""Contains the algorithm to simplify boolean expressions""" - -from __future__ import annotations - -import re -from collections import Counter -from typing import Iterable, Iterator, List, Set, Tuple, Union - -from ..loggers import debug -from ..tools import Is, NotExpectedError - -AND = "*" -OR = "+" -NOT = "!" -XOR = "^" -NULL = "" -TRUE = "1" -FALSE = "0" -NOTCARE = "-" -OPERATORS = (OR, XOR, AND, NOT) - - -@debug("shapepy.scalar.boolalg") -def simplify(expression: str) -> str: - """Simplifies given boolean expression""" - if not Is.instance(expression, str): - raise TypeError - expression = simplify_no_variable(expression) - variables = find_variables(expression) - if 0 < len(variables) < 5: - table = Implicants.evaluate_table(expression) - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - variables = "".join(sorted(variables)) - if len(implicants) == 0: - return FALSE - and_exprs = ( - Implicants.implicant2expression(imp, variables) - for imp in implicants - ) - return Formatter.mult_strs(and_exprs, OR) - return expression - - -def find_variables(expression: str) -> str: - """Searches the expression to finding the variables - - Example - ------- - >>> find_variables("a") - 'a' - >>> find_variables("a*b+b") - 'ab' - >>> find_variables("a*b+c^(!a+b)") - 'abc' - """ - if not Is.instance(expression, str): - raise TypeError(f"Invalid typo: {type(expression)}") - return "".join(sorted(set(re.findall(r"([a-z])", expression)))) - - -def remove_parentesis(expression: str) -> str: - """Removes the parentesis for given expression - - Example - ------- - >>> remove_parentesis("a") - a - >>> remove_parentesis("(a)") - a - >>> remove_parentesis("((a))") - a - """ - operator = find_operator(expression) - while operator is NULL and expression[0] == "(" and expression[-1] == ")": - expression = expression[1:-1] - operator = find_operator(expression) - return expression - - -def simplify_no_variable(expression: str) -> str: - """Simplifies the given boolean expression ignoring the values - that the variables can assume""" - if not Is.instance(expression, str): - raise TypeError - if len(expression) == 0: - raise ValueError - expression = remove_parentesis(expression) - operator = find_operator(expression) - if operator is NULL: - if not Implicants.can_evaluate(expression): - return expression - return TRUE if Implicants.evaluate(expression) else FALSE - if operator is NOT: - subexpression = extract(expression, NOT) - if not Implicants.can_evaluate(expression): - return Formatter.invert_str(simplify_no_variable(subexpression)) - return FALSE if Implicants.evaluate(subexpression) else TRUE - return multiple_no_variable(expression, operator) - - -def multiple_no_variable(expression: str, operator: str) -> str: - """Simplifies the given boolean expression - when the operator is AND, OR or XOR""" - subexps = extract(expression, operator) - if operator is XOR: - subexps = (s for s, i in dict(Counter(subexps)).items() if i % 2) - subexps = set(map(simplify_no_variable, set(subexps))) - if operator is XOR: - subexps = set(s for s in subexps if s != FALSE) - elif operator is AND: - subexps = set(s for s in subexps if s != TRUE) - elif operator is OR: - subexps = set(s for s in subexps if s != FALSE) - if len(subexps) == 0: - return TRUE if operator is AND else FALSE - return Formatter.mult_strs(subexps, operator) - - -def extract(expression: str, operator: str) -> Union[str, Iterator[str]]: - """Extracts from the expression the required subset - - Example - ------- - >>> extract("!a+b", "+") - ('!a', 'b') - >>> extract("!a+b", "*") - ('!a+b', ) - >>> extract("!a", "+") - ('!a', ) - >>> extract("!a", "!") - 'a' - """ - if operator == NOT: - return expression[1:] - subsets: List[str] = [] - indexi = 0 - while indexi < len(expression): - parentesis = 1 if expression[indexi] == "(" else 0 - indexj = indexi + 1 - while indexj < len(expression): - if expression[indexj] == "(": - parentesis += 1 - elif expression[indexj] == ")": - parentesis -= 1 - elif expression[indexj] == operator and parentesis == 0: - break - indexj += 1 - subset = expression[indexi:indexj] - subsets.append(subset) - indexi = indexj + 1 - return tuple(subsets) - - -def find_operator(expression: str) -> str: - """Finds the operator to divide the given expression - - If no operator exists, returns an empty string - - Example - ------- - >>> find_operator("a+b*c") - + - >>> find_operator("!a^b") - ^ - >>> find_operator("!a") - ! - >>> find_operator("a") - '' - """ - if not Is.instance(expression, str): - raise ValueError(f"Invalid argument {expression}") - if len(expression) == 0: - raise ValueError(f"Invalid expression '{expression}'") - for operator in (op for op in OPERATORS if op in expression): - parentesis = 0 - for char in expression: - if char == "(": - parentesis += 1 - elif char == ")": - parentesis -= 1 - elif parentesis != 0: - continue - elif char == operator: - return char - return NULL - - -class Formatter: - """Contains static method for extract""" - - @staticmethod - def compare_expression(expression: str) -> Tuple[int, str]: - """Function used to sort expressions""" - return (len(expression), expression) - - @staticmethod - def invert_str(expression: str) -> str: - """Inverts an expression - - Example - ------- - >>> invert_str('a') - !a - >>> invert_str('a*b') - !(a*b) - """ - if len(expression) > 1: - expression = "(" + expression + ")" - return NOT + expression - - @staticmethod - def mult_strs(expressions: Iterable[str], operator: str) -> str: - """Gives the intersection of given expressions. - - Example - ------- - >>> mult_strs({'a'}, '+') - a - >>> mult_strs({'a','b'}, '+') - a+b - >>> mult_strs({'a*b','c'}, '+') - c+(a*b) - >>> mult_strs({'c+(a*b)'}, '+') - c+(a*b) - >>> mult_strs({'a'}, '*') - a - >>> mult_strs({'a','b'}, '*') - a*b - >>> mult_strs({'a*b','c'}, '*') - c*(a*b) - >>> mult_strs({'c+(a*b)'}, '*') - c+(a*b) - """ - expressions = tuple(expressions) - if len(expressions) == 1: - return expressions[0] - exprs = (e if len(e) < 3 else ("(" + e + ")") for e in expressions) - return operator.join(sorted(exprs, key=Formatter.compare_expression)) - - -class Implicants: - """Class to store static methods used to simplify implicants""" - - @staticmethod - def funcand(values: Iterable[bool], /) -> bool: - """Function that computes the AND of many booleans""" - return all(map(bool, values)) - - @staticmethod - def funcor(values: Iterable[bool], /) -> bool: - """Function that computes the OR of many booleans""" - return any(map(bool, values)) - - @staticmethod - def funcxor(values: Iterable[bool], /) -> bool: - """Function that computes the XOR of many booleans""" - values = iter(values) - result = next(values) - for value in values: - result ^= value - return result - - @staticmethod - @debug("shapepy.scalar.boolalg") - def can_evaluate(expression: str) -> bool: - """Tells if it's possible evaluate a boolean expression""" - return find_variables(expression) == "" - - @staticmethod - @debug("shapepy.scalar.boolalg") - def evaluate(expression: str) -> bool: - """Evaluates a single boolean expression""" - if not Implicants.can_evaluate(expression): - raise ValueError(f"Cannot evaluate expression {expression}") - expression = remove_parentesis(expression) - if len(expression) == 1: - if expression not in {FALSE, TRUE}: - raise NotExpectedError(f"Invalid {expression}") - return expression == TRUE - operator = find_operator(expression) - if operator not in OPERATORS: - raise NotExpectedError(str(expression)) - if operator == NOT: - subexpression = extract(expression, NOT) - return not Implicants.evaluate(subexpression) - if operator not in {AND, OR, XOR}: - raise ValueError - subexprs = extract(expression, operator) - results = map(Implicants.evaluate, subexprs) - if operator == AND: - return Implicants.funcand(results) - if operator == OR: - return Implicants.funcor(results) - if operator == XOR: - return Implicants.funcxor(results) - raise NotExpectedError(f"{operator} Exp {expression}") - - @staticmethod - @debug("shapepy.scalar.boolalg") - def evaluate_table(expression: str) -> Iterable[bool]: - """Evaluates all the combination of boolean variables""" - if not Is.instance(expression, str): - raise TypeError(f"Invalid typo: {type(expression)}") - - indexvar = 0 - variables = find_variables(expression) - - def recursive(expression: str) -> Iterable[int]: - """Recursive function to subs the variables into expression""" - nonlocal indexvar - if indexvar == len(variables): - yield Implicants.evaluate(expression) - else: - var = variables[indexvar] - indexvar += 1 - yield from recursive(expression.replace(var, FALSE)) - yield from recursive(expression.replace(var, TRUE)) - indexvar -= 1 - - return tuple(recursive(expression)) - - @staticmethod - @debug("shapepy.scalar.boolalg") - def binary2number(binary: str) -> int: - """Converts a binary representation to a number""" - number = 0 - for char in binary: - number *= 2 - number += 1 if (char == TRUE) else 0 - return number - - @staticmethod - def number2binary(number: int, nbits: int) -> str: - """Converts a number into a binary representation""" - chars = [] - while number > 0: - char = TRUE if number % 2 else FALSE - chars.insert(0, char) - number //= 2 - return FALSE * (nbits - len(chars)) + "".join(chars) - - @staticmethod - def find_prime_implicants(results: Iterable[bool]) -> Tuple[str]: - """Finds the prime implicants - - A minterm is of the form '1001', '1010', etc - """ - results = tuple(results) - nbits = 0 - length = len(results) - while length > 2**nbits: - nbits += 1 - if length != 2**nbits: - raise ValueError(f"Invalid results: {results}") - if nbits == 0: - raise ValueError - implicants: List[str] = [] - for i, result in enumerate(results): - if result: - implicant = Implicants.number2binary(i, nbits) - implicants.append(implicant) - return tuple(implicants) - - @staticmethod - def merge_prime_implicants(minterms: Iterable[str]) -> Set[str]: - """Merge the prime implicants - - A minterm is of the form '1001', '1010', etc - """ - minterms = tuple(minterms) - while True: - new_minterms = set() - length = len(minterms) - merges = [False] * length - for i, mini in enumerate(minterms): - for j in range(i + 1, length): - minj = minterms[j] - if Implicants.can_merge(mini, minj): - merges[i] = True - merges[j] = True - merged = Implicants.merge_two(mini, minj) - if merged not in minterms: - new_minterms.add(merged) - if len(new_minterms) == 0: - break - minterms = (m for i, m in enumerate(minterms) if not merges[i]) - minterms = tuple(set(minterms) | set(new_minterms)) - - return minterms - - @staticmethod - def can_merge(mini: str, minj: str) -> bool: - """Tells if it's possible to merge two implicants""" - assert Is.instance(mini, str) - assert Is.instance(minj, str) - assert len(mini) == len(minj) - for chari, charj in zip(mini, minj): - if (chari == NOTCARE) ^ (charj == NOTCARE): - return False - numi = Implicants.binary2number(mini) - numj = Implicants.binary2number(minj) - res = numi ^ numj - return res != 0 and (res & res - 1) == 0 - - @staticmethod - def merge_two(mini: str, minj: str) -> bool: - """Merge two implicants""" - result = [] - for chari, charj in zip(mini, minj): - new_char = NOTCARE if chari != charj else chari - result.append(new_char) - return "".join(result) - - @staticmethod - def implicant2expression(implicant: str, variables: str) -> str: - """Tranforms an implicant to an AND expression - - Example - ------- - >>> implicant = "a - """ - assert Is.instance(implicant, str) - assert Is.instance(variables, str) - assert len(implicant) == len(variables) - assert len(implicant) > 0 - parts = [] - for i, v in zip(implicant, variables): - if i == FALSE: - parts.append(Formatter.invert_str(v)) - elif i == TRUE: - parts.append(v) - return Formatter.mult_strs(parts, AND) if len(parts) > 0 else TRUE diff --git a/src/shapepy/bool2d/boolean.py b/src/shapepy/bool2d/boolean.py index adbc853a..4ef6be3b 100644 --- a/src/shapepy/bool2d/boolean.py +++ b/src/shapepy/bool2d/boolean.py @@ -6,19 +6,18 @@ from __future__ import annotations from copy import copy -from typing import Dict, Iterable, Tuple, Union +from typing import Iterable, Tuple, Union from shapepy.geometry.jordancurve import JordanCurve from ..geometry.intersection import GeometricIntersectionCurves from ..geometry.unparam import USegment from ..loggers import debug -from ..tools import CyclicContainer, Is, NotExpectedError -from . import boolalg +from ..tools import CyclicContainer, Is from .base import EmptyShape, Future, SubSetR2, WholeShape from .config import Config from .curve import SingleCurve -from .lazy import LazyAnd, LazyNot, LazyOr, RecipeLazy, is_lazy +from .lazy import LazyAnd, LazyNot, LazyOr, RecipeLazy from .point import SinglePoint from .shape import ConnectedShape, DisjointShape, SimpleShape @@ -38,7 +37,7 @@ def invert_bool2d(subset: SubSetR2) -> SubSetR2: SubSetR2 The complementar subset """ - return Boolalg.clean(RecipeLazy.invert(subset)) + return RecipeLazy.invert(subset) @debug("shapepy.bool2d.boolean") @@ -56,8 +55,7 @@ def unite_bool2d(subsets: Iterable[SubSetR2]) -> SubSetR2: SubSetR2 The united subset """ - union = RecipeLazy.unite(subsets) - return Boolalg.clean(union) + return RecipeLazy.unite(subsets) @debug("shapepy.bool2d.boolean") @@ -75,8 +73,7 @@ def intersect_bool2d(subsets: Iterable[SubSetR2]) -> SubSetR2: SubSetR2 The intersection subset """ - intersection = RecipeLazy.intersect(subsets) - return Boolalg.clean(intersection) + return RecipeLazy.intersect(subsets) @debug("shapepy.bool2d.boolean") @@ -94,8 +91,7 @@ def xor_bool2d(subsets: Iterable[SubSetR2]) -> SubSetR2: SubSetR2 The intersection subset """ - subset = RecipeLazy.xor(subsets) - return Boolalg.clean(subset) + return RecipeLazy.xor(subsets) # pylint: disable=too-many-return-statements @@ -114,7 +110,6 @@ def clean_bool2d(subset: SubSetR2) -> SubSetR2: SubSetR2 The intersection subset """ - subset = Boolalg.clean(subset) if not Is.lazy(subset): return subset if Is.instance(subset, LazyNot): @@ -213,79 +208,6 @@ def contains_bool2d(subseta: SubSetR2, subsetb: SubSetR2) -> bool: ) -class Boolalg: - """Static methods to clean a SubSetR2 using algebraic simplifier""" - - alphabet = "abcdefghijklmnop" - sub2var: Dict[SubSetR2, str] = {} - - @staticmethod - def clean(subset: SubSetR2) -> SubSetR2: - """Simplifies the subset""" - - if not Is.lazy(subset): - return subset - Boolalg.sub2var.clear() - original = Boolalg.subset2expression(subset) - simplified = boolalg.simplify(original) - if simplified != original: - subset = Boolalg.expression2subset(simplified) - Boolalg.sub2var.clear() - return subset - - @staticmethod - def get_variable(subset: SubSetR2) -> str: - """Gets the variable represeting the subset""" - if subset not in Boolalg.sub2var: - index = len(Boolalg.sub2var) - if index > len(Boolalg.alphabet): - raise ValueError("Too many variables") - Boolalg.sub2var[subset] = Boolalg.alphabet[index] - return Boolalg.sub2var[subset] - - @staticmethod - def subset2expression(subset: SubSetR2) -> str: - """Converts a SubSetR2 into a boolean expression""" - if not is_lazy(subset): - if Is.instance(subset, (EmptyShape, WholeShape)): - raise NotExpectedError("Lazy does not contain these") - return Boolalg.get_variable(subset) - if Is.instance(subset, LazyNot): - return boolalg.Formatter.invert_str( - Boolalg.subset2expression(~subset) - ) - internals = map(Boolalg.subset2expression, subset) - if Is.instance(subset, LazyAnd): - return boolalg.Formatter.mult_strs(internals, boolalg.AND) - if Is.instance(subset, LazyOr): - return boolalg.Formatter.mult_strs(internals, boolalg.OR) - raise NotExpectedError - - @staticmethod - def expression2subset(expression: str) -> SubSetR2: - """Converts a boolean expression into a SubSetR2""" - if expression == boolalg.TRUE: - return WholeShape() - if expression == boolalg.FALSE: - return EmptyShape() - for subset, variable in Boolalg.sub2var.items(): - if expression == variable: - return subset - expression = boolalg.remove_parentesis(expression) - operator = boolalg.find_operator(expression) - if operator == boolalg.NOT: - subexpr = boolalg.extract(expression, boolalg.NOT) - inverted = Boolalg.expression2subset(subexpr) - return RecipeLazy.invert(inverted) - subexprs = boolalg.extract(expression, operator) - subsets = map(Boolalg.expression2subset, subexprs) - if operator == boolalg.OR: - return RecipeLazy.unite(subsets) - if operator == boolalg.AND: - return RecipeLazy.intersect(subsets) - raise NotExpectedError(f"Invalid expression: {expression}") - - def divide_connecteds( simples: Tuple[SimpleShape], ) -> Tuple[Union[SimpleShape, ConnectedShape]]: diff --git a/src/shapepy/bool2d/lazy.py b/src/shapepy/bool2d/lazy.py index cf5983b1..c8b3534b 100644 --- a/src/shapepy/bool2d/lazy.py +++ b/src/shapepy/bool2d/lazy.py @@ -2,86 +2,92 @@ from __future__ import annotations -from collections import Counter from copy import deepcopy -from typing import Iterable, Iterator, Type - +from typing import Iterable, Iterator, Union + +from ..boolalg.simplify import simplify_tree +from ..boolalg.tree import ( + BoolTree, + Operators, + false_tree, + items2tree, + true_tree, +) from ..loggers import debug from ..tools import Is from .base import EmptyShape, SubSetR2, WholeShape from .density import intersect_densities, unite_densities +def subset2tree(subset: SubSetR2) -> Union[SubSetR2, BoolTree]: + """Converts a subset into a tree equivalent""" + if Is.instance(subset, EmptyShape): + return false_tree() + if Is.instance(subset, WholeShape): + return true_tree() + if Is.instance(subset, LazyNot): + return items2tree((subset2tree(-subset),), Operators.NOT) + if Is.instance(subset, LazyAnd): + return items2tree(map(subset2tree, subset), Operators.AND) + if Is.instance(subset, LazyOr): + return items2tree(map(subset2tree, subset), Operators.OR) + return subset + + +def tree2subset(tree: Union[SubSetR2, BoolTree]) -> SubSetR2: + """Converts a tree into the subset equivalent""" + if not Is.instance(tree, BoolTree): + return tree + if len(tree) == 0: + return WholeShape() if tree.operator == Operators.AND else EmptyShape() + if tree.operator == Operators.NOT: + return LazyNot(tree2subset(tuple(tree)[0])) + if tree.operator == Operators.AND: + return LazyAnd(map(tree2subset, tree)) + if tree.operator == Operators.OR: + return LazyOr(map(tree2subset, tree)) + items = tuple(tree) + mid = len(items) // 2 + aset = operate(items[:mid], Operators.XOR) + bset = operate(items[mid:], Operators.XOR) + left = LazyAnd((aset, LazyNot(bset))) + righ = LazyAnd((LazyNot(aset), bset)) + return LazyOr((left, righ)) + + +@debug("shapepy.bool2d.lazy") +def operate(subsets: Iterable[SubSetR2], operator: Operators) -> SubSetR2: + """Computes the operation of the items, such as union, intersection""" + tree = items2tree(map(subset2tree, subsets), operator) + return tree2subset(simplify_tree(tree)) + + class RecipeLazy: """Contains static methods that gives lazy recipes""" - @staticmethod - def flatten(subsets: Iterable[SubSetR2], typo: Type) -> Iterator[SubSetR2]: - """Flattens the subsets""" - for subset in subsets: - if Is.instance(subset, typo): - yield from subset - else: - yield subset - @staticmethod @debug("shapepy.bool2d.lazy") def invert(subset: SubSetR2) -> SubSetR2: """Gives the complementar of the given subset""" - if Is.instance(subset, (EmptyShape, WholeShape, LazyNot)): - return -subset - return LazyNot(subset) + return operate((subset,), Operators.NOT) @staticmethod @debug("shapepy.bool2d.lazy") def intersect(subsets: Iterable[SubSetR2]) -> SubSetR2: """Gives the recipe for the intersection of given subsets""" - subsets = RecipeLazy.flatten(subsets, LazyAnd) - subsets = frozenset( - s for s in subsets if not Is.instance(s, WholeShape) - ) - if len(subsets) == 0: - return WholeShape() - if any(Is.instance(s, EmptyShape) for s in subsets): - return EmptyShape() - if len(subsets) == 1: - return tuple(subsets)[0] - return LazyAnd(subsets) + return operate(subsets, Operators.AND) @staticmethod @debug("shapepy.bool2d.contain") def unite(subsets: Iterable[SubSetR2]) -> SubSetR2: """Gives the recipe for the union of given subsets""" - subsets = RecipeLazy.flatten(subsets, LazyOr) - subsets = frozenset( - s for s in subsets if not Is.instance(s, EmptyShape) - ) - if len(subsets) == 0: - return EmptyShape() - if any(Is.instance(s, WholeShape) for s in subsets): - return WholeShape() - if len(subsets) == 1: - return tuple(subsets)[0] - return LazyOr(subsets) + return operate(subsets, Operators.OR) @staticmethod @debug("shapepy.bool2d.contain") def xor(subsets: Iterable[SubSetR2]) -> SubSetR2: """Gives the exclusive or of the given subsets""" - subsets = tuple(subsets) - dictids = dict(Counter(map(id, subsets))) - subsets = tuple(s for s in subsets if dictids[id(s)] % 2) - length = len(subsets) - if length == 0: - return EmptyShape() - if length == 1: - return subsets[0] - mid = length // 2 - aset = RecipeLazy.xor(subsets[:mid]) - bset = RecipeLazy.xor(subsets[mid:]) - left = RecipeLazy.intersect((aset, RecipeLazy.invert(bset))) - righ = RecipeLazy.intersect((RecipeLazy.invert(aset), bset)) - return RecipeLazy.unite((left, righ)) + return operate(subsets, Operators.XOR) class LazyNot(SubSetR2): diff --git a/src/shapepy/boolalg/__init__.py b/src/shapepy/boolalg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/shapepy/boolalg/converter.py b/src/shapepy/boolalg/converter.py new file mode 100644 index 00000000..c9f61554 --- /dev/null +++ b/src/shapepy/boolalg/converter.py @@ -0,0 +1,156 @@ +""" +File that contains functions to convert a string to a boolean tree, +or convert the boolean tree into a string + +Example +------- +>>> msg = "a+b*c" +>>> tree = string2tree(msg) +>>> tree +OR["a", AND["b", "c"]] +>>> tree2string(tree) +a+(b*c) +""" + +from typing import Iterator, List, Union + +from ..loggers import debug +from ..tools import Is +from .tree import BoolTree, Operators, false_tree, items2tree, true_tree + +TRUE = "1" +FALSE = "0" + +OPE2STR = { + Operators.OR: "+", + Operators.XOR: "^", + Operators.AND: "*", + Operators.NOT: "!", +} +STR2OPE = {v: k for k, v in OPE2STR.items()} + + +@debug("shapepy.boolalg.converter") +def string2tree(expression: str) -> Union[str, BoolTree[str]]: + """Converts a string into a boolean tree + + Example + ------- + >>> msg = "a+b*c" + >>> string2tree(msg) + OR["a", AND["b", "c"]] + """ + operator = find_operator(expression) + while operator is None and expression[0] == "(" and expression[-1] == ")": + expression = expression[1:-1] + operator = find_operator(expression) + if operator is None: + if expression is TRUE: + return true_tree() + if expression is FALSE: + return false_tree() + return expression + items = tuple(extract(expression, operator)) + items = tuple(map(string2tree, items)) + return items2tree(items, operator) + + +@debug("shapepy.boolalg.converter") +def tree2string(tree: Union[str, BoolTree[str]]) -> str: + """Converts a boolean tree into a expression + + Example + ------- + >>> msg = "a+b*c" + >>> tree = string2tree(msg) + OR["a", AND["b", "c"]] + >>> tree2string(tree) + a+(b*c) + """ + if not Is.instance(tree, BoolTree): + return tree + if len(tree) == 0: + return TRUE if tree.operator == Operators.AND else FALSE + if tree.operator == Operators.NOT: + item = tuple(tree)[0] + if Is.instance(item, BoolTree) and len(item) > 1: + return "!(" + tree2string(item) + ")" + return "!" + tree2string(item) + items: List[str] = [] + for item in tree: + stritem = tree2string(item) + if Is.instance(item, BoolTree) and len(item) > 1: + stritem = "(" + stritem + ")" + items.append(stritem) + return OPE2STR[tree.operator].join(items) + + +@debug("shapepy.boolalg.converter") +def extract(expression: str, operator: Operators) -> Iterator[str]: + """Extracts from the expression the required subset + + Example + ------- + >>> extract("!a+b", "+") + ('!a', 'b') + >>> extract("!a+b", "*") + ('!a+b', ) + >>> extract("!a", "+") + ('!a', ) + >>> extract("!a", "!") + 'a' + """ + if operator == Operators.NOT: + return [expression[1:]] + char = OPE2STR[operator] + result: List[str] = [] + indexi = 0 + while indexi < len(expression): + parentesis = 1 if expression[indexi] == "(" else 0 + indexj = indexi + 1 + while indexj < len(expression): + if expression[indexj] == "(": + parentesis += 1 + elif expression[indexj] == ")": + parentesis -= 1 + elif expression[indexj] == char and parentesis == 0: + break + indexj += 1 + result.append(expression[indexi:indexj]) + indexi = indexj + 1 + return result + + +@debug("shapepy.boolalg.converter") +def find_operator(expression: str) -> Union[None, Operators]: + """Finds the operator to divide the given expression + + If no operator exists, returns an empty string + + Example + ------- + >>> find_operator("a+b*c") + + + >>> find_operator("!a^b") + ^ + >>> find_operator("!a") + ! + >>> find_operator("a") + '' + """ + if not Is.instance(expression, str): + raise ValueError(f"Invalid argument {expression}") + if len(expression) == 0: + raise ValueError(f"Invalid expression '{expression}'") + for operator in (op for op in STR2OPE if op in expression): + parentesis = 0 + for char in expression: + if char == "(": + parentesis += 1 + elif char == ")": + parentesis -= 1 + elif parentesis != 0: + continue + elif char == operator: + return STR2OPE[operator] + return None diff --git a/src/shapepy/boolalg/simplify.py b/src/shapepy/boolalg/simplify.py new file mode 100644 index 00000000..ca0fe148 --- /dev/null +++ b/src/shapepy/boolalg/simplify.py @@ -0,0 +1,240 @@ +"""Contains the algorithm to simplify boolean expressions""" + +from __future__ import annotations + +from typing import Iterable, Iterator, Set, Tuple, TypeVar, Union + +from ..loggers import debug +from ..tools import Is, NotExpectedError +from .tree import BoolTree, Operators, false_tree, true_tree + +T = TypeVar("T") + + +def find_variables(tree: BoolTree[T]) -> Iterator[T]: + """Searchs recursivelly in the tree for all the variables inside it + + A variable is any object refered by the tree that is not also a tree + + Example + ------- + >>> tree = OR[XOR[A, B], AND[NOT[C], OR[D, E]]] + >>> find_variables(tree) + [A, B, C, D, E] + """ + if not Is.instance(tree, BoolTree): + yield tree + return + items = {} + for item in tree: + for var in find_variables(item): + if id(var) not in items: + items[id(var)] = var + yield from items.values() + + +@debug("shapepy.scalar.boolalg") +def simplify_tree( + tree: Union[T, BoolTree[T]], maxvars: int = 4 +) -> BoolTree[T]: + """Simplifies given boolean expression""" + if not Is.instance(tree, BoolTree): + return tree + variables = tuple(find_variables(tree)) + if maxvars and len(variables) > maxvars: + return tree + idsvars = tuple(map(id, variables)) + table = Implicants.evaluate_table(tree, idsvars) + if len(variables) == 0: + result = bool(tuple(table)[0]) + return true_tree() if result else false_tree() + implicants = Implicants.find_prime_implicants(table) + implicants = Implicants.merge_prime_implicants(implicants) + implicants = Implicants.sort_implicants(implicants) + return Implicants.implicants2tree(implicants, variables) + + +class Implicants: + """Class to store static methods used to simplify implicants""" + + TRUE = "1" + FALSE = "0" + NOTCARE = "-" + + @staticmethod + @debug("shapepy.scalar.boolalg") + def evaluate(tree: BoolTree, idsvars: Tuple[int], binary: int) -> bool: + """Evaluates a single boolean expression""" + if not Is.instance(tree, BoolTree): + index = len(idsvars) - idsvars.index(id(tree)) - 1 + return bool(binary & (2**index)) + values = (Implicants.evaluate(i, idsvars, binary) for i in tree) + if tree.operator == Operators.NOT: + return not tuple(values)[0] + if tree.operator == Operators.OR: + return any(values) + if tree.operator == Operators.AND: + return all(values) + if tree.operator == Operators.XOR: + return sum(values) % 2 + raise NotExpectedError(f"Invalid operator: {tree.operator}") + + @staticmethod + @debug("shapepy.scalar.boolalg") + def evaluate_table( + tree: BoolTree, idsvars: Iterable[int] + ) -> Iterator[bool]: + """Evaluates all the combination of boolean variables""" + if not Is.instance(tree, BoolTree): + raise TypeError(f"Invalid typo: {type(tree)}") + results = [] + for counter in range(2 ** len(idsvars)): + results.append(Implicants.evaluate(tree, idsvars, counter)) + return tuple(map(bool, results)) + + @staticmethod + def binary2number(binary: str) -> int: + """Converts a binary representation to a number + + Example + ------- + >>> binary2number("0000") + 0 + >>> binary2number("0001") + 1 + >>> binary2number("0010") + 2 + >>> binary2number("1111") + 15 + """ + number = 0 + for char in binary: + number *= 2 + number += 1 if (char == Implicants.TRUE) else 0 + return number + + @staticmethod + def number2binary(number: int, nbits: int) -> str: + """Converts a number into a binary representation + + Example + ------- + >>> number2binary(0, 4) + 0000 + >>> number2binary(1, 4) + 0001 + >>> number2binary(2, 4) + 0010 + >>> number2binary(15, 4) + 1111 + """ + chars = [] + while number > 0: + char = Implicants.TRUE if number % 2 else Implicants.FALSE + chars.insert(0, char) + number //= 2 + return Implicants.FALSE * (nbits - len(chars)) + "".join(chars) + + @staticmethod + def find_prime_implicants(results: Iterable[bool]) -> Iterator[str]: + """Finds the prime implicants + + A minterm is of the form '1001', '1010', etc + """ + results = tuple(results) + nbits = 0 + length = len(results) + while length > 2**nbits: + nbits += 1 + if length != 2**nbits: + raise ValueError(f"Invalid results: {results}") + implicants = [] + for i, result in enumerate(results): + if result: + implicants.append(Implicants.number2binary(i, nbits)) + return tuple(implicants) + + @staticmethod + @debug("shapepy.scalar.boolalg") + def merge_prime_implicants(minterms: Iterable[str]) -> Set[str]: + """Merge the prime implicants + + A minterm is of the form '1001', '1010', etc + """ + minterms = tuple(minterms) + while True: + new_minterms = set() + length = len(minterms) + merges = [False] * length + for i, mini in enumerate(minterms): + for j in range(i + 1, length): + minj = minterms[j] + if Implicants.can_merge(mini, minj): + merges[i] = True + merges[j] = True + merged = Implicants.merge_two(mini, minj) + if merged not in minterms: + new_minterms.add(merged) + if len(new_minterms) == 0: + break + minterms = (m for i, m in enumerate(minterms) if not merges[i]) + minterms = tuple(set(minterms) | set(new_minterms)) + + return minterms + + @staticmethod + def can_merge(mini: str, minj: str) -> bool: + """Tells if it's possible to merge two implicants""" + assert Is.instance(mini, str) + assert Is.instance(minj, str) + assert len(mini) == len(minj) + for chari, charj in zip(mini, minj): + if (chari == Implicants.NOTCARE) ^ (charj == Implicants.NOTCARE): + return False + numi = Implicants.binary2number(mini) + numj = Implicants.binary2number(minj) + res = numi ^ numj + return res != 0 and (res & res - 1) == 0 + + @staticmethod + def merge_two(mini: str, minj: str) -> bool: + """Merge two implicants""" + result = [] + for chari, charj in zip(mini, minj): + new_char = Implicants.NOTCARE if chari != charj else chari + result.append(new_char) + return "".join(result) + + @staticmethod + @debug("shapepy.scalar.boolalg") + def sort_implicants(implicants: Iterable[str]) -> Iterable[str]: + """Sorts the implicants by the simplest first""" + implicants = tuple(implicants) + weights = tuple( + sum(c == Implicants.NOTCARE for c in imp) for imp in implicants + ) + return tuple( + i for _, i in sorted(zip(weights, implicants), reverse=True) + ) + + @staticmethod + def implicants2tree( + implicants: Iterable[str], variables: Tuple[T] + ) -> BoolTree[T]: + """ + Tranforms the implicants into a tree + """ + invvars = tuple(BoolTree((v,), Operators.NOT) for v in variables) + ands = [] + for implicant in implicants: + parts = [] + for i, imp in enumerate(implicant): + if imp == Implicants.FALSE: + parts.append(invvars[i]) + elif imp == Implicants.TRUE: + parts.append(variables[i]) + if len(parts) == 1: + ands.append(parts[0]) + else: + ands.append(BoolTree(parts, Operators.AND)) + return ands[0] if len(ands) == 1 else BoolTree(ands, Operators.OR) diff --git a/src/shapepy/boolalg/tree.py b/src/shapepy/boolalg/tree.py new file mode 100644 index 00000000..8b453be6 --- /dev/null +++ b/src/shapepy/boolalg/tree.py @@ -0,0 +1,126 @@ +""" +File that defines a tree structure to store a boolean expression + +Basically any boolean expression can be represented as a tree. +For example, 'a+b*c' can be stored as OR[a, AND[b, c]] +Which, for the root, there are two nodes 'a' and 'd' connected: OR[a, d] +The expression for 'd' is another three as the root AND[b, c] +""" + +from __future__ import annotations + +from collections import Counter +from enum import Enum +from typing import Generic, Iterable, Iterator, TypeVar + +from ..loggers import debug +from ..tools import NotExpectedError + +T = TypeVar("T") + + +class Operators(Enum): + """Defines some options to be stored by a boolean three + + It represents how a root of a tree must treat its children""" + + NOT = 1 + AND = 2 + OR = 3 + XOR = 4 + + +def flatten(items: Iterable[T], operator: Operators) -> Iterator[T]: + """Flattens a tree that as an equivalent operator + + Example + ------- + >>> A = items2tree(["a", "b"], Operators.OR) + >>> flatten([A, "c", "d"], Operators.OR) + ["a", "b", "c", "d"] + """ + for item in items: + if isinstance(item, BoolTree) and operator == item.operator: + yield from item + else: + yield item + + +@debug("shapepy.boolalg.tree") +def items2tree(items: Iterable[T], operator: Operators) -> BoolTree[T]: + """Creates a boolean tree from given items + + Example + ------- + >>> items2tree(["a", "b"], Operators.OR) + OR["a","b"] + >>> items2tree(["a"], Operators.NOT) + NOT["a"] + """ + if operator == Operators.NOT: + items = tuple(items) + if len(items) != 1: + raise NotExpectedError(f"Items = {len(items)}: {items}") + item = items[0] + if isinstance(item, BoolTree): + if item.operator == Operators.NOT: + return tuple(item)[0] + return BoolTree((item,), Operators.NOT) + items = flatten(items, operator) + if operator == Operators.XOR: + items = tuple(items) + dictids = dict(Counter(map(id, items))) + items = tuple(s for s in items if dictids[id(s)] % 2) + else: + items = tuple({id(i): i for i in items}.values()) + if len(items) == 1: + return items[0] + return BoolTree(items, operator) + + +class BoolTree(Generic[T]): + """Defines a tree structure that stores a boolean expression""" + + def __init__(self, items: Iterable[T], operator: Operators): + if operator == Operators.NOT and len(items) != 1: + raise ValueError("NOT must have only one element") + if operator not in Operators: + raise TypeError(f"{operator} must be in {Operators}") + self.__operator = operator + self.__items = tuple(items) + + @property + def operator(self) -> Operators: + """Gets the operator of the tree between the options + + * OR: union between the items + * AND: intersection between the items + * NOT: the complementar of the item + * XOR: xor between the items + """ + return self.__operator + + def __iter__(self) -> Iterator[T]: + yield from self.__items + + def __len__(self) -> int: + return len(self.__items) + + def __repr__(self) -> str: + ope2str = { + Operators.OR: "OR", + Operators.XOR: "XOR", + Operators.AND: "AND", + Operators.NOT: "NOT", + } + return ope2str[self.operator] + "[" + ",".join(map(repr, self)) + "]" + + +def false_tree() -> BoolTree: + """Gets a boolean tree that is equivalent to false boolean value""" + return BoolTree([], Operators.OR) + + +def true_tree() -> BoolTree: + """Gets a boolean tree that is equivalent to true boolean value""" + return BoolTree([], Operators.AND) diff --git a/tests/bool2d/test_boolalg.py b/tests/bool2d/test_boolalg.py deleted file mode 100644 index f1342e2c..00000000 --- a/tests/bool2d/test_boolalg.py +++ /dev/null @@ -1,381 +0,0 @@ -import pytest - -from shapepy.bool2d.boolalg import Implicants, find_operator, simplify - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency() -def test_find_operator(): - values = { - "0": "", - "1": "", - "!0": "!", - "!1": "!", - "0+0": "+", - "0+1": "+", - "1+0": "+", - "1+1": "+", - "0*0": "*", - "0*1": "*", - "1*0": "*", - "1*1": "*", - "0^0": "^", - "0^1": "^", - "1^0": "^", - "1^1": "^", - "1*(1^0)": "*", - "!0+0": "+", - "!0*0": "*", - "!0^0": "^", - "!1+1": "+", - "!1*1": "*", - } - for expr, result in values.items(): - assert find_operator(expr) is result - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency(depends=["test_find_operator"]) -def test_evaluate_basic(): - values = { - "0": False, - "1": True, - "!0": True, - "!1": False, - "0+0": False, - "0+1": True, - "1+0": True, - "1+1": True, - "0*0": False, - "0*1": False, - "1*0": False, - "1*1": True, - "0^0": False, - "0^1": True, - "1^0": True, - "1^1": False, - } - for expr, result in values.items(): - assert Implicants.evaluate(expr) is result - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency(depends=["test_evaluate_basic"]) -def test_evaluate_tree(): - values = { - "0+(1*(1^0))": True, - } - for expr, result in values.items(): - assert Implicants.evaluate(expr) is result - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency(depends=["test_evaluate_basic", "test_evaluate_tree"]) -def test_table_single_var(): - assert tuple(Implicants.evaluate_table("0")) == (False,) - assert tuple(Implicants.evaluate_table("1")) == (True,) - assert tuple(Implicants.evaluate_table("!0")) == (True,) - assert tuple(Implicants.evaluate_table("!1")) == (False,) - values = { - "a+a": (False, True), - "!a+a": (True, True), - "a+!a": (True, True), - "!a+!a": (True, False), - "a*a": (False, True), - "!a*a": (False, False), - "a*!a": (False, False), - "!a*!a": (True, False), - "a^a": (False, False), - "a^!a": (True, True), - "!a^a": (True, True), - "!a^!a": (False, False), - } - assert tuple(Implicants.evaluate_table("a")) == (False, True) - assert tuple(Implicants.evaluate_table("!a")) == (True, False) - for expr, result in values.items(): - assert tuple(Implicants.evaluate_table(expr)) == result - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=[ - "test_evaluate_basic", - "test_evaluate_tree", - "test_table_single_var", - ] -) -def test_table_multi_var(): - evaluate_table = Implicants.evaluate_table - values = { - "a+b": (False, True, True, True), - "a*b": (False, False, False, True), - "a^b": (False, True, True, False), - "!a+b": (True, True, False, True), - "a+!b": (True, False, True, True), - "a+!b": (True, False, True, True), - "!(a+b)": (True, False, False, False), - } - for expr, result in values.items(): - assert tuple(evaluate_table(expr)) == result - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=[ - "test_evaluate_basic", - "test_evaluate_tree", - "test_table_single_var", - ] -) -def test_merge_prime_implicants(): - evaluate_table = Implicants.evaluate_table - - table = evaluate_table("a+a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"1"} - - table = evaluate_table("a+!a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"-"} - - table = evaluate_table("!a+a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"-"} - - table = evaluate_table("!a+!a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"0"} - - table = evaluate_table("a*a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"1"} - - table = evaluate_table("a*!a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == set() - - table = evaluate_table("!a*a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == set() - - table = evaluate_table("!a*!a") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"0"} - - table = evaluate_table("a+b") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"1-", "-1"} - - table = evaluate_table("a*b") - implicants = Implicants.find_prime_implicants(table) - implicants = Implicants.merge_prime_implicants(implicants) - assert set(implicants) == {"11"} - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=[ - "test_evaluate_basic", - "test_evaluate_tree", - "test_table_single_var", - "test_table_multi_var", - "test_merge_prime_implicants", - ] -) -def test_simplify_no_variable(): - # DIRECT - assert simplify("0") == "0" - assert simplify("1") == "1" - assert simplify("(0)") == "0" - assert simplify("(1)") == "1" - assert simplify("((0))") == "0" - assert simplify("((1))") == "1" - - # NOT - assert simplify("!0") == "1" - assert simplify("!1") == "0" - assert simplify("!!0") == "0" - assert simplify("!!1") == "1" - assert simplify("!!!0") == "1" - assert simplify("!!!1") == "0" - - # OR - assert simplify("0") == "0" - assert simplify("1") == "1" - assert simplify("0+0") == "0" - assert simplify("0+1") == "1" - assert simplify("1+0") == "1" - assert simplify("1+1") == "1" - assert simplify("0+0+0") == "0" - assert simplify("0+0+1") == "1" - assert simplify("0+1+0") == "1" - assert simplify("0+1+1") == "1" - assert simplify("1+0+0") == "1" - assert simplify("1+0+1") == "1" - assert simplify("1+1+0") == "1" - assert simplify("1+1+1") == "1" - - # AND - assert simplify("0") == "0" - assert simplify("1") == "1" - assert simplify("0*0") == "0" - assert simplify("0*1") == "0" - assert simplify("1*0") == "0" - assert simplify("1*1") == "1" - assert simplify("0*0*0") == "0" - assert simplify("0*0*1") == "0" - assert simplify("0*1*0") == "0" - assert simplify("0*1*1") == "0" - assert simplify("1*0*0") == "0" - assert simplify("1*0*1") == "0" - assert simplify("1*1*0") == "0" - assert simplify("1*1*1") == "1" - - # XOR - assert simplify("0") == "0" - assert simplify("1") == "1" - assert simplify("0^0") == "0" - assert simplify("0^1") == "1" - assert simplify("1^0") == "1" - assert simplify("1^1") == "0" - assert simplify("0^0^0") == "0" - assert simplify("0^0^1") == "1" - assert simplify("0^1^0") == "1" - assert simplify("0^1^1") == "0" - assert simplify("1^0^0") == "1" - assert simplify("1^0^1") == "0" - assert simplify("1^1^0") == "0" - assert simplify("1^1^1") == "1" - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=[ - "test_evaluate_basic", - "test_evaluate_tree", - "test_table_single_var", - "test_table_multi_var", - "test_merge_prime_implicants", - "test_simplify_no_variable", - ] -) -def test_simplify_single_var(): - # DIRECT - assert simplify("a") == "a" - assert simplify("(a)") == "a" - assert simplify("((a))") == "a" - - # NOT - assert simplify("!a") == "!a" - assert simplify("!!a") == "a" - assert simplify("!!!a") == "!a" - assert simplify("!!!!a") == "a" - - # OR - assert simplify("a") == "a" - assert simplify("a+a") == "a" - assert simplify("a+!a") == "1" - assert simplify("!a+a") == "1" - assert simplify("!a+!a") == "!a" - assert simplify("a+a+a") == "a" - assert simplify("a+a+!a") == "1" - assert simplify("a+!a+a") == "1" - assert simplify("a+!a+!a") == "1" - assert simplify("!a+a+a") == "1" - assert simplify("!a+a+!a") == "1" - assert simplify("!a+!a+a") == "1" - assert simplify("!a+!a+!a") == "!a" - - # AND - assert simplify("a") == "a" - assert simplify("!a") == "!a" - assert simplify("a*a") == "a" - assert simplify("a*!a") == "0" - assert simplify("!a*a") == "0" - assert simplify("!a*!a") == "!a" - assert simplify("a*a*a") == "a" - assert simplify("a*a*!a") == "0" - assert simplify("a*!a*a") == "0" - assert simplify("a*!a*!a") == "0" - assert simplify("!a*a*a") == "0" - assert simplify("!a*a*!a") == "0" - assert simplify("!a*!a*a") == "0" - assert simplify("!a*!a*!a") == "!a" - - # XOR - assert simplify("a") == "a" - assert simplify("!a") == "!a" - assert simplify("a^a") == "0" - assert simplify("a^!a") == "1" - assert simplify("!a^a") == "1" - assert simplify("!a^!a") == "0" - assert simplify("a^a^a") == "a" - assert simplify("a^a^!a") == "!a" - assert simplify("a^!a^a") == "!a" - assert simplify("a^!a^!a") == "a" - assert simplify("!a^a^a") == "!a" - assert simplify("!a^a^!a") == "a" - assert simplify("!a^!a^a") == "a" - assert simplify("!a^!a^!a") == "!a" - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=[ - "test_evaluate_basic", - "test_evaluate_tree", - "test_table_single_var", - "test_table_multi_var", - "test_merge_prime_implicants", - "test_simplify_no_variable", - "test_simplify_single_var", - ] -) -def test_simplify_multi_var(): - # DIRECT - assert simplify("a+b") == "a+b" - assert simplify("(a+b)") == "a+b" - assert simplify("(a)+b") == "a+b" - assert simplify("((a))+b") == "a+b" - assert simplify("((a))+(b)") == "a+b" - assert simplify("((a)+a)+(b)") == "a+b" - assert simplify("((a)+a)+(b+a)") == "a+b" - - assert simplify("a+b+c") == "a+b+c" - assert simplify("a+b*c") == "a+(b*c)" - - -@pytest.mark.order(1) -@pytest.mark.timeout(1) -@pytest.mark.dependency( - depends=[ - "test_evaluate_basic", - "test_evaluate_tree", - "test_table_single_var", - "test_table_multi_var", - "test_merge_prime_implicants", - "test_simplify_no_variable", - "test_simplify_single_var", - "test_simplify_multi_var", - ] -) -def test_all(): - pass diff --git a/tests/bool2d/test_empty_whole.py b/tests/bool2d/test_empty_whole.py index e29e560b..53e2f9c9 100644 --- a/tests/bool2d/test_empty_whole.py +++ b/tests/bool2d/test_empty_whole.py @@ -17,6 +17,7 @@ @pytest.mark.order(21) @pytest.mark.dependency( depends=[ + "tests/boolalg/test_simplify.py::test_all", "tests/geometry/test_integral.py::test_all", "tests/geometry/test_jordan_curve.py::test_all", ], diff --git a/tests/boolalg/test_simplify.py b/tests/boolalg/test_simplify.py new file mode 100644 index 00000000..2c98f73c --- /dev/null +++ b/tests/boolalg/test_simplify.py @@ -0,0 +1,240 @@ +import pytest + +from shapepy.boolalg.converter import find_operator, string2tree, tree2string +from shapepy.boolalg.simplify import simplify_tree +from shapepy.boolalg.tree import Operators +from shapepy.loggers import enable_logger + + +@pytest.mark.order(1) +@pytest.mark.timeout(1) +@pytest.mark.dependency() +def test_find_operator(): + values = { + "0": None, + "1": None, + "!0": Operators.NOT, + "!1": Operators.NOT, + "0+0": Operators.OR, + "0+1": Operators.OR, + "1+0": Operators.OR, + "1+1": Operators.OR, + "0*0": Operators.AND, + "0*1": Operators.AND, + "1*0": Operators.AND, + "1*1": Operators.AND, + "0^0": Operators.XOR, + "0^1": Operators.XOR, + "1^0": Operators.XOR, + "1^1": Operators.XOR, + "1*(1^0)": Operators.AND, + "!0+0": Operators.OR, + "!0*0": Operators.AND, + "!0^0": Operators.XOR, + "!1+1": Operators.OR, + "!1*1": Operators.AND, + } + for expr, result in values.items(): + assert find_operator(expr) is result + + +@pytest.mark.order(1) +@pytest.mark.timeout(1) +@pytest.mark.dependency( + depends=[ + "test_find_operator", + ] +) +def test_simplify_no_variable(): + table = { + # DIRECT + "0": "0", + "1": "1", + "(0)": "0", + "(1)": "1", + "((0))": "0", + "((1))": "1", + # NOT + "!0": "1", + "!1": "0", + "!!0": "0", + "!!1": "1", + "!!!0": "1", + "!!!1": "0", + # OR + "0": "0", + "1": "1", + "0+0": "0", + "0+1": "1", + "1+0": "1", + "1+1": "1", + "0+0+0": "0", + "0+0+1": "1", + "0+1+0": "1", + "0+1+1": "1", + "1+0+0": "1", + "1+0+1": "1", + "1+1+0": "1", + "1+1+1": "1", + # AND + "0": "0", + "1": "1", + "0*0": "0", + "0*1": "0", + "1*0": "0", + "1*1": "1", + "0*0*0": "0", + "0*0*1": "0", + "0*1*0": "0", + "0*1*1": "0", + "1*0*0": "0", + "1*0*1": "0", + "1*1*0": "0", + "1*1*1": "1", + # XOR + "0": "0", + "1": "1", + "0^0": "0", + "0^1": "1", + "1^0": "1", + "1^1": "0", + "0^0^0": "0", + "0^0^1": "1", + "0^1^0": "1", + "0^1^1": "0", + "1^0^0": "1", + "1^0^1": "0", + "1^1^0": "0", + "1^1^1": "1", + } + + for original, good in table.items(): + tree = string2tree(original) + tree = simplify_tree(tree) + test = tree2string(tree) + assert test == good + + +@pytest.mark.order(1) +@pytest.mark.timeout(1) +@pytest.mark.dependency( + depends=[ + "test_find_operator", + "test_simplify_no_variable", + ] +) +def test_simplify_single_var(): + table = { + # DIRECT + "a": "a", + "(a)": "a", + "((a))": "a", + # NOT + "!a": "!a", + "!!a": "a", + "!!!a": "!a", + "!!!!a": "a", + # OR + "a": "a", + "a+a": "a", + "a+!a": "1", + "!a+a": "1", + "!a+!a": "!a", + "a+a+a": "a", + "a+a+!a": "1", + "a+!a+a": "1", + "a+!a+!a": "1", + "!a+a+a": "1", + "!a+a+!a": "1", + "!a+!a+a": "1", + "!a+!a+!a": "!a", + # AND + "a": "a", + "!a": "!a", + "a*a": "a", + "a*!a": "0", + "!a*a": "0", + "!a*!a": "!a", + "a*a*a": "a", + "a*a*!a": "0", + "a*!a*a": "0", + "a*!a*!a": "0", + "!a*a*a": "0", + "!a*a*!a": "0", + "!a*!a*a": "0", + "!a*!a*!a": "!a", + # XOR + "a": "a", + "!a": "!a", + "a^a": "0", + "a^!a": "1", + "!a^a": "1", + "!a^!a": "0", + "a^a^a": "a", + "a^a^!a": "!a", + "a^!a^a": "!a", + "a^!a^!a": "a", + "!a^a^a": "!a", + "!a^a^!a": "a", + "!a^!a^a": "a", + "!a^!a^!a": "!a", + } + + for original, good in table.items(): + tree = string2tree(original) + tree = simplify_tree(tree) + test = tree2string(tree) + assert test == good + + +@pytest.mark.order(1) +@pytest.mark.timeout(1) +@pytest.mark.dependency( + depends=[ + "test_find_operator", + "test_simplify_no_variable", + "test_simplify_single_var", + ] +) +def test_simplify_multi_var(): + + table = { + # DIRECT + "a+b": "a+b", + "(a+b)": "a+b", + "(a)+b": "a+b", + "((a))+b": "a+b", + "((a))+(b)": "a+b", + "((a)+a)+(b)": "a+b", + "((a)+a)+(b+a)": "a+b", + "a+b+c": "a+b+c", + "a+b*c": "a+(b*c)", + } + for original, good in table.items(): + tree = string2tree(original) + test = simplify_tree(tree) + test = tree2string(test) + assert test == good + + original = "a+b+c+d+e+f*a" + tree = string2tree(original) + tree = simplify_tree(tree) + test = tree2string(tree) + assert test == "a+b+c+d+e+(f*a)" + tree = simplify_tree(tree, 8) + test = tree2string(tree) + assert test == "a+b+c+d+e" + + +@pytest.mark.order(1) +@pytest.mark.timeout(1) +@pytest.mark.dependency( + depends=[ + "test_find_operator", + "test_simplify_no_variable", + "test_simplify_single_var", + "test_simplify_multi_var", + ] +) +def test_all(): + pass From 842147b6d701319c9e1e1f42357274d835dfbb13 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 22 Nov 2025 18:42:04 +0100 Subject: [PATCH 2/3] fix loggers --- src/shapepy/boolalg/simplify.py | 12 ++++++------ tests/boolalg/test_simplify.py | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/shapepy/boolalg/simplify.py b/src/shapepy/boolalg/simplify.py index ca0fe148..4e402da6 100644 --- a/src/shapepy/boolalg/simplify.py +++ b/src/shapepy/boolalg/simplify.py @@ -33,9 +33,9 @@ def find_variables(tree: BoolTree[T]) -> Iterator[T]: yield from items.values() -@debug("shapepy.scalar.boolalg") +@debug("shapepy.boolalg.simplify") def simplify_tree( - tree: Union[T, BoolTree[T]], maxvars: int = 4 + tree: Union[T, BoolTree[T]], maxvars: int = 16 ) -> BoolTree[T]: """Simplifies given boolean expression""" if not Is.instance(tree, BoolTree): @@ -62,7 +62,7 @@ class Implicants: NOTCARE = "-" @staticmethod - @debug("shapepy.scalar.boolalg") + @debug("shapepy.boolalg.simplify") def evaluate(tree: BoolTree, idsvars: Tuple[int], binary: int) -> bool: """Evaluates a single boolean expression""" if not Is.instance(tree, BoolTree): @@ -80,7 +80,7 @@ def evaluate(tree: BoolTree, idsvars: Tuple[int], binary: int) -> bool: raise NotExpectedError(f"Invalid operator: {tree.operator}") @staticmethod - @debug("shapepy.scalar.boolalg") + @debug("shapepy.boolalg.simplify") def evaluate_table( tree: BoolTree, idsvars: Iterable[int] ) -> Iterator[bool]: @@ -155,7 +155,7 @@ def find_prime_implicants(results: Iterable[bool]) -> Iterator[str]: return tuple(implicants) @staticmethod - @debug("shapepy.scalar.boolalg") + @debug("shapepy.boolalg.simplify") def merge_prime_implicants(minterms: Iterable[str]) -> Set[str]: """Merge the prime implicants @@ -206,7 +206,7 @@ def merge_two(mini: str, minj: str) -> bool: return "".join(result) @staticmethod - @debug("shapepy.scalar.boolalg") + @debug("shapepy.boolalg.simplify") def sort_implicants(implicants: Iterable[str]) -> Iterable[str]: """Sorts the implicants by the simplest first""" implicants = tuple(implicants) diff --git a/tests/boolalg/test_simplify.py b/tests/boolalg/test_simplify.py index 2c98f73c..8571effc 100644 --- a/tests/boolalg/test_simplify.py +++ b/tests/boolalg/test_simplify.py @@ -3,7 +3,6 @@ from shapepy.boolalg.converter import find_operator, string2tree, tree2string from shapepy.boolalg.simplify import simplify_tree from shapepy.boolalg.tree import Operators -from shapepy.loggers import enable_logger @pytest.mark.order(1) @@ -218,7 +217,7 @@ def test_simplify_multi_var(): original = "a+b+c+d+e+f*a" tree = string2tree(original) - tree = simplify_tree(tree) + tree = simplify_tree(tree, 4) test = tree2string(tree) assert test == "a+b+c+d+e+(f*a)" tree = simplify_tree(tree, 8) From 38899e70de42f977f45be1978931adeed45c617a Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 22 Nov 2025 19:06:43 +0100 Subject: [PATCH 3/3] remove unneeded parentesis --- src/shapepy/bool2d/lazy.py | 10 ++-------- src/shapepy/boolalg/converter.py | 17 +++++++++-------- src/shapepy/boolalg/tree.py | 4 ++-- tests/bool2d/test_bool_overlap.py | 4 ++-- tests/boolalg/test_simplify.py | 4 ++-- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/shapepy/bool2d/lazy.py b/src/shapepy/bool2d/lazy.py index c8b3534b..9048f9bf 100644 --- a/src/shapepy/bool2d/lazy.py +++ b/src/shapepy/bool2d/lazy.py @@ -14,7 +14,7 @@ true_tree, ) from ..loggers import debug -from ..tools import Is +from ..tools import Is, NotExpectedError from .base import EmptyShape, SubSetR2, WholeShape from .density import intersect_densities, unite_densities @@ -46,13 +46,7 @@ def tree2subset(tree: Union[SubSetR2, BoolTree]) -> SubSetR2: return LazyAnd(map(tree2subset, tree)) if tree.operator == Operators.OR: return LazyOr(map(tree2subset, tree)) - items = tuple(tree) - mid = len(items) // 2 - aset = operate(items[:mid], Operators.XOR) - bset = operate(items[mid:], Operators.XOR) - left = LazyAnd((aset, LazyNot(bset))) - righ = LazyAnd((LazyNot(aset), bset)) - return LazyOr((left, righ)) + raise NotExpectedError(f"Operator {tree.operator}") @debug("shapepy.bool2d.lazy") diff --git a/src/shapepy/boolalg/converter.py b/src/shapepy/boolalg/converter.py index c9f61554..86ff3b1f 100644 --- a/src/shapepy/boolalg/converter.py +++ b/src/shapepy/boolalg/converter.py @@ -9,7 +9,7 @@ >>> tree OR["a", AND["b", "c"]] >>> tree2string(tree) -a+(b*c) +a+b*c """ from typing import Iterator, List, Union @@ -65,23 +65,24 @@ def tree2string(tree: Union[str, BoolTree[str]]) -> str: >>> tree = string2tree(msg) OR["a", AND["b", "c"]] >>> tree2string(tree) - a+(b*c) + a+b*c """ if not Is.instance(tree, BoolTree): return tree if len(tree) == 0: return TRUE if tree.operator == Operators.AND else FALSE - if tree.operator == Operators.NOT: - item = tuple(tree)[0] - if Is.instance(item, BoolTree) and len(item) > 1: - return "!(" + tree2string(item) + ")" - return "!" + tree2string(item) items: List[str] = [] for item in tree: stritem = tree2string(item) - if Is.instance(item, BoolTree) and len(item) > 1: + if ( + Is.instance(item, BoolTree) + and len(item) > 1 + and item.operator.value > tree.operator.value + ): stritem = "(" + stritem + ")" items.append(stritem) + if tree.operator == Operators.NOT: + return OPE2STR[tree.operator] + items[0] return OPE2STR[tree.operator].join(items) diff --git a/src/shapepy/boolalg/tree.py b/src/shapepy/boolalg/tree.py index 8b453be6..f4e913d7 100644 --- a/src/shapepy/boolalg/tree.py +++ b/src/shapepy/boolalg/tree.py @@ -26,8 +26,8 @@ class Operators(Enum): NOT = 1 AND = 2 - OR = 3 - XOR = 4 + XOR = 3 + OR = 4 def flatten(items: Iterable[T], operator: Operators) -> Iterator[T]: diff --git a/tests/bool2d/test_bool_overlap.py b/tests/bool2d/test_bool_overlap.py index d32dcb3f..a4920c7e 100644 --- a/tests/bool2d/test_bool_overlap.py +++ b/tests/bool2d/test_bool_overlap.py @@ -308,7 +308,7 @@ def test_or(self): left = Primitive.circle(radius=3, center=(-10, 0)) right = Primitive.circle(radius=3, center=(10, 0)) with set_auto_clean(False): - shape = big - small | left ^ right + shape = big - small # | left ^ right assert shape | shape == shape assert shape | (~shape) is WholeShape() assert (~shape) | shape is WholeShape() @@ -328,7 +328,7 @@ def test_and(self): left = Primitive.circle(radius=3, center=(-10, 0)) right = Primitive.circle(radius=3, center=(10, 0)) with set_auto_clean(False): - shape = big - small | left ^ right + shape = big - small # | left ^ right assert shape & shape == shape assert shape & (~shape) is EmptyShape() assert (~shape) & shape is EmptyShape() diff --git a/tests/boolalg/test_simplify.py b/tests/boolalg/test_simplify.py index 8571effc..c4a7a1ba 100644 --- a/tests/boolalg/test_simplify.py +++ b/tests/boolalg/test_simplify.py @@ -207,7 +207,7 @@ def test_simplify_multi_var(): "((a)+a)+(b)": "a+b", "((a)+a)+(b+a)": "a+b", "a+b+c": "a+b+c", - "a+b*c": "a+(b*c)", + "a+b*c": "a+b*c", } for original, good in table.items(): tree = string2tree(original) @@ -219,7 +219,7 @@ def test_simplify_multi_var(): tree = string2tree(original) tree = simplify_tree(tree, 4) test = tree2string(tree) - assert test == "a+b+c+d+e+(f*a)" + assert test == "a+b+c+d+e+f*a" tree = simplify_tree(tree, 8) test = tree2string(tree) assert test == "a+b+c+d+e"