| from __future__ import annotations |
|
|
| from collections import defaultdict |
| from dataclasses import dataclass, field |
| from typing import Iterable, Iterator, Sequence |
|
|
| from sgjm.graph.address import Address, AddressBook, Signature |
| from sgjm.graph.node import Node, NodeStatus |
|
|
|
|
| @dataclass |
| class GraphManager: |
| address_book: AddressBook = field(default_factory=AddressBook) |
| _nodes: dict[Address, Node] = field(default_factory=dict) |
| _children: dict[Address, set[Address]] = field(default_factory=lambda: defaultdict(set)) |
| _root: Address | None = None |
|
|
| def __len__(self) -> int: |
| return len(self._nodes) |
|
|
| def root(self) -> Node | None: |
| return self._nodes.get(self._root) if self._root is not None else None |
|
|
| def get(self, addr: Address) -> Node: |
| return self._nodes[addr] |
|
|
| def children(self, addr: Address) -> tuple[Node, ...]: |
| return tuple(self._nodes[c] for c in self._children.get(addr, ())) |
|
|
| def parents(self, addr: Address) -> tuple[Node, ...]: |
| return tuple(self._nodes[p] for p in self._nodes[addr].parents) |
|
|
| def frontier(self) -> tuple[Node, ...]: |
| leaves = [] |
| for addr, node in self._nodes.items(): |
| if node.status in (NodeStatus.REJECTED, NodeStatus.MERGED): |
| continue |
| if not self._children.get(addr): |
| leaves.append(node) |
| return tuple(leaves) |
|
|
| def add_root( |
| self, |
| tokens: Sequence[int], |
| latent: Sequence[float] = (), |
| signature: Signature | None = None, |
| ) -> Node: |
| if self._root is not None: |
| raise RuntimeError("graph already has a root") |
| sig = signature or self._signature_for(tokens, latent) |
| addr, _ = self.address_book.resolve_or_allocate(sig) |
| node = Node( |
| address=addr, |
| parents=(), |
| tokens=tuple(tokens), |
| signature=sig, |
| latent=tuple(latent), |
| status=NodeStatus.COMMITTED, |
| depth=0, |
| ) |
| self._nodes[addr] = node |
| self._root = addr |
| return node |
|
|
| def add_child( |
| self, |
| parent: Address, |
| tokens: Sequence[int], |
| latent: Sequence[float] = (), |
| score: float = 0.0, |
| signature: Signature | None = None, |
| ) -> tuple[Node, bool]: |
| |
| |
| if parent not in self._nodes: |
| raise KeyError(f"unknown parent {parent}") |
| sig = signature or self._signature_for(tokens, latent) |
| addr, fresh = self.address_book.resolve_or_allocate(sig) |
| if not fresh and addr in self._nodes: |
| existing = self._nodes[addr] |
| if parent not in existing.parents: |
| merged_parents = existing.parents + (parent,) |
| self._nodes[addr] = Node( |
| address=existing.address, |
| parents=merged_parents, |
| tokens=existing.tokens, |
| signature=existing.signature, |
| latent=existing.latent, |
| status=existing.status, |
| score=max(existing.score, score), |
| depth=existing.depth, |
| metadata=existing.metadata, |
| ) |
| self._children[parent].add(addr) |
| return self._nodes[addr], False |
| depth = self._nodes[parent].depth + 1 |
| node = Node( |
| address=addr, |
| parents=(parent,), |
| tokens=tuple(tokens), |
| signature=sig, |
| latent=tuple(latent), |
| status=NodeStatus.DRAFT, |
| score=score, |
| depth=depth, |
| ) |
| self._nodes[addr] = node |
| self._children[parent].add(addr) |
| return node, True |
|
|
| def set_status(self, addr: Address, status: NodeStatus) -> None: |
| node = self._nodes[addr] |
| self._nodes[addr] = Node( |
| address=node.address, |
| parents=node.parents, |
| tokens=node.tokens, |
| signature=node.signature, |
| latent=node.latent, |
| status=status, |
| score=node.score, |
| depth=node.depth, |
| metadata=node.metadata, |
| ) |
|
|
| def merge_into(self, src: Address, dst: Address) -> None: |
| if src == dst: |
| return |
| for grand in self._children.pop(src, set()): |
| grand_node = self._nodes[grand] |
| new_parents = tuple(p if p != src else dst for p in grand_node.parents) |
| self._nodes[grand] = Node( |
| address=grand_node.address, |
| parents=new_parents, |
| tokens=grand_node.tokens, |
| signature=grand_node.signature, |
| latent=grand_node.latent, |
| status=grand_node.status, |
| score=grand_node.score, |
| depth=grand_node.depth, |
| metadata=grand_node.metadata, |
| ) |
| self._children[dst].add(grand) |
| for kids in self._children.values(): |
| kids.discard(src) |
| self.set_status(src, NodeStatus.MERGED) |
|
|
| def walk(self, start: Address | None = None) -> Iterator[Node]: |
| seen: set[Address] = set() |
| stack = [start if start is not None else self._root] |
| while stack: |
| cur = stack.pop() |
| if cur is None or cur in seen: |
| continue |
| seen.add(cur) |
| yield self._nodes[cur] |
| stack.extend(self._children.get(cur, ())) |
|
|
| def _signature_for(self, tokens: Sequence[int], latent: Sequence[float]) -> Signature: |
| if latent: |
| return Signature.from_latent(latent) |
| return Signature.from_tokens(tokens) |
|
|