Code/app/plan_d_action/plan_d_action.py
2025-06-02 17:00:08 +02:00

811 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import streamlit as st
import yaml
import re
import matplotlib.pyplot as plt
PRECONISATIONS = {
'Facile': [
"Constituer des stocks stratégiques.",
"Surveiller activement les signaux géopolitiques.",
"Renforcer la surveillance des régions critiques."
],
'Modérée': [
"Diversifier progressivement les fournisseurs.",
"Favoriser la modularité des produits.",
"Augmenter progressivement les taux de recyclage."
],
'Difficile': [
"Investir fortement en R&D pour la substitution.",
"Développer des technologies alternatives robustes.",
"Établir des partenariats stratégiques locaux solides."
],
'Extraction': {
'Facile': [
"Constituer des stocks « in-country » (site minier / port) pour 30 jours.",
"Activer un moniteur de prix spot quotidien.",
"Lancer une veille ESG locale (manifestations, météo extrême)."
],
'Modérée': [
"Négocier des contrats « take-or-pay » avec au moins 2 exploitants distincts.",
"Mettre en place un audit semestriel des pratiques de sécurité/logistique des mines.",
"Financer en co-investissement un entrepôt portuaire multi-produits."
],
'Difficile': [
"Participer au capital dun producteur émergent hors zone de concentration.",
"Obtenir des droits d« off-take » de 5 ans sur 20 % de la production dune mine alternative.",
"Soutenir (CAPEX) louverture dune nouvelle voie ferroviaire ou portuaire sécurisée."
]
},
'Traitement': {
'Facile': [
"Sécuriser un stock tampon sur site (90 jours).",
"Faire certifier la traçabilité chimique du concentré."
],
'Modérée': [
"Valider un second affineur dans une région politiquement stable.",
"Imposer des clauses « force-majeure » limitant larrêt total à 48 h.",
"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 dautres lignes en cas de rupture composant."
],
'Modérée': [
"Avoir un site dassemblage secondaire (low-volume) dans une région verte, testé tous les 6 mois.",
"Segmenter les nomenclatures : version « premium » avec composant haut de gamme, version « fallback » avec composant moins critique."
],
'Difficile': [
"Investir dans une plateforme dassemblage flexible (robots modulaires) capable de basculer vers un composant de substitution en < 72 h.",
"Signer un accord gouvernemental pour un soutien logistique prioritaire (corridor aérien dédié) en cas de crise géopolitique.",
"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 dobtention des permis dexport."
],
'Difficile': [
"Capacité annuelle dune mine alternative financée (% du besoin interne).",
"Progrès physique de linfrastructure logistique (Km de voie, % achevé)."
]
},
'Traitement': {
'Facile': [
"Couverture stock tampon (jours).",
"Certificats de traçabilité obtenus (% lots)."
],
'Modérée': [
"Nombre daffineurs validés (objectif ≥ 2).",
"Taux de rendement global du procédé (%)."
],
'Difficile': [
"Part de production refinée hors zone rouge (%).",
"Capex cumulé investi dans de nouveaux procédés (M€)."
]
},
'Fabrication': {
'Facile': [
"Stock de composants critiques (jours).",
"Capacité libre des EMS (%) rapportée chaque vendredi."
],
'Modérée': [
"Part du second fondeur dans la production du composant audio (%).",
"Nombre de PCB « design-for-substitution » validés."
],
'Difficile': [
"Dépenses R&D substituts (€) vs budget.",
"TRI attendu sur les investisseurs fondeurs alternatifs."
]
},
'Assemblage': {
'Facile': [
"Jours de produits finis en entrepôt.",
"Temps de retouche ligne en cas de rupture (heures)."
],
'Modérée': [
"Volume annuel produit sur le site de secours (%).",
"Temps de requalification dune ligne vers la version « fallback »."
],
'Difficile': [
"Taux dautomatisation reconfigurable (% machines modulaires).",
"Nb dheures du corridor aérien prioritaire utilisé vs capacité."
]
}
}
# ------------------------- PARSEUR -------------------------
def parse_chains_md(filepath: str) -> tuple[dict, dict, dict, list, dict, dict]:
re_start_section = re.compile(r"^##\s*Chaînes\s+avec\s+risque\s+critique", re.IGNORECASE)
re_other_h2 = re.compile(r"^##\s+(?!(Chaînes\s+avec\s+risque\s+critique))")
re_chain_heading = re.compile(r"^###\s*(.+)\s*→\s*(.+)\s*→\s*(.+)$")
re_phase = re.compile(r"^\*\s*(Assemblage|Fabrication|Minerai|Extraction|Traitement)", re.IGNORECASE)
re_IHH = re.compile(r"IHH\s*[:]\s*([0-9]+(?:\.[0-9]+)?)")
re_ISG = re.compile(r"ISG\s*combiné\s*[:]\s*([0-9]+(?:\.[0-9]+)?)|ISG\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
re_ICS = re.compile(r"ICS\s*moyen\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
re_IVC = re.compile(r"IVC\s*[:]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE)
produits, composants, mineraux, chains = {}, {}, {}, []
descriptions = {}
details_sections = {}
current_chain = None
current_phase = None
current_section = None
in_section = False
with open(filepath, encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
if not in_section:
if re_start_section.match(line):
in_section = True
continue
if re_other_h2.match(line):
break
m_chain = re_chain_heading.match(line)
if m_chain:
prod, comp, miner = map(str.strip, m_chain.groups())
produits.setdefault(prod, {"IHH_Assemblage": None, "ISG_Assemblage": None})
composants.setdefault(comp, {"IHH_Fabrication": None, "ISG_Fabrication": None})
mineraux.setdefault(miner, {
"ICS": None, "IVC": None,
"IHH_Extraction": None, "ISG_Extraction": None,
"IHH_Traitement": None, "ISG_Traitement": None
})
chains.append({"produit": prod, "composant": comp, "minerai": miner})
current_chain = {"prod": prod, "comp": comp, "miner": miner}
current_phase = None
current_section = f"{prod}{comp}{miner}"
descriptions[current_section] = ""
continue
if current_chain is None:
continue
m_phase = re_phase.match(line)
if m_phase:
current_phase = m_phase.group(1).capitalize()
continue
if current_phase:
p = current_chain
if current_phase == "Assemblage":
if (m := re_IHH.search(line)):
produits[p["prod"]]["IHH_Assemblage"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
produits[p["prod"]]["ISG_Assemblage"] = float(raw)
continue
if current_phase == "Fabrication":
if (m := re_IHH.search(line)):
composants[p["comp"]]["IHH_Fabrication"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
composants[p["comp"]]["ISG_Fabrication"] = float(raw)
continue
if current_phase == "Minerai":
if (m := re_ICS.search(line)):
mineraux[p["miner"]]["ICS"] = float(m.group(1))
continue
if (m := re_IVC.search(line)):
mineraux[p["miner"]]["IVC"] = float(m.group(1))
continue
if current_phase == "Extraction":
if (m := re_IHH.search(line)):
mineraux[p["miner"]]["IHH_Extraction"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
mineraux[p["miner"]]["ISG_Extraction"] = float(raw)
continue
if current_phase == "Traitement":
if (m := re_IHH.search(line)):
mineraux[p["miner"]]["IHH_Traitement"] = float(m.group(1))
continue
if (m := re_ISG.search(line)):
raw = m.group(1) or m.group(2)
mineraux[p["miner"]]["ISG_Traitement"] = float(raw)
continue
else:
if current_section:
descriptions[current_section] += raw_line
# Parse detailed sections from the complete file
with open(filepath, encoding="utf-8") as f:
content = f.read()
# Extract sections using regex patterns
lines = content.split('\n')
# Find section boundaries
operations_start = None
minerais_start = None
for i, line in enumerate(lines):
if line.strip() == "## Détails des opérations":
operations_start = i
elif line.strip() == "## Détails des minerais":
minerais_start = i
if operations_start is not None:
# Parse operations section (assemblage and fabrication)
operations_end = minerais_start if minerais_start else len(lines)
operations_lines = lines[operations_start:operations_end]
current_section_name = None
current_content = []
for line in operations_lines:
if line.startswith("### ") and " et " in line:
# Save previous section
if current_section_name and current_content:
details_sections[current_section_name] = '\n'.join(current_content)
# Start new section
section_title = line.replace("### ", "").strip()
if " et Assemblage" in section_title:
product_name = section_title.replace(" et Assemblage", "").strip()
current_section_name = f"{product_name}_assemblage"
elif " et Fabrication" in section_title:
component_name = section_title.replace(" et Fabrication", "").strip()
current_section_name = f"{component_name}_fabrication"
current_content = []
elif current_section_name:
current_content.append(line)
# Save last section
if current_section_name and current_content:
details_sections[current_section_name] = '\n'.join(current_content)
if minerais_start is not None:
# Parse minerais section
minerais_lines = lines[minerais_start:]
current_minerai = None
current_section_type = "general"
current_content = []
for line in minerais_lines:
if line.startswith("### ") and "" not in line and " et " not in line:
# Save previous section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
# Start new minerai
current_minerai = line.replace("### ", "").strip()
current_section_type = "general"
current_content = []
elif line.startswith("#### Extraction"):
# Save previous section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
current_section_type = "extraction"
current_content = []
elif line.startswith("#### Traitement"):
# Save previous section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
current_section_type = "traitement"
current_content = []
elif line.startswith("## ") and current_minerai:
# End of minerais section
if current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
break
elif current_minerai:
current_content.append(line)
# Save last section
if current_minerai and current_content:
details_sections[f"{current_minerai}_{current_section_type}"] = '\n'.join(current_content)
return produits, composants, mineraux, chains, descriptions, details_sections
# ---------------------- AFFICHAGE ----------------------
def afficher_bloc_ihh_isg(titre, ihh, isg, details_content=""):
st.markdown(f"### {titre}")
if not details_content:
st.markdown("Données non disponibles")
return
lines = details_content.split('\n')
# 1. Afficher vulnérabilité combinée en premier
if "#### Vulnérabilité combinée IHH-ISG" in details_content:
st.markdown("#### Vulnérabilité combinée IHH-ISG")
afficher_section_texte(lines, "#### Vulnérabilité combinée IHH-ISG", "###")
# 2. Afficher ISG des pays impliqués
if "##### ISG des pays impliqués" in details_content:
print(details_content)
st.markdown("#### ISG des pays impliqués")
afficher_section_avec_tableau(lines, "##### ISG des pays impliqués")
# Afficher le résumé ISG combiné
for line in lines:
if "**ISG combiné:" in line:
st.markdown(line)
break
# 3. Afficher la section IHH complète
if "#### Indice de Herfindahl-Hirschmann" in details_content:
st.markdown("#### Indice de Herfindahl-Hirschmann")
# Tableau de résumé IHH
afficher_section_avec_tableau(lines, "#### Indice de Herfindahl-Hirschmann")
# IHH par entreprise
if "##### IHH par entreprise (acteurs)" in details_content:
st.markdown("##### IHH par entreprise (acteurs)")
afficher_section_texte(lines, "##### IHH par entreprise (acteurs)", "##### IHH par pays")
# IHH par pays
if "##### IHH par pays" in details_content:
st.markdown("##### IHH par pays")
afficher_section_texte(lines, "##### IHH par pays", "##### En résumé")
# En résumé
if "##### En résumé" in details_content:
st.markdown("##### En résumé")
afficher_section_texte(lines, "##### En résumé", "####")
def afficher_section_avec_tableau(lines, section_start, section_end=None):
"""Affiche une section contenant un tableau"""
in_section = False
table_lines = []
for line in lines:
if section_start in line:
in_section = True
continue
elif in_section and section_end and section_end in line:
break
elif in_section and line.startswith('#') and section_start not in line:
break
elif in_section:
if line.strip().startswith('|'):
table_lines.append(line)
elif table_lines and not line.strip().startswith('|'):
# Fin du tableau
break
if table_lines:
st.markdown('\n'.join(table_lines))
def afficher_section_texte(lines, section_start, section_end_marker=None):
"""Affiche le texte d'une section sans les tableaux"""
in_section = False
for line in lines:
if section_start in line:
in_section = True
continue
elif in_section and section_end_marker and line.startswith(section_end_marker):
break
elif in_section and line.startswith('#') and section_start not in line:
break
elif in_section and line.strip() and not line.strip().startswith('|'):
st.markdown(line)
def afficher_description(titre, description):
st.markdown(f"## {titre}")
if description:
lines = description.split('\n')
description_lines = []
# Extraire le premier paragraphe descriptif
for line in lines:
line = line.strip()
if not line:
if description_lines: # Si on a déjà du contenu, une ligne vide termine le paragraphe
break
continue
# Arrêter aux titres de sections ou tableaux
if (line.startswith('####') or
line.startswith('|') or
line.startswith('**Unité')):
break
description_lines.append(line)
if description_lines:
# Rejoindre les lignes en un seul paragraphe
full_description = ' '.join(description_lines)
st.markdown(full_description)
else:
st.markdown("Description non disponible")
else:
st.markdown("Description non disponible")
def afficher_caracteristiques_minerai(minerai, mineraux_data, details_content=""):
st.markdown("### Caractéristiques générales")
if not details_content:
st.markdown("Données non disponibles")
return
lines = details_content.split('\n')
# 3. Afficher la vulnérabilité combinée ICS-IVC en dernier
if "#### Vulnérabilité combinée ICS-IVC" in details_content:
st.markdown("#### Vulnérabilité combinée ICS-IVC")
afficher_section_texte(lines, "#### Vulnérabilité combinée ICS-IVC", "####")
# 1. Afficher la section ICS complète
if "#### ICS" in details_content:
st.markdown("#### ICS")
# Afficher le premier tableau ICS (avec toutes les colonnes)
afficher_section_avec_tableau(lines, "#### ICS", "##### Valeurs d'ICS par composant")
# Afficher la sous-section "Valeurs d'ICS par composant"
if "##### Valeurs d'ICS par composant" in details_content:
st.markdown("##### Valeurs d'ICS par composant")
afficher_section_avec_tableau(lines, "##### Valeurs d'ICS par composant", "**ICS moyen")
# Afficher le résumé ICS moyen
for line in lines:
if "**ICS moyen" in line:
st.markdown(line)
break
# 2. Afficher la section IVC complète
if "#### IVC" in details_content:
st.markdown("#### IVC")
# Afficher la valeur IVC principale
for line in lines:
if "**IVC:" in line:
st.markdown(line)
break
# Afficher tous les détails de la section IVC
afficher_section_texte(lines, "#### IVC", "#### Vulnérabilité combinée ICS-IVC")
def get_seuil(seuils_dict, key):
try:
if key in seuils_dict:
data = seuils_dict[key]
for niveau in ["rouge", "orange", "vert"]:
if niveau in data:
seuil = data[niveau]
if "min" in seuil and seuil["min"] is not None:
return seuil["min"]
if "max" in seuil and seuil["max"] is not None:
return seuil["max"]
except:
pass
return None
def set_vulnerability(v1, v2, t1, t2, seuils):
v1_poids = 1
v1_couleur = "Vert"
if v1 > seuils[t1]["rouge"]["min"]:
v1_poids = 3
v1_couleur = "Rouge"
elif v1 > seuils[t1]["vert"]["max"]:
v1_poids = 2
v1_couleur = "Orange"
v2_poids = 1
v2_couleur = "Vert"
if v2 > seuils[t2]["rouge"]["min"]:
v2_poids = 3
v2_couleur = "Rouge"
elif v2 > seuils[t2]["vert"]["max"]:
v2_poids = 2
v2_couleur = "Orange"
poids = v1_poids * v2_poids
couleur = "Rouge"
if poids <= 2:
couleur = "Vert"
elif poids <= 4:
couleur = "Orange"
return poids, couleur, v1_couleur, v2_couleur
def colorer_couleurs(la_couleur):
if la_couleur.lower() == "rouge":
return f":red-badge[{la_couleur}]"
if la_couleur.lower() == "orange":
return f":orange-badge[{la_couleur}]"
if la_couleur.lower() == "vert":
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 densemble des criticités", expanded=True):
st.markdown("## Vue densemble des criticités", unsafe_allow_html=True)
col_left, col_right = st.columns([1, 1], gap="small", border=True)
with col_left:
fig1, ax1 = plt.subplots(figsize=(1, 1))
ax1.scatter([produits[sel_prod]["ISG_Assemblage"]], [produits[sel_prod]["IHH_Assemblage"]], label="Assemblage".ljust(20), s=5)
ax1.scatter([composants[sel_comp]["ISG_Fabrication"]], [composants[sel_comp]["IHH_Fabrication"]], label="Fabrication".ljust(20), s=5)
ax1.scatter([mineraux[sel_miner]["ISG_Extraction"]], [mineraux[sel_miner]["IHH_Extraction"]], label="Extraction".ljust(20), s=5)
ax1.scatter([mineraux[sel_miner]["ISG_Traitement"]], [mineraux[sel_miner]["IHH_Traitement"]], label="Traitement".ljust(20), s=5)
# Seuils ISG (vertical)
ax1.axvline(seuils["ISG"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax1.axvline(seuils["ISG"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
# Seuils IHH (horizontal)
ax1.axhline(seuils["IHH"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax1.axhline(seuils["IHH"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
ax1.set_xlim(0, 100)
ax1.set_ylim(0, 100)
ax1.set_xlabel("ISG", fontsize=4)
ax1.set_ylabel("IHH", fontsize=4)
ax1.tick_params(axis='both', which='major', labelsize=4)
ax1.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=4)
plt.tight_layout()
st.pyplot(fig1)
with col_right:
fig2, ax2 = plt.subplots(figsize=(1, 1))
ax2.scatter([mineraux[sel_miner]["IVC"]], [mineraux[sel_miner]["ICS"]], color='green', s=5, label=sel_miner.ljust(20))
# Seuils IVC (vertical)
ax2.axvline(seuils["IVC"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax2.axvline(seuils["IVC"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
# Seuils ICS (horizontal)
ax2.axhline(seuils["ICS"]["vert"]["max"], linestyle='--', color='green', alpha=0.7, linewidth=0.5) # Seuil vert-orange
ax2.axhline(seuils["ICS"]["rouge"]["min"], linestyle='--', color='red', alpha=0.7, linewidth=0.5) # Seuil orange-rouge
ax2.set_xlim(0, max(100, mineraux[sel_miner]["IVC"]))
ax2.set_ylim(0, 1)
ax2.set_xlabel("IVC", fontsize=4)
ax2.set_ylabel("ICS", fontsize=4)
ax2.tick_params(axis='both', which='major', labelsize=4)
ax2.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=4)
plt.tight_layout()
st.pyplot(fig2)
st.markdown(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("---")
with st.expander("Préconisations et indicateurs génériques"):
col_left, col_right = st.columns([1, 1], gap="small", border=True)
with col_left:
st.markdown("### Préconisations :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in PRECONISATIONS.items():
if niveau in niveau_criticite:
contenu_md = f"* {niveau}\n"
for p in PRECONISATIONS[niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
with col_right:
st.markdown("### Indicateurs :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in INDICATEURS.items():
if niveau in niveau_criticite:
contenu_md = f"* {niveau}\n"
for p in INDICATEURS[niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
def affectation_poids(poids_operation):
if poids_operation < 3:
niveau_criticite = {"Facile"}
elif poids_operation < 6:
niveau_criticite = {"Facile", "Modérée"}
else:
niveau_criticite = {"Facile", "Modérée", "Difficile"}
return niveau_criticite
niveau_criticite_operation = {}
niveau_criticite_operation["Assemblage"] = affectation_poids(poids_A)
niveau_criticite_operation["Fabrication"] = affectation_poids(poids_F)
niveau_criticite_operation["Traitement"] = affectation_poids(poids_T)
niveau_criticite_operation["Extraction"] = affectation_poids(poids_E)
for operation in ["Assemblage", "Fabrication", "Traitement", "Extraction"]:
with st.expander(f"Préconisations et indicateurs spécifiques - {operation}"):
st.markdown(f"### {operation}")
col_left, col_right = st.columns([1, 1], gap="small", border=True)
with col_left:
st.markdown("#### Préconisations :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in PRECONISATIONS[operation].items():
if niveau in niveau_criticite_operation[operation]:
contenu_md = f"* {niveau}\n"
for p in PRECONISATIONS[operation][niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
with col_right:
st.markdown("#### Indicateurs :\n\n")
st.markdown("Mise en œuvre : \n")
for niveau, contenu in INDICATEURS[operation].items():
if niveau in niveau_criticite_operation[operation]:
contenu_md = f"* {niveau}\n"
for p in INDICATEURS[operation][niveau]:
contenu_md += f" - {p}\n"
st.markdown(contenu_md)
st.markdown("---")
with st.expander(f"{sel_prod} et Assemblage"):
assemblage_details = details_sections.get(f"{sel_prod}_assemblage", "")
afficher_description(f"{sel_prod} et Assemblage", assemblage_details)
afficher_bloc_ihh_isg("Assemblage", produits[sel_prod]["IHH_Assemblage"], produits[sel_prod]["ISG_Assemblage"], assemblage_details)
with st.expander(f"{sel_comp} et Fabrication"):
fabrication_details = details_sections.get(f"{sel_comp}_fabrication", "")
afficher_description(f"{sel_comp} et Fabrication", fabrication_details)
afficher_bloc_ihh_isg("Fabrication", composants[sel_comp]["IHH_Fabrication"], composants[sel_comp]["ISG_Fabrication"], fabrication_details)
with st.expander(f"{sel_miner} — Vue globale"):
minerai_general = details_sections.get(f"{sel_miner}_general", "")
afficher_description(f"{sel_miner} — Vue globale", minerai_general)
extraction_details = details_sections.get(f"{sel_miner}_extraction", "")
afficher_bloc_ihh_isg("Extraction", mineraux[sel_miner]["IHH_Extraction"], mineraux[sel_miner]["ISG_Extraction"], extraction_details)
traitement_details = details_sections.get(f"{sel_miner}_traitement", "")
afficher_bloc_ihh_isg("Traitement", mineraux[sel_miner]["IHH_Traitement"], mineraux[sel_miner]["ISG_Traitement"], traitement_details)
afficher_caracteristiques_minerai(sel_miner, mineraux[sel_miner], minerai_general)
st.markdown("---")