| """ |
| Metrics and basic statistics for Collatz graphs. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from typing import Dict, Any, Iterable |
|
|
| import pandas as pd |
|
|
|
|
| def _to_int_nodes(nodes: Iterable[Any]) -> list[int]: |
| """ |
| Safely convert an iterable of node labels to a list of integers. |
| Non-convertible labels are skipped. |
| """ |
| int_nodes: list[int] = [] |
| for n in nodes: |
| try: |
| int_nodes.append(int(n)) |
| except Exception: |
| continue |
| return int_nodes |
|
|
|
|
| def compute_basic_graph_stats(df_edges: pd.DataFrame) -> Dict[str, Any]: |
| """ |
| Compute basic statistics for a Collatz graph represented |
| as a DataFrame of edges with columns ["Source", "Target"]. |
| |
| Returns a dictionary with: |
| - num_nodes: total number of distinct nodes |
| - num_edges: total number of edges |
| - num_odd: number of odd-valued nodes |
| - num_even: number of even-valued nodes |
| - min_node: minimum node value (if any) |
| - max_node: maximum node value (if any) |
| - num_cycles: number of cycles (here always 1: the trivial 1β2β4β1 cycle) |
| """ |
| if not {"Source", "Target"}.issubset(df_edges.columns): |
| raise ValueError("df_edges must contain 'Source' and 'Target' columns.") |
|
|
| raw_nodes = set(df_edges["Source"]).union(set(df_edges["Target"])) |
| nodes = _to_int_nodes(raw_nodes) |
|
|
| num_nodes = len(nodes) |
| num_edges = len(df_edges) |
|
|
| num_odd = sum(1 for n in nodes if n % 2 == 1) |
| num_even = sum(1 for n in nodes if n % 2 == 0) |
|
|
| min_node = min(nodes) if nodes else None |
| max_node = max(nodes) if nodes else None |
|
|
| |
| num_cycles = 1 if num_nodes > 0 else 0 |
|
|
| return { |
| "num_nodes": num_nodes, |
| "num_edges": num_edges, |
| "num_odd": num_odd, |
| "num_even": num_even, |
| "min_node": min_node, |
| "max_node": max_node, |
| "num_cycles": num_cycles, |
| } |
|
|
|
|
| def format_stats_markdown(stats: Dict[str, Any]) -> str: |
| """ |
| Format the statistics dictionary returned by compute_basic_graph_stats |
| into a human-readable Markdown string for display in the UI. |
| """ |
| if not stats: |
| return "_No statistics available._" |
|
|
| num_nodes = stats.get("num_nodes", 0) |
| num_edges = stats.get("num_edges", 0) |
| num_odd = stats.get("num_odd", 0) |
| num_even = stats.get("num_even", 0) |
| min_node = stats.get("min_node", None) |
| max_node = stats.get("max_node", None) |
| num_cycles = stats.get("num_cycles", 0) |
|
|
| lines = [ |
| "### Graph Statistics", |
| "", |
| f"- **Nodes:** {num_nodes}", |
| f"- **Edges:** {num_edges}", |
| f"- **Odd nodes:** {num_odd}", |
| f"- **Even nodes:** {num_even}", |
| ] |
|
|
| if min_node is not None and max_node is not None: |
| lines.append(f"- **Node value range:** {min_node} to {max_node}") |
|
|
| |
| if num_cycles: |
| lines.append(f"- **Cycles:** {num_cycles} (trivial cycle 1 β 2 β 4 β 1)") |
|
|
| return "\n".join(lines) |