| import json |
| import sys |
| import argparse |
| import re |
| from pathlib import Path |
| from typing import List, Dict, Tuple, cast |
| from difflib import SequenceMatcher |
| import numpy as np |
| from f1_nodes_functions import * |
|
|
|
|
| try: |
| from rapidfuzz import fuzz |
| except ImportError: |
| class fuzz: |
| @staticmethod |
| def ratio(a, b): |
| return SequenceMatcher(None, a, b).ratio() * 100 |
|
|
| try: |
| from scipy.optimize import linear_sum_assignment |
| HAS_SCIPY = True |
| except ImportError: |
| HAS_SCIPY = False |
|
|
| |
| class C: |
| HDR = "\033[95m" |
| OKB = "\033[94m" |
| OKG = "\033[92m" |
| OKGREEN = "\033[92m" |
| W = "\033[93m" |
| R = "\033[91m" |
| B = "\033[1m" |
| DIM = "\033[2m" |
| END = "\033[0m" |
| HEADER = "\033[95m" |
| OKCYAN = "\033[96m" |
| FAIL = "\033[91m" |
| WARNING = "\033[93m" |
| ENDC = "\033[0m" |
|
|
|
|
|
|
| LOG_FILE = None |
|
|
| def custom_print(message: str = "", end: str = "\n"): |
| """ |
| Write the message to the global log file and to the console. |
| """ |
| global LOG_FILE |
| full_message = message + end |
| |
| |
| if LOG_FILE: |
| LOG_FILE.write(full_message) |
| LOG_FILE.flush() |
| |
| |
| sys.stdout.write(full_message) |
| sys.stdout.flush() |
|
|
| |
|
|
| def _normalize_name_nodes(name: str) -> str: |
| """Standardize node names for comparison (based on gen_F1_nodes.py).""" |
| return str(name or "").replace('-', '').replace('_', '').upper() |
|
|
| def rf_score(a: str, b: str) -> float: |
| """Calculate the similarity score between two node names.""" |
| a_norm = _normalize_name_nodes(a) |
| b_norm = _normalize_name_nodes(b) |
| if hasattr(fuzz, "token_set_ratio"): |
| return float(fuzz.token_set_ratio(a_norm, b_norm)) |
| else: |
| return 100.0 * SequenceMatcher(None, a_norm, b_norm).ratio() |
|
|
|
|
| def load_nodes(data: Dict) -> List[Dict]: |
| nodes = [] |
| |
| devices = data["result"]["network_topology"].get("devices", []) |
| for device in devices: |
|
|
| nodes.append({ |
| "device_name": device.get("device_name"), |
| "network_layer": str(device.get("network_layer", "access")).lower(), |
| "device_type": device.get("device_type", "router"), |
| "location_site": str(device.get("location_site")).lower(), |
| }) |
| |
| return nodes |
|
|
|
|
| def hungarian_match_nodes(sim: np.ndarray): |
| """Apply the Hungarian algorithm (based on gen_F1_nodes.py logic).""" |
| if not HAS_SCIPY: |
| return [] |
| |
| m, n = sim.shape |
| cost = 100.0 - sim |
| row_ind, col_ind = linear_sum_assignment(cost) |
| pairs = [(r, c, float(sim[r, c])) for r, c in zip(row_ind, col_ind) if r < m and c < n] |
| return pairs |
|
|
|
|
|
|
| def normalize_gen_to_ref_topology(ref_nodes, gen_nodes, min_sim: float = 9.0): |
| """Normalize GEN sites and types to match REF using the Hungarian algorithm.""" |
| if not gen_nodes or not ref_nodes: |
| return gen_nodes |
| |
| |
| ref_sites = list(set(n.get('location_site') for n in ref_nodes)) |
| ref_types = list(set(n.get('device_type') for n in ref_nodes)) |
| |
| |
| gen_sites = list(set(n.get('location_site') for n in gen_nodes)) |
| gen_types = list(set(n.get('device_type') for n in gen_nodes)) |
| |
| site_mapping = {} |
| type_mapping = {} |
| |
| |
| if ref_sites and gen_sites: |
| site_sim = build_similarity_matrix_nodes(ref_sites, gen_sites) |
| |
| for j, gen_site in enumerate(gen_sites): |
| best_ref_idx = max(range(len(ref_sites)), key=lambda i: site_sim[i][j]) |
| best_score = site_sim[best_ref_idx][j] |
| |
| if best_score >= min_sim: |
| site_mapping[gen_site] = ref_sites[best_ref_idx] |
| print(f" GEN site: {gen_site} → REF site (max similarity): {ref_sites[best_ref_idx]} (score: {best_score:.2f})") |
| else: |
| print(f" Warning: GEN site '{gen_site}' has no reliable match (best score: {best_score:.2f})") |
| |
| |
| print(f" Applying site normalization:") |
| for node in gen_nodes: |
| original_site = node.get('location_site') |
| if original_site in site_mapping: |
| new_site = site_mapping[original_site] |
| node['location_site'] = new_site |
| print(f" Node {node['device_name']}: site {original_site} → {new_site}") |
| |
| |
| print(f" Pre-normalizing types containing 'server':") |
| for node in gen_nodes: |
| original_type = node.get('device_type', '') |
| if original_type and any(keyword in original_type.lower() for keyword in ['server', 'videodelivery', 'haproxy']): |
| node['device_type'] = 'Server' |
| print(f" Node {node['device_name']}: type {original_type} → Server (contains 'server')") |
| |
| |
| if ref_types and gen_types: |
| print(f" Normalizing types by layer:") |
| |
| |
| ref_nodes_by_layer = {} |
| gen_nodes_by_layer = {} |
| |
| for node in ref_nodes: |
| layer = node.get('network_layer') |
| if layer not in ref_nodes_by_layer: |
| ref_nodes_by_layer[layer] = [] |
| ref_nodes_by_layer[layer].append(node) |
| |
| for node in gen_nodes: |
| layer = node.get('network_layer') |
| if layer not in gen_nodes_by_layer: |
| gen_nodes_by_layer[layer] = [] |
| gen_nodes_by_layer[layer].append(node) |
| |
| all_layers = set(ref_nodes_by_layer.keys()) | set(gen_nodes_by_layer.keys()) |
| |
| for layer in sorted(all_layers): |
| ref_layer_nodes = ref_nodes_by_layer.get(layer, []) |
| gen_layer_nodes = gen_nodes_by_layer.get(layer, []) |
| |
| if not ref_layer_nodes or not gen_layer_nodes: |
| continue |
| |
| ref_layer_types = list(set(n.get('device_type') for n in ref_layer_nodes)) |
| gen_layer_types = list(set(n.get('device_type') for n in gen_layer_nodes)) |
| |
| print(f" Layer {layer}: {len(ref_layer_types)} REF types vs {len(gen_layer_types)} GEN types") |
| |
| type_sim = build_similarity_matrix_nodes(ref_layer_types, gen_layer_types) |
| type_pairs = hungarian_match_nodes(type_sim) |
| |
| layer_type_mapping = {} |
| for i, j, s in type_pairs: |
| if s >= min_sim: |
| layer_type_mapping[gen_layer_types[j]] = ref_layer_types[i] |
| print(f" GEN type: {gen_layer_types[j]} → REF type: {ref_layer_types[i]} (score: {s:.2f})") |
| |
| type_mapping.update(layer_type_mapping) |
| |
| |
| print(f" Applying type normalization:") |
| for node in gen_nodes: |
| original_type = node.get('device_type') |
| if original_type in type_mapping: |
| new_type = type_mapping[original_type] |
| node['device_type'] = new_type |
| print(f" Node {node['device_name']}: type {original_type} → {new_type}") |
| |
| print(f"Normalization complete: {len(site_mapping)} sites and {len(type_mapping)} types normalized") |
| return gen_nodes, site_mapping, type_mapping |
|
|
|
|
|
|
| def get_mapping(ref_nodes, gen_nodes, min_sim: float = 9.0): |
| """Hierarchical mapping: layer → site → type using the Hungarian algorithm (adapted from gen_F1_nodes.py logic).""" |
| if not gen_nodes: |
| return {} |
| |
| mapping: Dict[str, str] = {} |
| |
| |
| ref_groups: Dict[str, Dict[str, List[str]]] = {} |
| gen_groups: Dict[str, Dict[str, List[str]]] = {} |
| |
| |
| for n in ref_nodes: |
| if n["device_name"] and isinstance(n["device_name"], str): |
| layer = n["network_layer"] |
| site = n.get("location_site") |
| device_type = n.get("device_type") |
| |
| if layer not in ref_groups: |
| ref_groups[layer] = {} |
| if site not in ref_groups[layer]: |
| ref_groups[layer][site] = {} |
| if device_type not in ref_groups[layer][site]: |
| ref_groups[layer][site][device_type] = [] |
| |
| ref_groups[layer][site][device_type].append(n["device_name"]) |
| |
| |
| for n in gen_nodes: |
| if n["device_name"] and isinstance(n["device_name"], str): |
| layer = n["network_layer"] |
| site = n.get("location_site") |
| device_type = n.get("device_type") |
| |
| if layer not in gen_groups: |
| gen_groups[layer] = {} |
| if site not in gen_groups[layer]: |
| gen_groups[layer][site] = {} |
| if device_type not in gen_groups[layer][site]: |
| gen_groups[layer][site][device_type] = [] |
| |
| gen_groups[layer][site][device_type].append(n["device_name"]) |
| |
| all_keys = set(ref_groups.keys()) | set(gen_groups.keys()) |
| |
| custom_print(f"\n{C.HDR}{C.B}===== Hierarchical Mapping: Layer → Site → Type (Hungarian Algorithm) ====={C.END}") |
| |
| for layer in sorted(all_keys, key=lambda x: (x != 'null', x)): |
| ref_layer_dict = ref_groups.get(layer, {}) |
| gen_layer_dict = gen_groups.get(layer, {}) |
| |
| display_layer = layer.upper() if layer != 'null' else layer |
| custom_print(f"\n{C.OKB}--- Layer: {display_layer} ---{C.END}") |
| |
| |
| all_sites = set(ref_layer_dict.keys()) | set(gen_layer_dict.keys()) |
| |
| for site in sorted(all_sites): |
| ref_site_types = ref_layer_dict.get(site, {}) |
| gen_site_types = gen_layer_dict.get(site, {}) |
| |
| if not ref_site_types and not gen_site_types: |
| continue |
| |
| custom_print(f" Site: {site}") |
| |
| |
| all_types = set(ref_site_types.keys()) | set(gen_site_types.keys()) |
| |
| for device_type in sorted(all_types): |
| ref_names = ref_site_types.get(device_type, []) |
| gen_names = gen_site_types.get(device_type, []) |
| |
| if not ref_names or not gen_names: |
| if ref_names: |
| custom_print(f" {C.R}Local FN {device_type}: {', '.join(ref_names)}{C.END}") |
| if gen_names: |
| custom_print(f" {C.R}Local FP {device_type}: {', '.join(gen_names)}{C.END}") |
| continue |
| |
| |
| sim = build_similarity_matrix_nodes(ref_names, gen_names) |
| pairs = hungarian_match_nodes(sim) |
| |
| group_matches = {} |
| matches = [] |
| for i, j, s in pairs: |
| r_name = ref_names[i] |
| g_name = gen_names[j] |
| if s >= min_sim and r_name and g_name: |
| group_matches[r_name] = g_name |
| mapping[r_name] = g_name |
| matches.append((r_name, g_name, s)) |
| |
| if matches: |
| custom_print(f" {C.OKG}TP {device_type}: {len(matches)} match(s){C.END}") |
| for r_name, g_name, score in matches: |
| custom_print(f" - REF: {r_name:<20} <--> GEN: {g_name:<20} (Score: {score:.2f})") |
| else: |
| custom_print(f" {C.W}No matches for {device_type} (threshold: {min_sim:.1f}%){C.END}") |
| |
| |
| fn_nodes = [name for name in ref_names if name not in group_matches] |
| fp_nodes = [name for name in gen_names if name not in group_matches.values()] |
| |
| if fn_nodes: |
| custom_print(f" {C.R}Local FN {device_type} ({len(fn_nodes)}): {', '.join(fn_nodes[:3])}{'...' if len(fn_nodes) > 3 else ''}{C.END}") |
| if fp_nodes: |
| custom_print(f" {C.R}Local FP {device_type} ({len(fp_nodes)}): {', '.join(fp_nodes[:3])}{'...' if len(fp_nodes) > 3 else ''}{C.END}") |
| |
| custom_print(f"\n{C.OKG}Final mapping: {len(mapping)} correspondences{C.END}") |
| return mapping |
|
|
| def rf_score_nodes(a: str, b: str) -> float: |
| """Calculate the similarity score between two node names.""" |
| a_norm = _normalize_name_nodes(a) |
| b_norm = _normalize_name_nodes(b) |
| if hasattr(fuzz, 'token_set_ratio'): |
| return float(fuzz.token_set_ratio(a_norm, b_norm)) |
| else: |
| return fuzz.ratio(a_norm, b_norm) |
|
|
| def build_similarity_matrix_nodes(ref_names: List[str], gen_names: List[str]) -> np.ndarray: |
| """Build the similarity matrix (based on gen_F1_nodes.py logic).""" |
| M = np.zeros((len(ref_names), len(gen_names)), dtype=float) |
| for i, a in enumerate(ref_names): |
| for j, b in enumerate(gen_names): |
| if a and b: |
| M[i, j] = rf_score_nodes(a, b) |
| return M |
|
|
| def hungarian_match(sim: np.ndarray): |
| """Apply the Hungarian algorithm.""" |
| if not HAS_SCIPY: |
| custom_print(f"{C.R}Error: SciPy is not installed. Required for the Hungarian algorithm.{C.END}") |
| sys.exit(1) |
|
|
| m, n = sim.shape |
| cost = 100.0 - sim |
| row_ind, col_ind = linear_sum_assignment(cost) |
| pairs = [(r, c, cast(float, sim[r, c])) for r, c in zip(row_ind, col_ind) if r < m and c < n] |
| return pairs |
|
|
| def convert_nodes_format(nodes: List[Dict]) -> List[Dict]: |
| converted = [] |
| for n in nodes: |
| |
| device_type = n.get("device_type", "router") |
| |
| converted.append({ |
| "device_name": n["name"], |
| "network_layer": n["layer"], |
| "device_type": device_type, |
| "location_site": n.get("site", "default"), |
| "connections": [] |
| }) |
| return converted |
|
|
| def mapping_by_layer(ref_nodes: List[Dict], gen_nodes: List[Dict], |
| min_sim: float, test_filename: str) -> Dict[str, str]: |
| """Create the initial REF->GEN mapping using hierarchical layer/site/type grouping.""" |
| |
| if not gen_nodes: |
| return {} |
| |
| ref_nodes_converted = convert_nodes_format(ref_nodes) |
| gen_nodes_converted = convert_nodes_format(gen_nodes) |
| |
| mapping = get_mapping(ref_nodes_converted, gen_nodes_converted, min_sim) |
| |
| custom_print(f"\n{C.OKG}➡️ Final mapping size (TP_nodes):{C.END} {len(mapping)}") |
| return mapping |
|
|
| def calculate_node_metrics(mapping: Dict[str, str], ref_nodes: List[Dict], gen_nodes: List[Dict]): |
| """ |
| Calculate TP, FP, FN, precision, recall, and F1. |
| **Exact calculation logic as in reference** + F1 penalty = 0 for invalid generated JSON. |
| """ |
| |
| ref_count = len(ref_nodes) |
| gen_count = len(gen_nodes) |
| |
| |
| if gen_count == 0 and len(mapping) == 0: |
| |
| TP_nodes = 0 |
| FP_nodes = 0 |
| FN_nodes = ref_count |
| P_nodes = 0.0 |
| R_nodes = 0.0 |
| F1_nodes = 0.0 |
| percent_mapping = 0.0 |
| else: |
| TP_nodes = len(mapping) |
| FN_nodes = ref_count - TP_nodes |
| FP_nodes = gen_count - TP_nodes |
| |
| P_nodes = TP_nodes / (TP_nodes + FP_nodes) if (TP_nodes + FP_nodes) else 0.0 |
| R_nodes = TP_nodes / (TP_nodes + FN_nodes) if (TP_nodes + FN_nodes) else 0.0 |
| F1_nodes = 2 * P_nodes * R_nodes / (P_nodes + R_nodes) if (P_nodes + R_nodes) else 0.0 |
| percent_mapping = R_nodes * 100.0 |
| |
| |
| |
| |
| custom_print(f"\n{C.HDR}{C.B}===== NODE F1-SCORE RESULTS (Final Calculation) ====={C.END}") |
| custom_print(f" • REF nodes (Total): {ref_count}") |
| custom_print(f" • GEN nodes (Total): {gen_count}") |
| custom_print("-" * 50) |
| custom_print(f" • True Positives ({C.B}TP_nodes{C.END}): {TP_nodes}") |
| custom_print(f" • False Positives ({C.B}FP_nodes{C.END}): {FP_nodes}") |
| custom_print(f" • False Negatives ({C.B}FN_nodes{C.END}): {FN_nodes}") |
| custom_print("-" * 50) |
| custom_print(f" • Precision ($P_{{nodes}}$): {P_nodes:.6f}") |
| custom_print(f" • Recall ($R_{{nodes}}$): {R_nodes:.6f}") |
| custom_print(f" • {C.B}Mapping percentage{C.END}: {percent_mapping:.2f} %") |
| custom_print(f"{C.OKG} • Node F1-score ($F1_{{nodes}}$): {F1_nodes:.6f}{C.END}") |
|
|
| return { |
| "TP_nodes": TP_nodes, "FP_nodes": FP_nodes, "FN_nodes": FN_nodes, |
| "P_nodes": P_nodes, "R_nodes": R_nodes, "F1_nodes": F1_nodes, |
| "ref_count": ref_count, "gen_count": gen_count, "percent_mapping": percent_mapping |
| } |
|
|
| |
|
|
| def main(): |
| |
| ap = argparse.ArgumentParser(description="Calculate node metrics (TP/FP/FN/P/R/F1) for topologies.") |
| ap.add_argument("--gen", type=Path, nargs="+", required=True, |
| help="One or more files/directories containing generated JSON topologies to evaluate.") |
| ap.add_argument("--ref", type=Path, |
| help="Reference file to use (optional). If omitted, the script looks for topo_ref_case_*.json automatically.") |
| ap.add_argument("--min-sim", type=float, default=9.0, |
| help="Minimum similarity threshold (%) for Hungarian matching.") |
| ap.add_argument("--output", type=Path, default=Path("analyse_metriques_nodes_detailed.txt"), |
| help="Output file for detailed results.") |
| args = ap.parse_args() |
| |
| input_base = args.gen[0] if args.gen[0].is_dir() else args.gen[0].parent |
| results_dir = input_base / "results" |
| results_dir.mkdir(parents=True, exist_ok=True) |
| if not args.output.is_absolute() and args.output.parent == Path('.'): |
| args.output = results_dir / args.output |
|
|
| global LOG_FILE |
| |
| try: |
| LOG_FILE = open(args.output, "w", encoding="utf-8") |
| custom_print(f"{C.OKGREEN}✅ Detailed output saved to: {args.output}{C.ENDC}") |
| except Exception as e: |
| custom_print(f"{C.FAIL}❌ ERROR: Unable to open output file {args.output}. Logging will only print to console. Error: {e}{C.ENDC}") |
| LOG_FILE = None |
| summary_table = [] |
| f1_scores = [] |
| current_ref_path = None |
| ref_nodes = [] |
|
|
| |
| gen_files = [] |
| for p in args.gen: |
| path = Path(p) |
| if path.is_dir(): |
| |
| for json_file in sorted(path.glob("*.json")): |
| gen_files.append(json_file) |
| elif path.is_file() and path.suffix.lower() == ".json": |
| gen_files.append(path) |
| else: |
| custom_print(f"{C.WARNING} Skipped unsupported path or format: {path}{C.ENDC}") |
|
|
| if not gen_files: |
| custom_print(f"{C.FAIL}No JSON files found in the provided paths!{C.ENDC}") |
| sys.exit(1) |
|
|
| for gen_file in gen_files: |
| gen_path = gen_file |
| |
| if args.ref: |
| ref_path = args.ref.resolve() |
| if not ref_path.exists(): |
| raise FileNotFoundError(f"Specified reference file not found: {ref_path}") |
| else: |
| match = re.search(r'case_(\d+)', gen_path.name) |
| if not match: |
| custom_print(f"No case_X found in filename {gen_path.name}") |
| continue |
| case_id = match.group(1) |
| ref_path = gen_path.parent / f"topo_ref_case_{case_id}.json" |
| if not ref_path.exists(): |
| custom_print(f"Missing reference for {gen_path.name}") |
| continue |
| |
| if ref_path != current_ref_path: |
| if args.ref: |
| custom_print(f"\n{C.HEADER}*** SPECIFIED REFERENCE: {ref_path.name} ({ref_path.parent}) ***{C.ENDC}") |
| else: |
| custom_print(f"\n{C.HEADER}*** NEW REFERENCE: {ref_path.name} ({ref_path.parent}) ***{C.ENDC}") |
| with open(ref_path) as f: ref_nodes = load_nodes(json.load(f)) |
| |
| current_ref_path = ref_path |
|
|
| custom_print(f"\n{C.HEADER}{'='*70}") |
| custom_print(f"ANALYSIS: {gen_path.name}") |
| custom_print(f"{'='*70}{C.ENDC}") |
|
|
| |
| with open(gen_path) as f: |
| gen_data = json.load(f) |
| |
| |
| gen_devices = gen_data.get("result", {}).get("network_topology", {}).get("devices", []) |
| if len(gen_devices) == 0: |
| custom_print(f"{C.WARNING}⚠️ Empty devices list detected - format has devices: []{C.ENDC}") |
| custom_print(f"{C.FAIL}Generated topology is empty, no mapping processing will be performed{C.ENDC}") |
| |
| |
| fn_final = len(ref_nodes) |
| tp_final = 0 |
| fp_final = 0 |
| |
| custom_print(f"{C.OKCYAN}Immediate results:{C.ENDC}") |
| custom_print(f" Final TP: {tp_final}") |
| custom_print(f" Final FP: {fp_final}") |
| custom_print(f" Final FN: {fn_final}") |
| |
| |
| p = tp_final / (tp_final + fp_final) if (tp_final + fp_final) > 0 else 0 |
| r = tp_final / (tp_final + fn_final) if (tp_final + fn_final) > 0 else 0 |
| f1 = 2 * p * r / (p + r) if (p + r) > 0 else 0 |
| |
| custom_print(f"{C.OKG}FINAL RESULTS (EMPTY DEVICES):{C.END}") |
| custom_print(f" Precision (P): {p:.4f}") |
| custom_print(f" Recall (R): {r:.4f}") |
| custom_print(f" F1 score: {f1:.4f}") |
| |
| f1_scores.append(f1) |
| summary_table.append({ |
| 'test': gen_path.name, |
| 'ref_path': ref_path.parent.name, |
| 'TP': tp_final, |
| 'FP': fp_final, |
| 'FN': fn_final, |
| 'precision': p, |
| 'recall': r, |
| 'F1': f1, |
| 'mapping_size': f"0/{len(ref_nodes)}", |
| }) |
| continue |
|
|
| |
| gen_nodes = load_nodes(gen_data) |
| |
| gen_nodes, site_mapping, type_mapping = normalize_gen_to_ref_topology(ref_nodes, gen_nodes, args.min_sim) |
| |
| |
| custom_print(f"{C.OKCYAN}FULL DEBUG - NODE ANALYSIS{C.ENDC}") |
| custom_print(f"Total REF nodes: {len(ref_nodes)}") |
| custom_print(f"Total GEN nodes: {len(gen_nodes)}") |
| |
| mapping = get_mapping(ref_nodes, gen_nodes, args.min_sim) |
| mapped_fp_names = set(mapping.values()) |
| mapped_fn_names = set(mapping.keys()) |
| |
| custom_print(f"Mapping found: {len(mapping)} correspondences") |
| custom_print(f"mapped_fn_names (REF): {len(mapped_fn_names)} nodes") |
| custom_print(f"mapped_fp_names (GEN): {len(mapped_fp_names)} nodes") |
| |
| |
| all_ref_names = {n['device_name'] for n in ref_nodes} |
| all_gen_names = {n['device_name'] for n in gen_nodes} |
| |
| custom_print(f"All REF names: {all_ref_names}") |
| custom_print(f"All GEN names: {all_gen_names}") |
|
|
| stats = calculate_node_metrics(mapping, ref_nodes, gen_nodes) |
| |
| summary_table.append({ |
| "test": gen_path.name, |
| "ref_path": ref_path.parent.name, |
| "TP": stats["TP_nodes"], |
| "FP": stats["FP_nodes"], |
| "FN": stats["FN_nodes"], |
| "precision": stats["P_nodes"], |
| "recall": stats["R_nodes"], |
| "F1": stats["F1_nodes"], |
| "mapping_size": f"{len(mapping)}/{max(len(ref_nodes), len(gen_nodes))}", |
| }) |
|
|
| |
| if summary_table: |
| custom_print(f"\n{C.HDR}{C.B}===== GLOBAL NODE SUMMARY TABLE ====={C.END}") |
|
|
| |
| custom_print(f"{'Test':<25} {'RefDir':<10} {'TP':<5} {'FP':<5} {'FN':<5} {'P':<6} {'R':<6} {'F1':<6} {'C(mapp)':<10}") |
| custom_print("-" * 85) |
| for s in summary_table: |
| custom_print(f"{s['test']:<25} {s['ref_path']:<10} {s['TP']:<5} {s['FP']:<5} {s['FN']:<5} " |
| f"{s['precision']:.2f} {s['recall']:.2f} {s['F1']:.2f} {s['mapping_size']}") |
|
|
| |
| f1_scores = [s['F1'] for s in summary_table] |
| if f1_scores: |
| mean_f1 = np.mean(f1_scores) |
| std_f1 = np.std(f1_scores) |
| |
| custom_print("-" * 85) |
| custom_print(f"{'MEAN':<25} {'':<10} {'':<5} {'':<5} {'':<5} {'':<6} {'':<6} {C.OKG}{mean_f1:.2f}{C.END}{'':<6} {'':<10}") |
| custom_print(f"{'STD DEV':<25} {'':<10} {'':<5} {'':<5} {'':<5} {'':<6} {'':<6} {C.OKG}{std_f1:.2f}{C.END}{'':<6} {'':<10}") |
| custom_print(f"{'NUMBER OF TESTS':<25} {'':<10} {'':<5} {'':<5} {'':<5} {'':<6} {'':<6} {len(f1_scores):.0f}{'':<6} {'':<10}") |
| |
| if gen_files: |
| |
| first_gen_path = gen_files[0] |
| summary_path = results_dir / f"{first_gen_path.parent.name}_F1_nodes.txt" |
| |
| |
| if LOG_FILE and args.output.exists(): |
| with open(args.output, "r", encoding="utf-8") as log_file: |
| log_content = log_file.read() |
| |
| with open(summary_path, "w", encoding="utf-8") as f: |
| f.write(log_content) |
| else: |
| |
| with open(summary_path, "w", encoding="utf-8") as f: |
| f.write("Node analysis - global summary\n") |
| f.write(f"{'Test':<25} {'RefDir':<10} {'TP':<5} {'FP':<5} {'FN':<5} {'P':<6} {'R':<6} {'F1':<6} {'C(mapp)':<10}\n") |
| f.write("-" * 85 + "\n") |
| for s in summary_table: |
| f.write(f"{s['test']:<25} {s['ref_path']:<10} {s['TP']:<5} {s['FP']:<5} {s['FN']:<5} " |
| f"{s['precision']:.2f} {s['recall']:.2f} {s['F1']:.2f} {s['mapping_size']}\n") |
| |
| if f1_scores: |
| f.write("-" * 85 + "\n") |
| f.write(f"{'MEAN':<25} {'':<10} {'':<5} {'':<5} {'':<5} {'':<6} {'':<6} {mean_f1:.2f}{'':<6} {'':<10}\n") |
| f.write(f"{'STD DEV':<25} {'':<10} {'':<5} {'':<5} {'':<5} {'':<6} {'':<6} {std_f1:.2f}{'':<6} {'':<10}\n") |
| f.write(f"{'NUMBER OF TESTS':<25} {'':<10} {'':<5} {'':<5} {'':<5} {'':<6} {'':<6} {len(f1_scores):.0f}{'':<6} {'':<10}\n") |
|
|
| custom_print(f"\n{C.OKG} Global summary saved to:{C.END} {summary_path}\n") |
| else: |
| custom_print(f"\n{C.W}No results to display in the global summary table.{C.END}") |
|
|
|
|
| if __name__ == "__main__": |
| try: |
| main() |
| except KeyboardInterrupt: |
| custom_print("\nInterrupted by user.") |
| sys.exit(130) |
| finally: |
| if LOG_FILE: |
| LOG_FILE.close() |
|
|
|
|
|
|