Source code for kappybara.algebra

import math
import operator
from collections import deque
from typing import Self, Optional, Callable, TYPE_CHECKING

if TYPE_CHECKING:
    from kappybara.pattern import Component
    from kappybara.system import System


string_to_operator = {
    # Unary
    "[log]": math.log,
    "[exp]": math.exp,
    "[sin]": math.sin,
    "[cos]": math.cos,
    "[tan]": math.tan,
    "[sqrt]": math.sqrt,
    # Binary
    "+": operator.add,
    "-": operator.sub,
    "*": operator.mul,
    "/": operator.truediv,
    "^": operator.pow,
    "mod": operator.mod,
    # Comparisons
    "=": operator.eq,
    "<": operator.lt,
    ">": operator.gt,
    # List
    "[max]": max,
    "[min]": min,
}


[docs] def parse_operator(kappa_operator: str) -> Callable: """Convert a Kappa string operator to a Python function. Args: kappa_operator: Kappa language operator string. Returns: Python function counterpart. Raises: ValueError: If the operator is not recognized. """ try: return string_to_operator[kappa_operator] except KeyError: raise ValueError(f"Unknown operator: {kappa_operator}")
[docs] class Expression: """Algebraic expressions as specified by the Kappa language. Attributes: type: Type of expression (literal, variable, binary_op, etc.). attrs: Dictionary of attributes specific to the expression type. """
[docs] @classmethod def from_kappa(cls, kappa_str: str) -> Self: """Parse an Expression from a Kappa string. Args: kappa_str: Kappa expression string. Returns: Parsed Expression object. Raises: AssertionError: If the string doesn't represent a valid expression. """ from kappybara.grammar import kappa_parser, parse_tree_to_expression input_tree = kappa_parser.parse(kappa_str) assert input_tree.data == "kappa_input" expr_tree = input_tree.children[0] assert expr_tree.data in ["!algebraic_expression", "algebraic_expression"] return parse_tree_to_expression(expr_tree)
def __init__(self, type, **attrs): self.type = type self.attrs = attrs @property def kappa_str(self) -> str: """Get the expression representation in Kappa format. Returns: Kappa string representation of the expression. Raises: ValueError: If expression type is not supported for string conversion. """ if self.type == "literal": return str(self.evaluate()) elif self.type == "boolean_literal": return "[true]" if self.attrs["value"] else "[false]" elif self.type == "variable": return f"'{self.attrs["name"]}'" elif self.type in ("binary_op", "comparison"): left_str = self.attrs["left"].kappa_str right_str = self.attrs["right"].kappa_str return f"({left_str}) {self.attrs['operator']} ({right_str})" elif self.type == "unary_op": return f"{self.attrs['operator']} ({self.attrs['child'].kappa_str})" elif self.type == "list_op": children_str = " ".join( f"({child.kappa_str})" for child in self.attrs["children"] ) return f"{self.attrs["operator"]} {children_str}" elif self.type == "defined_constant": return f"{self.attrs["name"]}" elif self.type == "parentheses": return self.attrs["child"].kappa_str elif self.type == "conditional": true_expr_str = self.attrs["true_expr"].kappa_str false_expr_str = self.attrs["false_expr"].kappa_str return f"{self.attrs["condition"].kappa_str} [?] {true_expr_str} [:] {false_expr_str}" elif self.type in ("logical_or", "logical_and"): left_str = self.attrs["left"].kappa_str right_str = self.attrs["right"].kappa_str op = {"logical_or": "||", "logical_and": "&&"} return f"({left_str}) {op[self.type]} ({right_str})" elif self.type == "logical_not": return f"[not] ({self.attrs['child'].kappa_str})" elif self.type == "reserved_variable": return self.attrs["value"].kappa_str elif self.type == "component_pattern": return f"|{self.attrs['value'].kappa_str}|" raise ValueError(f"Unsupported node type: {self.type}")
[docs] def evaluate(self, system: Optional["System"] = None) -> int | float: """Evaluate the expression to get its value. Args: system: System context for variable evaluation (required for variables). Returns: Result of evaluating the expression. Raises: ValueError: If evaluation fails due to missing context or unsupported type. """ if self.type in ("literal", "boolean_literal"): return self.attrs["value"] elif self.type == "variable": name = self.attrs["name"] if system is None: raise ValueError(f"{self} needs a System to evaluate variable '{name}'") return system[name] elif self.type in ("binary_op", "comparison"): left_val = self.attrs["left"].evaluate(system) right_val = self.attrs["right"].evaluate(system) return parse_operator(self.attrs["operator"])(left_val, right_val) elif self.type == "unary_op": child_val = self.attrs["child"].evaluate(system) return parse_operator(self.attrs["operator"])(child_val) elif self.type == "list_op": children_vals = [child.evaluate(system) for child in self.attrs["children"]] return parse_operator(self.attrs["operator"])(children_vals) elif self.type == "defined_constant": const = self.attrs["name"] if const == "[pi]": return math.pi else: raise ValueError(f"Unknown constant: {const}") elif self.type == "parentheses": return self.attrs["child"].evaluate(system) elif self.type == "conditional": cond_val = self.attrs["condition"].evaluate(system) return ( self.attrs["true_expr"].evaluate(system) if cond_val else self.attrs["false_expr"].evaluate(system) ) elif self.type == "logical_or": left_val = self.attrs["left"].evaluate(system) right_val = self.attrs["right"].evaluate(system) return left_val or right_val elif self.type == "logical_and": left_val = self.attrs["left"].evaluate(system) right_val = self.attrs["right"].evaluate(system) return left_val and right_val elif self.type == "logical_not": return not self.attrs["child"].evaluate(system) elif self.type == "reserved_variable": value = self.attrs["value"] if value.type == "component_pattern": component: Component = value.attrs["value"] if system is None: raise ValueError( f"{self} needs a System to evaluate pattern {component}" ) return ( len(system.mixture.embeddings(component)) // component.n_automorphisms ) else: raise NotImplementedError( f"Reserved variable {value.type} not implemented yet." ) raise ValueError(f"Unsupported node type: {self.type}")
[docs] def filter(self, type_str: str) -> list[Self]: """ Returns all nodes in the expression tree whose type matches the provided string. Note: Doesn't detect nodes indirectly nested in named variables. """ result = [] stack = deque([self]) # DFS from the root while stack: node = stack.pop() if node.type == type_str: result.append(node) # Add child nodes to the stack if hasattr(node, "attrs"): for attr_value in node.attrs.values(): if isinstance(attr_value, Expression): stack.append(attr_value) elif isinstance(attr_value, (list, tuple)): stack.extend(v for v in attr_value if isinstance(v, Expression)) return result