BioSight/python/phi/ast_parse.py
allaun 41fcbc3daa feat(init): initial BioSight commit — equation-to-DNA Φ encoding
BioSight encodes mathematical equations as 30-base hachimoji DNA
sequences for Adleman/Lipton-style DNA computing.

4-layer Φ mapping:
  Layer 1: F(E) — byte-class histogram on Δ₇
  Layer 3: τ(E) + δ(E) — parse tree structure
  Layer 4: 6 consistency rules → allele-specific PCR pass/fail

Independent phi/ modules:
  charclass, ast_parse, consistency, embed, output

Build: python3 -m py_compile — all modules clean
2026-06-23 18:27:35 -05:00

182 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
phi.ast_parse — Layer 3: Parse tree analysis → τ and δ distributions
Converts equation strings to Python ASTs (after preprocessing math
notation), then computes:
τ(E) — node-type frequency histogram over Δ_{k-1}
δ(E) — child-ordering frequency histogram over Δ_{2k²-1}
Dependencies: Python ast, re (stdlib). Independent of phi.charclass.
"""
from __future__ import annotations
import ast
import re
from typing import Dict, List, Optional, Tuple
# ── Node types recognized by the parser ──────────────────────────────────
NODE_TYPES = [
"bin_add", "bin_sub", "bin_mul", "bin_div",
"bin_pow", "bin_eq", "un_neg", "un_not",
"var", "const", "call", "subscript", "tuple",
"list", "dict", "set", "lambda", "if_exp",
]
# ── Math notation preprocessor ───────────────────────────────────────────
def preprocess_math(text: str) -> str:
"""Preprocess math notation into Python-parseable form.
Handles:
- Single = → == (but preserves <=, >=, !=, ==)
- |x| → abs(x)
- Parenthesized subscripts: p_(i+j) → p[i+j]
- Implicit multiplication: (x)(y) → (x)*(y)
- TeX markers: removed
"""
s = text.strip()
if s.endswith(":"):
return ""
s = re.sub(r'\$|\$\$|\\\[|\\\]|\\\(|\\\)', '', s)
s = re.sub(r'(?<![<>=!])=(?!=)', '==', s)
s = re.sub(r'\|([^|=]+)\|', r'abs(\1)', s)
s = re.sub(r'([a-zA-Z])_\(([^)]+)\)', r'\1[\2]', s)
s = re.sub(r'\)\(', r')*(', s)
return s
def parse_to_ast(equation: str) -> Optional[ast.AST]:
"""Parse an equation string into a Python AST.
Tries direct parse first, then wraps in parens for expressions
where math notation drops outer parentheses.
"""
text = preprocess_math(equation)
if not text:
return None
try:
return ast.parse(text, mode="eval")
except SyntaxError:
pass
try:
return ast.parse(f"({text})", mode="eval")
except SyntaxError:
return None
# ── AST node classification ──────────────────────────────────────────────
def _classify_node(node: ast.AST) -> str:
"""Map an AST node to one of the NODE_TYPES."""
if isinstance(node, ast.BinOp):
if isinstance(node.op, ast.Add):
return "bin_add"
if isinstance(node.op, ast.Sub):
return "bin_sub"
if isinstance(node.op, ast.Mult):
return "bin_mul"
if isinstance(node.op, ast.Div):
return "bin_div"
if isinstance(node.op, ast.Pow):
return "bin_pow"
return "bin_add"
if isinstance(node, ast.UnaryOp):
if isinstance(node.op, ast.USub):
return "un_neg"
if isinstance(node.op, ast.Not):
return "un_not"
return "un_neg"
if isinstance(node, ast.Name):
return "var"
if isinstance(node, ast.Constant):
return "const"
if isinstance(node, ast.Call):
return "call"
if isinstance(node, ast.Subscript):
return "subscript"
if isinstance(node, ast.Tuple):
return "tuple"
return "const"
# ── τ(E): node-type frequencies → Δ_{k-1} ───────────────────────────────
def compute_tau(equation: str) -> Optional[List[float]]:
"""Compute τ(E) — normalized node-type histogram.
Returns 18 floats (one per NODE_TYPE) summing to 1.0,
or None if the equation cannot be parsed.
"""
tree = parse_to_ast(equation)
if tree is None:
return None
counts = {t: 0 for t in NODE_TYPES}
for node in ast.walk(tree):
t = _classify_node(node)
counts[t] = counts.get(t, 0) + 1
total = sum(counts.values())
if total == 0:
return [1.0 / len(NODE_TYPES)] * len(NODE_TYPES)
return [counts[t] / total for t in NODE_TYPES]
# ── δ(E): child-ordering frequencies → Δ_{2k²-1} ────────────────────────
# Which (parent, child) type combinations are tracked in δ
DELTA_PARENT_TYPES = [
"bin_add", "bin_sub", "bin_mul", "bin_div", "bin_pow",
"un_neg", "un_not", "call", "subscript",
]
DELTA_CHILD_TYPES = [
"bin_add", "bin_sub", "bin_mul", "bin_div", "bin_pow",
"un_neg", "var", "const", "call",
]
MAX_CHILDREN = 3
def compute_delta(equation: str) -> Optional[List[float]]:
"""Compute δ(E) — child-ordering frequency histogram.
For each (parent_type, child_type, child_index) triple, returns
the fraction of all parent-child edges. Captures syntactic
topology beyond raw node counts.
Returns a flat list of length len(DELTA_PARENT_TYPES) ×
len(DELTA_CHILD_TYPES) × MAX_CHILDREN, or None if parse fails.
"""
tree = parse_to_ast(equation)
if tree is None:
return None
edges: Dict[Tuple[str, str, int], int] = {}
total_edges = 0
def walk(node: ast.AST, _child_idx: int = 0):
nonlocal total_edges
parent_t = _classify_node(node)
for i, child in enumerate(ast.iter_child_nodes(node)):
child_t = _classify_node(child)
key = (parent_t, child_t, i)
edges[key] = edges.get(key, 0) + 1
total_edges += 1
walk(child, i)
walk(tree)
if total_edges == 0:
return None
result = []
for pt in DELTA_PARENT_TYPES:
for ct in DELTA_CHILD_TYPES:
for i in range(MAX_CHILDREN):
result.append(edges.get((pt, ct, i), 0) / total_edges)
return result