#!/usr/bin/env python3
"""
Ontic Spinor Calculus

One ontic class only:
  pair generation, juxtaposition, wire

No labels. No flavors. No species.

Rewrite:
  ◁▷  -> ─

Everything is structural. Distinction is multiplicity and arrangement only.
Ontically, the primitive event is paired emergence, not a lone pole.
"""

from dataclasses import dataclass
from typing import Union, List


@dataclass(frozen=True)
class Ket:
    def __str__(self) -> str:
        return "▷"


@dataclass(frozen=True)
class Bra:
    def __str__(self) -> str:
        return "◁"


@dataclass(frozen=True)
class Juxt:
    left: "Term"
    right: "Term"

    def __str__(self) -> str:
        return f"{self.left}{self.right}"


@dataclass(frozen=True)
class Wire:
    def __str__(self) -> str:
        return "─"


@dataclass(frozen=True)
class Cycle:
    atoms: tuple["Atom", ...]

    def __str__(self) -> str:
        return "".join(str(a) for a in self.atoms)


Atom = Union[Ket, Bra]
Term = Union[Wire, Ket, Bra, Juxt, Cycle]


def juxt(left: Term, right: Term) -> Term:
    if isinstance(left, Wire) and isinstance(right, Wire):
        return Wire()
    if isinstance(left, Wire):
        return right
    if isinstance(right, Wire):
        return left
    return Juxt(left, right)


def atoms(term: Term) -> List[Atom]:
    if isinstance(term, Wire):
        return []
    if isinstance(term, (Ket, Bra)):
        return [term]
    if isinstance(term, Cycle):
        return list(term.atoms)
    return atoms(term.left) + atoms(term.right)


def term_from_atoms(items: List[Atom]) -> Term:
    if not items:
        return Wire()
    result: Term = items[0]
    for item in items[1:]:
        result = juxt(result, item)
    return result


def atom_key(atom: Atom) -> int:
    return 0 if isinstance(atom, Ket) else 1


def canonical_cycle(items: List[Atom]) -> Term:
    """Canonical representative of a cyclic ontic word."""
    if not items:
        return Wire()
    rotations = [tuple(items[i:] + items[:i]) for i in range(len(items))]
    best = min(rotations, key=lambda seq: tuple(atom_key(a) for a in seq))
    return Cycle(best)


def yank_one(term: Term) -> Term:
    """Remove one adjacent inward-outward edge-sharing pair."""
    if isinstance(term, Cycle):
        seq = list(term.atoms)
        if not seq:
            return Wire()
        for i in range(len(seq) - 1):
            a, b = seq[i], seq[i + 1]
            if isinstance(a, Bra) and isinstance(b, Ket):
                return canonical_cycle(seq[:i] + seq[i + 2 :])
        first, last = seq[0], seq[-1]
        if isinstance(last, Bra) and isinstance(first, Ket):
            return canonical_cycle(seq[1:-1])
        return term

    seq = atoms(term)
    for i in range(len(seq) - 1):
        a, b = seq[i], seq[i + 1]
        if isinstance(a, Bra) and isinstance(b, Ket):
            return term_from_atoms(seq[:i] + seq[i + 2 :])
    return term


def normalize(term: Term, max_steps: int = 1000) -> Term:
    for _ in range(max_steps):
        yanked = yank_one(term)
        if yanked == term:
            return term
        term = yanked
    return term


def kets(n: int) -> Term:
    """Derived convenience: n residual outward poles."""
    if n < 0:
        raise ValueError("kets(n) requires n >= 0")
    return term_from_atoms([Ket() for _ in range(n)])


def bras(n: int) -> Term:
    """Derived convenience: n residual inward poles."""
    if n < 0:
        raise ValueError("bras(n) requires n >= 0")
    return term_from_atoms([Bra() for _ in range(n)])


def pair() -> Term:
    """Ontic primitive: dual poles emerge together."""
    return juxt(Ket(), Bra())


def pairs(n: int) -> Term:
    """n-fold paired generation before any further interpretation."""
    if n < 0:
        raise ValueError("pairs(n) requires n >= 0")
    return term_from_atoms([atom for _ in range(n) for atom in (Ket(), Bra())])


def bulk_pairs(n: int) -> Term:
    """Pure bulk generation: only pair events."""
    return pairs(n)


