mirror of
https://github.com/allaunthefox/BioSight.git
synced 2026-07-30 18:56:17 +00:00
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
182 lines
5.7 KiB
Python
182 lines
5.7 KiB
Python
"""
|
||
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
|