| import json |
| import sys |
| import argparse |
| import re |
| import os |
| from pathlib import Path |
| from typing import List, Dict, Set, Tuple |
| import numpy as np |
| from collections import Counter |
|
|
|
|
|
|
|
|
| def count_total_connections(nodes): |
| """Count the total number of connections in a topology.""" |
| seen_pairs = set() |
| total = 0 |
| |
| for node in nodes: |
| for conn in node.get('connections', []): |
| peer = conn['peer_device'] |
| pair = tuple(sorted([node['device_name'], peer])) |
| if pair not in seen_pairs: |
| seen_pairs.add(pair) |
| total += 1 |
| |
| return total |
|
|
| def get_layer_priority(layer): |
| """Define layer priority for connection filtering.""" |
| layer_priority = { |
| 'Core': 0, |
| 'Distribution': 1, |
| 'Access': 2, |
| 'Endpoint': 3 |
| } |
| return layer_priority.get(layer, float('inf')) |
| |
| def get_peer_layer(peer_device, all_nodes): |
| """Obtenir la couche du peer device""" |
| for node in all_nodes: |
| if node['device_name'] == peer_device: |
| return node.get('network_layer', 'Unknown') |
| return 'Unknown' |
| |
| def should_count_connection(node_layer, peer_layer, node_priority): |
| """Determine whether a connection should be counted according to the specified rules.""" |
| peer_priority = get_layer_priority(peer_layer) |
| |
| |
| if node_layer == peer_layer: |
| return True |
| |
| |
| |
| if peer_priority < node_priority: |
| return True |
| |
| return False |
|
|
|
|
| try: |
| from rapidfuzz import fuzz |
| except ImportError: |
| import difflib |
| class fuzz: |
| @staticmethod |
| def ratio(a, b): |
| return difflib.SequenceMatcher(None, a, b).ratio() * 100 |
|
|
| try: |
| from scipy.optimize import linear_sum_assignment |
| HAS_SCIPY = True |
| except ImportError: |
| HAS_SCIPY = False |
|
|
| class C: |
| HEADER = '\033[95m' |
| OKBLUE = '\033[94m' |
| OKCYAN = '\033[96m' |
| OKGREEN = '\033[92m' |
| WARNING = '\033[93m' |
| FAIL = '\033[91m' |
| ENDC = '\033[0m' |
| BOLD = '\033[1m' |
|
|
| LOG_FILE = None |
|
|
| def custom_print(message: str = "", end: str = "\n"): |
| global LOG_FILE |
| full_message = message + end |
| if LOG_FILE: |
| with open(LOG_FILE, 'a', encoding='utf-8') as f: |
| f.write(full_message) |
| print(full_message, end='') |
|
|
|
|
| def _normalize_name(name: str) -> str: |
| return re.sub(r'[^a-z0-9]', '', str(name).lower()) |
|
|
| def _normalize_if_type(if_name: str) -> str: |
| n = if_name.lower() |
| if 'se' in n: return "Serial" |
| if 'eth' in n: return "Ethernet" |
| return "Ethernet" |
|
|
|
|
| def build_edges(input_json: Dict) -> Dict: |
| """ |
| Function that completes the JSON by creating reverse connections for each existing connection. |
| """ |
| output_json = json.loads(json.dumps(input_json)) |
| |
| devices = output_json["result"]["network_topology"]["devices"] |
| device_map = {device["device_name"]: device for device in devices} |
| |
| |
| for device in devices: |
| current_device_name = device.get("device_name") |
| used_interfaces = device.get("device_interfaces", {}).get("used_interfaces", []) |
| |
| |
| for conn in used_interfaces: |
| peer_connection = conn.get("peer_connection") |
| if not peer_connection: |
| continue |
| peer_device_name = peer_connection.get("peer_device") |
| if not peer_device_name: |
| continue |
| |
| peer_device = device_map.get(peer_device_name) |
| if not peer_device: |
| continue |
| |
| |
| peer_used_interfaces = peer_device.get("device_interfaces", {}).get("used_interfaces", []) |
| reverse_exists = False |
| |
| for peer_conn in peer_used_interfaces: |
| if peer_conn.get("peer_connection", {}).get("peer_device") == current_device_name: |
| reverse_exists = True |
| break |
| |
| |
| if not reverse_exists: |
| interface_name = conn.get("interface_name") |
| interface_type = conn.get("interface_type") |
| peer_interface = conn.get("peer_connection", {}).get("peer_interface") |
| peer_interface_type = conn.get("peer_connection", {}).get("peer_interface_type") |
| |
| new_reverse_conn = { |
| "interface_name": peer_interface, |
| "interface_type": peer_interface_type, |
| "peer_connection": { |
| "peer_device": current_device_name, |
| "peer_interface": interface_name, |
| "peer_interface_type": interface_type |
| } |
| } |
| |
| peer_device_interfaces = peer_device.get("device_interfaces", {}) |
| |
| |
| peer_device_interfaces["used_interfaces"].append(new_reverse_conn) |
| custom_print(f"{C.OKGREEN}Reverse connection added: {peer_device_name}({peer_interface}) -> {current_device_name}({interface_name}){C.ENDC}") |
| |
| return output_json |
| |
|
|
| def load_nodes(data: Dict) -> List[Dict]: |
| nodes = [] |
| |
| devices = data["result"]["network_topology"].get("devices", []) |
| for device in devices: |
| |
| connections = [] |
| interfaces = device.get("device_interfaces", {}) |
| used_interfaces = interfaces.get("used_interfaces", []) |
| |
| for interface in used_interfaces: |
| peer = interface.get("peer_connection", {}) |
|
|
| if peer is None: |
| peer = {} |
| |
| if peer.get("peer_device"): |
| connections.append({ |
| "peer_device": peer["peer_device"], |
| "interface_name": interface.get("interface_name"), |
| "peer_interface": peer.get("peer_interface"), |
| "interface_type": _normalize_if_type(interface.get("interface_name", "")) |
| }) |
| |
| 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", "default")).lower(), |
| "connections": connections |
| }) |
| |
| return nodes |
|
|
|
|
| def _normalize_name_nodes(name: str) -> str: |
| """Standardize node names for comparison (based on gen_F1_nodes.py logic).""" |
| return str(name or "").replace('-', '').replace('_', '').upper() |
|
|
| 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: |
| """Construit la matrice de similarité (logique gen_F1_nodes.py).""" |
| 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_nodes(sim: np.ndarray): |
| """Applique l'algorithme Hongrois (logique gen_F1_nodes.py).""" |
| 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 = 1.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 Sim): {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" Application de la normalisation des sites:") |
| 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-normalization of 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 (contient 'server')") |
| |
| |
| if ref_types and gen_types: |
| print(f" Normalisation des types par 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)} types REF vs {len(gen_layer_types)} types GEN") |
| |
| |
| 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" Type GEN: {gen_layer_types[j]} → Type REF: {ref_layer_types[i]} (score: {s:.2f})") |
| |
| |
| type_mapping.update(layer_type_mapping) |
| |
| |
| print(f" Application de la normalisation des types:") |
| 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 = 1.0): |
| """Hierarchical mapping: layer → site → type with 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.HEADER}===== Hierarchical Mapping: Layer → Site → Type (Hungarian Algorithm) ====={C.ENDC}") |
| |
| 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.OKBLUE}--- Layer: {display_layer} ---{C.ENDC}") |
| |
| |
| 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" {C.OKCYAN}Site: {site}{C.ENDC}") |
| |
| |
| 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.FAIL}Local FN {device_type}: {', '.join(ref_names)}{C.ENDC}") |
| if gen_names: |
| custom_print(f" {C.FAIL}Local FP {device_type}: {', '.join(gen_names)}{C.ENDC}") |
| 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.OKGREEN}TP {device_type}: {len(matches)} match(s){C.ENDC}") |
| 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.WARNING}No matches for {device_type} (threshold: {min_sim:.1f}%){C.ENDC}") |
| |
| |
| 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.FAIL}FN Locaux {device_type} ({len(fn_nodes)}): {', '.join(fn_nodes[:10])}{'...' if len(fn_nodes) > 10 else ''}{C.ENDC}") |
| if fp_nodes: |
| custom_print(f" {C.FAIL}FP Locaux {device_type} ({len(fp_nodes)}): {', '.join(fp_nodes[:10])}{'...' if len(fp_nodes) > 10 else ''}{C.ENDC}") |
| |
| custom_print(f"\n{C.OKGREEN}Mapping final : {len(mapping)} correspondances{C.ENDC}") |
| return mapping |
|
|
|
|
|
|
| def get_correct_edges(nodes_list: List[Dict]): |
| """ |
| Fonction pour calculer les FP en vérifiant les connexions réciproques. |
| Retourne les edges valides (connexions bidirectionnelles) et le nombre de FP. |
| """ |
| edges = set() |
| extra_fp = 0 |
| node_map = {n['device_name']: n for n in nodes_list} |
| |
| |
| connections_seen = {} |
| |
| |
| fp_links = [] |
| reciprocal_links = [] |
| bidirectional_pairs = set() |
| |
| |
| for node in nodes_list: |
| u = node['device_name'] |
| for c in node.get('connections', []): |
| v, u_if, v_if = c['peer_device'], c['interface_name'], c['peer_interface'] |
| u_type = c.get('interface_type') |
| |
| |
| conn_key = (u, v, u_if, v_if) |
| |
| |
| peer = node_map.get(v) |
| if not peer: |
| continue |
| |
| |
| if conn_key not in connections_seen: |
| connections_seen[conn_key] = {"has_reverse": False, "type": u_type} |
| |
| |
| for conn_key, conn_info in connections_seen.items(): |
| u, v, u_if, v_if = conn_key |
| u_type = conn_info["type"] |
| |
| |
| reverse_key = (v, u, v_if, u_if) |
| |
| |
| if reverse_key in connections_seen: |
| |
| reverse_type = connections_seen[reverse_key]["type"] |
| |
| |
| connections_seen[conn_key]["has_reverse"] = True |
| connections_seen[reverse_key]["has_reverse"] = True |
| |
| |
| if u_type != reverse_type: |
| |
| extra_fp += 1 |
| fp_links.append(f"FP_TYPE: {u}({u_type}) <-> {v}({reverse_type}) via {u_if}/{v_if}") |
| else: |
| |
| peer = node_map.get(v) |
| u_device_type = node_map.get(u, {}).get('device_type') |
| v_device_type = peer.get('device_type') |
| |
| |
| pair_signature = tuple(sorted([u, v])) |
| |
| |
| if pair_signature not in bidirectional_pairs: |
| bidirectional_pairs.add(pair_signature) |
| |
| |
| edge_id = tuple(sorted([(u, u_device_type, u_if, u_type), (v, v_device_type, v_if, u_type)])) |
| edges.add(edge_id) |
| reciprocal_links.append(f"VALID: {u} <-> {v} via {u_if}/{v_if} ({u_type})") |
| |
| |
| non_bidirectional_connections = [] |
| connections_to_remove = [] |
| counted_pairs = set() |
| |
| for conn_key, conn_info in connections_seen.items(): |
| if not conn_info["has_reverse"]: |
| u, v, u_if, v_if = conn_key |
| u_type = conn_info["type"] |
| |
| |
| pair_signature = tuple(sorted([u, v])) |
| |
| |
| if pair_signature not in counted_pairs: |
| extra_fp += 1 |
| counted_pairs.add(pair_signature) |
| fp_links.append(f"FP_NON_BIDIRECTIONAL: {u} <-> {v} (non-reciprocal connection)") |
| |
| non_bidirectional_connections.append(f"{u} -> {v}") |
| connections_to_remove.append((u, v, u_if, v_if)) |
| |
| |
| if connections_to_remove: |
| custom_print(f"{C.FAIL}PHYSICAL REMOVAL of non-bidirectional connections: {len(connections_to_remove)}{C.ENDC}") |
| |
| for u, v, u_if, v_if in connections_to_remove: |
| u_node = node_map.get(u) |
| if u_node: |
| before_count = len(u_node.get('connections', [])) |
| |
| u_node['connections'] = [c for c in u_node.get('connections', []) |
| if not (c['peer_device'] == v and |
| c['interface_name'] == u_if and |
| c['peer_interface'] == v_if)] |
| after_count = len(u_node.get('connections', [])) |
| removed = before_count - after_count |
| custom_print(f" ❌ Removed {u} -> {v} via {u_if}/{v_if} ({removed} connection(s))") |
| |
| |
| if reciprocal_links: |
| custom_print(f"{C.OKGREEN}Valid reciprocal connections: {len(reciprocal_links)}{C.ENDC}") |
| for link in reciprocal_links[:3]: |
| custom_print(f" ✓ {link}", C.OKGREEN) |
| if len(reciprocal_links) > 3: |
| custom_print(f" ... and {len(reciprocal_links) - 3} more", C.OKGREEN) |
| |
| if non_bidirectional_connections: |
| custom_print(f"{C.WARNING}Non-bidirectional connections: {len(non_bidirectional_connections)}{C.ENDC}") |
| for link in non_bidirectional_connections[:3]: |
| custom_print(f" - {link}", C.WARNING) |
| if len(non_bidirectional_connections) > 3: |
| custom_print(f" ... and {len(non_bidirectional_connections) - 3} more", C.WARNING) |
| |
| if fp_links: |
| custom_print(f"{C.FAIL}Total FP: {len(fp_links)}{C.ENDC}") |
| for link in fp_links[:5]: |
| custom_print(f" - {link}", C.FAIL) |
| if len(fp_links) > 5: |
| custom_print(f" ... and {len(fp_links) - 5} more", C.FAIL) |
| |
| return edges, extra_fp |
|
|
|
|
|
|
|
|
| def get_neighbors(node_name: str, nodes_list: List[Dict]) -> Set[str]: |
| """Extract the set of neighbors for a given node.""" |
| for node in nodes_list: |
| if node['device_name'] == node_name: |
| return {conn['peer_device'] for conn in node.get('connections', [])} |
| return set() |
|
|
|
|
|
|
| def get_neighbors_with_type(node_name: str, nodes_list: List[Dict]) -> Dict[str, Set[str]]: |
| """Extract neighbors of a given node grouped by device type.""" |
| neighbors_by_type = {} |
| |
| for node in nodes_list: |
| if node['device_name'] == node_name: |
| for conn in node.get('connections', []): |
| peer_name = conn['peer_device'] |
| peer_node = next((n for n in nodes_list if n['device_name'] == peer_name), None) |
| |
| if peer_node: |
| device_type = peer_node.get('device_type') |
| if device_type not in neighbors_by_type: |
| neighbors_by_type[device_type] = set() |
| neighbors_by_type[device_type].add(peer_name) |
| |
| return neighbors_by_type |
|
|
| |
|
|
| def get_all_normalized_types(nodes): |
| """Extract ALL normalized node and neighbor types.""" |
| types_set = set() |
| |
| |
| for node in nodes: |
| types_set.add(node['device_type']) |
| |
| |
| for node in nodes: |
| for conn in node.get('connections', []): |
| peer_name = conn['peer_device'] |
| |
| for peer_node in nodes: |
| if peer_node['device_name'] == peer_name: |
| types_set.add(peer_node['device_type']) |
| break |
| |
| return sorted(list(types_set)) |
|
|
|
|
|
|
| def create_neighbor_vector(node, all_nodes, type_order): |
| """Create fixed-length neighborhood vector with zero padding""" |
| vector = [0] * len(type_order) |
| |
| for conn in node.get('connections', []): |
| peer_name = conn['peer_device'] |
| |
| for peer_node in all_nodes: |
| if peer_node['device_name'] == peer_name: |
| peer_type = peer_node['device_type'] |
| if peer_type in type_order: |
| idx = type_order.index(peer_type) |
| vector[idx] += 1 |
| break |
| |
| return vector |
|
|
| def manhattan_distance(vec1, vec2): |
| """Calculer la distance Manhattan entre deux vecteurs""" |
| return sum(abs(a - b) for a, b in zip(vec1, vec2)) |
|
|
| def group_nodes_by_criteria(nodes): |
| """Group nodes by (site, layer, type).""" |
| groups = {} |
| |
| for node in nodes: |
| key = (node['location_site'], node['network_layer'], node['device_type']) |
| if key not in groups: |
| groups[key] = [] |
| groups[key].append(node) |
| |
| return groups |
|
|
|
|
| def find_best_structural_matches(ref_group, gen_group, ref_vectors, gen_vectors, all_types): |
| """Trouver les meilleurs matchs structurels en minimisant la distance Manhattan""" |
| if not ref_group or not gen_group: |
| return {} |
| |
| |
| n_ref = len(ref_group) |
| n_gen = len(gen_group) |
|
|
| |
| if n_ref > n_gen * 2 or n_gen > n_ref * 2: |
| return _simple_greedy_matching(ref_group, gen_group, ref_vectors, gen_vectors) |
|
|
| |
| size = max(n_ref, n_gen) |
| cost_matrix = np.full((size, size), 999999.0) |
| |
| |
| for i, ref_node in enumerate(ref_group): |
| for j, gen_node in enumerate(gen_group): |
| ref_name = ref_node['device_name'] |
| gen_name = gen_node['device_name'] |
| |
| if ref_name in ref_vectors and gen_name in gen_vectors: |
| distance = manhattan_distance(ref_vectors[ref_name], gen_vectors[gen_name]) |
| cost_matrix[i, j] = distance |
| |
| |
| try: |
| row_indices, col_indices = linear_sum_assignment(cost_matrix) |
| except ValueError: |
| |
| return _simple_greedy_matching(ref_group, gen_group, ref_vectors, gen_vectors) |
| |
| |
| mapping = {} |
| used_gen = set() |
| |
| for i, j in zip(row_indices, col_indices): |
| if i < n_ref and j < n_gen: |
| ref_node = ref_group[i] |
| gen_node = gen_group[j] |
| |
| |
| distance = cost_matrix[i, j] |
| if distance < 999999.0 and gen_node['device_name'] not in used_gen: |
| mapping[ref_node['device_name']] = gen_node['device_name'] |
| used_gen.add(gen_node['device_name']) |
| |
| return mapping |
|
|
|
|
|
|
|
|
| def _simple_greedy_matching(ref_group, gen_group, ref_vectors, gen_vectors): |
| """Simple greedy matching approach for groups with very different sizes.""" |
| mapping = {} |
| used_gen = set() |
| |
| |
| for ref_node in ref_group: |
| ref_name = ref_node['device_name'] |
| best_gen = None |
| best_distance = float('inf') |
| |
| for gen_node in gen_group: |
| gen_name = gen_node['device_name'] |
| |
| if gen_name not in used_gen and ref_name in ref_vectors and gen_name in gen_vectors: |
| distance = manhattan_distance(ref_vectors[ref_name], gen_vectors[gen_name]) |
| if distance < best_distance: |
| best_distance = distance |
| best_gen = gen_name |
| |
| if best_gen is not None and best_distance < float('inf'): |
| mapping[ref_name] = best_gen |
| used_gen.add(best_gen) |
| |
| return mapping |
|
|
| def find_node_by_name(name, nodes): |
| """Find a node by its name.""" |
| for node in nodes: |
| if node['device_name'] == name: |
| return node |
| return None |
|
|
|
|
|
|
|
|
| def apply_structural_mapping_to_nodes(gen_nodes, structural_mapping): |
| """Apply the new structural mapping to GEN nodes and ALL their peer_device references.""" |
| gen_nodes_remapped = [] |
| |
| for gen_node in gen_nodes: |
| |
| new_name = None |
| for ref_name, gen_name in structural_mapping.items(): |
| if gen_node['device_name'] == gen_name: |
| new_name = ref_name |
| break |
| |
| |
| remapped_node = gen_node.copy() |
| |
| |
| if new_name: |
| remapped_node['device_name'] = new_name |
| |
| |
| updated_connections = [] |
| for conn in remapped_node.get('connections', []): |
| updated_conn = conn.copy() |
| |
| |
| peer_updated = False |
| for ref_name, gen_name in structural_mapping.items(): |
| if updated_conn['peer_device'] == gen_name: |
| updated_conn['peer_device'] = ref_name |
| peer_updated = True |
| break |
| |
| updated_connections.append(updated_conn) |
| |
| remapped_node['connections'] = updated_connections |
| gen_nodes_remapped.append(remapped_node) |
| |
| return gen_nodes_remapped |
|
|
| def count_matching_connections(ref_node, gen_node): |
| """Count matching connections between two nodes (including interface types).""" |
| ref_connections = set() |
| gen_connections = set() |
| |
| |
| for conn in ref_node.get('connections', []): |
| |
| interface_type = conn.get('interface_type') |
| link_id = tuple(sorted([ref_node['device_name'], conn['peer_device'], interface_type])) |
| ref_connections.add(link_id) |
| |
| |
| for conn in gen_node.get('connections', []): |
| |
| interface_type = conn.get('interface_type') |
| link_id = tuple(sorted([gen_node['device_name'], conn['peer_device'], interface_type])) |
| gen_connections.add(link_id) |
| |
| |
| matching = len(ref_connections & gen_connections) |
| return matching |
|
|
|
|
|
|
|
|
|
|
|
|
| def calculate_structural_f1_scores(ref_nodes, gen_nodes_remapped, structural_mapping): |
| """ |
| Calculate TP/FP/FN with structural remapping. |
| Approach: REF (intact) vs GEN (modified to resemble REF). |
| Uses the pair approach (u,v) and (v,u) with a seen boolean. |
| """ |
| |
| |
| def get_pair(u, v): |
| return tuple(sorted([u, v])) |
| |
| |
| def get_peers(node): |
| return {conn['peer_device'] for conn in node.get('connections', [])} |
| |
| |
| def count_pairs(pairs, seen_set): |
| count = 0 |
| for u, v in pairs: |
| pair = get_pair(u, v) |
| if pair not in seen_set: |
| seen_set.add(pair) |
| count += 1 |
| return count |
| |
| mapped_ref_names = set(structural_mapping.values()) |
| |
| |
| tp_structural = 0 |
| seen_tp = set() |
| |
| for gen_original, ref_target in structural_mapping.items(): |
| ref_node = find_node_by_name(ref_target, ref_nodes) |
| gen_node = find_node_by_name(ref_target, gen_nodes_remapped) |
| |
| if ref_node and gen_node: |
| ref_peers = get_peers(ref_node) |
| gen_peers = get_peers(gen_node) |
| common_peers = ref_peers & gen_peers |
| |
| tp_pairs = [(ref_target, peer) for peer in common_peers] |
| tp_structural += count_pairs(tp_pairs, seen_tp) |
| |
| |
| fp_structural = 0 |
| seen_fp = set() |
| |
| |
| for gen_node in gen_nodes_remapped: |
| if gen_node['device_name'] not in mapped_ref_names: |
| peers = get_peers(gen_node) |
| fp_pairs = [(gen_node['device_name'], peer) for peer in peers] |
| fp_structural += count_pairs(fp_pairs, seen_fp) |
| |
| |
| for gen_original, ref_target in structural_mapping.items(): |
| ref_node = find_node_by_name(ref_target, ref_nodes) |
| gen_node = find_node_by_name(ref_target, gen_nodes_remapped) |
| |
| if ref_node and gen_node: |
| ref_peers = get_peers(ref_node) |
| gen_peers = get_peers(gen_node) |
| extra_peers = gen_peers - ref_peers |
| |
| fp_pairs = [(ref_target, peer) for peer in extra_peers] |
| fp_structural += count_pairs(fp_pairs, seen_fp) |
| |
| |
| fn_structural = 0 |
| seen_fn = set() |
| |
| |
| all_ref_pairs = [] |
| for ref_node in ref_nodes: |
| |
| |
| if ref_node['device_name'] in mapped_ref_names: |
| peers = get_peers(ref_node) |
| all_ref_pairs.extend([(ref_node['device_name'], peer) for peer in peers]) |
| |
| total_ref = count_pairs(all_ref_pairs, seen_fn) |
| fn_structural = total_ref - tp_structural |
| |
| return tp_structural, fp_structural, fn_structural |
|
|
|
|
| def save_cleaned_topology(gen_nodes_remapped, first_mapping, output_path): |
| """ |
| Save the remapped GEN topology in JSON with the same format as the original. |
| Contains only nodes that were originally in GEN and have been remapped |
| with the first mapping (get_mapping). |
| |
| Connections are assumed to have already been cleaned by get_correct_edges(). |
| |
| Args: |
| gen_nodes_remapped: List of GEN nodes after structural remapping |
| first_mapping: REF → GEN mapping (first mapping from get_mapping) |
| output_path: Output JSON file path |
| """ |
| |
| |
| mapped_ref_names = set(first_mapping.keys()) |
| |
| |
| mapped_nodes = [] |
| for node in gen_nodes_remapped: |
| if node['device_name'] in mapped_ref_names: |
| mapped_nodes.append(node) |
| |
| |
| total_connections = 0 |
| for node in mapped_nodes: |
| total_connections += len(node.get('connections', [])) |
| |
| custom_print(f"{C.OKCYAN}Saving topology (connections already cleaned)...{C.ENDC}") |
| custom_print(f"{C.OKGREEN}Remaining connections: {total_connections}{C.ENDC}") |
| |
| |
| cleaned_devices = [] |
| for node in mapped_nodes: |
| device_data = { |
| "device_name": node["device_name"], |
| "device_type": node.get("device_type", "router"), |
| "network_layer": node.get("network_layer", "access"), |
| "location_site": node.get("location_site", "default"), |
| "device_interfaces": { |
| "used_interfaces": [], |
| "unused_interfaces": [] |
| } |
| } |
| |
| |
| for conn in node.get('connections', []): |
| peer_device = conn['peer_device'] |
| peer_interface = conn['peer_interface'] |
| interface_name = conn['interface_name'] |
| interface_type = conn.get('interface_type', 'Ethernet') |
| |
| device_data["device_interfaces"]["used_interfaces"].append({ |
| "interface_name": interface_name, |
| "interface_type": interface_type, |
| "peer_connection": { |
| "peer_device": peer_device, |
| "peer_interface": peer_interface, |
| "peer_interface_type": interface_type |
| } |
| }) |
| |
| cleaned_devices.append(device_data) |
| |
| |
| topology_data = { |
| "status": "SUCCESS", |
| "result": { |
| "network_topology": { |
| "devices": cleaned_devices |
| } |
| } |
| } |
| |
| |
| with open(output_path, 'w', encoding='utf-8') as f: |
| json.dump(topology_data, f, indent=2, ensure_ascii=False) |
| |
| custom_print(f"{C.OKGREEN}Topology saved: {output_path}{C.ENDC}") |
| custom_print(f"{C.OKGREEN} - {len(cleaned_devices)} devices{C.ENDC}") |
| custom_print(f"{C.OKGREEN} - {total_connections} connections{C.ENDC}") |
| return len(cleaned_devices) |
|
|
|
|
|
|