Découpage modulaire de plan_da_action
This commit is contained in:
parent
16b1ad37d7
commit
9ca623aef1
@ -6,9 +6,7 @@ suivant la structure définie dans Remarques.md.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import networkx as nx
|
|
||||||
import uuid
|
import uuid
|
||||||
import re
|
|
||||||
from utils.translations import _
|
from utils.translations import _
|
||||||
from utils.widgets import html_expander
|
from utils.widgets import html_expander
|
||||||
from networkx.drawing.nx_agraph import write_dot
|
from networkx.drawing.nx_agraph import write_dot
|
||||||
@ -22,171 +20,27 @@ from batch_ia import (
|
|||||||
generate_report,
|
generate_report,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .plan_d_action import initialiser_interface
|
from app.plan_d_action.utils.data.plan_d_action import initialiser_interface
|
||||||
|
|
||||||
from utils.graph_utils import (
|
from app.plan_d_action.utils.interface.parser import preparer_graphe
|
||||||
extraire_chemins_depuis,
|
from app.plan_d_action.utils.interface.niveau_utils import extraire_niveaux
|
||||||
extraire_chemins_vers
|
from app.plan_d_action.utils.interface.selection import (
|
||||||
|
selectionner_minerais,
|
||||||
|
selectionner_noeuds,
|
||||||
|
extraire_chemins_selon_criteres
|
||||||
|
)
|
||||||
|
from app.plan_d_action.utils.interface.export import (
|
||||||
|
exporter_graphe_filtre,
|
||||||
|
extraire_liens_filtres
|
||||||
|
)
|
||||||
|
from app.plan_d_action.utils.interface.visualization import remplacer_par_badge
|
||||||
|
from app.plan_d_action.utils.interface.config import (
|
||||||
|
niveau_labels,
|
||||||
|
JOBS
|
||||||
)
|
)
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Répertoire courant du script
|
|
||||||
CURRENT_DIR = Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
# Répertoire "jobs" dans app/plan_d_action
|
|
||||||
JOBS = CURRENT_DIR / "jobs"
|
|
||||||
JOBS.mkdir(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()}
|
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_action.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_action.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_action.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_action.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
|
|
||||||
|
|
||||||
CORRESPONDANCE_COULEURS = {
|
|
||||||
"Rouge": "red",
|
|
||||||
"Orange": "orange",
|
|
||||||
"Vert": "green",
|
|
||||||
"FAIBLE": "green",
|
|
||||||
"MODÉRÉE": "orange",
|
|
||||||
"ÉLEVÉE à CRITIQUE": "red"
|
|
||||||
}
|
|
||||||
|
|
||||||
def remplacer_par_badge(markdown_text, correspondance=CORRESPONDANCE_COULEURS):
|
|
||||||
# Échappe les mots à remplacer s'ils contiennent des accents ou espaces
|
|
||||||
for mot, couleur in correspondance.items():
|
|
||||||
# Utilise des bords de mots (\b) pour éviter les remplacements partiels
|
|
||||||
pattern = r'\b' + re.escape(mot) + r'\b'
|
|
||||||
remplacement = f":{couleur}-badge[{mot}]"
|
|
||||||
markdown_text = re.sub(pattern, remplacement, markdown_text)
|
|
||||||
return markdown_text
|
|
||||||
|
|
||||||
def interface_plan_d_action(G_temp):
|
def interface_plan_d_action(G_temp):
|
||||||
st.markdown(f"# {str(_('pages.plan_d_action.title'))}")
|
st.markdown(f"# {str(_('pages.plan_d_action.title'))}")
|
||||||
|
|||||||
11344
app/plan_d_action/jobs/5b17bbfd.md
Normal file
11344
app/plan_d_action/jobs/5b17bbfd.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,821 +0,0 @@
|
|||||||
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 d’un producteur émergent hors zone de concentration.",
|
|
||||||
"Obtenir des droits d’« off-take » de 5 ans sur 20 % de la production d’une mine alternative.",
|
|
||||||
"Soutenir (CAPEX) l’ouverture d’une 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 l’arrêt total à 48 h.",
|
|
||||||
"Explorer les possibilités de recyclage et d'économie circulaire"
|
|
||||||
],
|
|
||||||
'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).",
|
|
||||||
"Constituer des réserves stratégiques pour les périodes de tension"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'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.",
|
|
||||||
"Maintenir une veille technologique sur les évolutions du marché"
|
|
||||||
],
|
|
||||||
'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é.",
|
|
||||||
"Optimiser les processus d'approvisionnement existants"
|
|
||||||
],
|
|
||||||
'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 d’autres lignes en cas de rupture composant."
|
|
||||||
],
|
|
||||||
'Modérée': [
|
|
||||||
"Avoir un site d’assemblage 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 d’assemblage 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.",
|
|
||||||
"Mettre en place des contrats à long terme avec des clauses de garantie d'approvisionnement"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 d’obtention des permis d’export."
|
|
||||||
],
|
|
||||||
'Difficile': [
|
|
||||||
"Capacité annuelle d’une mine alternative financée (% du besoin interne).",
|
|
||||||
"Progrès physique de l’infrastructure logistique (Km de voie, % achevé)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'Traitement': {
|
|
||||||
'Facile': [
|
|
||||||
"Couverture stock tampon (jours).",
|
|
||||||
"Certificats de traçabilité obtenus (% lots)."
|
|
||||||
],
|
|
||||||
'Modérée': [
|
|
||||||
"Nombre d’affineurs 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 d’une ligne vers la version « fallback »."
|
|
||||||
],
|
|
||||||
'Difficile': [
|
|
||||||
"Taux d’automatisation reconfigurable (% machines modulaires).",
|
|
||||||
"Nb d’heures 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:
|
|
||||||
conteneur, = st.columns([1], gap="small", border=True)
|
|
||||||
with conteneur:
|
|
||||||
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}")
|
|
||||||
conteneur, = st.columns([1], gap="small", border=True)
|
|
||||||
with conteneur:
|
|
||||||
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:
|
|
||||||
conteneur, = st.columns([1], gap="small", border=True)
|
|
||||||
with conteneur:
|
|
||||||
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 colorer_couleurs(la_couleur):
|
|
||||||
t = la_couleur.lower()
|
|
||||||
if t == "rouge" or t == "difficile":
|
|
||||||
return f":red-badge[{la_couleur}]"
|
|
||||||
if t == "orange" or t == "modérée":
|
|
||||||
return f":orange-badge[{la_couleur}]"
|
|
||||||
if t == "vert" or t == "facile":
|
|
||||||
return f":green-badge[{la_couleur}]"
|
|
||||||
return la_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** : {colorer_couleurs(couleur_A)} ({poids_A})")
|
|
||||||
st.markdown(f"* **{sel_comp} - Fabrication** : {colorer_couleurs(couleur_F)} ({poids_F})")
|
|
||||||
st.markdown(f"* **{sel_miner} - Traitement** : {colorer_couleurs(couleur_T)} ({poids_T})")
|
|
||||||
st.markdown(f"* **{sel_miner} - Extraction** : {colorer_couleurs(couleur_E)} ({poids_E})")
|
|
||||||
st.markdown(f"* **{sel_miner} - Minerai** : {colorer_couleurs(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.error(f"**Criticité globale : {criticite_chaine} ({poids_total})**")
|
|
||||||
|
|
||||||
with st.expander("Vue d’ensemble des criticités", expanded=True):
|
|
||||||
st.markdown("## Vue d’ensemble 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(f"""
|
|
||||||
Les lignes pointillées en {colorer_couleurs("vert")} ou {colorer_couleurs("rouge")} 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 niveau élevé sur l'incapacité à 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"]
|
|
||||||
nb_vert = compte["Vert"]
|
|
||||||
|
|
||||||
st.markdown(f"""
|
|
||||||
Pour cette chaîne :blue-background[**{sel_prod} <-> {sel_comp} <-> {sel_miner}**], avec {nb_rouge} criticité(s) de niveau {colorer_couleurs("Rouge")}, {nb_orange} {colorer_couleurs("Orange")} et {nb_vert} {colorer_couleurs("Vert")}, les indices individuels par opération sont :
|
|
||||||
|
|
||||||
* **{sel_prod} - Assemblage** : {colorer_couleurs(couleur_A)} ({poids_A})
|
|
||||||
* IHH = {produits[sel_prod]["IHH_Assemblage"]} ({colorer_couleurs(couleur_A_ihh)}) <-> ISG = {produits[sel_prod]["ISG_Assemblage"]} ({colorer_couleurs(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** : {colorer_couleurs(couleur_F)} ({poids_F})
|
|
||||||
* IHH = {composants[sel_comp]["IHH_Fabrication"]} ({colorer_couleurs(couleur_F_ihh)}) <-> ISG = {composants[sel_comp]["ISG_Fabrication"]} ({colorer_couleurs(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** : {colorer_couleurs(couleur_A)} ({poids_A})
|
|
||||||
* IHH = {mineraux[sel_miner]["IHH_Traitement"]} ({colorer_couleurs(couleur_T_ihh)}) <-> ISG = {mineraux[sel_miner]["ISG_Traitement"]} ({colorer_couleurs(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** : {colorer_couleurs(couleur_E)} ({poids_E})
|
|
||||||
* IHH = {mineraux[sel_miner]["IHH_Extraction"]} ({colorer_couleurs(couleur_E_ihh)}) <-> ISG = {mineraux[sel_miner]["ISG_Extraction"]} ({colorer_couleurs(couleur_E_isg)})
|
|
||||||
* pondération de l'Extraction dans le calcul de la criticité globale : 1
|
|
||||||
* **{sel_miner} - Minerai** : {colorer_couleurs(couleur_M)} ({poids_M})
|
|
||||||
* ICS = {mineraux[sel_miner]["ICS"]} ({colorer_couleurs(couleur_M_ics)}) <-> IVC = {mineraux[sel_miner]["IVC"]} ({colorer_couleurs(couleur_M_ivc)})
|
|
||||||
* pondération de la Substitution dans le calcul de la criticité globale : 2
|
|
||||||
""")
|
|
||||||
|
|
||||||
st.markdown("## Préconisations et indicateurs")
|
|
||||||
|
|
||||||
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"* {colorer_couleurs(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"* {colorer_couleurs(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"]:
|
|
||||||
if operation == "Assemblage":
|
|
||||||
item = sel_prod
|
|
||||||
elif operation == "Fabrication":
|
|
||||||
item = sel_comp
|
|
||||||
else:
|
|
||||||
item = sel_miner
|
|
||||||
with st.expander(f"Préconisations et indicateurs spécifiques - {operation}"):
|
|
||||||
st.markdown(f"### {operation} -> :blue-background[{item}]")
|
|
||||||
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"* {colorer_couleurs(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"* {colorer_couleurs(niveau)}\n"
|
|
||||||
for p in INDICATEURS[operation][niveau]:
|
|
||||||
contenu_md += f" - {p}\n"
|
|
||||||
st.markdown(contenu_md)
|
|
||||||
|
|
||||||
st.markdown("## Détails des opérations")
|
|
||||||
|
|
||||||
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", "").removesuffix("\n---\n")
|
|
||||||
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)
|
|
||||||
8
app/plan_d_action/utils/data/__init__.py
Normal file
8
app/plan_d_action/utils/data/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from .config import (
|
||||||
|
PRECONISATIONS,
|
||||||
|
INDICATEURS
|
||||||
|
)
|
||||||
|
from .data_utils import(
|
||||||
|
colorer_couleurs,
|
||||||
|
set_vulnerability
|
||||||
|
)
|
||||||
157
app/plan_d_action/utils/data/config.py
Normal file
157
app/plan_d_action/utils/data/config.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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 d’un producteur émergent hors zone de concentration.",
|
||||||
|
"Obtenir des droits d’« off-take » de 5 ans sur 20 % de la production d’une mine alternative.",
|
||||||
|
"Soutenir (CAPEX) l’ouverture d’une 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 l’arrêt total à 48 h.",
|
||||||
|
"Explorer les possibilités de recyclage et d'économie circulaire"
|
||||||
|
],
|
||||||
|
'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).",
|
||||||
|
"Constituer des réserves stratégiques pour les périodes de tension"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'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.",
|
||||||
|
"Maintenir une veille technologique sur les évolutions du marché"
|
||||||
|
],
|
||||||
|
'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é.",
|
||||||
|
"Optimiser les processus d'approvisionnement existants"
|
||||||
|
],
|
||||||
|
'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 d’autres lignes en cas de rupture composant."
|
||||||
|
],
|
||||||
|
'Modérée': [
|
||||||
|
"Avoir un site d’assemblage 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 d’assemblage 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.",
|
||||||
|
"Mettre en place des contrats à long terme avec des clauses de garantie d'approvisionnement"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 d’obtention des permis d’export."
|
||||||
|
],
|
||||||
|
'Difficile': [
|
||||||
|
"Capacité annuelle d’une mine alternative financée (% du besoin interne).",
|
||||||
|
"Progrès physique de l’infrastructure logistique (Km de voie, % achevé)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Traitement': {
|
||||||
|
'Facile': [
|
||||||
|
"Couverture stock tampon (jours).",
|
||||||
|
"Certificats de traçabilité obtenus (% lots)."
|
||||||
|
],
|
||||||
|
'Modérée': [
|
||||||
|
"Nombre d’affineurs 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 d’une ligne vers la version « fallback »."
|
||||||
|
],
|
||||||
|
'Difficile': [
|
||||||
|
"Taux d’automatisation reconfigurable (% machines modulaires).",
|
||||||
|
"Nb d’heures du corridor aérien prioritaire utilisé vs capacité."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
192
app/plan_d_action/utils/data/data_processing.py
Normal file
192
app/plan_d_action/utils/data/data_processing.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
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
|
||||||
67
app/plan_d_action/utils/data/data_utils.py
Normal file
67
app/plan_d_action/utils/data/data_utils.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import yaml
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
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 colorer_couleurs(la_couleur):
|
||||||
|
t = la_couleur.lower()
|
||||||
|
if t == "rouge" or t == "difficile":
|
||||||
|
return f":red-badge[{la_couleur}]"
|
||||||
|
if t == "orange" or t == "modérée":
|
||||||
|
return f":orange-badge[{la_couleur}]"
|
||||||
|
if t == "vert" or t == "facile":
|
||||||
|
return f":green-badge[{la_couleur}]"
|
||||||
|
return la_couleur
|
||||||
|
|
||||||
|
def initialiser_seuils(config_path):
|
||||||
|
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é.")
|
||||||
|
|
||||||
|
return seuils
|
||||||
167
app/plan_d_action/utils/data/pda_interface.py
Normal file
167
app/plan_d_action/utils/data/pda_interface.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
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:
|
||||||
|
conteneur, = st.columns([1], gap="small", border=True)
|
||||||
|
with conteneur:
|
||||||
|
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}")
|
||||||
|
conteneur, = st.columns([1], gap="small", border=True)
|
||||||
|
with conteneur:
|
||||||
|
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:
|
||||||
|
conteneur, = st.columns([1], gap="small", border=True)
|
||||||
|
with conteneur:
|
||||||
|
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")
|
||||||
294
app/plan_d_action/utils/data/plan_d_action.py
Normal file
294
app/plan_d_action/utils/data/plan_d_action.py
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from app.plan_d_action.utils.data.config import (
|
||||||
|
PRECONISATIONS,
|
||||||
|
INDICATEURS
|
||||||
|
)
|
||||||
|
from app.plan_d_action.utils.data.data_processing import parse_chains_md
|
||||||
|
from app.plan_d_action.utils.data.data_utils import (
|
||||||
|
set_vulnerability,
|
||||||
|
colorer_couleurs
|
||||||
|
)
|
||||||
|
from app.plan_d_action.utils.data.pda_interface import (
|
||||||
|
afficher_bloc_ihh_isg,
|
||||||
|
afficher_description,
|
||||||
|
afficher_caracteristiques_minerai
|
||||||
|
)
|
||||||
|
from app.plan_d_action.utils.data.data_utils import initialiser_seuils
|
||||||
|
|
||||||
|
def tableau_de_bord(chains, produits, composants, mineraux, seuils):
|
||||||
|
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** : {colorer_couleurs(couleur_A)} ({poids_A})")
|
||||||
|
st.markdown(f"* **{sel_comp} - Fabrication** : {colorer_couleurs(couleur_F)} ({poids_F})")
|
||||||
|
st.markdown(f"* **{sel_miner} - Traitement** : {colorer_couleurs(couleur_T)} ({poids_T})")
|
||||||
|
st.markdown(f"* **{sel_miner} - Extraction** : {colorer_couleurs(couleur_E)} ({poids_E})")
|
||||||
|
st.markdown(f"* **{sel_miner} - Minerai** : {colorer_couleurs(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.error(f"**Criticité globale : {criticite_chaine} ({poids_total})**")
|
||||||
|
|
||||||
|
return (
|
||||||
|
sel_prod, sel_comp, sel_miner, niveau_criticite,
|
||||||
|
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M,
|
||||||
|
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc
|
||||||
|
)
|
||||||
|
|
||||||
|
def afficher_criticites(produits, composants, mineraux, sel_prod, sel_comp, sel_miner, seuils):
|
||||||
|
with st.expander("Vue d’ensemble des criticités", expanded=True):
|
||||||
|
st.markdown("## Vue d’ensemble 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(f"""
|
||||||
|
Les lignes pointillées en {colorer_couleurs("vert")} ou {colorer_couleurs("rouge")} 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 niveau élevé sur l'incapacité à continuer la production.
|
||||||
|
""")
|
||||||
|
|
||||||
|
def afficher_explications_et_details(
|
||||||
|
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M,
|
||||||
|
produits, composants, mineraux, sel_prod, sel_comp, sel_miner,
|
||||||
|
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc):
|
||||||
|
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"]
|
||||||
|
nb_vert = compte["Vert"]
|
||||||
|
|
||||||
|
st.markdown(f"""
|
||||||
|
Pour cette chaîne :blue-background[**{sel_prod} <-> {sel_comp} <-> {sel_miner}**], avec {nb_rouge} criticité(s) de niveau {colorer_couleurs("Rouge")}, {nb_orange} {colorer_couleurs("Orange")} et {nb_vert} {colorer_couleurs("Vert")}, les indices individuels par opération sont :
|
||||||
|
|
||||||
|
* **{sel_prod} - Assemblage** : {colorer_couleurs(couleur_A)} ({poids_A})
|
||||||
|
* IHH = {produits[sel_prod]["IHH_Assemblage"]} ({colorer_couleurs(couleur_A_ihh)}) <-> ISG = {produits[sel_prod]["ISG_Assemblage"]} ({colorer_couleurs(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** : {colorer_couleurs(couleur_F)} ({poids_F})
|
||||||
|
* IHH = {composants[sel_comp]["IHH_Fabrication"]} ({colorer_couleurs(couleur_F_ihh)}) <-> ISG = {composants[sel_comp]["ISG_Fabrication"]} ({colorer_couleurs(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** : {colorer_couleurs(couleur_A)} ({poids_A})
|
||||||
|
* IHH = {mineraux[sel_miner]["IHH_Traitement"]} ({colorer_couleurs(couleur_T_ihh)}) <-> ISG = {mineraux[sel_miner]["ISG_Traitement"]} ({colorer_couleurs(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** : {colorer_couleurs(couleur_E)} ({poids_E})
|
||||||
|
* IHH = {mineraux[sel_miner]["IHH_Extraction"]} ({colorer_couleurs(couleur_E_ihh)}) <-> ISG = {mineraux[sel_miner]["ISG_Extraction"]} ({colorer_couleurs(couleur_E_isg)})
|
||||||
|
* pondération de l'Extraction dans le calcul de la criticité globale : 1
|
||||||
|
* **{sel_miner} - Minerai** : {colorer_couleurs(couleur_M)} ({poids_M})
|
||||||
|
* ICS = {mineraux[sel_miner]["ICS"]} ({colorer_couleurs(couleur_M_ics)}) <-> IVC = {mineraux[sel_miner]["IVC"]} ({colorer_couleurs(couleur_M_ivc)})
|
||||||
|
* pondération de la Substitution dans le calcul de la criticité globale : 2
|
||||||
|
""")
|
||||||
|
|
||||||
|
def afficher_preconisations_et_indicateurs_generiques(niveau_criticite, poids_A, poids_F, poids_T, poids_E, poids_M):
|
||||||
|
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"* {colorer_couleurs(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"* {colorer_couleurs(niveau)}\n"
|
||||||
|
for p in INDICATEURS[niveau]:
|
||||||
|
contenu_md += f" - {p}\n"
|
||||||
|
st.markdown(contenu_md)
|
||||||
|
|
||||||
|
def afficher_preconisations_et_indicateurs_specifiques(sel_prod, sel_comp, sel_miner, niveau_criticite_operation):
|
||||||
|
for operation in ["Assemblage", "Fabrication", "Traitement", "Extraction"]:
|
||||||
|
if operation == "Assemblage":
|
||||||
|
item = sel_prod
|
||||||
|
elif operation == "Fabrication":
|
||||||
|
item = sel_comp
|
||||||
|
else:
|
||||||
|
item = sel_miner
|
||||||
|
with st.expander(f"Préconisations et indicateurs spécifiques - {operation}"):
|
||||||
|
st.markdown(f"### {operation} -> :blue-background[{item}]")
|
||||||
|
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"* {colorer_couleurs(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"* {colorer_couleurs(niveau)}\n"
|
||||||
|
for p in INDICATEURS[operation][niveau]:
|
||||||
|
contenu_md += f" - {p}\n"
|
||||||
|
st.markdown(contenu_md)
|
||||||
|
|
||||||
|
def afficher_preconisations_et_indicateurs(niveau_criticite, sel_prod, sel_comp, sel_miner, poids_A, poids_F, poids_T, poids_E, poids_M):
|
||||||
|
st.markdown("## Préconisations et indicateurs")
|
||||||
|
|
||||||
|
afficher_preconisations_et_indicateurs_generiques(niveau_criticite, poids_A, poids_F, poids_T, poids_E, poids_M)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
afficher_preconisations_et_indicateurs_specifiques(sel_prod, sel_comp, sel_miner, niveau_criticite_operation)
|
||||||
|
|
||||||
|
def afficher_details_operations(produits, composants, mineraux, sel_prod, sel_comp, sel_miner, details_sections):
|
||||||
|
st.markdown("## Détails des opérations")
|
||||||
|
|
||||||
|
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", "").removesuffix("\n---\n")
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 = initialiser_seuils(config_path)
|
||||||
|
|
||||||
|
sel_prod, sel_comp, sel_miner, niveau_criticite, \
|
||||||
|
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M, \
|
||||||
|
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc \
|
||||||
|
= tableau_de_bord(chains, produits, composants, mineraux, seuils)
|
||||||
|
|
||||||
|
afficher_criticites(produits, composants, mineraux, sel_prod, sel_comp, sel_miner, seuils)
|
||||||
|
|
||||||
|
afficher_explications_et_details(
|
||||||
|
couleur_A, poids_A, couleur_F, poids_F, couleur_T, poids_T, couleur_E, poids_E, couleur_M, poids_M,
|
||||||
|
produits, composants, mineraux, sel_prod, sel_comp, sel_miner,
|
||||||
|
couleur_A_ihh, couleur_A_isg, couleur_F_ihh, couleur_F_isg, couleur_T_ihh, couleur_T_isg,couleur_E_ihh, couleur_E_isg, couleur_M_ics, couleur_M_ivc)
|
||||||
|
|
||||||
|
afficher_preconisations_et_indicateurs(niveau_criticite, sel_prod, sel_comp, sel_miner, poids_A, poids_F, poids_T, poids_E, poids_M)
|
||||||
|
|
||||||
|
afficher_details_operations(produits, composants, mineraux, sel_prod, sel_comp, sel_miner, details_sections)
|
||||||
17
app/plan_d_action/utils/interface/__init__.py
Normal file
17
app/plan_d_action/utils/interface/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from .parser import preparer_graphe
|
||||||
|
from .niveau_utils import extraire_niveaux
|
||||||
|
from .selection import (
|
||||||
|
selectionner_minerais,
|
||||||
|
selectionner_noeuds,
|
||||||
|
extraire_chemins_selon_criteres
|
||||||
|
)
|
||||||
|
from .export import (
|
||||||
|
exporter_graphe_filtre,
|
||||||
|
extraire_liens_filtres
|
||||||
|
)
|
||||||
|
from .visualization import remplacer_par_badge
|
||||||
|
from .config import (
|
||||||
|
niveau_labels,
|
||||||
|
JOBS,
|
||||||
|
CORRESPONDANCE_COULEURS
|
||||||
|
)
|
||||||
27
app/plan_d_action/utils/interface/config.py
Normal file
27
app/plan_d_action/utils/interface/config.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CORRESPONDANCE_COULEURS = {
|
||||||
|
"Rouge": "red",
|
||||||
|
"Orange": "orange",
|
||||||
|
"Vert": "green",
|
||||||
|
"FAIBLE": "green",
|
||||||
|
"MODÉRÉE": "orange",
|
||||||
|
"ÉLEVÉE à CRITIQUE": "red"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Répertoire courant du script
|
||||||
|
CURRENT_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Répertoire "jobs" dans app/plan_d_action
|
||||||
|
JOBS = CURRENT_DIR / "jobs"
|
||||||
|
JOBS.mkdir(exist_ok=True)
|
||||||
33
app/plan_d_action/utils/interface/export.py
Normal file
33
app/plan_d_action/utils/interface/export.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import networkx as nx
|
||||||
|
|
||||||
|
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
|
||||||
8
app/plan_d_action/utils/interface/niveau_utils.py
Normal file
8
app/plan_d_action/utils/interface/niveau_utils.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
11
app/plan_d_action/utils/interface/parser.py
Normal file
11
app/plan_d_action/utils/interface/parser.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
74
app/plan_d_action/utils/interface/selection.py
Normal file
74
app/plan_d_action/utils/interface/selection.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import networkx as nx
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
|
from utils.graph_utils import (
|
||||||
|
extraire_chemins_depuis,
|
||||||
|
extraire_chemins_vers
|
||||||
|
)
|
||||||
|
|
||||||
|
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_action.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_action.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_action.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_action.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_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
|
||||||
11
app/plan_d_action/utils/interface/visualization.py
Normal file
11
app/plan_d_action/utils/interface/visualization.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import re
|
||||||
|
from app.plan_d_action.utils.interface.config import CORRESPONDANCE_COULEURS
|
||||||
|
|
||||||
|
def remplacer_par_badge(markdown_text, correspondance=CORRESPONDANCE_COULEURS):
|
||||||
|
# Échappe les mots à remplacer s'ils contiennent des accents ou espaces
|
||||||
|
for mot, couleur in correspondance.items():
|
||||||
|
# Utilise des bords de mots (\b) pour éviter les remplacements partiels
|
||||||
|
pattern = r'\b' + re.escape(mot) + r'\b'
|
||||||
|
remplacement = f":{couleur}-badge[{mot}]"
|
||||||
|
markdown_text = re.sub(pattern, remplacement, markdown_text)
|
||||||
|
return markdown_text
|
||||||
11344
app/plan_d_action/utils/jobs/57b102c2.md
Normal file
11344
app/plan_d_action/utils/jobs/57b102c2.md
Normal file
File diff suppressed because it is too large
Load Diff
11344
app/plan_d_action/utils/jobs/bc52d99e.md
Normal file
11344
app/plan_d_action/utils/jobs/bc52d99e.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user