429 lines
18 KiB
Python
429 lines
18 KiB
Python
"""
|
|
|
|
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)
|