Nouvelle analyse avec plan d'actions et correction de schema.txt pour le

traitement du béryllium.
This commit is contained in:
Fabrication du Numérique 2025-06-02 10:32:09 +02:00
parent 8a601aa24a
commit d47e8608cc
16 changed files with 21925 additions and 8 deletions

View File

@ -0,0 +1,2 @@
# __init__.py app/fiches
from .interface import interface_plan_d_actions

View File

@ -0,0 +1,237 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour générer un rapport factorisé des vulnérabilités critiques
suivant la structure définie dans Remarques.md.
"""
import streamlit as st
import networkx as nx
import uuid
from utils.translations import _
from utils.widgets import html_expander
from networkx.drawing.nx_agraph import write_dot
from batch_ia import (
load_config,
write_report,
parse_graphs,
extract_data_from_graph,
calculate_vulnerabilities,
generate_report,
)
from .plan_d_actions import initialiser_interface
from utils.graph_utils import (
extraire_chemins_depuis,
extraire_chemins_vers
)
from pathlib import Path
# Répertoire courant du script
CURRENT_DIR = Path(__file__).resolve().parent
# Répertoire "jobs" dans app/plan_d_actions
JOBS = CURRENT_DIR / "jobs"
JOBS.mkdir(parents=True, exist_ok=True)
niveau_labels = {
0: "Produit final",
1: "Composant",
2: "Minerai",
10: "Opération",
11: "Pays d'opération",
12: "Acteur d'opération",
99: "Pays géographique"
}
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
def preparer_graphe(G):
"""Nettoie et prépare le graphe pour l'analyse."""
niveaux_temp = {
node: int(str(attrs.get("niveau")).strip('"'))
for node, attrs in G.nodes(data=True)
if attrs.get("niveau") and str(attrs.get("niveau")).strip('"').isdigit()
}
G.remove_nodes_from([n for n in G.nodes() if n not in niveaux_temp])
G.remove_nodes_from(
[n for n in G.nodes() if niveaux_temp.get(n) == 10 and 'Reserves' in n])
return G, niveaux_temp
def selectionner_minerais(G, noeuds_depart):
"""Interface pour sélectionner les minerais si nécessaire."""
minerais_selection = None
st.markdown(f"## {str(_('pages.plan_d_actions.select_minerals'))}")
# Étape 1 : récupérer tous les nœuds descendants depuis les produits finaux
descendants = set()
for start in noeuds_depart:
descendants.update(nx.descendants(G, start)) # tous les successeurs (récursifs)
# Étape 2 : ne garder que les nœuds de niveau 2 parmi les descendants
minerais_nodes = sorted([
n for n in descendants
if G.nodes[n].get("niveau") and int(str(G.nodes[n].get("niveau")).strip('"')) == 2
])
minerais_selection = st.multiselect(
str(_("pages.plan_d_actions.filter_by_minerals")),
minerais_nodes,
key="analyse_minerais"
)
return minerais_selection
def selectionner_noeuds(G, niveaux_temp, niveau_depart):
"""Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée."""
st.markdown("---")
st.markdown(f"## {str(_('pages.plan_d_actions.fine_selection'))}")
depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart]
noeuds_arrivee = [n for n in G.nodes() if niveaux_temp.get(n) == 99]
noeuds_depart = st.multiselect(str(_("pages.plan_d_actions.filter_start_nodes")),
sorted(depart_nodes),
key="analyse_noeuds_depart")
noeuds_depart = noeuds_depart if noeuds_depart else None
return noeuds_depart, noeuds_arrivee
def extraire_niveaux(G):
"""Extrait les niveaux des nœuds du graphe"""
niveaux = {}
for node, attrs in G.nodes(data=True):
niveau_str = attrs.get("niveau")
if niveau_str:
niveaux[node] = int(str(niveau_str).strip('"'))
return niveaux
def extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais):
"""Extrait les chemins selon les critères spécifiés"""
chemins = []
if noeuds_depart and noeuds_arrivee:
for nd in noeuds_depart:
for na in noeuds_arrivee:
tous_chemins = extraire_chemins_depuis(G, nd)
chemins.extend([chemin for chemin in tous_chemins if na in chemin])
elif noeuds_depart:
for nd in noeuds_depart:
chemins.extend(extraire_chemins_depuis(G, nd))
elif noeuds_arrivee:
for na in noeuds_arrivee:
chemins.extend(extraire_chemins_vers(G, na, niveau_depart))
else:
sources_depart = [n for n in G.nodes() if niveaux.get(n) == niveau_depart]
for nd in sources_depart:
chemins.extend(extraire_chemins_depuis(G, nd))
if minerais:
chemins = [chemin for chemin in chemins if any(n in minerais for n in chemin)]
return chemins
def exporter_graphe_filtre(G, liens_chemins):
"""Gère l'export du graphe filtré au format DOT"""
G_export = nx.DiGraph()
for u, v in liens_chemins:
G_export.add_node(u, **G.nodes[u])
G_export.add_node(v, **G.nodes[v])
data = G.get_edge_data(u, v)
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
G_export.add_edge(u, v, **data[0])
elif isinstance(data, dict):
G_export.add_edge(u, v, **data)
else:
G_export.add_edge(u, v)
return(G_export)
def extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux):
"""Extrait les liens des chemins en respectant les niveaux"""
liens = set()
for chemin in chemins:
for i in range(len(chemin) - 1):
u, v = chemin[i], chemin[i + 1]
niveau_u = niveaux.get(u, 999)
niveau_v = niveaux.get(v, 999)
if (
(niveau_depart <= niveau_u <= niveau_arrivee or niveau_u in niveaux_speciaux)
and (niveau_depart <= niveau_v <= niveau_arrivee or niveau_v in niveaux_speciaux)
):
liens.add((u, v))
return liens
def interface_plan_d_actions(G_temp):
st.markdown(f"# {str(_('pages.plan_d_actions.title'))}")
if "plan_d_actions" not in st.session_state:
st.session_state["plan_d_actions"] = 0
if "g_md_done" not in st.session_state:
st.session_state["g_md_done"] = False
if "uuid" not in st.session_state:
st.session_state["uuid"] = str(uuid.uuid4())[:8]
st.session_state["G_dot"] = f"{JOBS}/{st.session_state["uuid"]}.dot"
st.session_state["G_md"] = f"{JOBS}/{st.session_state["uuid"]}.md"
if st.session_state["plan_d_actions"] == 0:
html_expander(f"{str(_('pages.plan_d_actions.help'))}", content="\n".join(_("pages.plan_d_actions.help_content")), open_by_default=False, details_class="details_introduction")
# Préparation du graphe
G_temp, niveaux_temp = preparer_graphe(G_temp)
# Sélection des niveaux
niveau_depart = 0
niveau_arrivee = 99
# Sélection fine des noeuds
noeuds_depart, noeuds_arrivee = selectionner_noeuds(G_temp, niveaux_temp, niveau_depart)
# Sélection des minerais si nécessaire
if noeuds_depart:
minerais = selectionner_minerais(G_temp, noeuds_depart)
# Étape 1 : Extraction des niveaux des nœuds
niveaux = extraire_niveaux(G_temp)
# Étape 2 : Extraction des chemins selon les critères
chemins = extraire_chemins_selon_criteres(G_temp, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
# Extraction des liens sans filtrage
liens_chemins = extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux)
else:
liens_chemins = None
if liens_chemins:
G_final = exporter_graphe_filtre(G_temp, liens_chemins)
st.session_state["G_final"] = G_final
# formulaire ou sélection
if st.button(str(_("pages.plan_d_actions.submit_request"))):
# On déclenche la suite — mais on NE traite rien maintenant
st.session_state["plan_d_actions"] = 1
st.rerun() # force la réexécution immédiatement avec état mis à jour
elif st.session_state["plan_d_actions"] == 1:
# Traitement lourd une seule fois
if not st.session_state["g_md_done"]:
write_dot(st.session_state["G_final"], st.session_state["G_dot"])
config = load_config()
graph, ref_graph = parse_graphs(st.session_state["G_dot"])
data = extract_data_from_graph(graph, ref_graph)
results = calculate_vulnerabilities(data, config)
report, file_names = generate_report(data, results, config)
write_report(report, st.session_state["G_md"])
st.session_state["g_md_done"] = True # pour ne pas re-traiter à chaque affichage
# Affichage de linterface Streamlit
initialiser_interface(st.session_state["G_md"])
if (st.button("Réinitialiser")):
st.session_state["plan_d_actions"] = 0
st.session_state["g_md_done"] = False
for f in JOBS.glob(f"*{st.session_state["uuid"]}*"):
if f.is_file():
f.unlink()
st.rerun()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,788 @@
import streamlit as st
import yaml
import re
import matplotlib.pyplot as plt
PRECONISATIONS = {
'Facile': [
"Constituer des stocks stratégiques.",
"Surveiller activement les signaux géopolitiques.",
"Renforcer la surveillance des régions critiques."
],
'Modérée': [
"Diversifier progressivement les fournisseurs.",
"Favoriser la modularité des produits.",
"Augmenter progressivement les taux de recyclage."
],
'Difficile': [
"Investir fortement en R&D pour la substitution.",
"Développer des technologies alternatives robustes.",
"Établir des partenariats stratégiques locaux solides."
],
'Extraction': {
'Facile': [
"Constituer des stocks « in-country » (site minier / port) pour 30 jours.",
"Activer un moniteur de prix spot quotidien.",
"Lancer une veille ESG locale (manifestations, météo extrême)."
],
'Modérée': [
"Négocier des contrats « take-or-pay » avec au moins 2 exploitants distincts.",
"Mettre en place un audit semestriel des pratiques de sécurité/logistique des mines.",
"Financer en co-investissement un entrepôt portuaire multi-produits."
],
'Difficile': [
"Participer au capital dun producteur émergent hors zone de concentration.",
"Obtenir des droits d« off-take » de 5 ans sur 20 % de la production dune mine alternative.",
"Soutenir (CAPEX) louverture dune nouvelle voie ferroviaire ou portuaire sécurisée."
]
},
'Traitement': {
'Facile': [
"Sécuriser un stock tampon sur site (90 jours).",
"Faire certifier la traçabilité chimique du concentré."
],
'Modérée': [
"Valider un second affineur dans une région politiquement stable.",
"Imposer des clauses « force-majeure » limitant larrêt total à 48 h."
],
'Difficile': [
"Co-développer un site de raffinage dans une zone « friend-shore ».",
"Financer un procédé de purification à rendement plus élevé (réduit la dépendance au minerai primaire)."
]
},
'Fabrication': {
'Facile': [
"Mettre un seuil minimal de sécurité (45 jours) sur le composant critique en usine SMT.",
"Suivre hebdomadairement la capacité libre des fondeurs/EMS."
],
'Modérée': [
"Dual-sourcer le composant critique intégrant un minerai critique (au moins 30 % chez un second fondeur).",
"Déployer le « design-for-substitution » : même PCB compatible avec le composant concerné."
],
'Difficile': [
"Lancer un programme R&D de substitution ou d'alternative budgeté sur 3 ans.",
"Contractualiser un accord exclusif avec un fondeur hors zone rouge pour 25 % des volumes."
]
},
'Assemblage': {
'Facile': [
"Allonger la rotation des stocks de produits finis (en aval) pour amortir un retard de 2 semaines.",
"Mettre en place un plan de re-déploiement du personnel sur dautres lignes en cas de rupture composant."
],
'Modérée': [
"Avoir un site dassemblage secondaire (low-volume) dans une région verte, testé tous les 6 mois.",
"Segmenter les nomenclatures : version « premium » avec composant haut de gamme, version « fallback » avec composant moins critique."
],
'Difficile': [
"Investir dans une plateforme dassemblage flexible (robots modulaires) capable de basculer vers un composant de substitution en < 72 h.",
"Signer un accord gouvernemental pour un soutien logistique prioritaire (corridor aérien dédié) en cas de crise géopolitique."
]
}
}
INDICATEURS = {
'Facile': [
"Suivi régulier de la stabilité géopolitique (ISG).",
"Durée réelle d'utilisation du matériel.",
"Niveau des stocks stratégiques disponibles."
],
'Modérée': [
"Taux de diversification des fournisseurs par région.",
"Évolution trimestrielle de la concurrence intersectorielle (IVC).",
"Taux annuel de recyclage des composants critiques."
],
'Difficile': [
"Budget annuel investi dans la recherche technologique.",
"Nombre de brevets déposés pour des substituts.",
"Progrès réel en matière de substitution technologique (ICS)."
],
'Extraction': {
'Facile': [
"Jours de stock portuaire (objectif ≥ 30).",
"Indice ISG moyen pondéré des pays extracteurs (alerte ≥ 60).",
"Volatilité hebdo du prix spot (écart-type %)."
],
'Modérée': [
"Part du 2ᵉ fournisseur dans le volume total (objectif ≥ 20 %).",
"Délai moyen dobtention des permis dexport."
],
'Difficile': [
"Capacité annuelle dune mine alternative financée (% du besoin interne).",
"Progrès physique de linfrastructure logistique (Km de voie, % achevé)."
]
},
'Traitement': {
'Facile': [
"Couverture stock tampon (jours).",
"Certificats de traçabilité obtenus (% lots)."
],
'Modérée': [
"Nombre daffineurs validés (objectif ≥ 2).",
"Taux de rendement global du procédé (%)."
],
'Difficile': [
"Part de production refinée hors zone rouge (%).",
"Capex cumulé investi dans de nouveaux procédés (M€)."
]
},
'Fabrication': {
'Facile': [
"Stock de composants critiques (jours).",
"Capacité libre des EMS (%) rapportée chaque vendredi."
],
'Modérée': [
"Part du second fondeur dans la production du composant audio (%).",
"Nombre de PCB « design-for-substitution » validés."
],
'Difficile': [
"Dépenses R&D substituts (€) vs budget.",
"TRI attendu sur les investisseurs fondeurs alternatifs."
]
},
'Assemblage': {
'Facile': [
"Jours de produits finis en entrepôt.",
"Temps de retouche ligne en cas de rupture (heures)."
],
'Modérée': [
"Volume annuel produit sur le site de secours (%).",
"Temps de requalification dune ligne vers la version « fallback »."
],
'Difficile': [
"Taux dautomatisation reconfigurable (% machines modulaires).",
"Nb dheures du corridor aérien prioritaire utilisé vs capacité."
]
}
}
# ------------------------- PARSEUR -------------------------
def parse_chains_md(filepath: str) -> tuple[dict, dict, dict, list, dict, dict]:
re_start_section = re.compile(r"^##\s*Chaînes\s+avec\s+risque\s+critique", re.IGNORECASE)
re_other_h2 = re.compile(r"^##\s+(?!(Chaînes\s+avec\s+risque\s+critique))")
re_chain_heading = re.compile(r"^###\s*(.+)\s*→\s*(.+)\s*→\s*(.+)$")
re_phase = re.compile(r"^\*\s*(Assemblage|Fabrication|Minerai|Extraction|Traitement)", re.IGNORECASE)
re_IHH = re.compile(r"IHH\s*[:]\s*([0-9]+(?:\.[0-9]+)?)")
re_ISG = re.compile(r"ISG\s*combiné\s*[:]\s*([0-9]+(?:\.[0-9]+)?)|ISG\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
re_ICS = re.compile(r"ICS\s*moyen\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
re_IVC = re.compile(r"IVC\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
produits, composants, mineraux, chains = {}, {}, {}, []
descriptions = {}
details_sections = {}
current_chain = None
current_phase = None
current_section = None
in_section = False
with open(filepath, encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
if not in_section:
if re_start_section.match(line):
in_section = True
continue
if re_other_h2.match(line):
break
m_chain = re_chain_heading.match(line)
if m_chain:
prod, comp, miner = map(str.strip, m_chain.groups())
produits.setdefault(prod, {"IHH_Assemblage": None, "ISG_Assemblage": None})
composants.setdefault(comp, {"IHH_Fabrication": None, "ISG_Fabrication": None})
mineraux.setdefault(miner, {
"ICS": None, "IVC": None,
"IHH_Extraction": None, "ISG_Extraction": None,
"IHH_Traitement": None, "ISG_Traitement": None
})
chains.append({"produit": prod, "composant": comp, "minerai": miner})
current_chain = {"prod": prod, "comp": comp, "miner": miner}
current_phase = None
current_section = f"{prod}{comp}{miner}"
descriptions[current_section] = ""
continue
if current_chain is None:
continue
m_phase = re_phase.match(line)
if m_phase:
current_phase = m_phase.group(1).capitalize()
continue
if current_phase:
p = current_chain
if current_phase == "Assemblage":
if (m := re_IHH.search(line)):
produits[p["prod"]]["IHH_Assemblage"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
produits[p["prod"]]["ISG_Assemblage"] = float(raw)
continue
if current_phase == "Fabrication":
if (m := re_IHH.search(line)):
composants[p["comp"]]["IHH_Fabrication"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
composants[p["comp"]]["ISG_Fabrication"] = float(raw)
continue
if current_phase == "Minerai":
if (m := re_ICS.search(line)):
mineraux[p["miner"]]["ICS"] = float(m.group(1))
continue
if (m := re_IVC.search(line)):
mineraux[p["miner"]]["IVC"] = float(m.group(1))
continue
if current_phase == "Extraction":
if (m := re_IHH.search(line)):
mineraux[p["miner"]]["IHH_Extraction"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
mineraux[p["miner"]]["ISG_Extraction"] = float(raw)
continue
if current_phase == "Traitement":
if (m := re_IHH.search(line)):
mineraux[p["miner"]]["IHH_Traitement"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
mineraux[p["miner"]]["ISG_Traitement"] = float(raw)
continue
else:
if current_section:
descriptions[current_section] += raw_line
# Parse detailed sections from the complete file
with open(filepath, encoding="utf-8") as f:
content = f.read()
# Extract sections using regex patterns
lines = content.split('\n')
# Find section boundaries
operations_start = None
minerais_start = None
for i, line in enumerate(lines):
if line.strip() == "## Détails des opérations":
operations_start = i
elif line.strip() == "## Détails des minerais":
minerais_start = i
if operations_start is not None:
# Parse operations section (assemblage and fabrication)
operations_end = minerais_start if minerais_start else len(lines)
operations_lines = lines[operations_start:operations_end]
current_section_name = None
current_content = []
for line in operations_lines:
if line.startswith("### ") and " et " in line:
# Save previous section
if current_section_name and current_content:
details_sections[current_section_name] = '\n'.join(current_content)
# Start new section
section_title = line.replace("### ", "").strip()
if " et Assemblage" in section_title:
product_name = section_title.replace(" et Assemblage", "").strip()
current_section_name = f"{product_name}_assemblage"
elif " et Fabrication" in section_title:
component_name = section_title.replace(" et Fabrication", "").strip()
current_section_name = f"{component_name}_fabrication"
current_content = []
elif current_section_name:
current_content.append(line)
# Save last section
if current_section_name and current_content:
details_sections[current_section_name] = '\n'.join(current_content)
if minerais_start is not None:
# Parse minerais section
minerais_lines = lines[minerais_start:]
current_minerai = None
current_section_type = "general"
current_content = []
for line in minerais_lines:
if line.startswith("### ") and "" not in line and " et " not in line:
# Save previous section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
# Start new minerai
current_minerai = line.replace("### ", "").strip()
current_section_type = "general"
current_content = []
elif line.startswith("#### Extraction"):
# Save previous section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
current_section_type = "extraction"
current_content = []
elif line.startswith("#### Traitement"):
# Save previous section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
current_section_type = "traitement"
current_content = []
elif line.startswith("## ") and current_minerai:
# End of minerais section
if current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
break
elif current_minerai:
current_content.append(line)
# Save last section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
return produits, composants, mineraux, chains, descriptions, details_sections
# ---------------------- AFFICHAGE ----------------------
def afficher_bloc_ihh_isg(titre, ihh, isg, details_content=""):
st.markdown(f"### {titre}")
if not details_content:
st.markdown("Données non disponibles")
return
lines = details_content.split('\n')
# 1. Afficher vulnérabilité combinée en premier
if "#### Vulnérabilité combinée IHH-ISG" in details_content:
st.markdown("#### Vulnérabilité combinée IHH-ISG")
afficher_section_texte(lines, "#### Vulnérabilité combinée IHH-ISG", "###")
# 2. Afficher ISG des pays impliqués
if "##### ISG des pays impliqués" in details_content:
print(details_content)
st.markdown("#### ISG des pays impliqués")
afficher_section_avec_tableau(lines, "##### ISG des pays impliqués")
# Afficher le résumé ISG combiné
for line in lines:
if "**ISG combiné:" in line:
st.markdown(line)
break
# 3. Afficher la section IHH complète
if "#### Indice de Herfindahl-Hirschmann" in details_content:
st.markdown("#### Indice de Herfindahl-Hirschmann")
# Tableau de résumé IHH
afficher_section_avec_tableau(lines, "#### Indice de Herfindahl-Hirschmann")
# IHH par entreprise
if "##### IHH par entreprise (acteurs)" in details_content:
st.markdown("##### IHH par entreprise (acteurs)")
afficher_section_texte(lines, "##### IHH par entreprise (acteurs)", "##### IHH par pays")
# IHH par pays
if "##### IHH par pays" in details_content:
st.markdown("##### IHH par pays")
afficher_section_texte(lines, "##### IHH par pays", "##### En résumé")
# En résumé
if "##### En résumé" in details_content:
st.markdown("##### En résumé")
afficher_section_texte(lines, "##### En résumé", "####")
def afficher_section_avec_tableau(lines, section_start, section_end=None):
"""Affiche une section contenant un tableau"""
in_section = False
table_lines = []
for line in lines:
if section_start in line:
in_section = True
continue
elif in_section and section_end and section_end in line:
break
elif in_section and line.startswith('#') and section_start not in line:
break
elif in_section:
if line.strip().startswith('|'):
table_lines.append(line)
elif table_lines and not line.strip().startswith('|'):
# Fin du tableau
break
if table_lines:
st.markdown('\n'.join(table_lines))
def afficher_section_texte(lines, section_start, section_end_marker=None):
"""Affiche le texte d'une section sans les tableaux"""
in_section = False
for line in lines:
if section_start in line:
in_section = True
continue
elif in_section and section_end_marker and line.startswith(section_end_marker):
break
elif in_section and line.startswith('#') and section_start not in line:
break
elif in_section and line.strip() and not line.strip().startswith('|'):
st.markdown(line)
def afficher_description(titre, description):
st.markdown(f"## {titre}")
if description:
lines = description.split('\n')
description_lines = []
# Extraire le premier paragraphe descriptif
for line in lines:
line = line.strip()
if not line:
if description_lines: # Si on a déjà du contenu, une ligne vide termine le paragraphe
break
continue
# Arrêter aux titres de sections ou tableaux
if (line.startswith('####') or
line.startswith('|') or
line.startswith('**Unité')):
break
description_lines.append(line)
if description_lines:
# Rejoindre les lignes en un seul paragraphe
full_description = ' '.join(description_lines)
st.markdown(full_description)
else:
st.markdown("Description non disponible")
else:
st.markdown("Description non disponible")
def afficher_caracteristiques_minerai(minerai, mineraux_data, details_content=""):
st.markdown("### Caractéristiques générales")
if not details_content:
st.markdown("Données non disponibles")
return
lines = details_content.split('\n')
# 3. Afficher la vulnérabilité combinée ICS-IVC en dernier
if "#### Vulnérabilité combinée ICS-IVC" in details_content:
st.markdown("#### Vulnérabilité combinée ICS-IVC")
afficher_section_texte(lines, "#### Vulnérabilité combinée ICS-IVC", "####")
# 1. Afficher la section ICS complète
if "#### ICS" in details_content:
st.markdown("#### ICS")
# Afficher le premier tableau ICS (avec toutes les colonnes)
afficher_section_avec_tableau(lines, "#### ICS", "##### Valeurs d'ICS par composant")
# Afficher la sous-section "Valeurs d'ICS par composant"
if "##### Valeurs d'ICS par composant" in details_content:
st.markdown("##### Valeurs d'ICS par composant")
afficher_section_avec_tableau(lines, "##### Valeurs d'ICS par composant", "**ICS moyen")
# Afficher le résumé ICS moyen
for line in lines:
if "**ICS moyen" in line:
st.markdown(line)
break
# 2. Afficher la section IVC complète
if "#### IVC" in details_content:
st.markdown("#### IVC")
# Afficher la valeur IVC principale
for line in lines:
if "**IVC:" in line:
st.markdown(line)
break
# Afficher tous les détails de la section IVC
afficher_section_texte(lines, "#### IVC", "#### Vulnérabilité combinée ICS-IVC")
def get_seuil(seuils_dict, key):
try:
if key in seuils_dict:
data = seuils_dict[key]
for niveau in ["rouge", "orange", "vert"]:
if niveau in data:
seuil = data[niveau]
if "min" in seuil and seuil["min"] is not None:
return seuil["min"]
if "max" in seuil and seuil["max"] is not None:
return seuil["max"]
except:
pass
return None
def set_vulnerability(v1, v2, t1, t2, seuils):
v1_poids = 1
v1_couleur = "Vert"
if v1 > seuils[t1]["rouge"]["min"]:
v1_poids = 3
v1_couleur = "Rouge"
elif v1 > seuils[t1]["vert"]["max"]:
v1_poids = 2
v1_couleur = "Orange"
v2_poids = 1
v2_couleur = "Vert"
if v2 > seuils[t2]["rouge"]["min"]:
v2_poids = 3
v2_couleur = "Rouge"
elif v2 > seuils[t2]["vert"]["max"]:
v2_poids = 2
v2_couleur = "Orange"
poids = v1_poids * v2_poids
couleur = "Rouge"
if poids <= 2:
couleur = "Vert"
elif poids <= 4:
couleur = "Orange"
return poids, couleur, v1_couleur, v2_couleur
def initialiser_interface(filepath: str, config_path: str = "assets/config.yaml"):
produits, composants, mineraux, chains, descriptions, details_sections = parse_chains_md(filepath)
if not chains:
st.warning("Aucune chaîne critique trouvée dans le fichier.")
return
seuils = {}
try:
with open(config_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
seuils = config.get("seuils", seuils)
except FileNotFoundError:
st.warning(f"Fichier de configuration {config_path} non trouvé.")
col_left, col_right = st.columns([1, 1], gap="small", border=True)
with col_left:
st.markdown("**<u>Panneau de sélection</u>**", unsafe_allow_html=True)
produits_disponibles = sorted({c["produit"] for c in chains})
sel_prod = st.selectbox("Produit", produits_disponibles)
composants_dispo = sorted({c["composant"] for c in chains if c["produit"] == sel_prod})
sel_comp = st.selectbox("Composant", composants_dispo)
mineraux_dispo = sorted({c["minerai"] for c in chains if c["produit"] == sel_prod and c["composant"] == sel_comp})
sel_miner = st.selectbox("Minerai", mineraux_dispo)
with col_right:
st.markdown("**<u>Synthèse des criticités</u>**", unsafe_allow_html=True)
poids_A, couleur_A, couleur_A_ihh, couleur_A_isg = set_vulnerability(produits[sel_prod]["IHH_Assemblage"], produits[sel_prod]["ISG_Assemblage"], "IHH", "ISG", seuils)
poids_F, couleur_F, couleur_F_ihh, couleur_F_isg = set_vulnerability(composants[sel_comp]["IHH_Fabrication"], composants[sel_comp]["ISG_Fabrication"], "IHH", "ISG", seuils)
poids_T, couleur_T, couleur_T_ihh, couleur_T_isg = set_vulnerability(mineraux[sel_miner]["IHH_Traitement"], mineraux[sel_miner]["ISG_Traitement"], "IHH", "ISG", seuils)
poids_E, couleur_E, couleur_E_ihh, couleur_E_isg = set_vulnerability(mineraux[sel_miner]["IHH_Extraction"], mineraux[sel_miner]["ISG_Extraction"], "IHH", "ISG", seuils)
poids_M, couleur_M, couleur_M_ics, couleur_M_ivc = set_vulnerability(mineraux[sel_miner]["ICS"], mineraux[sel_miner]["IVC"], "ICS", "IVC", seuils)
st.markdown(f"* **{sel_prod} - Assemblage** : {couleur_A} ({poids_A})")
st.markdown(f"* **{sel_comp} - Fabrication** : {couleur_F} ({poids_F})")
st.markdown(f"* **{sel_miner} - Traitement** : {couleur_T} ({poids_T})")
st.markdown(f"* **{sel_miner} - Extraction** : {couleur_E} ({poids_E})")
st.markdown(f"* **{sel_miner} - Minerai** : {couleur_M} ({poids_M})")
poids_operation = {
'Extraction': 1,
'Traitement': 1.5,
'Assemblage': 1.5,
'Fabrication': 2,
'Substitution': 2
}
poids_total = (\
poids_A * poids_operation["Assemblage"] + \
poids_F * poids_operation["Fabrication"] + \
poids_T * poids_operation["Traitement"] + \
poids_E * poids_operation["Extraction"] + \
poids_M * poids_operation["Substitution"] \
) / sum(poids_operation.values())
if poids_total < 3:
criticite_chaine = "Modérée"
niveau_criticite = {"Facile"}
elif poids_total < 6:
criticite_chaine = "Élevée"
niveau_criticite = {"Facile", "Modérée"}
else:
criticite_chaine = "Critique"
niveau_criticite = {"Facile", "Modérée", "Difficile"}
st.info(f"**Criticité globale : {criticite_chaine} ({poids_total})**")
with st.expander("Vue densemble des criticités", expanded=True):
st.markdown("## Vue densemble des criticités", unsafe_allow_html=True)
col_left, col_right = st.columns([1, 1], gap="small", border=True)
with col_left:
fig1, ax1 = plt.subplots(figsize=(1, 1))
ax1.scatter([produits[sel_prod]["ISG_Assemblage"]], [produits[sel_prod]["IHH_Assemblage"]], label="Assemblage".ljust(20), s=5)
ax1.scatter([composants[sel_comp]["ISG_Fabrication"]], [composants[sel_comp]["IHH_Fabrication"]], label="Fabrication".ljust(20), s=5)
ax1.scatter([mineraux[sel_miner]["ISG_Extraction"]], [mineraux[sel_miner]["IHH_Extraction"]], label="Extraction".ljust(20), s=5)
ax1.scatter([mineraux[sel_miner]["ISG_Traitement"]], [mineraux[sel_miner]["IHH_Traitement"]], label="Traitement".ljust(20), s=5)
# Seuils ISG (vertical)
ax1.axvline(seuils["ISG"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax1.axvline(seuils["ISG"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
# Seuils IHH (horizontal)
ax1.axhline(seuils["IHH"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax1.axhline(seuils["IHH"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
ax1.set_xlim(0, 100)
ax1.set_ylim(0, 100)
ax1.set_xlabel("ISG", fontsize=4)
ax1.set_ylabel("IHH", fontsize=4)
ax1.tick_params(axis='both', which='major', labelsize=4)
ax1.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=4)
plt.tight_layout()
st.pyplot(fig1)
with col_right:
fig2, ax2 = plt.subplots(figsize=(1, 1))
ax2.scatter([mineraux[sel_miner]["IVC"]], [mineraux[sel_miner]["ICS"]], color='green', s=5, label=sel_miner.ljust(20))
# Seuils IVC (vertical)
ax2.axvline(seuils["IVC"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax2.axvline(seuils["IVC"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
# Seuils ICS (horizontal)
ax2.axhline(seuils["ICS"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax2.axhline(seuils["ICS"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
ax2.set_xlim(0, max(100, mineraux[sel_miner]["IVC"]))
ax2.set_ylim(0, 1)
ax2.set_xlabel("IVC", fontsize=4)
ax2.set_ylabel("ICS", fontsize=4)
ax2.tick_params(axis='both', which='major', labelsize=4)
ax2.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=4)
plt.tight_layout()
st.pyplot(fig2)
st.markdown("""
Les lignes pointillées vertes et rouges représentent les seuils des indices concernés.\n
Les indices ISG (stabilité géopolitique) et IVC (concurrence intersectorielle) influent sur la probabilité de survenance d'un risque.\n
Les indices IHH (concentration géographique) et ICS (capacité de substitution) influent sur le niveau d'impact d'un risque.\n
Une opération se trouvant au-dessus des deux seuils a donc une forte probabilité d'être impactée avec un fort impact sur la capacité à continuer la production.
""")
with st.expander("Explications et détails", expanded = True):
from collections import Counter
couleurs = [couleur_A, couleur_F, couleur_T, couleur_E, couleur_M]
compte = Counter(couleurs)
nb_rouge = compte["Rouge"]
nb_orange = compte["Orange"]
st.markdown(f"""
Pour cette chaîne **{sel_prod} => {sel_comp} => {sel_miner}**, avec {nb_rouge} criticité(s) Rouge(s), {nb_orange} criticité(s) Orange(s), les indices individuels par opération sont :
* **{sel_prod} - Assemblage** : {couleur_A} ({poids_A})
* IHH = {produits[sel_prod]["IHH_Assemblage"]} ({couleur_A_ihh}) ISG = {produits[sel_prod]["ISG_Assemblage"]} ({couleur_A_isg})
* pondération de l'Assemblage dans le calcul de la criticité globale : 1,5
* se référer à **{sel_prod} et Assemblage** plus bas pour le détail complet
* **{sel_comp} - Fabrication** : {couleur_F} ({poids_F})
* IHH = {composants[sel_comp]["IHH_Fabrication"]} ({couleur_F_ihh}) ISG = {composants[sel_comp]["ISG_Fabrication"]} ({couleur_F_isg})
* pondération de la Fabrication dans le calcul de la criticité globale : 2
* se référer à **{sel_comp} et Fabrication** plus bas pour le détail complet
* **{sel_miner} - Traitement** : {couleur_A} ({poids_A})
* IHH = {mineraux[sel_miner]["IHH_Traitement"]} ({couleur_T_ihh}) ISG = {mineraux[sel_miner]["ISG_Traitement"]} ({couleur_T_isg})
* pondération du Traitement dans le calcul de la criticité globale : 1,5
* se référer à **{sel_miner} Vue globale** plus bas pour le détail complet de l'ensemble du minerai
* **{sel_miner} - Extraction** : {couleur_F} ({poids_F})
* IHH = {mineraux[sel_miner]["IHH_Extraction"]} ({couleur_E_ihh}) ISG = {mineraux[sel_miner]["ISG_Extraction"]} ({couleur_E_isg})
* pondération de l'Extraction dans le calcul de la criticité globale : 1
* **{sel_miner} - Minerai** : {couleur_F} ({poids_F})
* ICS = {mineraux[sel_miner]["ICS"]} ({couleur_M_ics}) IVC = {mineraux[sel_miner]["IVC"]} ({couleur_M_ivc})
* pondération de la Substitution dans le calcul de la criticité globale : 2
""")
with st.expander("Préconisations et indicateurs génériques"):
col_left, col_right = st.columns([1, 1], gap="small", border=True)
with col_left:
st.markdown("### Préconisations :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in PRECONISATIONS.items():
if niveau in niveau_criticite:
contenu_md = f"* {niveau}\n"
for p in PRECONISATIONS[niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
with col_right:
st.markdown("### Indicateurs :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in INDICATEURS.items():
if niveau in niveau_criticite:
contenu_md = f"* {niveau}\n"
for p in INDICATEURS[niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
def affectation_poids(poids_operation):
if poids_operation < 3:
niveau_criticite = {"Facile"}
elif poids_operation < 6:
niveau_criticite = {"Facile", "Modérée"}
else:
niveau_criticite = {"Facile", "Modérée", "Difficile"}
return niveau_criticite
niveau_criticite_operation = {}
niveau_criticite_operation["Assemblage"] = affectation_poids(poids_A)
niveau_criticite_operation["Fabrication"] = affectation_poids(poids_F)
niveau_criticite_operation["Traitement"] = affectation_poids(poids_T)
niveau_criticite_operation["Extraction"] = affectation_poids(poids_E)
for operation in ["Assemblage", "Fabrication", "Traitement", "Extraction"]:
with st.expander(f"Préconisations et indicateurs spécifiques - {operation}"):
st.markdown(f"### {operation}")
col_left, col_right = st.columns([1, 1], gap="small", border=True)
with col_left:
st.markdown("#### Préconisations :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in PRECONISATIONS[operation].items():
if niveau in niveau_criticite_operation[operation]:
contenu_md = f"* {niveau}\n"
for p in PRECONISATIONS[operation][niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
with col_right:
st.markdown("#### Indicateurs :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in INDICATEURS[operation].items():
if niveau in niveau_criticite_operation[operation]:
contenu_md = f"* {niveau}\n"
for p in INDICATEURS[operation][niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
with st.expander(f"{sel_prod} et Assemblage"):
assemblage_details = details_sections.get(f"{sel_prod}_assemblage", "")
afficher_description(f"{sel_prod} et Assemblage", assemblage_details)
afficher_bloc_ihh_isg("Assemblage", produits[sel_prod]["IHH_Assemblage"], produits[sel_prod]["ISG_Assemblage"], assemblage_details)
with st.expander(f"{sel_comp} et Fabrication"):
fabrication_details = details_sections.get(f"{sel_comp}_fabrication", "")
afficher_description(f"{sel_comp} et Fabrication", fabrication_details)
afficher_bloc_ihh_isg("Fabrication", composants[sel_comp]["IHH_Fabrication"], composants[sel_comp]["ISG_Fabrication"], fabrication_details)
with st.expander(f"{sel_miner} — Vue globale"):
minerai_general = details_sections.get(f"{sel_miner}_general", "")
afficher_description(f"{sel_miner} — Vue globale", minerai_general)
extraction_details = details_sections.get(f"{sel_miner}_extraction", "")
afficher_bloc_ihh_isg("Extraction", mineraux[sel_miner]["IHH_Extraction"], mineraux[sel_miner]["ISG_Extraction"], extraction_details)
traitement_details = details_sections.get(f"{sel_miner}_traitement", "")
afficher_bloc_ihh_isg("Traitement", mineraux[sel_miner]["IHH_Traitement"], mineraux[sel_miner]["ISG_Traitement"], traitement_details)
afficher_caracteristiques_minerai(sel_miner, mineraux[sel_miner], minerai_general)

View File

@ -44,6 +44,7 @@
"personnalisation": "Customization",
"analyse": "Analysis",
"ia_nalyse": "AI'nalysis",
"plan_d_actions": "Actions plan",
"visualisations": "Visualizations",
"fiches": "Cards"
},
@ -148,6 +149,26 @@
"submit_request": "Submit your request",
"empty_graph": "The graph is empty. Please make another selection."
},
"plan_d_actions": {
"title": "Graph analysis for action",
"help": "How to use this tab?",
"help_content": [
"The graph covers all levels, from end products to geographic countries.\n",
"1. You can select minerals through which the paths go.",
"2. You can choose end products to perform an analysis tailored to your context.\n",
"These two selections are optional, but strongly recommended for a more relevant analysis.",
"The recommendations for actions and indicator monitoring are generic. They must therefore be adapted to the context.",
"The proposed actions or indicators depend on the type of organization concerned and can be applied directly or required of digital providers."
],
"select_minerals": "Select one or more minerals",
"filter_by_minerals": "Filter by minerals (optional, but highly recommended)",
"fine_selection": "End product selection",
"filter_start_nodes": "Filter by start nodes (optional, but recommended)",
"run_analysis": "Run analysis",
"confirm_download": "Confirm download",
"submit_request": "Submit your request",
"empty_graph": "The graph is empty. Please make another selection."
},
"visualisations": {
"title": "Visualizations",
"help": "How to use this tab?",

View File

@ -44,6 +44,7 @@
"personnalisation": "Personnalisation",
"analyse": "Analyse",
"ia_nalyse": "IA'nalyse",
"plan_d_actions": "Plan d'actions",
"visualisations": "Visualisations",
"fiches": "Fiches"
},
@ -148,6 +149,26 @@
"submit_request": "Soumettre votre demande",
"empty_graph": "Le graphe est vide. Veuillez faire une autre sélection."
},
"plan_d_actions": {
"title": "Analyse du graphe pour action",
"help": "Comment utiliser cet onglet ?",
"help_content": [
"Le graphe intègre l'ensemble des niveaux, des produits finaux aux pays géographiques.\n",
"1. Vous pouvez sélectionner des minerais par lesquels passent les chemins.",
"2. Vous pouvez choisir des produits finaux pour faire une analyse adaptée à votre contexte.\n",
" Ces deux sélections sont optionnelles, mais fortement recommandées pour avoir une meilleure pertinence de l'analyse.",
"Les préconisations d'actions et de suivi d'indicateurs sont génériques. Elles doivent donc être adaptées au contexte.",
"Les actions ou les indicateurs proposés dépendent du type d'organisation concernée et peuvent être appliquées directement ou exigées des fournisseurs de numérique."
],
"select_minerals": "Sélectionner un ou plusieurs minerais",
"filter_by_minerals": "Filtrer par minerais (optionnel, mais recommandé)",
"fine_selection": "Sélection des produits finaux",
"filter_start_nodes": "Filtrer par noeuds de départ (optionnel, mais recommandé)",
"run_analysis": "Lancer l'analyse",
"confirm_download": "Confirmer le téléchargement",
"submit_request": "Soumettre votre demande",
"empty_graph": "Le graphe est vide. Veuillez faire une autre sélection."
},
"visualisations": {
"title": "Visualisations",
"help": "Comment utiliser cet onglet ?",

27
batch_ia/__init__.py Normal file
View File

@ -0,0 +1,27 @@
# batch_ia/__init__.py
# config.py
from .utils.config import TEMPLATE_PATH, load_config
# files.py
from .utils.files import write_report
# graphs.py
from .utils.graphs import (
parse_graphs,
extract_data_from_graph,
calculate_vulnerabilities
)
# sections.py
from .utils.sections import generate_report
__all__ = [
"TEMPLATE_PATH",
"load_config",
"write_report",
"parse_graphs",
"extract_data_from_graph",
"calculate_vulnerabilities",
"generate_report",
]

View File

@ -0,0 +1 @@

View File

@ -1,7 +1,7 @@
import os
import re
from utils.config import (
from .config import (
CORPUS_DIR
)

View File

@ -2,7 +2,7 @@ import os
import sys
from networkx.drawing.nx_agraph import read_dot
from utils.config import (
from .config import (
REFERENCE_GRAPH_PATH,
determine_threshold_color, get_weight_for_color
)

View File

@ -1,20 +1,20 @@
import os
import re
from utils.config import (
from .config import (
CORPUS_DIR,
TEMPLATE_PATH,
determine_threshold_color
)
from utils.files import (
from .files import (
find_prefixed_directory,
find_corpus_file,
write_report,
read_corpus_file
)
from utils.sections_utils import (
from .sections_utils import (
trouver_dossier_composant,
extraire_sections_par_mot_cle
)
@ -302,7 +302,7 @@ def generate_operations_section(data, results, config):
manufacturing_id = component["manufacturing"]
operation = data["operations"][manufacturing_id]
template.append("#### ISG des pays impliqués\n")
template.append("##### ISG des pays impliqués\n")
template.append("| Pays | Part de marché | ISG | Criticité |")
template.append("| :-- | :-- | :-- | :-- |")

View File

@ -3,7 +3,7 @@ import re
from pathlib import Path
from collections import defaultdict
from utils.config import (
from .config import (
CORPUS_DIR
)

View File

@ -22,6 +22,7 @@ def afficher_menu():
str(_("navigation.personnalisation")),
str(_("navigation.analyse")),
*([str(_("navigation.ia_nalyse"))] if st.session_state.get("logged_in", False) else []),
*([str(_("navigation.plan_d_actions"))]),
str(_("navigation.visualisations")),
str(_("navigation.fiches"))
]

View File

@ -79,6 +79,7 @@ from app.visualisations import interface_visualisations
from app.personnalisation import interface_personnalisation
from app.analyse import interface_analyse
from app.ia_nalyse import interface_ia_nalyse
from app.plan_d_actions import interface_plan_d_actions
# Initialisation des traductions (langue française par défaut)
init_translations()
@ -170,6 +171,7 @@ fiches_tab = _("navigation.fiches")
personnalisation_tab = _("navigation.personnalisation")
analyse_tab = _("navigation.analyse")
ia_nalyse_tab = _("navigation.ia_nalyse")
plan_d_actions_tab = _("navigation.plan_d_actions")
visualisations_tab = _("navigation.visualisations")
if st.session_state.onglet == instructions_tab:
@ -193,6 +195,10 @@ elif dot_file_path and st.session_state.onglet == ia_nalyse_tab:
G_temp = st.session_state["G_temp"]
interface_ia_nalyse(G_temp)
elif dot_file_path and st.session_state.onglet == plan_d_actions_tab:
G_temp = st.session_state["G_temp"]
interface_plan_d_actions(G_temp)
elif dot_file_path and st.session_state.onglet == visualisations_tab:
G_temp = st.session_state["G_temp"]
G_temp_ivc = st.session_state["G_temp_ivc"]

View File

@ -9648,7 +9648,7 @@ digraph Hierarchie_Composants_Electroniques_Simplifiee {
// Relations sortantes
Beryllium -> Reserves_Beryllium [];
Beryllium -> Extraction_Beryllium [];
Beryllium -> Traitement_Beryllium [];
subgraph cluster_Reserves_Beryllium {
label="Reserves_Beryllium";
@ -9732,6 +9732,65 @@ digraph Hierarchie_Composants_Electroniques_Simplifiee {
XinjiangNonferrous_Chine_Extraction_Beryllium -> Chine_geographique [color="darkgreen"];
}
}
subgraph cluster_Traitement_Beryllium {
label="Traitement_Beryllium";
fillcolor="#ffd699";
style="filled";
Traitement_Beryllium [fillcolor="#ffd699", ihh_acteurs="47", ihh_pays="47", label="Traitement", niveau="10"];
// Relations sortantes du nœud de niveau 10
Traitement_Beryllium -> USA_Traitement_Beryllium [color="purple", fontcolor="purple", label="65%", poids="3"];
Traitement_Beryllium -> Chine_Traitement_Beryllium [color="purple", fontcolor="purple", label="20%", poids="1"];
Traitement_Beryllium -> Russie_Traitement_Beryllium [color="purple", fontcolor="purple", label="10%", poids="1"];
subgraph cluster_USA_Traitement_Beryllium {
label="USA_Traitement_Beryllium";
fillcolor="#e6f2ff";
style="filled";
USA_Traitement_Beryllium [fillcolor="#e6f2ff", label="États-Unis", niveau="11"];
// Relations sortantes du nœud de niveau 11
USA_Traitement_Beryllium -> USA_geographique [color="darkgreen", poids="0", type_lien="10"];
USA_Traitement_Beryllium -> Materion_EtatUnis_Traitement_Beryllium [color="purple", fontcolor="purple", label="65%", poids="3"];
Materion_EtatUnis_Traitement_Beryllium [fillcolor="#d1e0ff", label="Materion Corporation", niveau="12"];
// Relations des nœuds destination
Materion_EtatUnis_Traitement_Beryllium -> USA_geographique [color="darkgreen", poids="0", type_lien="10"];
Materion_EtatUnis_Traitement_Beryllium -> EtatsUnis_Extraction_Beryllium [color="darkblue", fontcolor="darkblue", label="100%"];
}
subgraph cluster_Chine_Traitement_Beryllium {
label="Chine_Traitement_Beryllium";
fillcolor="#e6f2ff";
style="filled";
Chine_Traitement_Beryllium [fillcolor="#e6f2ff", label="Chine", niveau="11"];
// Relations sortantes du nœud de niveau 11
Chine_Traitement_Beryllium -> Chine_geographique [color="darkgreen", poids="0", type_lien="10"];
Chine_Traitement_Beryllium -> CNMCNickel_Chine_Traitement_Beryllium [color="purple", fontcolor="purple", label="20%", poids="1"];
CNMCNickel_Chine_Traitement_Beryllium [fillcolor="#d1e0ff", label="CNMC Nickel", niveau="12"];
// Relations des nœuds destination
CNMCNickel_Chine_Traitement_Beryllium -> Chine_geographique [color="darkgreen", poids="0", type_lien="10"];
CNMCNickel_Chine_Traitement_Beryllium -> Chine_Extraction_Beryllium [color="darkblue", fontcolor="darkblue", label="100%"];
}
subgraph cluster_Russie_Traitement_Beryllium {
label="Russie_Traitement_Beryllium";
fillcolor="#e6f2ff";
style="filled";
Russie_Traitement_Beryllium [fillcolor="#e6f2ff", label="Russie", niveau="11"];
// Relations sortantes du nœud de niveau 11
Russie_Traitement_Beryllium -> Russie_geographique [color="darkgreen", poids="0", type_lien="10"];
Russie_Traitement_Beryllium -> Ulba_Russie_Traitement_Beryllium [color="purple", fontcolor="purple", label="10%", poids="1"];
Ulba_Russie_Traitement_Beryllium [fillcolor="#d1e0ff", label="Ulba Metallurgical Plant", niveau="12"];
// Relations des nœuds destination
Ulba_Russie_Traitement_Beryllium -> Russie_geographique [color="darkgreen", poids="0", type_lien="10"];
}
}
}
subgraph cluster_Or {