""" Version 1.0 Date 27/03/2025 Auteur : Stéphan Peccini Ce script permet de récupérer un graphe DOT syntaxiquement correct, pour le formater selon la structure hiérarchique souhaitée. Exemple d'utilisation : ----------------------- Le script ihh.py met à jour les données ihh en fonction des nouvelles informations. Pour faciliter le travail, tout est fait avec un graphe NetworkX et ensuite sauvé au format DOT sans aucune structure hiérarchique. Il suffit alors de passer le fichier résultat de ihh.py pour avoir un fichier DOT conforme aux attentes. """ import sys import networkx as nx from networkx.drawing.nx_agraph import read_dot import logging logging.basicConfig( filename="beautify_debug.log", filemode="w", level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) def formater_noeuds_par_niveau(schema, niveau, indentation=4): """ Génère la syntaxe DOT avec une hiérarchie complète de sous-graphes imbriqués: - Sous-graphes pour les nœuds du niveau spécifié (0, 1, 2) - Sous-graphes pour les nœuds de niveau 10 liés à ces nœuds - Sous-graphes pour les nœuds de niveau 11 liés aux nœuds de niveau 10 - Pour les nœuds de niveau 11, inclut leurs nœuds destination (sauf niveau 99) """ # Convertir le niveau en chaîne si nécessaire niveau_str = str(niveau) # Identifier les nœuds du niveau spécifié noeuds_niveau = [noeud for noeud, attr in schema.nodes(data=True) if attr.get('niveau') == niveau_str] # Récupérer les attributs de ces nœuds attributs_niveau = {noeud: schema.nodes[noeud] for noeud in noeuds_niveau} # Définir les couleurs selon la légende couleurs_operations = { "Assemblage": "#a0d6ff", "Niveau Composant": "#b3ffe0", "Fabrication": "#ffe0cc", "Extraction": "#ffd699", "Reserves": "#ffd699", "Pays": "#e6f2ff", "Zone": "#e6f2ff", "Entreprise": "#f0f0f0" } # Fonction pour déterminer l'opération et la couleur def determiner_operation_et_couleur(nom_noeud): operation = nom_noeud.split('_')[0] if '_' in nom_noeud else nom_noeud couleur = "#ffffff" # Blanc par défaut for op_cle, op_couleur in couleurs_operations.items(): if op_cle in operation: couleur = op_couleur break return operation, couleur # Fonction pour formater une arête avec ses attributs def formater_arete(source, dest, edge_attrs, indent_level): edge_str = f"{indent_level}{source} -> {dest} [" edge_attrs_formatted = [] for edge_attr, edge_val in edge_attrs.items(): if edge_val: edge_attrs_formatted.append(f'{edge_attr}="{edge_val}"') edge_str += ", ".join(edge_attrs_formatted) edge_str += "];\n" return edge_str # Fonction pour formater un nœud avec ses attributs def formater_noeud(nom_noeud, attributs_noeud, indent_level): node_str = f"{indent_level}{nom_noeud} [" attrs_formattés = [] for attr_nom, attr_val in attributs_noeud.items(): if attr_val and attr_nom in ordre_attributs: attrs_formattés.append(f'{attr_nom}="{attr_val}"') node_str += ", ".join(attrs_formattés) node_str += "];\n" return node_str # Fonction pour trouver toutes les relations sortantes d'un nœud def trouver_relations_sortantes(noeud_source): relations = [] for s, d, attrs in schema.edges(data=True): if s == noeud_source: relations.append((s, d, attrs)) # Suppression des doublons en convertissant le dictionnaire en tuple trié relations_sans_doublons = set((a, b, tuple(sorted(c.items()))) for a, b, c in relations) # Reconversion en dictionnaire relations_finales = [(a, b, dict(c)) for a, b, c in relations_sans_doublons] return relations_finales # Définir les niveaux d'indentation indent = " " * (indentation + 4) indent_inner = " " * (indentation + 8) indent_inner2 = " " * (indentation + 12) indent_inner3 = " " * (indentation + 16) # Préparer la chaîne de sortie sortie = f"\n{indent}// Sous-graphes pour les nœuds de niveau {niveau} avec leurs relations\n" # Définir l'ordre des attributs ordre_attributs = ["ihh_pays", "ihh_acteurs", "ivc", "isg", "label", "niveau", "fillcolor", "orphelin", "shape", "style", "fontname"] # Formater chaque nœud comme un subgraph avec ses relations sortantes for nom_noeud, attributs in attributs_niveau.items(): # Débuter le sous-graphe principal sortie += f"{indent}subgraph cluster_{nom_noeud} {{\n" sortie += f"{indent_inner}label=\"{nom_noeud}\";\n" # Ajouter les autres attributs du sous-graphe for attr_nom, attr_valeur in attributs.items(): if attr_valeur and attr_nom in ["style", "fillcolor", "color", "fontname"]: sortie += f"{indent_inner}{attr_nom}=\"{attr_valeur}\";\n" # Ajouter le nœud lui-même dans le sous-graphe sortie += formater_noeud(nom_noeud, attributs, indent_inner) # Trouver toutes les relations dont ce nœud est la source relations_sortantes = [] sous_graphes_niveau_10 = {} # Dictionnaire pour stocker les sous-graphes de niveau 10 for source, dest, edge_attrs in trouver_relations_sortantes(nom_noeud): # Vérifier si la destination est un nœud de niveau 10 dest_attrs = schema.nodes[dest] # est_niveau_10 = dest_attrs.get('niveau') == "10" est_niveau_10 = str(dest_attrs.get('niveau')) in {"10", "1010"} # Formater l'arête pour le sous-graphe parent edge_str = formater_arete(source, dest, edge_attrs, indent_inner) relations_sortantes.append(edge_str) # Traiter les nœuds de niveau 10 if est_niveau_10 and dest not in sous_graphes_niveau_10: # Début du sous-graphe de niveau 10 operation, couleur = determiner_operation_et_couleur(dest) sg_str = f"\n{indent_inner}subgraph cluster_{dest} {{\n" sg_str += f"{indent_inner2}label=\"{dest}\";\n" # Couleur de remplissage if 'fillcolor' in dest_attrs and dest_attrs['fillcolor']: sg_str += f"{indent_inner2}fillcolor=\"{dest_attrs['fillcolor']}\";\n" else: sg_str += f"{indent_inner2}fillcolor=\"{couleur}\";\n" sg_str += f"{indent_inner2}style=\"filled\";\n" # Ajouter le nœud de niveau 10 lui-même sg_str += formater_noeud(dest, dest_attrs, indent_inner2) # Collecter les relations sortantes du nœud de niveau 10 relations_niveau_10 = trouver_relations_sortantes(dest) sous_graphes_niveau_11 = {} # Pour stocker les sous-graphes de niveau 11 if relations_niveau_10: sg_str += f"\n{indent_inner2}// Relations sortantes du nœud de niveau 10\n" for src, dst, rel_attrs in relations_niveau_10: # Ajouter la relation sg_str += formater_arete(src, dst, rel_attrs, indent_inner2) # Vérifier si la destination est un nœud de niveau 11 if dst in schema.nodes: dst_attrs = schema.nodes[dst] dst_niveau = dst_attrs.get('niveau', '') # Créer un sous-graphe pour les nœuds de niveau 11 if (dst_niveau == "11" or dst_niveau == "1011") and dst not in sous_graphes_niveau_11: # Début du sous-graphe de niveau 11 dst_operation, dst_couleur = determiner_operation_et_couleur(dst) dst_sg = f"\n{indent_inner2}subgraph cluster_{dst} {{\n" dst_sg += f"{indent_inner3}label=\"{dst}\";\n" # Couleur de remplissage if 'fillcolor' in dst_attrs and dst_attrs['fillcolor']: dst_sg += f"{indent_inner3}fillcolor=\"{dst_attrs['fillcolor']}\";\n" else: dst_sg += f"{indent_inner3}fillcolor=\"{dst_couleur}\";\n" dst_sg += f"{indent_inner3}style=\"filled\";\n" # Ajouter le nœud de niveau 11 lui-même dst_sg += formater_noeud(dst, dst_attrs, indent_inner3) # Collecter les relations sortantes du nœud de niveau 11 relations_niveau_11 = trouver_relations_sortantes(dst) noeuds_destination = {} # Pour stocker les nœuds destination relations_additionnelles = [] # Pour stocker les relations des nœuds destination if relations_niveau_11: dst_sg += f"\n{indent_inner3}// Relations sortantes du nœud de niveau 11\n" for n11_src, n11_dst, n11_attrs in relations_niveau_11: # Ajouter la relation dst_sg += formater_arete(n11_src, n11_dst, n11_attrs, indent_inner3) # Ajouter le nœud destination sauf s'il est de niveau 99 if n11_dst in schema.nodes: n11_dst_attrs = schema.nodes[n11_dst] n11_dst_niveau = n11_dst_attrs.get('niveau', '') if n11_dst_niveau != "99" and n11_dst not in noeuds_destination and n11_dst != dst: noeuds_destination[n11_dst] = n11_dst_attrs # Collecter les relations sortantes du nœud destination for dest_src, dest_dst, dest_attrs in trouver_relations_sortantes(n11_dst): relations_additionnelles.append((dest_src, dest_dst, dest_attrs)) # Ajouter les nœuds destination for dest_nom, dest_attrs in noeuds_destination.items(): dst_sg += formater_noeud(dest_nom, dest_attrs, indent_inner3) # Ajouter les relations additionnelles if relations_additionnelles: dst_sg += f"\n{indent_inner3}// Relations des nœuds destination\n" for rel_src, rel_dst, rel_attrs in relations_additionnelles: dst_sg += formater_arete(rel_src, rel_dst, rel_attrs, indent_inner3) # Fermer le sous-graphe de niveau 11 dst_sg += f"{indent_inner2}}}\n" # Ajouter à la collection sous_graphes_niveau_11[dst] = dst_sg # Ajouter les sous-graphes de niveau 11 au sous-graphe de niveau 10 if sous_graphes_niveau_11: sg_str += "".join(sous_graphes_niveau_11.values()) # Fermer le sous-graphe de niveau 10 sg_str += f"{indent_inner}}}\n" # Ajouter à la collection sous_graphes_niveau_10[dest] = sg_str # Ajouter les relations au sous-graphe principal if relations_sortantes: sortie += f"\n{indent_inner}// Relations sortantes\n" sortie += "".join(relations_sortantes) # Ajouter les sous-graphes de niveau 10 if sous_graphes_niveau_10: sortie += "\n" sortie += "".join(sous_graphes_niveau_10.values()) # Terminer le sous-graphe principal sortie += f"{indent}}}\n\n" return sortie def generer_rank_same(schema, indentation=4): """ Génère des instructions rank=same pour les nœuds de même niveau dans un graphe DOT. Args: schema: Le graphe NetworkX chargé depuis un fichier DOT indentation: Nombre d'espaces pour l'indentation (par défaut 4) Returns: Une chaîne contenant les instructions rank=same formatées pour le fichier DOT """ # Dictionnaire pour stocker les nœuds par niveau noeuds_par_niveau = {} # Parcourir tous les nœuds du graphe for noeud, attrs in schema.nodes(data=True): niveau = attrs.get('niveau') if niveau: # Si le nœud a un attribut niveau if niveau not in noeuds_par_niveau: noeuds_par_niveau[niveau] = [] noeuds_par_niveau[niveau].append(noeud) # Préparer l'indentation indent = " " * indentation # Générer les instructions rank=same sortie = "\n" + indent + "// Alignement des nœuds par niveau\n" # Trier les niveaux numériquement for niveau, noeuds in sorted(noeuds_par_niveau.items(), key=lambda x: int(x[0]) if x[0].isdigit() else 99): if noeuds: # S'il y a des nœuds pour ce niveau sortie += indent + "{ rank=same; " sortie += "; ".join(noeuds) sortie += "; }\n" return sortie def formater_attributs(attributs): # Filtrer les attributs non vides et les formater formatted_attrs = [] for attr, value in attributs.items(): if value: # Ne pas inclure les attributs vides # Ajouter des guillemets pour les valeurs textuelles (sauf pour certains attributs) if attr in ('shape', 'style') or value.isdigit(): formatted_attrs.append(f"{attr}={value}") else: formatted_attrs.append(f'{attr}="{value}"') # Gérer le cas spécial pour style=filled si nécessaire if 'style' not in attributs or not attributs['style']: formatted_attrs.append("style=filled") return formatted_attrs def main(fichier_entree, fichier_sortie): # Charger le graphe DOT avec NetworkX try: schema = read_dot(fichier_entree) except Exception as e: logging.error(f"Erreur de lecture DOT : {e}", exc_info=True) print(f"Erreur à l'ouverture du fichier DOT : {e}") return sortie = """digraph Hierarchie_Composants_Electroniques_Simplifiee { // Configuration globale graph [compound="True", rankdir="TB", ranksep="10.0"] node [fontname="Arial", shape=box, style=filled]; edge [fontname="Arial", fontsize=10, style=filled]; """ # Subgraph ASSEMBLAGE sortie += """ // Niveau Assemblage subgraph cluster_assemblage { label="ASSEMBLAGE"; bgcolor="#f0f0f0"; node [fillcolor="#a0d6ff"]; """ # Ajout des informations pour tous les nœudss de niveau 0 sortie += formater_noeuds_par_niveau(schema, "0") sortie += formater_noeuds_par_niveau(schema, "1000") sortie += """ }""" # Subgraph COMPOSANTS sortie += """ // Niveau Composants subgraph cluster_composants { label="Composants"; bgcolor="#f0f0f0"; node [fillcolor="#a0d6ff"];""" # Ajout des informations pour tous les nœudss de niveau 1 sortie += formater_noeuds_par_niveau(schema, "1") sortie += formater_noeuds_par_niveau(schema, "1001") sortie += """ }""" # Subgraph MATÉRIAUX sortie += """ // Niveau Matériaux subgraph cluster_materiaux { label="Matériaux"; bgcolor="#f0f0f0"; node [fillcolor="#a0d6ff"];""" # Ajout des informations pour tous les nœudss de niveau 2 sortie += formater_noeuds_par_niveau(schema, "2") sortie += """ }""" # Subgraph PAYS GÉOGRAPHIQUES sortie += """ // Niveau Pays géographiques subgraph cluster_pays_geographiques { label="Pays géographiques"; bgcolor="#f0f0f0"; node [fillcolor="#a0d6ff"];""" # Ajout des informations pour tous les nœudss de niveau 99 sortie += formater_noeuds_par_niveau(schema, "99") sortie += """ }""" # Ajouter les instructions rank=same pour l'alignement horizontal sortie += generer_rank_same(schema) sortie += """ // Légende subgraph cluster_legende { label="LÉGENDE"; bgcolor="white"; node [shape=box, style=filled, width=0.2, height=0.2]; L1 [label="Fabrication des wafers", fillcolor="#f0fff0"]; L2 [label="Assemblage", fillcolor="#a0d6ff"]; L3 [label="Niveau Composant", fillcolor="#b3ffe0"]; L4 [label="Fabrication N3 (Matériaux)", fillcolor="#ffe0cc"]; L5 [label="Fabrication N3 (Terres Rares)", fillcolor="#ffd699"]; L6 [label="Extraction/Réserves", fillcolor="#ffd699"]; L7 [label="Pays/Zone geographique", fillcolor="#e6f2ff"]; L8 [label="Entreprise", fillcolor="#f0f0f0"]; L9 [label="Liens réserves", color="red", fontcolor="white"]; L10 [label="Liens opération(traitement, fabrication, assemblage)", color="purple", fontcolor="white"]; L11 [label="Liens extraction", color="orange", fontcolor="white"]; L12 [label="Liens d'origine géographique", color="darkgreen", fontcolor="white"]; L13 [label="Liens origine des minerais", color="darkblue", fontcolor="white"]; } } """ try: with open(f"{fichier_sortie}", "w", encoding="utf-8") as f: print(f"{sortie}", file=f) except FileNotFoundError: print(f"Erreur : Le chemin vers '{fichier_sortie}' n'existe pas") except PermissionError: print(f"Erreur : Permissions insuffisantes pour écrire dans '{fichier_sortie}'") except IOError as e: print(f"Erreur d'E/S lors de l'écriture dans le fichier : {e}") except Exception as e: print(f"Erreur inattendue : {e}") else: print(f"Écriture dans '{fichier_sortie}' réussie") if len(sys.argv) != 3: print("Usage: python script.py fichier_entree.dot fichier_sortie.dot") else: fichier_entree = sys.argv[1] fichier_sortie = sys.argv[2] main(fichier_entree, fichier_sortie)