diff --git a/netkit.go b/netkit.go index 16e9f38..61a0bdd 100644 --- a/netkit.go +++ b/netkit.go @@ -1,6 +1,2 @@ -// Package netkit provides common types and utilities used across this -// repository. +// Package netkit provides common types and utilities used across this repository. package netkit - -// Any is an alias for interface{} for convenience. -type Any = interface{} diff --git a/network-graph/algorithm/algorithm.go b/network-graph/algorithm/algorithm.go index 443147c..2afcff8 100644 --- a/network-graph/algorithm/algorithm.go +++ b/network-graph/algorithm/algorithm.go @@ -1,55 +1,2 @@ // Package algorithm provides graph algorithms for network analysis. package algorithm - -import ( - "github.com/elecbug/netkit/network-graph/algorithm/config" - "github.com/elecbug/netkit/network-graph/graph" -) - -// MetricType represents the type of metric to be calculated. -type MetricType int - -// Metric types for graph analysis. -const ( - BETWEENNESS_CENTRALITY MetricType = iota - CLOSENESS_CENTRALITY - CLUSTERING_COEFFICIENT - DEGREE_ASSORTATIVITY_COEFFICIENT - DEGREE_CENTRALITY - DIAMETER - EDGE_BETWEENNESS_CENTRALITY - EIGENVECTOR_CENTRALITY - MODULARITY - PAGE_RANK - SHORTEST_PATHS -) - -// Metric calculates the specified metric for the given graph. -func Metric(g *graph.Graph, cfg *config.Config, metricType MetricType) any { - switch metricType { - case BETWEENNESS_CENTRALITY: - return BetweennessCentrality(g, cfg) - case CLOSENESS_CENTRALITY: - return ClosenessCentrality(g, cfg) - case CLUSTERING_COEFFICIENT: - return ClusteringCoefficient(g, cfg) - case DEGREE_ASSORTATIVITY_COEFFICIENT: - return DegreeAssortativityCoefficient(g, cfg) - case DEGREE_CENTRALITY: - return DegreeCentrality(g, cfg) - case DIAMETER: - return Diameter(g, cfg) - case EDGE_BETWEENNESS_CENTRALITY: - return EdgeBetweennessCentrality(g, cfg) - case EIGENVECTOR_CENTRALITY: - return EigenvectorCentrality(g, cfg) - case MODULARITY: - return Modularity(g, cfg) - case PAGE_RANK: - return PageRank(g, cfg) - case SHORTEST_PATHS: - return AllShortestPaths(g, cfg) - default: - return nil - } -} diff --git a/network-graph/graph/edge.go b/network-graph/graph/edge.go index 9bd95c0..a05547a 100644 --- a/network-graph/graph/edge.go +++ b/network-graph/graph/edge.go @@ -25,7 +25,7 @@ func (g *Graph) AddEdge(from, to node.ID) error { g.edges[from][to] = true - if g.bidirectional { + if g.isUndirected { if _, ok := g.edges[to]; !ok { g.edges[to] = make(map[node.ID]bool) } @@ -52,7 +52,7 @@ func (g *Graph) RemoveEdge(from, to node.ID) error { delete(g.edges[from], to) - if g.bidirectional { + if g.isUndirected { if _, ok := g.edges[to]; !ok { g.edges[to] = make(map[node.ID]bool) } diff --git a/network-graph/graph/graph.go b/network-graph/graph/graph.go index e71edbf..19d3df5 100644 --- a/network-graph/graph/graph.go +++ b/network-graph/graph/graph.go @@ -12,17 +12,17 @@ import ( // Graph maintains nodes and adjacency edges. type Graph struct { - nodes map[node.ID]bool - edges map[node.ID]map[node.ID]bool - bidirectional bool + nodes map[node.ID]bool + edges map[node.ID]map[node.ID]bool + isUndirected bool } // New creates and returns an empty Graph. -func New(bidirectional bool) *Graph { +func New(isUndirected bool) *Graph { return &Graph{ - nodes: make(map[node.ID]bool), - edges: make(map[node.ID]map[node.ID]bool), - bidirectional: bidirectional, + nodes: make(map[node.ID]bool), + edges: make(map[node.ID]map[node.ID]bool), + isUndirected: isUndirected, } } @@ -59,7 +59,7 @@ func Save(g *Graph) (string, error) { return "", fmt.Errorf("failed to marshal edges: %v", err) } - bidirectional, err := json.Marshal(g.bidirectional) + bidirectional, err := json.Marshal(g.isUndirected) if err != nil { return "", fmt.Errorf("failed to marshal bidirectional: %v", err) @@ -92,15 +92,21 @@ func Load(data string) (*Graph, error) { } return &Graph{ - nodes: nodes, - edges: edges, - bidirectional: bidirectional, + nodes: nodes, + edges: edges, + isUndirected: bidirectional, }, nil } +// [deprecated] This function is deprecated. Use graph.IsUndirected() instead. // IsBidirectional returns true if the graph is bidirectional. func (g *Graph) IsBidirectional() bool { - return g.bidirectional + return g.isUndirected +} + +// IsUndirected returns true if the graph is undirected. +func (g *Graph) IsUndirected() bool { + return g.isUndirected } // Hash returns the SHA-256 hash of the graph. diff --git a/network-graph/graph/standard_graph/barabasi_albert.go b/network-graph/graph/standard_graph/barabasi_albert.go new file mode 100644 index 0000000..fa31330 --- /dev/null +++ b/network-graph/graph/standard_graph/barabasi_albert.go @@ -0,0 +1,63 @@ +package standard_graph + +import ( + "github.com/elecbug/netkit/network-graph/graph" + "github.com/elecbug/netkit/network-graph/node" +) + +// BarabasiAlbertGraph generates a graph based on the Barabási–Albert preferential attachment model. +func BarabasiAlbertGraph(n int, m int, isUndirected bool) *graph.Graph { + if m < 1 || n <= m { + return nil + } + + ra := genRand() + g := graph.New(isUndirected) + + // --- 1. initialize --- + for i := 0; i < m; i++ { + g.AddNode(node.ID(toString(i))) + } + for i := 0; i < m; i++ { + for j := i + 1; j < m; j++ { + g.AddEdge(node.ID(toString(i)), node.ID(toString(j))) + } + } + + // --- 2. preferential attachment --- + for i := m; i < n; i++ { + newNode := node.ID(toString(i)) + g.AddNode(newNode) + + // calculate current node degrees + degrees := make(map[node.ID]int) + totalDegree := 0 + for _, id := range g.Nodes() { + d := len(g.Neighbors(id)) // each node degree + degrees[id] = d + totalDegree += d + } + + // degree based sampling + chosen := make(map[node.ID]bool) + for len(chosen) < m { + r := ra.Intn(totalDegree) + accum := 0 + var target node.ID + for id, d := range degrees { + accum += d + if r < accum { + target = id + break + } + } + // self-loop and duplicate edges are not allowed + if target != newNode && !chosen[target] { + g.AddEdge(newNode, target) + chosen[target] = true + } + } + } + + return g +} diff --git a/network-graph/graph/standard_graph/erdos_reyni.go b/network-graph/graph/standard_graph/erdos_reyni.go new file mode 100644 index 0000000..6102046 --- /dev/null +++ b/network-graph/graph/standard_graph/erdos_reyni.go @@ -0,0 +1,33 @@ +package standard_graph + +import ( + "github.com/elecbug/netkit/network-graph/graph" + "github.com/elecbug/netkit/network-graph/node" +) + +// ErdosRenyiGraph generates a random graph based on the Erdős-Rényi model. +func ErdosRenyiGraph(n int, p float64, isUndirected bool) *graph.Graph { + ra := genRand() + + g := graph.New(isUndirected) + + if isUndirected { + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + if ra.Float64() < p { + g.AddEdge(node.ID(toString(i)), node.ID(toString(j))) + } + } + } + } else { + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + if i != j && ra.Float64() < p { + g.AddEdge(node.ID(toString(i)), node.ID(toString(j))) + } + } + } + } + + return g +} diff --git a/network-graph/graph/standard_graph/random_geometric.go b/network-graph/graph/standard_graph/random_geometric.go new file mode 100644 index 0000000..b1f08fe --- /dev/null +++ b/network-graph/graph/standard_graph/random_geometric.go @@ -0,0 +1,53 @@ +package standard_graph + +import ( + "math" + + "github.com/elecbug/netkit/network-graph/graph" + "github.com/elecbug/netkit/network-graph/node" +) + +// RandomGeometricGraph generates a random geometric graph (RGG). +// n = number of nodes +// r = connection radius (0~1) +// isUndirected = undirected or directed graph +func RandomGeometricGraph(n int, r float64, isUndirected bool) *graph.Graph { + if n < 1 || r <= 0 { + return nil + } + + ra := genRand() + g := graph.New(isUndirected) + + // --- 1. Generate Nodes --- + type point struct{ x, y float64 } + positions := make(map[node.ID]point) + + for i := 0; i < n; i++ { + id := node.ID(toString(i)) + g.AddNode(id) + positions[id] = point{ + x: ra.Float64(), + y: ra.Float64(), + } + } + + // --- 2. Generate Edges --- + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + id1 := node.ID(toString(i)) + id2 := node.ID(toString(j)) + p1, p2 := positions[id1], positions[id2] + + dx := p1.x - p2.x + dy := p1.y - p2.y + dist := math.Sqrt(dx*dx + dy*dy) + + if dist <= r { + g.AddEdge(id1, id2) + } + } + } + + return g +} diff --git a/network-graph/graph/standard_graph/random_regular.go b/network-graph/graph/standard_graph/random_regular.go new file mode 100644 index 0000000..d6651a4 --- /dev/null +++ b/network-graph/graph/standard_graph/random_regular.go @@ -0,0 +1,82 @@ +package standard_graph + +import ( + "github.com/elecbug/netkit/network-graph/graph" + "github.com/elecbug/netkit/network-graph/node" +) + +// RandomRegularGraph generates a random k-regular graph with n nodes. +// Each node has exactly degree k. Returns nil if impossible. +func RandomRegularGraph(n, k int, isUndirected bool) *graph.Graph { + if k < 0 || n < 1 || k >= n { + return nil + } + if (n*k)%2 != 0 { + // if undirected, n*k must be even + return nil + } + + ra := genRand() + g := graph.New(isUndirected) + + // add nodes + for i := 0; i < n; i++ { + g.AddNode(node.ID(toString(i))) + } + + // duplicate each node k times as stubs + stubs := make([]node.ID, 0, n*k) + for i := 0; i < n; i++ { + for j := 0; j < k; j++ { + stubs = append(stubs, node.ID(toString(i))) + } + } + + // shuffle + ra.Shuffle(len(stubs), func(i, j int) { stubs[i], stubs[j] = stubs[j], stubs[i] }) + + // attempt to create edges (self-loop / duplicate prevention) + maxTries := n * k * 10 + edges := make(map[[2]string]bool) + try := 0 + + for len(stubs) > 1 && try < maxTries { + a := stubs[len(stubs)-1] + b := stubs[len(stubs)-2] + stubs = stubs[:len(stubs)-2] + + // self-loop is not allowed + if a == b { + // put them back and shuffle + stubs = append(stubs, a, b) + ra.Shuffle(len(stubs), func(i, j int) { stubs[i], stubs[j] = stubs[j], stubs[i] }) + try++ + continue + } + + // if undirected, edge key must be sorted + key := [2]string{string(a), string(b)} + if isUndirected && key[0] > key[1] { + key[0], key[1] = key[1], key[0] + } + + if edges[key] { + // edge already exists, put them back and shuffle + stubs = append(stubs, a, b) + ra.Shuffle(len(stubs), func(i, j int) { stubs[i], stubs[j] = stubs[j], stubs[i] }) + try++ + continue + } + + // add edge + g.AddEdge(a, b) + edges[key] = true + } + + if len(stubs) > 0 { + // if impossible to satisfy conditions + return nil + } + + return g +} diff --git a/network-graph/graph/standard_graph/seed.go b/network-graph/graph/standard_graph/seed.go new file mode 100644 index 0000000..5af109b --- /dev/null +++ b/network-graph/graph/standard_graph/seed.go @@ -0,0 +1,34 @@ +package standard_graph + +import ( + "fmt" + "math/rand" +) + +const randCode = 42 + +var seed = int64(randCode) + +// SetSeed sets the seed for random operations in the graph. +func SetSeed(value int64) { + seed = value +} + +// SetSeedRandom sets the seed to a random value for non-deterministic behavior. +func SetSeedRandom() { + seed = randCode +} + +func genRand() *rand.Rand { + localSeed := seed + + if seed == randCode { + localSeed = rand.Int63() + } + + return rand.New(rand.NewSource(localSeed)) +} + +func toString(id int) string { + return fmt.Sprintf("%d", id) +} diff --git a/network-graph/graph/standard_graph/standard_graph.go b/network-graph/graph/standard_graph/standard_graph.go new file mode 100644 index 0000000..ae2bc6f --- /dev/null +++ b/network-graph/graph/standard_graph/standard_graph.go @@ -0,0 +1,42 @@ +// Package standard_graph provides a standard implementation of a graph data structure. +package standard_graph + +// STANDARD_GRAPH_TYPE defines types of standard graphs. +type STANDARD_GRAPH_TYPE int + +const ( + ERDOS_RENYI STANDARD_GRAPH_TYPE = iota + RANDOM_REGULAR + BARABASI_ALBERT + WATTS_STROGATZ + RANDOM_GEOMETRIC + WAXMAN +) + +// String returns the string representation of the STANDARD_GRAPH_TYPE. +func (s STANDARD_GRAPH_TYPE) String(onlyAlphabet bool) string { + switch s { + case ERDOS_RENYI: + if onlyAlphabet { + return "Erdos-Renyi" + } else { + return "Erdős-Rényi" + } + case RANDOM_REGULAR: + return "Random-Regular" + case BARABASI_ALBERT: + if onlyAlphabet { + return "Barabasi-Albert" + } else { + return "Barabási-Albert" + } + case WATTS_STROGATZ: + return "Watts-Strogatz" + case RANDOM_GEOMETRIC: + return "Random-Geometric" + case WAXMAN: + return "Waxman" + default: + return "Unknown" + } +} diff --git a/network-graph/graph/standard_graph/watts_strogatz.go b/network-graph/graph/standard_graph/watts_strogatz.go new file mode 100644 index 0000000..cacbcac --- /dev/null +++ b/network-graph/graph/standard_graph/watts_strogatz.go @@ -0,0 +1,54 @@ +package standard_graph + +import ( + "github.com/elecbug/netkit/network-graph/graph" + "github.com/elecbug/netkit/network-graph/node" +) + +// WattsStrogatzGraph generates a Watts–Strogatz small-world graph. +// n = number of nodes +// k = each node is connected to k nearest neighbors in ring (must be even) +// beta = rewiring probability (0 = regular lattice, 1 = random graph) +func WattsStrogatzGraph(n, k int, beta float64, isUndirected bool) *graph.Graph { + if n < 1 || k < 2 || k >= n || k%2 != 0 { + return nil + } + + ra := genRand() + g := graph.New(isUndirected) + + // --- 1. Generate Nodes --- + for i := 0; i < n; i++ { + g.AddNode(node.ID(toString(i))) + } + + // --- 2. Generate Regular Ring Lattice --- + for i := 0; i < n; i++ { + for j := 1; j <= k/2; j++ { + neighbor := (i + j) % n + g.AddEdge(node.ID(toString(i)), node.ID(toString(neighbor))) + } + } + + // --- 3. Rewiring Phase --- + for i := 0; i < n; i++ { + for j := 1; j <= k/2; j++ { + neighbor := (i + j) % n + if ra.Float64() < beta { + // Remove existing edge + g.RemoveEdge(node.ID(toString(i)), node.ID(toString(neighbor))) + + // Select a random other node (self-loop, duplicate prevention) + for { + newNeighbor := node.ID(toString(ra.Intn(n))) + if newNeighbor != node.ID(toString(i)) && !g.HasEdge(node.ID(toString(i)), newNeighbor) { + g.AddEdge(node.ID(toString(i)), newNeighbor) + break + } + } + } + } + } + + return g +} diff --git a/network-graph/graph/standard_graph/waxman.go b/network-graph/graph/standard_graph/waxman.go new file mode 100644 index 0000000..2558024 --- /dev/null +++ b/network-graph/graph/standard_graph/waxman.go @@ -0,0 +1,59 @@ +package standard_graph + +import ( + "math" + + "github.com/elecbug/netkit/network-graph/graph" + "github.com/elecbug/netkit/network-graph/node" +) + +// WaxmanGraph generates a Waxman random graph. +// n = number of nodes +// alpha, beta = Waxman parameters (0 1 || beta <= 0 || beta > 1 { + return nil + } + + ra := genRand() + g := graph.New(isUndirected) + + // --- 1. Generate Node Positions --- + type point struct{ x, y float64 } + positions := make(map[node.ID]point) + + for i := 0; i < n; i++ { + id := node.ID(toString(i)) + g.AddNode(id) + positions[id] = point{ + x: ra.Float64(), + y: ra.Float64(), + } + } + + // Maximum distance (diagonal) + L := math.Sqrt(2.0) + + // --- 2. Generate Edges Based on Distance --- + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + id1 := node.ID(toString(i)) + id2 := node.ID(toString(j)) + p1, p2 := positions[id1], positions[id2] + + dx := p1.x - p2.x + dy := p1.y - p2.y + dist := math.Sqrt(dx*dx + dy*dy) + + // Waxman probability + prob := alpha * math.Exp(-dist/(beta*L)) + + if ra.Float64() < prob { + g.AddEdge(id1, id2) + } + } + } + + return g +} diff --git a/compare_metrics.py b/script/compare_metrics.py similarity index 83% rename from compare_metrics.py rename to script/compare_metrics.py index 18903fc..80eb59e 100644 --- a/compare_metrics.py +++ b/script/compare_metrics.py @@ -8,18 +8,46 @@ import networkx as nx +MAP_TYPE = 0 +EDGE_BETWEENNESS_CENTRALITY_TYPE = 1 +SHORTEST_PATH_TYPE = 2 +SINGLE_VALUE_TYPE = 3 -def _try_parse_3lines(content: str): - lines = [ln.strip() for ln in content.splitlines() if ln.strip()] - if len(lines) < 3: - raise ValueError("3-line format not detected") - nodes_map = json.loads(lines[0]) - adj_map = json.loads(lines[1]) - is_bidirectional = json.loads(lines[2]) - return nodes_map, adj_map, bool(is_bidirectional) +NUMERIC_NODE_METRICS = { + ("betweenness_centrality", MAP_TYPE), + ("closeness_centrality", MAP_TYPE), + ("clustering_coefficient", MAP_TYPE), + ("degree_assortativity_coefficient", SINGLE_VALUE_TYPE), + ("degree_centrality", MAP_TYPE), + ("edge_betweenness_centrality", EDGE_BETWEENNESS_CENTRALITY_TYPE), + ("eigenvector_centrality", MAP_TYPE), + ("modularity", SINGLE_VALUE_TYPE), + ("page_rank", MAP_TYPE), + ("shortest_paths", SHORTEST_PATH_TYPE) +} + + +def compute_metrics(G: nx.Graph, is_undirected: bool) -> Dict[str, Any]: + metrics: Dict[str, Any] = {} + + # NOTE: keep key names aligned with your Go outputs for easy comparison + metrics["betweenness_centrality"] = nx.betweenness_centrality(G) + metrics["closeness_centrality"] = nx.closeness_centrality(G) # wf_improved=True semantics in recent NX + metrics["clustering_coefficient"] = nx.clustering(G) # for DiGraph, NX uses underlying undirected + metrics["degree_assortativity_coefficient"] = nx.degree_assortativity_coefficient(G) + metrics["degree_centrality"] = nx.degree_centrality(G) + metrics["edge_betweenness_centrality"] = nx.edge_betweenness_centrality(G) + metrics["eigenvector_centrality"] = nx.eigenvector_centrality(G) + metrics["modularity"] = nx.algorithms.community.modularity(G, nx.algorithms.community.greedy_modularity_communities(G)) + metrics["page_rank"] = nx.pagerank(G, weight=None) # unweighted + metrics["shortest_paths"] = dict(nx.all_pairs_shortest_path_length(G)) + + # If you also want degree_centrality comparison, uncomment: + + return metrics -def _try_parse_flex(content: str): +def try_parse_flex(content: str): """ Accept flexible formats: - [nodes_map, adj_map, true] @@ -41,14 +69,24 @@ def _try_parse_flex(content: str): raise ValueError("Flexible JSON format not detected") +def try_parse_3lines(content: str): + lines = [ln.strip() for ln in content.splitlines() if ln.strip()] + if len(lines) < 3: + raise ValueError("3-line format not detected") + nodes_map = json.loads(lines[0]) + adj_map = json.loads(lines[1]) + is_bidirectional = json.loads(lines[2]) + return nodes_map, adj_map, bool(is_bidirectional) + + def load_graph_file(path: str): content = Path(path).read_text(encoding="utf-8") try: - return _try_parse_flex(content) + return try_parse_flex(content) except Exception: pass try: - return _try_parse_3lines(content) + return try_parse_3lines(content) except Exception: pass raise ValueError( @@ -83,26 +121,6 @@ def to_id(x): return G -def compute_metrics(G: nx.Graph, is_bidirectional: bool) -> Dict[str, Any]: - metrics: Dict[str, Any] = {} - - # NOTE: keep key names aligned with your Go outputs for easy comparison - metrics["betweenness_centrality"] = nx.betweenness_centrality(G) - metrics["closeness_centrality"] = nx.closeness_centrality(G) # wf_improved=True semantics in recent NX - metrics["clustering_coefficient"] = nx.clustering(G) # for DiGraph, NX uses underlying undirected - metrics["degree_assortativity_coefficient"] = nx.degree_assortativity_coefficient(G) - metrics["degree_centrality"] = nx.degree_centrality(G) - metrics["edge_betweenness_centrality"] = nx.edge_betweenness_centrality(G) - metrics["eigenvector_centrality"] = nx.eigenvector_centrality(G) - metrics["modularity"] = nx.algorithms.community.modularity(G, nx.algorithms.community.greedy_modularity_communities(G)) - metrics["page_rank"] = nx.pagerank(G, weight=None) # unweighted - metrics["shortest_paths"] = dict(nx.all_pairs_shortest_path_length(G)) - - # If you also want degree_centrality comparison, uncomment: - - return metrics - - def to_jsonable(d): if isinstance(d, dict): return {str(k): to_jsonable(v) for k, v in d.items()} @@ -111,22 +129,7 @@ def to_jsonable(d): return d -# -------- comparison helpers -------- - -NUMERIC_NODE_METRICS = { - "betweenness_centrality", - "closeness_centrality", - "clustering_coefficient", - "degree_assortativity_coefficient", - "degree_centrality", - "edge_betweenness_centrality", - "eigenvector_centrality", - "modularity", - "page_rank", - "shortest_paths" -} - -def _load_metrics_obj(path: str) -> Dict[str, Any]: +def load_metrics_obj(path: str) -> Dict[str, Any]: """ Accepts either: - {"metrics": {...}} (preferred) @@ -142,29 +145,30 @@ def _load_metrics_obj(path: str) -> Dict[str, Any]: raise ValueError(f"Unsupported metrics JSON structure in: {path}") -def _safe_rel_err(diff: float, ref: float, eps: float = 1e-15) -> float: +def safe_rel_err(diff: float, ref: float, eps: float = 1e-15) -> float: denom = abs(ref) if denom < eps: denom = eps return abs(diff) / denom -def compare_metric_maps(name: str, ref: Dict[str, float], cmp_: Dict[str, float], include_per_node: bool) -> Dict[str, Any]: - if name == "edge_betweenness_centrality": +def compare_metric_maps(name: str, _type: int, ref: Dict[str, float], cmp_: Dict[str, float], include_per_node: bool) -> Dict[str, Any]: + if _type == EDGE_BETWEENNESS_CENTRALITY_TYPE: ref_s = {str(k): float(v) for k, v in ref.items()} cmp_s = {f"({str(k1)}, {str(k2)})": float(v) for k1, v1 in cmp_.items() for k2, v in v1.items()} - elif name == "shortest_paths": + elif _type == SHORTEST_PATH_TYPE: ref_s = {f"({str(k1)}, {str(k2)})": float(v) for k1, v1 in ref.items() for k2, v in v1.items()} cmp_s = {f"({str(k1)}, {str(k2)})": float(v) for k1, v1 in cmp_.items() for k2, v in v1.items()} - elif name == "degree_assortativity_coefficient" or name == "modularity": + elif _type == SINGLE_VALUE_TYPE: # single float value ref_s = {"value": float(ref) if isinstance(ref, (int, float)) else 0.0} cmp_s = {"value": float(cmp_) if isinstance(cmp_, (int, float)) else 0.0} - else: + elif _type == MAP_TYPE: # align keys as strings ref_s = {str(k): float(v) for k, v in ref.items()} cmp_s = {str(k): float(v) for k, v in cmp_.items()} - + else: + raise ValueError(f"Unknown metric type {_type} for {name}") common = sorted(set(ref_s.keys()) & set(cmp_s.keys()), key=lambda x: (len(x), x)) miss_in_cmp = sorted(set(ref_s.keys()) - set(cmp_s.keys())) @@ -190,7 +194,7 @@ def compare_metric_maps(name: str, ref: Dict[str, float], cmp_: Dict[str, float] max_abs_err = ad max_abs_err_node = k mae += ad - mape += _safe_rel_err(diff, r) + mape += safe_rel_err(diff, r) if include_per_node: per_node[k] = {"ref": r, "cmp": c, "abs_error": ad, "signed_error": diff} @@ -236,9 +240,9 @@ def compare_shortest_path_length(ref: Dict[str, Dict[str, int]], cmp_: Dict[str, def compare_metrics(ref_metrics: Dict[str, Any], cmp_metrics: Dict[str, Any], include_per_node: bool) -> Dict[str, Any]: report: Dict[str, Any] = {"metrics_compared": []} - for name in sorted(NUMERIC_NODE_METRICS): + for name, _type in sorted(NUMERIC_NODE_METRICS): if name in ref_metrics and name in cmp_metrics: - report[name] = compare_metric_maps(name, ref_metrics[name], cmp_metrics[name], include_per_node) + report[name] = compare_metric_maps(name, _type, ref_metrics[name], cmp_metrics[name], include_per_node) report["metrics_compared"].append(name) # else: silently skip if either missing @@ -254,12 +258,12 @@ def main(): ap.add_argument("--per-node", action="store_true", help="Include per-node errors in the comparison report") args = ap.parse_args() - nodes_map, adj_map, is_bidirectional = load_graph_file(args.input) - G = build_nx_graph(nodes_map, adj_map, is_bidirectional) + nodes_map, adj_map, is_undirected = load_graph_file(args.input) + G = build_nx_graph(nodes_map, adj_map, is_undirected) - computed = compute_metrics(G, is_bidirectional) + computed = compute_metrics(G, is_undirected) out = { - "is_bidirectional": bool(is_bidirectional), + "is_bidirectional": bool(is_undirected), "n_nodes": G.number_of_nodes(), "n_edges": G.number_of_edges(), "metrics": to_jsonable(computed), @@ -276,7 +280,7 @@ def main(): # Optional: comparison if args.compare: try: - cmp_metrics_raw = _load_metrics_obj(args.compare) + cmp_metrics_raw = load_metrics_obj(args.compare) except Exception as e: raise SystemExit(f"[compare] Failed to load comparison metrics: {e}") diff --git a/test.sh b/test.sh index a359563..3d1fb7b 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,8 @@ go test -v ./network-graph/ -python3 compare_metrics.py -i ./network-graph/directional.graph.log -c ./network-graph/directional.metrics.log -r ./directional.report.log -o ./directional.out.log -python3 compare_metrics.py -i ./network-graph/bidirectional.graph.log -c ./network-graph/bidirectional.metrics.log -r ./bidirectional.report.log -o ./bidirectional.out.log \ No newline at end of file + +cd script + +python3 compare_metrics.py -i ../network-graph/directional.graph.log -c ../network-graph/directional.metrics.log -r ./directional.report.log -o ./directional.out.log +python3 compare_metrics.py -i ../network-graph/bidirectional.graph.log -c ../network-graph/bidirectional.metrics.log -r ./bidirectional.report.log -o ./bidirectional.out.log + +cd .. \ No newline at end of file