""" 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 # By construction, your graphs contain only the trivial 1–2–4–1 cycle. 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}") # Trivial cycle information if num_cycles: lines.append(f"- **Cycles:** {num_cycles} (trivial cycle 1 → 2 → 4 → 1)") return "\n".join(lines)