Source code for pykappa.expression

"""Represents Kappa arithmetic expressions."""

import math
import operator
from collections import deque
from dataclasses import dataclass
from types import MappingProxyType
from typing import Any, Self, Optional, Callable, TYPE_CHECKING

if TYPE_CHECKING:
    from pykappa.pattern import Component
    from pykappa.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,
}


def _parse_operator(kappa_operator: str) -> Callable:
    """Convert a Kappa string operator to a Python function.

    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] @dataclass(frozen=True, init=False) class Expression: """Algebraic expressions as specified by the Kappa language.""" _type: Any # Type of expression (literal, variable, binary_op, etc.) _attrs: MappingProxyType # Attributes specific to the expression type
[docs] @classmethod def from_kappa(cls, kappa_str: str) -> "Expression": """Parse an Expression from a Kappa string. Raises: AssertionError: If the string doesn't represent a valid expression. """ from pykappa._parsing import kappa_parser, ExpressionTransformer 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 ExpressionTransformer.from_tree(expr_tree)
def __init__(self, type, **attrs): if "children" in attrs: attrs["children"] = tuple(attrs["children"]) object.__setattr__(self, "_type", type) object.__setattr__(self, "_attrs", MappingProxyType(dict(attrs))) def __str__(self): return self.kappa_str @property def kappa_str(self) -> str: """ Raises: ValueError: If expression type is not supported for string conversion. """ if self._type == "literal": return str(self.evaluate()) if self._type == "boolean_literal": return "[true]" if self._attrs["value"] else "[false]" if self._type == "variable": return f"'{self._attrs['name']}'" if self._type in ("binary_op", "comparison", "logical_or", "logical_and"): operator = { "logical_or": "||", "logical_and": "&&", }.get(self._type, self._attrs.get("operator")) return f"({self._attrs['left'].kappa_str}) {operator} ({self._attrs['right'].kappa_str})" if self._type in ("unary_op", "logical_not"): operator = ( "[not]" if self._type == "logical_not" else self._attrs["operator"] ) return f"{operator} ({self._attrs['child'].kappa_str})" if self._type == "list_op": children = " ".join( f"({child.kappa_str})" for child in self._attrs["children"] ) return f"{self._attrs['operator']} {children}" if self._type == "defined_constant": return self._attrs["name"] if self._type == "parentheses": return self._attrs["child"].kappa_str if self._type == "conditional": return ( f"{self._attrs['condition'].kappa_str} [?] {self._attrs['true_expr'].kappa_str} " f"[:] {self._attrs['false_expr'].kappa_str}" ) if self._type == "reserved_variable": return self._attrs["value"].kappa_str if self._type == "component_pattern": return f"|{self._attrs['value'].kappa_str}|" if self._type == "token_value": return f"|{self._attrs['name']}|" 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). Raises: ValueError: If evaluation fails due to missing context or unsupported type. """ if self._type in ("literal", "boolean_literal"): return self._attrs["value"] if 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] if self._type in ("binary_op", "comparison", "logical_or", "logical_and"): left = self._attrs["left"].evaluate(system) right = self._attrs["right"].evaluate(system) if self._type == "logical_or": return left or right if self._type == "logical_and": return left and right return _parse_operator(self._attrs["operator"])(left, right) if self._type in ("unary_op", "logical_not"): child = self._attrs["child"].evaluate(system) return ( not child if self._type == "logical_not" else _parse_operator(self._attrs["operator"])(child) ) if self._type == "list_op": children = [child.evaluate(system) for child in self._attrs["children"]] return _parse_operator(self._attrs["operator"])(children) if self._type == "defined_constant": const = self._attrs["name"] if const == "[pi]": return math.pi raise ValueError(f"Unknown constant: {const}") if self._type == "parentheses": return self._attrs["child"].evaluate(system) if self._type == "conditional": return ( self._attrs["true_expr"].evaluate(system) if self._attrs["condition"].evaluate(system) else self._attrs["false_expr"].evaluate(system) ) if 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)) // value._attrs["n_symmetries"] ) raise NotImplementedError( f"Reserved variable {value._type} not implemented yet." ) if self._type == "token_value": name = self._attrs["name"] if system is None: raise ValueError(f"{self} needs a System to evaluate token '{name}'") return system.tokens.get(name, 0.0) raise ValueError(f"Unsupported node type: {self._type}")
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, type(self)): stack.append(attr_value) elif isinstance(attr_value, (list, tuple)): stack.extend(v for v in attr_value if isinstance(v, type(self))) return result