def pair_count(term: Term) -> int:
    """
    Count pair-generation events in a bulk term.

    For pure bulk words this is half the total pole count.
    """
    return len(atoms(term)) // 2


def suspend_closure(term: Term, hidden_closures: int | None = None) -> Term:
    """
    Expose a boundary by hiding some closing channels of a bulk process.

    For the current bulk of n generated pairs, the canonical exposed boundary
    hides n-1 closures and leaves one inward face visible:

      bulk(n) = (▷◁)^n   ->   ▷^n◁

    This is not a second ontology. It is incomplete closure of the bulk.
    """
    n = pair_count(term)
    if hidden_closures is None:
        hidden_closures = max(0, n - 1)
    if hidden_closures < 0 or hidden_closures > n:
        raise ValueError("hidden_closures must satisfy 0 <= hidden_closures <= pair_count(term)")
    exposed_kets = n
    exposed_bras = max(0, n - hidden_closures)
    if exposed_kets == 0 and exposed_bras == 0:
        return Wire()
    return term_from_atoms([Ket() for _ in range(exposed_kets)] + [Bra() for _ in range(exposed_bras)])


def boundary_number(n: int) -> Term:
    """Internal helper: boundary defect for a given exposed bulk count."""
    if n < 0:
        raise ValueError("boundary_number(n) requires n >= 0")
    return suspend_closure(bulk_pairs(n))


def balance(term: Term) -> int:
    """Net ontic orientation: #kets - #bras after normalization."""
    normalized = normalize(term)
    seq = atoms(normalized)
    return sum(1 if isinstance(atom, Ket) else -1 for atom in seq)


def canonical(n: int) -> Term:
    """Canonical residual form for an integer balance."""
    if n >= 0:
        return kets(n)
    return bras(-n)


def add(a: Term, b: Term) -> Term:
    """Addition on residual imbalance."""
    return canonical(balance(a) + balance(b))


def negate(term: Term) -> Term:
    """Negation swaps residual orientation."""
    return canonical(-balance(term))


def subtract(a: Term, b: Term) -> Term:
    """Subtraction on residual imbalance."""
    return canonical(balance(a) - balance(b))


def multiply(a: Term, b: Term) -> Term:
    """Multiplication on residual balances."""
    return canonical(balance(a) * balance(b))


def dual(atom: Union[Ket, Bra]) -> Union[Ket, Bra]:
    return Bra() if isinstance(atom, Ket) else Ket()


def reverse_dual(term: Term) -> Term:
    return term_from_atoms([dual(atom) for atom in reversed(atoms(normalize(term)))])


def quotient(target: Term, divisor: Term) -> Term:
    """
    Synthesize q such that:

      q · divisor ->* target

    In the one-class calculus, q = target · reverse_dual(divisor).
    """
    return normalize(juxt(normalize(target), reverse_dual(divisor)))


def raw_quotient(target: Term, divisor: Term) -> Term:
    """Unnormalized structural bridge before internal cancellations."""
    return juxt(normalize(target), reverse_dual(divisor))


def apply_quotient(target: Term, divisor: Term) -> Term:
    return normalize(juxt(quotient(target, divisor), normalize(divisor)))


def cycle(term: Term) -> Term:
    """Interpret a term as a cyclic ontic word."""
    normalized = normalize(term)
    return canonical_cycle(atoms(normalized))


def defect(term: Term) -> Term:
    """
    Manifestation as persistent defect.

    Generation happens, closure erases, and whatever remains is the defect.
    """
    return normalize(term)


def manifests(term: Term) -> bool:
    """A term manifests iff closure does not erase it to the wire."""
    return not isinstance(defect(term), Wire)


def defect_charge(term: Term) -> int:
    """Simple invariant: residual orientation after closure."""
    return balance(defect(term))


@dataclass(frozen=True)
class BulkGraph:
    """Toy causal bulk: pair-events connected by adjacency."""
    adjacency: dict[int, tuple[int, ...]]


def line_bulk(n: int) -> BulkGraph:
    """Simple 1D bulk where influence travels one edge per tick."""
    if n < 0:
        raise ValueError("line_bulk(n) requires n >= 0")
    adjacency: dict[int, tuple[int, ...]] = {}
    for i in range(n):
        nbrs: list[int] = []
        if i > 0:
            nbrs.append(i - 1)
        if i + 1 < n:
            nbrs.append(i + 1)
        adjacency[i] = tuple(nbrs)
    return BulkGraph(adjacency)


