ResiNet-LLM-topology / f1_nodes_main.py
KholoudLIA's picture
Upload folder using huggingface_hub
9696f7a verified
Raw
History Blame Contribute Delete
28.1 kB
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
# ========================= ANSI COLORS =========================
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
# Write to the log file if open
if LOG_FILE:
LOG_FILE.write(full_message)
LOG_FILE.flush()
# Write to the console (standard output)
sys.stdout.write(full_message)
sys.stdout.flush()
# ========================= CORE FUNCTIONS =========================
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
# 1. Extract unique REF sites and types
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))
# 2. Extract unique GEN sites and types
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 = {}
# 3. Normalize GEN sites by matching to REF sites
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})")
# 4. Apply site normalization to GEN nodes
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}")
# 5. Pre-normalize server-like types directly to Server
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')")
# 6. Normalize types by layer using the Hungarian algorithm
if ref_types and gen_types:
print(f" Normalizing types by layer:")
# Group nodes 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)
# 7. Apply normalized types to GEN nodes
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] = {}
# Hierarchical grouping: layer → site → type
ref_groups: Dict[str, Dict[str, List[str]]] = {}
gen_groups: Dict[str, Dict[str, List[str]]] = {}
# Group REF nodes by layer → site → type
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"])
# Group GEN nodes by layer → site → type
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}")
# Process each site in this layer
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}")
# Process each type within this site/layer
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
# Apply the Hungarian algorithm for this specific group
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}")
# Display local FN/FP for this group
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:
# Add default device_type if missing
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": [] # No connections for node mapping
})
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)
# --- COMPUTATION LOGIC ---
if gen_count == 0 and len(mapping) == 0:
# Penalty for invalid or empty generated JSON
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
# -------------------------
# Detailed display of final metrics (logs)
# **Keep .6f precision for detailed logs**
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
}
# ========================= MAIN =========================
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 opening the 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 = []
# Collect all generated file paths
gen_files = []
for p in args.gen:
path = Path(p)
if path.is_dir():
# Only process top-level files, not subdirectories
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}")
# Load the generated file
with open(gen_path) as f:
gen_data = json.load(f)
# Check if the devices list is empty
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}")
# Calculate FN = total nodes in the reference
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}")
# Compute F1
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
# If devices is not empty, continue normal processing
gen_nodes = load_nodes(gen_data)
# A. Normalize GEN topology to REF with Hungarian algorithm
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")
# Debug: list all 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))}",
})
# ======= FINAL GLOBAL TABLE =======
if summary_table:
custom_print(f"\n{C.HDR}{C.B}===== GLOBAL NODE SUMMARY TABLE ====={C.END}")
# console display
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']}")
# Compute global statistics
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:
# Use the root results folder for summary outputs
first_gen_path = gen_files[0]
summary_path = results_dir / f"{first_gen_path.parent.name}_F1_nodes.txt"
# Copy detailed log contents to the summary output file
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:
# If there is no log file, create a file containing the summary table
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()