Download graph/math_graph.py from tg1121/AI-RA: direct link, hf CLI and curl.
- Browser
- Download file 8.67 kB
-
https://huggingface.co/spaces/tg1121/AI-RA/resolve/main/graph/math_graph.py
- Command line
-
hf download hf://spaces/tg1121/AI-RA/graph/math_graph.py
-
curl -L -o math_graph.py https://huggingface.co/spaces/tg1121/AI-RA/resolve/main/graph/math_graph.py
8.67 kB
| """ | |
| Graph β core data structure. | |
| Every theorem, lemma, definition, corollary, proof, section, claim, | |
| concept, and evidence node becomes a Node. Every cross-reference becomes | |
| a directed Edge. | |
| Node importance = in-degree (how many other nodes reference this one). | |
| Proof nodes additionally carry proof_type: | |
| "direct" β construction / direct proof | |
| "induction" β proof by induction | |
| "contradiction" β proof by contradiction or counterexample | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| import json | |
| try: | |
| import networkx as nx | |
| _NX = True | |
| except ImportError: | |
| _NX = False | |
| NODE_TYPES = { | |
| # math | |
| "definition", "theorem", "lemma", "proof", "corollary", | |
| "proposition", "remark", "example", "equation", "section", | |
| # english/humanities | |
| "claim", "concept", "evidence", "critic_view", "primary_text", "external", | |
| } | |
| PROOF_TYPES = {"direct", "induction", "contradiction"} | |
| class Node: | |
| node_id: str # e.g. "thm_5", "def_lebesgue", "s2" | |
| label: str # display label e.g. "Theorem 5" | |
| node_type: str # one of NODE_TYPES | |
| section_id: str # which section this lives in | |
| raw_text: str = "" # text excerpt | |
| raw_latex: str = "" # latex if equation node | |
| position: int = 0 # document order position | |
| in_degree: int = 0 # computed after graph finalized | |
| proof_type: Optional[str] = None # only for node_type=="proof" | |
| qa: Optional[dict] = None # filled by section_qa pipeline | |
| class Edge: | |
| from_id: str | |
| to_id: str | |
| source: str # "regex" | "positional" | "llm" | |
| evidence: str = "" # text span that triggered this edge | |
| confidence: str = "certain" # "certain" | "inferred" | |
| class Graph: | |
| """ | |
| Holds all nodes and edges for one paper. | |
| Wraps networkx DiGraph when available, falls back to | |
| pure-Python lookups otherwise. | |
| """ | |
| def __init__(self): | |
| self.nodes: dict[str, Node] = {} | |
| self.edges: list[Edge] = [] | |
| self._graph = nx.DiGraph() if _NX else None | |
| self._ordered_node_ids: list[str] = [] | |
| self._label_index: dict[str, str] = {} # "theorem 5" β "thm_5" | |
| self._number_index: dict[str, str] = {} # "thm_5" β node_id | |
| # ββ building ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def add_node(self, node: Node): | |
| self.nodes[node.node_id] = node | |
| if _NX: | |
| self._graph.add_node(node.node_id, | |
| label=node.label, node_type=node.node_type, | |
| section_id=node.section_id, position=node.position, | |
| proof_type=node.proof_type) | |
| def add_edge(self, edge: Edge): | |
| if edge.from_id not in self.nodes or edge.to_id not in self.nodes: | |
| return | |
| if edge.from_id == edge.to_id: | |
| return | |
| for e in self.edges: | |
| if e.from_id == edge.from_id and e.to_id == edge.to_id: | |
| return | |
| self.edges.append(edge) | |
| if _NX: | |
| self._graph.add_edge(edge.from_id, edge.to_id, | |
| source=edge.source, evidence=edge.evidence, | |
| confidence=edge.confidence) | |
| def set_ordered_ids(self, ids: list[str]): | |
| self._ordered_node_ids = ids | |
| def finalize(self): | |
| """Compute in-degrees after all nodes and edges added.""" | |
| in_counts: dict[str, int] = {nid: 0 for nid in self.nodes} | |
| for edge in self.edges: | |
| in_counts[edge.to_id] = in_counts.get(edge.to_id, 0) + 1 | |
| for nid, node in self.nodes.items(): | |
| node.in_degree = in_counts.get(nid, 0) | |
| # ββ queries βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_ancestors(self, node_id: str, max_depth: int = 3) -> list[Node]: | |
| """Nodes this node depends on β walk edges backwards.""" | |
| if not _NX or node_id not in self._graph: | |
| return [] | |
| visited, result, frontier, depth = set(), [], [node_id], 0 | |
| while frontier and depth < max_depth: | |
| next_f = [] | |
| for nid in frontier: | |
| for pred in self._graph.predecessors(nid): | |
| if pred not in visited: | |
| visited.add(pred) | |
| result.append(self.nodes[pred]) | |
| next_f.append(pred) | |
| frontier = next_f | |
| depth += 1 | |
| return result | |
| def get_dependents(self, node_id: str) -> list[Node]: | |
| """Nodes that reference this node.""" | |
| if not _NX or node_id not in self._graph: | |
| return [] | |
| return [self.nodes[s] for s in self._graph.successors(node_id) | |
| if s in self.nodes] | |
| def topological_sections(self) -> list[str]: | |
| """Section ids in dependency order. Falls back to document order.""" | |
| if not _NX: | |
| return self._doc_order_sections() | |
| try: | |
| topo = list(nx.topological_sort(self._graph)) | |
| seen: list[str] = [] | |
| for nid in topo: | |
| node = self.nodes.get(nid) | |
| if node and node.section_id not in seen: | |
| seen.append(node.section_id) | |
| return seen | |
| except nx.NetworkXUnfeasible: | |
| return self._doc_order_sections() | |
| def _doc_order_sections(self) -> list[str]: | |
| seen: list[str] = [] | |
| for nid in self._ordered_node_ids: | |
| node = self.nodes.get(nid) | |
| if node and node.section_id not in seen: | |
| seen.append(node.section_id) | |
| return seen | |
| def nodes_by_importance(self) -> list[Node]: | |
| return sorted(self.nodes.values(), key=lambda n: n.in_degree, reverse=True) | |
| def section_nodes(self) -> list[Node]: | |
| return [self.nodes[nid] for nid in self._ordered_node_ids | |
| if nid in self.nodes and self.nodes[nid].node_type == "section"] | |
| def nodes_in_section(self, section_id: str) -> list[Node]: | |
| return [self.nodes[nid] for nid in self._ordered_node_ids | |
| if nid in self.nodes | |
| and self.nodes[nid].section_id == section_id | |
| and self.nodes[nid].node_type != "section"] | |
| def proof_nodes(self) -> list[Node]: | |
| return [n for n in self.nodes.values() if n.node_type == "proof"] | |
| # ββ serialization βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def to_dict(self) -> dict: | |
| return { | |
| "nodes": {nid: { | |
| "node_id": n.node_id, "label": n.label, | |
| "node_type": n.node_type, "section_id": n.section_id, | |
| "raw_text": n.raw_text, "raw_latex": n.raw_latex, | |
| "position": n.position, "in_degree": n.in_degree, | |
| "proof_type": n.proof_type, "qa": n.qa, | |
| } for nid, n in self.nodes.items()}, | |
| "edges": [{"from_id": e.from_id, "to_id": e.to_id, | |
| "source": e.source, "evidence": e.evidence, | |
| "confidence": e.confidence} | |
| for e in self.edges], | |
| "ordered_node_ids": self._ordered_node_ids, | |
| } | |
| def from_dict(cls, data: dict) -> "Graph": | |
| g = cls() | |
| for nd in data.get("nodes", {}).values(): | |
| g.add_node(Node( | |
| node_id=nd["node_id"], label=nd["label"], | |
| node_type=nd["node_type"], section_id=nd["section_id"], | |
| raw_text=nd.get("raw_text", ""), raw_latex=nd.get("raw_latex", ""), | |
| position=nd.get("position", 0), in_degree=nd.get("in_degree", 0), | |
| proof_type=nd.get("proof_type"), qa=nd.get("qa"), | |
| )) | |
| for ed in data.get("edges", []): | |
| g.add_edge(Edge( | |
| from_id=ed["from_id"], to_id=ed["to_id"], | |
| source=ed["source"], evidence=ed.get("evidence", ""), | |
| confidence=ed.get("confidence", "certain"), | |
| )) | |
| g.set_ordered_ids(data.get("ordered_node_ids", [])) | |
| g.finalize() | |
| return g | |
| def to_json(self) -> str: | |
| return json.dumps(self.to_dict(), ensure_ascii=False, indent=2) | |
| def from_json(cls, s: str) -> "Graph": | |
| return cls.from_dict(json.loads(s)) | |
| # backward-compat aliases β remove once all callers updated | |
| MathGraph = Graph | |
| MathNode = Node | |
| MathEdge = Edge | |