def causal_domain(graph: BulkGraph, observer: int, ticks: int) -> set[int]:
    """
    Reachable bulk events after `ticks` propagation steps.

    This is the toy light cone of the observer.
    """
    if observer not in graph.adjacency:
        return set()
    reached = {observer}
    frontier = {observer}
    for _ in range(max(0, ticks)):
        nxt: set[int] = set()
        for node in frontier:
            nxt.update(graph.adjacency.get(node, ()))
        nxt -= reached
        reached |= nxt
        frontier = nxt
        if not frontier:
            break
    return reached


def horizon(graph: BulkGraph, observer: int, ticks: int) -> set[int]:
    """The frontier of causal reachability at exactly `ticks` steps."""
    if observer not in graph.adjacency:
        return set()
    if ticks <= 0:
        return {observer}
    reached = {observer}
    frontier = {observer}
    for _ in range(ticks):
        nxt: set[int] = set()
        for node in frontier:
            nxt.update(graph.adjacency.get(node, ()))
        nxt -= reached
        reached |= nxt
        frontier = nxt
        if not frontier:
            break
    return frontier


def boundary_from_domain_size(size: int) -> Term:
    """
    Boundary number induced by the amount of causally accessible bulk.

    The current simple rule identifies the boundary defect with the number of
    causally accessible bulk pair-events.
    """
    return boundary_number(size)


def observer_boundary(graph: BulkGraph, observer: int, ticks: int) -> Term:
    """
    Boundary defect seen by an observer.

    Bulk events in the observer's causal domain are the accessible portion of
    the bulk. The current toy holographic rule maps the size of that domain to
    the exposed boundary defect.
    """
    domain = causal_domain(graph, observer, ticks)
    return boundary_from_domain_size(len(domain))


def occupancy_domain_size(bits: tuple[int, ...], observer: int, ticks: int) -> int:
    """
    Causally accessible occupied bulk events for a concrete bulk state.

    `bits[i] = 1` means node `i` is occupied by a pair-event.
    """
    graph = line_bulk(len(bits))
    domain = causal_domain(graph, observer, ticks)
    return sum(bits[i] for i in domain)


def occupancy_boundary(bits: tuple[int, ...], observer: int, ticks: int) -> Term:
    """Boundary defect induced by an occupancy bulk state."""
    return boundary_from_domain_size(occupancy_domain_size(bits, observer, ticks))


def enumerate_bulk_states(num_nodes: int) -> list[tuple[int, ...]]:
    """Enumerate all toy occupancy bulk states on `num_nodes` sites."""
    if num_nodes < 0:
        raise ValueError("num_nodes must be >= 0")
    states: list[tuple[int, ...]] = []
    for mask in range(1 << num_nodes):
        states.append(tuple((mask >> i) & 1 for i in range(num_nodes)))
    return states


def boundary_preimage_counts(num_nodes: int, observer: int, ticks: int) -> dict[str, int]:
    """
    Count how many bulk states cut to each boundary defect.

    This is the entropy multiplicity before taking a logarithm.
    """
    counts: dict[str, int] = {}
    for state in enumerate_bulk_states(num_nodes):
        b = str(occupancy_boundary(state, observer, ticks))
        counts[b] = counts.get(b, 0) + 1
    return counts


def parse(text: str) -> Term:
    """
    Parse a bare ontic string.

    Allowed atoms:
      ▷  ket
      ◁  bra
      ─  wire
    """
    text = text.strip()
    if not text or text == "─":
        return Wire()

    seq: List[Union[Ket, Bra]] = []
    i = 0
    while i < len(text):
        if text.startswith("▷", i):
            seq.append(Ket())
            i += 1
        elif text.startswith("◁", i):
            seq.append(Bra())
            i += 1
        elif text[i] in {" ", "\t", "\n"}:
            i += 1
        else:
            raise ValueError(f"Unexpected ontic symbol near: {text[i:i+1]!r}")
    return term_from_atoms(seq)


