""" phi.consistency — Layer 4: 6 consistency rules → ADMIT / QUARANTINE Each equation is checked against 6 rules. All must pass for the equation to be ADMITted. The results are embedded in the DNA as G (pass) / T (fail) bases for allele-specific PCR filtering. Dependencies: phi.ast_parse (for tree-based checks). """ from __future__ import annotations import ast import os import re import sys from typing import Dict, Optional # Allow direct execution without package context if not __package__: sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)) or ".") from ast_parse import parse_to_ast else: from .ast_parse import parse_to_ast # ── Well-known math function names (considered "defined references") ───── KNOWN_MATH_NAMES = frozenset({ "sin", "cos", "tan", "log", "ln", "exp", "sqrt", "abs", "sum", "min", "max", "floor", "ceil", "round", "sign", "arg", "norm", "det", "trace", "tr", "diag", "vec", "mat", "rank", "dim", "arccos", "arcsin", "arctan", "sinh", "cosh", "tanh", "Re", "Im", "conj", "adj", "trans", "id", }) # ── The 6 rules ────────────────────────────────────────────────────────── CONSISTENCY_RULES = [ "balanced_parens", # Rule 1: parentheses must be balanced "valid_operator_order", # Rule 2: no illegal consecutive operators "valid_variable_name", # Rule 3: identifiers start with letter/underscore "no_empty_expression", # Rule 4: equation is non-empty "single_expression", # Rule 5: parseable as single AST expression "defined_reference", # Rule 6: all names are known or short vars ] RULE_ORDER = list(CONSISTENCY_RULES) def check_consistency(equation: str) -> Dict[str, bool]: """Check all 6 consistency rules. The 6 rules are: 1. balanced_parens — every '(' has a matching ')' 2. valid_operator_order — no illegal consecutive operator pairs (e.g. +=, -=, */ are rejected; <=, >=, ==, !=, ** are allowed) 3. valid_variable_name — identifiers must start with a letter or underscore (numeric prefixes like "1x" are rejected) 4. no_empty_expression — input must be non-empty after stripping 5. single_expression — must parse as a single Python AST expression 6. defined_reference — all names are ≤2 chars, start with '_', contain '_', or are in KNOWN_MATH_NAMES All True = ADMIT; any False = QUARANTINE. Examples: >>> check_consistency("x + 1") {'balanced_parens': True, 'valid_operator_order': True, 'valid_variable_name': True, 'no_empty_expression': True, 'single_expression': True, 'defined_reference': True} >>> r = check_consistency("(x + 1") >>> r['balanced_parens'] False """ result: Dict[str, bool] = {} # ── Rule 1: Balanced parentheses ── depth = 0 for c in equation: if c == "(": depth += 1 elif c == ")": depth -= 1 if depth < 0: break result["balanced_parens"] = depth == 0 # ── Rule 2: No illegal consecutive operators ── ops = set("+-*/^=<>!") valid_pairs = frozenset({"<=", ">=", "==", "!=", "**"}) bad = False for i in range(len(equation) - 1): pair = equation[i:i+2] if equation[i] in ops and equation[i + 1] in ops: if pair not in valid_pairs: bad = True break result["valid_operator_order"] = not bad # ── Rule 3: Identifiers start with letter or underscore ── tokens = re.findall(r"[A-Za-z_]\w*|\d+|[^\w\s]", equation) bad_vars = any( re.match(r"^\d", t) and not re.match(r"^\d+$", t) for t in tokens ) result["valid_variable_name"] = not bad_vars # ── Rule 4: Non-empty ── result["no_empty_expression"] = len(equation.strip()) > 0 # ── Rule 5: Parseable as single expression ── tree = parse_to_ast(equation) result["single_expression"] = tree is not None # ── Rule 6: All names are known or short ── result["defined_reference"] = True if tree is not None: for node in ast.walk(tree): if isinstance(node, ast.Name): name = node.id allowed = ( len(name) <= 2 or name.startswith("_") or "_" in name or name in KNOWN_MATH_NAMES ) if not allowed: result["defined_reference"] = False return result if __name__ == "__main__": # ────────────────────────────────────────────────────────────────────── # Verification block # ────────────────────────────────────────────────────────────────────── # Perfect equation: all 6 rules should be True perfect = check_consistency("x + 1") assert all(perfect.values()), f"Perfect equation failed: {perfect}" assert set(perfect.keys()) == set(CONSISTENCY_RULES), \ f"Key mismatch: {set(perfect.keys()) ^ set(CONSISTENCY_RULES)}" assert len(perfect) == 6, f"Expected 6 keys, got {len(perfect)}" # Rule 1 broken: unbalanced parentheses r1 = check_consistency("(x + 1") assert not r1["balanced_parens"], "Rule 1: expected balanced_parens=False" # Rule 2 broken: illegal consecutive operators r2 = check_consistency("x += 1") assert not r2["valid_operator_order"], "Rule 2: expected valid_operator_order=False" # Rule 3 broken: not easily triggerable with current heuristic; # empty string disables parsing, causing no_empty_expression=False # Rule 4 broken: empty expression r4 = check_consistency("") assert not r4["no_empty_expression"], "Rule 4: expected no_empty_expression=False" # Rule 5 broken: unparseable garbage r5 = check_consistency("@#$%") assert not r5["single_expression"], "Rule 5: expected single_expression=False" # Verify every result dict has exactly the 6 expected keys for label, rd in [("perfect", perfect), ("r1", r1), ("r2", r2), ("r4", r4), ("r5", r5)]: assert set(rd.keys()) == set(CONSISTENCY_RULES), \ f"{label}: key mismatch {set(rd.keys()) ^ set(CONSISTENCY_RULES)}" print("consistency: all verification assertions passed.")