#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as np
from typing import Tuple
from .. import utils
_LOGIC_MAP = {
"AND": "&",
"and": "&",
"&&": "&",
"&": "&",
"OR": "|",
"or": "|",
"||": "|",
"|": "|",
"NOT": "~",
"not": "~",
"!": "~",
"~": "~",
}
_COMPARE_OPS = {"==", "!=", ">=", "<=", ">", "<"}
_ARITH_OPS = {"+", "-", "*", "%"}
[docs]
def f_from_expression(
expr: str,
max_degree: int = 16,
) -> Tuple[np.ndarray, np.ndarray]:
"""
Construct a Boolean function from a string expression.
The expression is evaluated symbolically over all Boolean input
combinations to produce the truth table of the corresponding Boolean
function. Variables are detected automatically based on their first
occurrence in the expression.
Parameters
----------
expr : str
Boolean expression to evaluate. The expression may contain logical
operators (``AND``, ``OR``, ``NOT`` or their lowercase equivalents),
arithmetic operators, and comparisons.
max_degree : int, optional
Maximum number of variables allowed. If the number of detected
variables exceeds ``max_degree``, an empty truth table is returned.
Returns
-------
f : np.ndarray
Boolean function values as an array of shape ``(2**n,)`` with entries
in ``{0, 1}``, where ``n`` is the number of detected variables.
variables : np.ndarray
Variable names in the order they were first encountered in the
expression.
Notes
-----
- Variables are ordered by first occurrence in ``expr``.
- Truth-table rows follow the standard lexicographic ordering with the
most significant bit first.
- The expression is evaluated using ``eval`` with restricted builtins.
- No syntactic or semantic validation of ``expr`` is performed beyond
basic parsing.
- Arithmetic operators (+, -, `*`, %) must be surrounded by whitespace.
This restriction avoids conflicts with biological identifiers such as Ca2+ or IL-2.
- Whenever uncertain, use whitespace.
Examples
--------
>>> f_from_expression('A AND NOT B')
(array([0, 0, 1, 0], dtype=uint8), array(['A', 'B'], dtype='<U1'))
>>> f_from_expression('x1 + x2 + x3 > 1')
(array([0, 0, 0, 1, 0, 1, 1, 1], dtype=uint8),
array(['x1', 'x2', 'x3'], dtype='<U2'))
>>> f_from_expression('(x1 + x2 + x3) % 2 == 0')
(array([1, 0, 0, 1, 0, 1, 1, 0], dtype=uint8),
array(['x1', 'x2', 'x3'], dtype='<U2'))
"""
# --------------------------------------------------
# 1. Normalize parentheses spacing
# --------------------------------------------------
expr = expr.replace("(", " ( ").replace(")", " ) ")
# comparisons
for op in ["==", "!=", ">=", "<=", ">", "<"]:
expr = expr.replace(op, f" {op} ")
expr = expr.replace('> =','>=')
expr = expr.replace('< =','<=')
# logical double operators -> canonical
expr = expr.replace("&&", " & ")
expr = expr.replace("||", " | ")
# single logical operators
for op in ["&", "|", "!", "~"]:
expr = expr.replace(op, f" {op} ")
raw_tokens = expr.split()
tokens = []
variables = []
seen = set()
# --------------------------------------------------
# 2. Token classification
# --------------------------------------------------
for token in raw_tokens:
if token in {"(", ")"}:
tokens.append(token)
continue
if token in _LOGIC_MAP:
tokens.append(_LOGIC_MAP[token])
continue
if token in _COMPARE_OPS:
tokens.append(token)
continue
if token in _ARITH_OPS:
tokens.append(token)
continue
if utils._is_number(token):
tokens.append(token)
continue
# Otherwise: biological identifier
if token not in seen:
seen.add(token)
variables.append(token)
tokens.append(token)
n = len(variables)
if n > max_degree:
return np.array([], dtype=np.uint8), np.array(variables)
# --------------------------------------------------
# 3. Map biological names → safe Python names
# --------------------------------------------------
safe_map = {var: f"v{i}" for i, var in enumerate(variables)}
safe_tokens = [
safe_map[token] if token in safe_map else token
for token in tokens
]
expr_mod = " ".join(safe_tokens)
# --------------------------------------------------
# 4. Build evaluation environment
# --------------------------------------------------
truth_table = utils.get_left_side_of_truth_table(n)
local_dict = {
safe_map[var]: truth_table[:, i].astype(np.int64)
for i, var in enumerate(variables)
}
# --------------------------------------------------
# 5. Evaluate expression
# --------------------------------------------------
try:
result = eval(expr_mod, {"__builtins__": None}, local_dict)
except Exception as e:
raise ValueError(
f"Error evaluating expression:\n{expr}\nParsed as:\n{expr_mod}\nError: {e}"
)
# --------------------------------------------------
# 6. Enforce Boolean semantics
# --------------------------------------------------
result = np.asarray(result)
if n == 0:
result = np.array([int(result)], dtype=np.int64)
else:
result = result.astype(np.int64)
# Fix NOT and enforce {0,1}
result = result & 1
return result.astype(np.uint8), np.array(variables)