def demo() -> None:
    p = pair()
    two_pairs = pairs(2)
    three_pairs = pairs(3)
    trivial_cycle = cycle(pair())
    persistent_cycle = cycle(juxt(pair(), pair()))
    closure = parse("◁▷")
    stable_unit = parse("▷◁")
    seam_defect = parse("▷▷◁")
    cyclic_defect = cycle(parse("▷▷"))
    bulk = line_bulk(7)
    dome0 = causal_domain(bulk, observer=3, ticks=0)
    dome1 = causal_domain(bulk, observer=3, ticks=1)
    dome2 = causal_domain(bulk, observer=3, ticks=2)
    hor2 = horizon(bulk, observer=3, ticks=2)
    obs_b0 = observer_boundary(bulk, observer=3, ticks=0)
    obs_b1 = observer_boundary(bulk, observer=3, ticks=1)
    obs_b2 = observer_boundary(bulk, observer=3, ticks=2)
    entropy_counts = boundary_preimage_counts(num_nodes=5, observer=2, ticks=1)

    print("=" * 60)
    print("ONTIC SPINOR CALCULUS")
    print("=" * 60)
    print()
    print("One class only: pair generation, juxtaposition, yanking.")
    print()
    print("Ontic primitive:")
    print(f"  pair() = {p}")
    print(f"  pairs(2) = {two_pairs} -> {normalize(two_pairs)}")
    print(f"  pairs(3) = {three_pairs} -> {normalize(three_pairs)}")
    print("  Generated pairs are stable; only shared-edge closure cancels.")
    print()
    print("Rewrite:")
    print("  ◁▷ -> ─")
    print("  only inward-outward edge-sharing closes")
    print()
    print("Cyclic readings:")
    print(f"  cycle(▷◁) = {trivial_cycle}")
    print(f"  cycle(▷◁▷◁) = {persistent_cycle}")
    print("  The printed form stays ontic-only; cyclic vs linear is contextual")
    print()
    print("Interpretation:")
    print("  Ontically: paired emergence is primitive and stable.")
    print("  Closure happens only when a bra meets a following ket: ◁▷.")
    print()
    print("Observer-relative numbers:")
    print("  Numbers are boundary defects induced by the observer's causal domain.")
    print("  The cut is suspended closure: inaccessible bulk closures are hidden.")
    print(f"  t=0 domain = {sorted(dome0)} -> number {obs_b0}")
    print(f"  t=1 domain = {sorted(dome1)} -> number {obs_b1}")
    print(f"  t=2 domain = {sorted(dome2)} -> number {obs_b2}")
    print()
    print("Manifestation as defect:")
    print(f"  defect(◁▷) = {defect(closure)}")
    print(f"  defect(▷◁) = {defect(stable_unit)}")
    print(f"  defect(▷▷◁) = {defect(seam_defect)}")
    print(f"  defect(cycle(▷▷)) = {defect(cyclic_defect)}")
    print()
    print("  Manifest iff something survives closure.")
    print(f"  manifests(◁▷) = {manifests(closure)}")
    print(f"  manifests(▷◁) = {manifests(stable_unit)}")
    print(f"  manifests(▷▷◁) = {manifests(seam_defect)}")
    print(f"  manifests(cycle(▷▷)) = {manifests(cyclic_defect)}")
    print()
    print("  Residual orientation (defect charge):")
    print(f"  charge(▷◁) = {defect_charge(stable_unit)}")
    print(f"  charge(▷▷◁) = {defect_charge(seam_defect)}")
    print(f"  charge(cycle(▷▷)) = {defect_charge(cyclic_defect)}")
    print()
    print("Observer causal access:")
    print("  toy bulk = line graph with nodes 0..6, observer at node 3")
    print(f"  domain at 0 ticks = {sorted(dome0)}")
    print(f"  domain at 1 tick = {sorted(dome1)}")
    print(f"  domain at 2 ticks = {sorted(dome2)}")
    print(f"  horizon at 2 ticks = {sorted(hor2)}")
    print(f"  observer boundary at 0 ticks = {obs_b0}")
    print(f"  observer boundary at 1 tick = {obs_b1}")
    print(f"  observer boundary at 2 ticks = {obs_b2}")
    print()
    print("Toy entropy as bulk-state degeneracy:")
    print("  5-node occupancy bulk, observer at node 2, ticks=1")
    for key in sorted(entropy_counts.keys(), key=len):
        print(f"  boundary {key}: {entropy_counts[key]} bulk state(s)")


if __name__ == "__main__":
    demo()
An unhandled error has occurred. Reload 🗙